Files
2026-11-03 09:00:29 +08:00

210 lines
9.0 KiB
JavaScript

/**
* 菜单 → 页面 的跳转分发
*
* 小程序端「已开发」的只有少数原生页面(记事本、日程、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: /账号管理|密码库|passwordStore|password-store/i, route: '/pages/tools/account/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 || '功能')
})
}