/** * 菜单 → 页面 的跳转分发 * * 小程序端「已开发」的只有少数原生页面(记事本、日程、OA 若干), * 其余菜单在端上都没有对应页面:过去统一丢进 webview 打开 Web 端, * 而 Web 端没登录态会被路由守卫送到统一认证登录页,用户看到的是「登录」而不是功能。 * * 现在的判定:命中原生页 → 打开原生页;命中 WEB_RULES(Web 端确已上线、可免登录打开) * → 走 webview;其余一律进入「功能开发中」占位页。 */ import { createAppTicket } from '@/api/auth.js' /** 移动端已有原生页面的功能。匹配顺序即优先级。 */ const NATIVE_RULES = [ // 进销存(移动端原生):按菜单 path 精确分流,列表所需的 doc_type / bill_type 直接挂在路由 query 上 { pattern: /\/apps\/erp\/(dashboard|workbench)/i, route: '/pages/erp/dashboard/index' }, { pattern: /\/apps\/erp\/basic\/warehouse/i, route: '/pages/erp/basic/warehouse' }, { pattern: /\/apps\/erp\/basic\/product/i, route: '/pages/erp/basic/product' }, { pattern: /\/apps\/erp\/suppliers/i, route: '/pages/erp/partner/index?partner_type=supplier' }, { pattern: /\/apps\/erp\/customers/i, route: '/pages/erp/partner/index?partner_type=customer' }, { pattern: /\/apps\/erp\/purchase\/order/i, route: '/pages/erp/document/index?doc_type=purchase_order' }, { pattern: /\/apps\/erp\/purchase\/inbound/i, route: '/pages/erp/document/index?doc_type=purchase_in' }, { pattern: /\/apps\/erp\/purchase\/return/i, route: '/pages/erp/document/index?doc_type=purchase_return' }, { pattern: /\/apps\/erp\/sales\/order/i, route: '/pages/erp/document/index?doc_type=sales_order' }, { pattern: /\/apps\/erp\/sales\/outbound/i, route: '/pages/erp/document/index?doc_type=sales_out' }, { pattern: /\/apps\/erp\/sales\/return/i, route: '/pages/erp/document/index?doc_type=sales_return' }, { pattern: /\/apps\/erp\/stock\/transfer/i, route: '/pages/erp/document/index?doc_type=transfer' }, { pattern: /\/apps\/erp\/stock\/taking/i, route: '/pages/erp/document/index?doc_type=stock_take' }, { pattern: /\/apps\/erp\/stock\/inbound/i, route: '/pages/erp/document/index?doc_type=other_in' }, { pattern: /\/apps\/erp\/stock\/outbound/i, route: '/pages/erp/document/index?doc_type=other_out' }, { pattern: /\/apps\/erp\/stock\/query/i, route: '/pages/erp/stock/index' }, { pattern: /\/apps\/erp\/stock\/warning/i, route: '/pages/erp/stock/warning' }, { pattern: /\/apps\/erp\/finance\/receivable/i, route: '/pages/erp/finance/bill?bill_type=1' }, { pattern: /\/apps\/erp\/finance\/payable/i, route: '/pages/erp/finance/bill?bill_type=2' }, { pattern: /\/apps\/erp\/finance\/payment/i, route: '/pages/erp/finance/fund' }, { pattern: /\/apps\/erp\/report/i, route: '/pages/erp/report/index' }, { pattern: /记事本|笔记|便签|notebook/i, route: '/pages/tools/notepad/index' }, // 「日程提醒」与「工作日程」是两个独立功能:提醒在前,命中"提醒"后即不再落到日历页 { pattern: /提醒|reminder/i, route: '/pages/tools/schedule/reminder' }, // 自定义审批流(移动端):放在工作日程之前,避免「待办审批」「审批中心」被工作日程吞掉 // 「我发起的审批 / 我的发起」也是同一页(业务口径就是申请人视角的实例列表) { pattern: /审批列表|我的审批|我发起的|审批管理|审批申请|approvalList|approval\/list/i, route: '/pages/approval/index' }, { pattern: /待我审批|我审批的|审批待办|审批中心|approvalTodo|approval\/todo/i, route: '/pages/approval/verify' }, { pattern: /工作日程|日程|日历|待办|calendar|schedule/i, route: '/pages/tools/schedule/index' }, // 报销管理(注意放在日程规则之后即可,与日程关键词无冲突) { pattern: /报销|reimburse|expense/i, route: '/pages/tools/expense/index' }, // OA 协同办公 { pattern: /工作日报|日报|dailyreport/i, route: '/pages/oa/dailyreport/index' }, { pattern: /通知公告|公告|notice/i, route: '/pages/oa/notice/index' }, { pattern: /文档库|文档管理|文档图谱|document/i, route: '/pages/oa/document/index' }, { pattern: /薪酬|工资|薪资|compensation|payroll/i, route: '/pages/oa/compensation/index' }, { pattern: /员工档案|人事档案|档案|employeefile|employeeFile/i, route: '/pages/oa/employeefile/index' }, { pattern: /通讯录|联系人|contact/i, route: '/pages/oa/contact/index' } ] /** Web 端容器页 */ const WEBVIEW_PAGE = '/pages/apps/webview' /** * 确已在 Web 端上线、可以免登录直接打开的功能(按菜单 title 或 path 关键词匹配)。 * 命中 → webview 打开 Web 端;未命中 → 「功能开发中」占位页。 * 新功能上线时把对应关键词加进来即可。 */ export const WEB_RULES = [ // 审批流配置(Web 端 /backend/approval):端上没有原生实现,直接跳 Web 配置页 '审批设置', '审批流配置', '审批流程', '审批配置' ] /** 开发中原生占位页 */ const DEVELOPING_PAGE = '/pages/common/developing' /** 该功能在小程序端尚未开发 */ function openDeveloping(name) { uni.navigateTo({ url: DEVELOPING_PAGE + '?t=' + encodeURIComponent(name || '功能') }) } /** 菜单是否命中移动端原生页面 */ export function resolveNativeRoute(menu = {}) { const title = String(menu.title || '') const path = String(menu.path || '') const haystack = title + ' ' + path const hit = NATIVE_RULES.find((r) => r.pattern.test(haystack)) // 调试日志:方便排查菜单跳转是否符合预期(HBuilderX / 微信开发者工具 控制台可见) console.log('[nav] resolveNativeRoute', { title, path, hit: hit ? hit.route : '' }) return hit ? hit.route : '' } /** 该功能是否在租户套餐内 */ export function isMenuLocked(menu = {}) { return menu.locked === true || menu.locked === 1 } /** * 打开一个菜单功能 * @param {object} menu /api/home 返回的菜单项 * @param {string} [fallbackTitle] 没有菜单时用于提示的功能名 */ export function openMenu(menu, fallbackTitle = '') { const name = (menu && (menu.title || menu.name)) || fallbackTitle || '该功能' if (!menu) { uni.showToast({ title: name + '暂未开放', icon: 'none' }) return } if (isMenuLocked(menu)) { uni.showModal({ title: '功能未开通', content: '「' + name + '」不在当前套餐范围内,请联系管理员开通后再使用。', showCancel: false, confirmText: '知道了' }) return } // 目录节点(还有下级)不直接跳转 if (menu.leaf === false) { uni.showToast({ title: '请选择「' + name + '」下的具体功能', icon: 'none' }) return } const native = resolveNativeRoute(menu) if (native) { uni.navigateTo({ url: native }) return } const target = String(menu.path || '').trim() // 完整外链直接打开,不走开发中判定 if (/^https?:\/\//i.test(target)) { openRawUrl(target, name) return } // Web 端确已上线的功能仍走 webview(带一次性票据免登录) const haystack = name + ' ' + target if (target && WEB_RULES.some((kw) => haystack.indexOf(kw) !== -1)) { openWebPage(target, name) return } // 端上没有对应页面 = 功能未开发:进占位页,不再跳 Web 的统一认证登录 openDeveloping(name) } /** * 用 Web 端打开指定业务页。 * * 关键:这里不传「完整 URL」,而是拆成 path + ticket 两个参数传给容器页。 * 原因是 uniapp 各端对 query 的编解码行为不一致 —— H5 端 vue-router 会先解码一次 * 并在序列化时把 % 转成 %25,小程序端则不解码;一旦传完整 URL(含 %、#、&), * 就会出现「双重编码」或被截断。path 与 ticket 都是纯 ASCII 无特殊字符, * 任何端都安全。 * * @param {string} path 菜单 path,如 /apps/oa/dashboard * @param {string} title 页面标题 */ export async function openWebPage(path, title = '') { const name = title || '功能' const target = String(path || '').trim() if (!target) { uni.showToast({ title: name + '暂未开放', icon: 'none' }) return } let ticket = '' try { const data = await createAppTicket() ticket = (data && data.ticket) || '' } catch (e) { // 票据失败不影响打开,只是需要在 webview 内登录一次 console.warn('[nav] 获取免登录票据失败,降级为直接打开:', e) } uni.navigateTo({ url: WEBVIEW_PAGE + '?p=' + encodeURIComponent(target) + '&k=' + encodeURIComponent(ticket) + '&t=' + encodeURIComponent(name) }) } /** * 打开 webview 容器页并直接指定完整地址(通用兜底,非免登录场景)。 * @param {string} url 完整地址(未编码) * @param {string} title 页面标题 */ export function openRawUrl(url, title = '') { if (!url) return uni.navigateTo({ url: WEBVIEW_PAGE + '?p=' + encodeURIComponent(url) + '&t=' + encodeURIComponent(title || '功能') }) }