/** * 菜单 → 页面 的跳转分发 * * 小程序端「已开发」的只有少数原生页面(记事本、日程、OA 若干), * 其余菜单在端上都没有对应页面:过去统一丢进 webview 打开 Web 端, * 而 Web 端没登录态会被路由守卫送到统一认证登录页,用户看到的是「登录」而不是功能。 * * 现在的判定:命中原生页 → 打开原生页;命中 WEB_RULES(Web 端确已上线、可免登录打开) * → 走 webview;其余一律进入「功能开发中」占位页。 */ import { createAppTicket } from '@/api/auth.js' /** 移动端已有原生页面的功能。匹配顺序即优先级。 */ const NATIVE_RULES = [ { pattern: /记事本|笔记|便签|notebook/i, route: '/pages/tools/notepad/index' }, // 「日程提醒」与「工作日程」是两个独立功能:提醒在前,命中"提醒"后即不再落到日历页 { pattern: /提醒|reminder/i, route: '/pages/tools/schedule/reminder' }, { pattern: /工作日程|日程|日历|待办|calendar|schedule/i, route: '/pages/tools/schedule/index' }, // 报销管理(注意放在日程规则之后即可,与日程关键词无冲突) { pattern: /报销|reimburse|expense/i, route: '/pages/tools/expense/index' }, // OA 协同办公 { 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 端;未命中 → 「功能开发中」占位页。 * 新功能上线时把对应关键词加进来即可。 */ const WEB_RULES = [] /** 开发中原生占位页 */ const DEVELOPING_PAGE = '/pages/common/developing' /** 该功能在小程序端尚未开发 */ function openDeveloping(name) { uni.navigateTo({ url: DEVELOPING_PAGE + '?t=' + encodeURIComponent(name || '功能') }) } /** 菜单是否命中移动端原生页面 */ export function resolveNativeRoute(menu = {}) { const haystack = (menu.title || '') + ' ' + (menu.path || '') const hit = NATIVE_RULES.find((r) => r.pattern.test(haystack)) 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 || '功能') }) }