/** * 菜单 → 页面 的跳转分发 * * 聚合平台里绝大多数业务功能(ERP / OA / CRM / CMS)目前只有 Web 端实现, * 端上没有对应页面;少数功能(记事本、日程提醒)已有原生页面。 * 这里统一判定:能走原生就走原生,否则丢进 webview 容器打开 Web 端深链。 */ import { createAppTicket } from '@/api/auth.js' /** 移动端已有原生页面的功能。匹配顺序即优先级。 */ const NATIVE_RULES = [ { pattern: /记事本|笔记|便签|notebook/i, route: '/pages/tools/notepad/index' }, { pattern: /日程|提醒|待办|calendar|schedule/i, route: '/pages/tools/schedule/index' } ] /** Web 端容器页 */ const WEBVIEW_PAGE = '/pages/apps/webview' /** 菜单是否命中移动端原生页面 */ 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 /app/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 } if (!menu.path) { uni.showToast({ title: name + '暂未开放', icon: 'none' }) return } // 走 Web 端:先换一次性票据实现免登录,失败则降级为直接打开(用户手动登录一次) openWebPage(menu.path, 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 || '功能') }) }