批量优化

This commit is contained in:
hero920103 committed 2026-10-03 23:46:55 +08:00
1 parent e3db229f41
commit aa92ca40a2
79 files changed
+7495 -1104

No files matched your search

+47 -3
View File
@@ -4,13 +4,20 @@ const TENANT_KEY = 'app_tenant_name'
const TID_KEY = 'app_tid'
const REFRESH_KEY = 'app_refresh_token'
const SID_KEY = 'app_sid'
const TENANTS_KEY = 'app_tenants'
/** 是否启用统一认证:.env 中配置 VITE_AUTH_MODE=sso */
/**
* 是否启用统一认证(UAC)。
* 统一认证是默认形态(与 Web 端 /auth/login 同一套账号体系);
* 仅在 .env 中显式配置 VITE_AUTH_MODE=legacy 时才回退旧的「租户名 + 账号」直登。
*/
export function isSSOEnabled() {
try {
return (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_AUTH_MODE) === 'sso'
const mode =
(typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_AUTH_MODE) || ''
return String(mode).toLowerCase() !== 'legacy'
} catch (e) {
return false
return true
}
}
@@ -36,6 +43,28 @@ export function setTid(tid) {
export function getTid() {
return Number(uni.getStorageSync(TID_KEY) || 0)
}
/** 统一认证:缓存「该账号可进入的企业」列表,供登录后展示/切换企业 */
export function setTenants(list) {
uni.setStorageSync(TENANTS_KEY, Array.isArray(list) ? list : [])
}
export function getTenants() {
const list = uni.getStorageSync(TENANTS_KEY)
return Array.isArray(list) ? list : []
}
/**
* 取「当前登录的那一家企业」的名称。
* 登录返回的 tenants 是该账号可进入的全部企业,顶部展示必须是当前会话选中的企业。
*/
export function resolveTenantName(info = {}, tid = 0) {
if (info && info.tenant_name) return info.tenant_name
const list = (info && info.tenants) || getTenants()
const hit = list.find((t) => String(t.tid) === String(info.tid || tid))
if (hit) return hit.tenant_name || ''
return list.length ? list[0].tenant_name || '' : ''
}
const REMEMBER_KEY = 'app_remember_me'
const REMEMBER_TENANT_KEY = 'app_remember_tenant'
const REMEMBER_ACCOUNT_KEY = 'app_remember_account'
@@ -74,6 +103,21 @@ export function logout() {
uni.removeStorageSync(USER_KEY)
uni.removeStorageSync(REFRESH_KEY)
uni.removeStorageSync(SID_KEY)
uni.removeStorageSync(TID_KEY)
uni.removeStorageSync(TENANTS_KEY)
}
/**
* 清除「待选企业」阶段的临时登录态。
* 一人多企业时 /auth/login 先签发一枚 tid=0、有效期 10 分钟的待选令牌,
* 用户若放弃选择或切换账号,必须清掉,避免半登录态残留去访问业务接口。
*/
export function clearPendingAuth() {
uni.removeStorageSync(TOKEN_KEY)
uni.removeStorageSync(REFRESH_KEY)
uni.removeStorageSync(SID_KEY)
uni.removeStorageSync(TID_KEY)
uni.removeStorageSync(TENANTS_KEY)
}
/** 是否开启记住账号登录信息 */
+3 -2
View File
@@ -116,10 +116,11 @@ function showGeetest4App(captchaId) {
/**
* 弹出极验 4.0 验证
* @param {string} [captchaIdOverride] 已从统一认证 /auth/verify-config 拿到 captcha_id 时直接传入
* @returns {Promise<{ captcha_id, lot_number, pass_token, gen_time, captcha_output }>}
*/
export async function showGeetest4() {
const captchaId = await fetchGeetest4CaptchaId()
export async function showGeetest4(captchaIdOverride = '') {
const captchaId = captchaIdOverride || (await fetchGeetest4CaptchaId())
if (!captchaId) {
throw new Error('未配置极验 captcha_id')
}
+131
View File
@@ -0,0 +1,131 @@
/**
* 菜单 → 页面 的跳转分发
*
* 聚合平台里绝大多数业务功能(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 || '功能')
})
}
+60
View File
@@ -0,0 +1,60 @@
/**
* 子系统数据获取(共享逻辑)
*
* 三个子系统页面(概览 / 功能 / 待办)都需要:
* 1. 该子系统的配置(config/subsystems.js)
* 2. 该子系统下的菜单(/app/home → menus 按 module 归组)
* 这里统一封装,避免三个页面各写一遍。
*/
import { ref, computed } from 'vue'
import { fetchHome } from '@/api/workspace.js'
import { getSubsystem } from '@/config/subsystems.js'
/**
* @param {string} slug 子系统目录名(crm / erp / oa / cms / tools)
* @param {import('vue').Ref<string>} currentTab 当前 tab,用于按需拉数据
*/
export function useSubsystem(slug) {
const sub = getSubsystem(slug)
const loading = ref(true)
const loadError = ref('')
const user = ref({})
const menus = ref([])
const moduleMeta = ref(null)
/** 该子系统下所有菜单(含目录) */
const allMenus = computed(() => menus.value.filter((m) => m.module === sub.module))
/** 可直接跳转的叶子菜单,未开通/锁定的排除 */
const leafMenus = computed(() =>
allMenus.value.filter((m) => m.leaf !== false && !m.locked)
)
/** 目录(有下级,不能直接跳) */
const dirMenus = computed(() => allMenus.value.filter((m) => m.leaf === false))
/** 是否已被租户套餐开通 */
const enabled = ref(true)
async function load(force = false) {
loading.value = true
try {
const data = await fetchHome(force)
user.value = data.user || {}
menus.value = Array.isArray(data.menus) ? data.menus : []
const hit = (Array.isArray(data.modules) ? data.modules : []).find(
(m) => String(m.code || '').toLowerCase() === String(sub.module).toLowerCase()
)
moduleMeta.value = hit || null
enabled.value = hit ? hit.enabled !== false : true
loadError.value = ''
} catch (e) {
console.error('[subsystem] load failed:', e)
loadError.value = (e && e.message) || '接口请求失败'
} finally {
loading.value = false
}
}
return { sub, loading, loadError, user, menus, moduleMeta, enabled, allMenus, leafMenus, dirMenus, load }
}