批量优化
This commit is contained in:
1 parent
e3db229f41
commit
aa92ca40a2
79 files changed
+7495
-1104
No files matched your search
+47
-3
@@ -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)
|
||||
}
|
||||
|
||||
/** 是否开启记住账号登录信息 */
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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 || '功能')
|
||||
})
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in new issue
Block a user