UNPKG

@ithinkdt/core

Version:

iThinkDT Core

1,521 lines (1,396 loc) 138 kB
import { inject, ref, shallowRef, reactive, markRaw, h, defineComponent, watch, computed, toRef, isRef, shallowReactive, nextTick, unref, onBeforeUnmount, onUnmounted, onActivated, onDeactivated, getCurrentInstance, provide, toRaw, onMounted, Transition, KeepAlive, Suspense, effectScope, isReactive, readonly, onScopeDispose, toRefs, toValue } from 'vue'; import { request, HttpError, walkTree, debounce, omitProps, copy, createI18n, lang as lang$1, useI18n, array2Tree, measureText, merge, message } from '@ithinkdt/common'; export { lang, useI18n } from '@ithinkdt/common'; import { promiseTimeout, useEventListener, until, syncRef, tryOnUnmounted, usePreferredColorScheme, watchDebounced, toReactive, useUrlSearchParams } from '@vueuse/core'; import { defineStore, getActivePinia, createPinia } from 'pinia'; import { createRouter, createWebHashHistory, createWebHistory, useRouter, onBeforeRouteLeave, RouterView } from 'vue-router'; import { hyphenate, isObject } from '@vue/shared'; import { format } from 'date-fns'; const $FETCH = Symbol(); let $fetch = request; function initHttp(init) { $fetch = request.extend(init); return { install(app) { app.provide($FETCH, $fetch); }, } } function useFetch(api, options = {}) { let fn, options2 = {}; if (!api) { api = 'get: '; } if (typeof api === 'function') { fn = api; } else { const $fetch = options?.$fetch ?? inject($FETCH, request); let _api; if (typeof api === 'string') { const index = api.indexOf(':'); const [method, reqType] = api.slice(0, Math.max(0, index)).split('|'); _api = { url: api.slice(Math.max(0, index + 1)), method, reqType, }; } else { _api = api; } options.method = _api.method ?? options?.method; const { url, method = options.method ?? 'get', before, after, shallow, resetOnExecute, initResult, ...requestOptions } = Object.assign({}, _api, options); options2 = { before, after, shallow, resetOnExecute, initResult }; const isGet = method.toLowerCase() === 'get'; fn = (params, options3 = {}) => { let req; if (params instanceof Request) { req = params; } else { req = { ...requestOptions, ...options3, }; if (isGet) { req.params = params; } else { req.body = params; } } return $fetch(method, url, req) }; } const loading = ref(false); const result = (options.shallow === false ? ref : shallowRef)(options?.initResult); const state = ref('inited'); const _before = [options.before, options2.before]; const _after = [options2.after, options.after]; const _fn = (params, options3) => { if (options2.resetOnExecute || options.resetOnExecute) result.value = options?.initResult; loading.value = true; state.value = 'loading'; // eslint-disable-next-line unicorn/no-array-reduce params = _before.reduce((params, before) => (before ? before(params) ?? params : params), params); const req = Promise.resolve(fn(params, options3)); req.then( (res) => { state.value = 'success'; // eslint-disable-next-line unicorn/no-array-reduce result.value = _after.reduce((res, after) => (after ? after(res) ?? res : res), res); }, (error) => { state.value = error?.name === 'AbortError' ? 'abort' : 'error'; if (state.value !== 'abort') { console[error instanceof HttpError ? 'debugger' : 'error'](`[use-fetch]: `, error); } }, ).finally(() => { loading.value = false; }); return req }; _fn.loading = loading; _fn.state = state; _fn.result = result; _fn.exec = _fn; return _fn } function initAuthInfo({ login, logout, loadAuthInfo, changePwd, router, locale }) { const fn = defineStore(`core:auth`, { state: () => ({ error: false, token: undefined, app: undefined, user: undefined, modules: [], fromModule: undefined, }), getters: { logged(state) { return !!state.token }, moduleMap(state) { const map = {}; walkTree(state.modules ?? [], (m, _i, parent) => { m.parent = parent; if (m.type !== 'action') { map[m.key] = m; if (m.path) { map[m.path] = m; } } }); return map }, menus: (() => { function _walk(m) { if (m?.hidden === true) return if (m?.type === 'group' && m?.children?.length) { const children = m.children.map((c) => _walk(c)).filter((m) => !!m); return children.length > 0 ? { ...m, children: children, } : undefined } return m?.type === 'action' ? undefined : { ...m, children: undefined } } return (state) => { const menus = state.modules?.map(_walk).filter((m) => !!m); console.debug('[auth] menus', menus); return menus } })(), menuMap() { const map = {}; walkTree(this.menus, (m) => { map[m.key] = m; if (m.path) { map[m.path] = m; } }); return map }, currentRouteInModule() { const route = router.currentRoute.value; return !route?.meta?.activedModule?.trim() && !route?.meta?.activedModuleName?.trim() }, currentModule() { const route = router.currentRoute.value; if (!route) return if (this.currentRouteInModule) { return this.moduleMap[route.path] ? route.path : route.matched.at(-1)?.path ?? '' } return ( route.meta.activedModule?.trim() || router.resolve({ name: route.meta.activedModuleName?.trim() })?.path ) }, activedModulePath() { const route = router.currentRoute.value; if (!route) return [] let menu = this.moduleMap[this.currentModule] || this.moduleMap[this.fromModule]; const paths = []; if ( !this.currentRouteInModule || !(this.moduleMap[route.path] || this.moduleMap[route.matched.at(-1)?.path ?? '']) ) { paths.push( reactive({ key: route.path, parentKey: menu?.key, parent: menu, label: route.path, type: 'module', path: route.path, }), ); } while (menu) { paths.unshift(menu); menu = menu.parent; } console.debug('[auth] activedModulePath', paths); return paths }, activedMenuPath() { const paths = this.activedModulePath.map((m) => this.menuMap[m.key]).filter((m) => !!m); console.debug('[auth] activedMenuPath', paths); return paths }, permissions(state) { const permissions = {}; walkTree(state.modules, (m) => { m.path && (permissions[m.path] = true); if (m.actions) for (const r of m.actions) permissions[r] = true; }); console.debug('[auth] permissions', permissions); return permissions }, }, actions: (() => { let _reject; let $load; const loginFn = useFetch(login); const logoutFn = logout && useFetch(logout); return { async login(user) { console.debug(`[login]: user [ ${user.username} ].`); try { const res = await loginFn(user); console.debug('[login]: login success.', res); return res } catch (error) { console.debug('[login]: login error.', error); throw error } }, async load(force = false) { if (!this.token) { console.debug('[auth] load not set token!'); $load = undefined; throw new Error('未设置用户 token') } if ($load) { if (!force) { console.debug('[auth] load having $load, reuse.'); return $load } console.debug('[auth] load having $load, but force it.'); _reject(new Error('重新 load')); } const req = ($load = new Promise((resolve, reject) => { _reject = reject; loadAuthInfo(this.token, locale.value).then( (authInfo) => { if (this.token === undefined || req !== $load) { console.debug('[auth] load fetched auth info, but token or request expired.'); return } for (const key of Object.keys(authInfo)) { this[key] = markRaw(authInfo[key]); } console.debug('[auth] load fetched auth info.', authInfo); resolve(); }, (error) => { if (req === $load) { if (error?.message === 'forbidden' || error?.message === 'timeout') { this.error = error; resolve(); return } console.debug('[auth] load fetch auth error.', error); this.error = error; $load = undefined; } reject(error); }, ); })); return $load }, async logout() { console.debug('[auth] logout.'); const { token, user, app } = this.$state; await logoutFn?.({ username: user?.username, appCode: app?.appCode, token: token }); this.$reset(); this.$clear(); await promiseTimeout(300); }, changePwd, } })(), persist: { storage: 'local', version: 1, excludes: ['app', 'modules'], }, }); return fn } function getFirstPageModule(modules) { for (const m of modules ?? []) { if (['module', 'lowcode', 'federate', 'external'].includes(m.type)) { return m } if (m.type === 'group') { const child = getFirstPageModule(m.children ?? []); if (child) { return child } } } return } let $msg; let $dialog; let $notice; function initFeedback({ messageApi, dialogApi, notificationApi }) { $msg = messageApi; $dialog = dialogApi; $notice = notificationApi; return { install() { // nothing }, } } const Route = { INDEX: '__$-app-index', LOGIN: '__$-app-login', LOGOUT: '__$-app-logout', NOT_FOUND: '__$-app-404', }; function initRouter({ router, base = '', routes, index, logged, Login, Logout, Error, sso = false, setToken, getToken, logout, type, getAuthErrorType, }) { const getIndexRoute = typeof index === 'function' ? index : () => index; console.debug(`[router] initing...`, routes); const warn = debounce( () => { $notice({ type: 'warning', title: '请求的资源不存在', content: `可能的原因:\n· 应用配置错误\n· 新版本发布`, meta: '若页面正常请忽略此报告!', action: () => h('span', [ h('span', { style: 'font-size: 12px' }, ['您可以 ']), h('a', { href: `javascript:location.reload()` }, ['刷新页面']), ]), }); }, 30_000, { leading: true }, ); walkTree(routes, (route) => { if (typeof route.component === 'function') { if (!route.meta) { route.meta = {}; } const comp = route.component; route.component = () => comp() .then((m) => { route.meta.__DT_ROUTE_LOAD_FAILURED = false; return m }) .catch((error) => { console.error(`[router] load route component error!`, route, error); route.meta.__DT_ROUTE_LOAD_FAILURED = error; warn(); return Error }); } }); function redirectOrIndex(redirect) { console.debug(`[login]: logged, redirect to ${redirect ? `'${redirect}'` : 'index'}.`); if (redirect) { location.replace(redirect); } return { path: '/', replace: true } } console.debug( sso ? `[router] sso enabled, self login ${sso.selfLogin ? 'enabled' : 'disabled'}.` : `[router] sso disabled`, ); if (router) { router.addRoute({ path: base, name: Route.INDEX + '/' + base, childrens: routes, beforeEnter: () => { const index = getIndexRoute() ?? '/403'; console.debug(`[router] goto page '${index}'.`); return { path: index, replace: true } }, }); } else { let _index, _routes = []; for (const r of routes) { if (r.path === '/') { _index = r; } else { _routes.push(r); } } const toIndex = () => { const index = getIndexRoute(); if (index) { console.debug(`[router] goto page '${index}'.`); return { path: index, replace: true } } }; router = createRouter({ history: (type === 'hash' ? createWebHashHistory : createWebHistory)(base), routes: [ _index ? { beforeEnter: (to) => { if ( to.name !== Route.INDEX || _index.component || _index.children?.find((r) => r.path === '') ) return return toIndex() }, ..._index, name: Route.INDEX, } : { path: '/', name: Route.INDEX, component: defineComponent({ render() { return h(Error, { type: getAuthErrorType() }) }, }), beforeEnter: toIndex, }, { path: '/login', alias: sso && sso.selfLogin ? [typeof sso.selfLogin === 'string' ? sso.selfLogin : '/login!'] : [], name: Route.LOGIN, component: Login, meta: { requiresLogin: false, keepAlive: false }, async beforeEnter(to) { console.debug('[login]: before each...'); if (logged.value) { await promiseTimeout(301); let redirect = to.query?.redirect; if (to.query?.redirect_with_token) { redirect += redirect.includes('?') ? (redirect.endsWith('?') ? '' : '&') : '?'; redirect += to.query?.redirect_with_token + '=' + getToken(); } return redirectOrIndex(redirect) } if (!sso || to.path !== '/login') { console.debug('[login]: enter self login!'); return } const token = await sso.parseToken(location.origin + (to.href ?? '')); console.debug( `[login]: sso paresd result {\n\ttoken: ${token},\n\tredirect: ${to.query?.redirect}\n}.`, ); if (token) { return setToken(token).then(() => redirectOrIndex(to.query?.redirect)) } console.debug(`[login]: redirect to sso login page...`); window.open(await sso.login(location.origin + (type === 'hash' ? base : '') + to.href), '_self'); return false }, }, { path: '/logout', name: Route.LOGOUT, component: Logout, meta: { requiresLogin: false, keepAlive: false }, async beforeEnter(to) { if (Logout) { return } await logout(); if (!sso) { if (to.query?.redirect) { window.open(to.query.redirect, '_self'); return } return { name: Route.LOGIN } } console.debug(`[login]: redirect to sso logout page...`); window.open( await sso.logout(`${location.origin}${base}`, { ...to.query, token: getToken() }), '_self', ); return false }, }, ..._routes, { path: '/:path(.*)', name: Route.NOT_FOUND, component: Error, meta: { requiresLogin: false, keepAlive: 'path&query' }, beforeEnter() { if (!logged.value) { console.debug(`[router] error view redirect to login...`); return { name: Route.LOGIN, replace: true, } } }, }, ], }); } router.base = base && base[0] !== '.' ? base : '/'; return router } let STORE_PREFIX = ''; function persistPlugin({ prefix: _prefix, getUsername }) { STORE_PREFIX = _prefix ?? STORE_PREFIX; return ({ options, store }) => { if (!options.persist) { return { $clear() { console.debug(`[store] the store '${store.$id}' not set persist, force it.`); }, } } const { prefix = _prefix, userIsolate = false, storage = 'session', version = 1, validate, excludes, saveOnlyChanged, onStore, onRestore, } = options.persist; const getKey = () => `${prefix || ''}${store.$id}${userIsolate && getUsername() ? `[${getUsername()}]` : ''}`; const _state = saveOnlyChanged ? omitProps({ ...store.$state }, ...(excludes || [])) : undefined; let force; const parse = () => { let data = parseData(storage, getKey(), version, validate); if (saveOnlyChanged) data = Object.assign({}, _state, data); if (onRestore) data = onRestore(data); if (data) { force = true; Object.assign(store.$state, data); } }; parse(); useEventListener('storage', (e) => { if (e.key !== getKey() || e.newValue === e.oldValue) { return } setTimeout(parse, 0); }); store.$subscribe((_, state) => { if (force) { force = false; return } let s = {}; for (const key of Object.keys(state)) { if ( typeof key !== 'symbol' && !excludes?.includes(key) && (!saveOnlyChanged || _state[key] !== state[key]) ) { s[key] = state[key]; } } s = onStore ? onStore(s) : s; saveData(storage, getKey(), s, version); }); return { $clear() { saveData(storage, getKey(), undefined, version); }, } } } function parseData(storage, key, version, validate) { const $storage = storage === 'local' ? localStorage : sessionStorage; const data = $storage.getItem(key); if (data) { const { s, v, t } = JSON.parse(data); console.debug(`[store] parsed '${key}' data from ${storage}, version: ${v}, date: ${t}, data:`, s); if (v !== version || validate?.(s, v, t) === false) { console.debug(`[store] '${key}' data not valid, remove it.`); $storage.removeItem(key); } else { return s } } return } function saveData(storage, key, data, version) { const $storage = storage === 'local' ? localStorage : sessionStorage; const date = Date.now(); const s = JSON.stringify({ t: date, v: version, s: data, }); let old = $storage.getItem(key) ?? ''; old = old.slice(Math.max(0, old.indexOf('"v":'))); let n = s.slice(Math.max(0, s.indexOf('"v":'))); if (old === n) return $storage.setItem(key, s); console.debug(`[store] save '${key}' data to ${storage}, version: ${version}, date: ${date}, data:`, copy(data)); } function initStore({ plugin = [], pinia, ...persist }) { pinia ||= getActivePinia() || createPinia(); pinia.use(persistPlugin(persist)); for (const p of plugin) pinia.use(p); return pinia } function jwt( router, getToken, setToken, { headerField, cookieField, cookieDomain, cookiePath = '/', tokenParser, tokenFormatter, unauthCodes, sameSite = 'Lax', }, ) { let _token, resolve; const setted = new Promise((_resolve) => (resolve = _resolve)); const interceptor = async (req, next) => { if (_token) { req.headers.set(headerField, tokenFormatter(_token)); } else if (req.url.includes('/cli/') || req.url.includes('/cer/')) { await setted; } try { return await next({ ...req, credentials: 'omit', }) } catch (error) { if ( error instanceof HttpError && error.code && unauthCodes.includes(error.code) && ![Route.NOT_FOUND, Route.LOGOUT, Route.LOGIN].includes(router.currentRoute.value.name) ) { console.debug('[auth] token expired'); setToken(); } throw error } }; interceptor.setCookieByToken = (token) => { if (cookieField) { // eslint-disable-next-line unicorn/no-document-cookie document.cookie = `${cookieField}=${_token ? encodeURIComponent(tokenFormatter(token)) : ''}; ${ cookieDomain ? `domain=${cookieDomain}; ` : '' }path=${cookiePath}; ${sameSite}`; } }; const readPathCookie = (path, field) => { try { const url = location.href.slice(location.origin.length); // eslint-disable-next-line unicorn/no-null history.replaceState(null, '', path); const reg = new RegExp(`(^| )${field}=([^;]*)(;|$)`); const v = reg.exec(document.cookie)?.[2]; // eslint-disable-next-line unicorn/no-null history.replaceState(null, '', url); return v } catch { return '' } }; interceptor.getTokenFromCookie = () => { const str = readPathCookie(cookiePath, cookieField); return str && tokenParser(decodeURIComponent(str)) }; const str = interceptor.getTokenFromCookie(); interceptor._init = () => { setted.then((token) => { if (str && str !== token) { console.debug('[jwt]: 从 cookie 读取到的 token 不同于当前值,将替换'); setToken(str); } }); watch( getToken, (token) => { _token = token; interceptor.setCookieByToken(token); token && resolve(token); }, { immediate: true, flush: 'sync' }, ); }; return interceptor } let useAuth = () => { throw new Error('未初始化 Auth 模块,不能调用 useAuth') }; let msgHandler = { loadMessages: () => Promise.resolve({ total: 0, records: [], }), }; let auth; /** * @type {import('vue-router').Router} */ let router; let $can; function initAuth({ jwt: jwtInit, domain, router: routerInit, loadMessages, readMessages, deleteMessages, locale, loadAppInfo, sso, logout, ...authInfoInit }) { msgHandler = { loadMessages, readMessages, deleteMessages, }; const logged = ref(false); function setToken(token) { if (auth.token === token) return if (token) { auth.token = token; } else { const href = router.resolve({ name: Route.LOGOUT, query: auth.user ? { username: auth.user.username, appCode: auth.app.appCode } : {}, }).href; auth.$reset(); setTimeout(() => { location.replace(href); }, 300); } return auth.load(true) } const getToken = () => { return auth?.token }; router = initRouter({ ...routerInit, index: routerInit.index ?? (() => { return getFirstPageModule(auth.modules ?? [])?.path ?? '/index' }), logged: logged, getToken, setToken, logout: () => auth.logout(), sso, getAuthErrorType: () => auth.error, }); const lastRoutes = []; let $init; useAuth = initAuthInfo({ ...authInfoInit, loadAuthInfo: (...params) => { return ($init = authInfoInit.loadAuthInfo(...params).then((result) => { for (const r of lastRoutes) { router.removeRoute(r); } lastRoutes.length = 0; const remoteUrls = new Set(); walkTree(result.modules, (m) => { if (m.type === 'federate') { remoteUrls.add(m.remote); } else if (m.type === 'lowcode') { lastRoutes.push('/lowcode' + m.path); router.addRoute({ name: '/lowcode' + m.path, path: m.path, component: routerInit.Lowcode, props: (route) => ({ ...route.params, ithinkdtLcPageKey: m.lcKey, ithinkdtLcAppCode: m.remote, }), }); } }); return Promise.allSettled( [...remoteUrls].map(async (url) => { try { const m = await import(/* @vite-ignore */ url); for (const r of m.default) router.addRoute(r); } catch (error) { console.error(`[auth] load federate remote app routes error!`, error); } }), ).then(() => result) })) }, logout: sso ? undefined : async (...params) => { const ret = await logout?.(...params); Promise.resolve(loadAppInfo(locale.value)).then((app) => { auth.app = app; }); return ret }, router, locale, }); const _can = (action) => { return auth.permissions[action] || false }; function can(...actions) { if (actions?.length > 1) { return actions.some((act) => _can(act)) } return _can(actions[0]) } function checkLogin(to) { if (auth.logged) { return } console.debug('[router] requires user login, redirect to login page.'); const redirect = to.matched[0]?.name === Route.INDEX ? undefined : to.href; return { name: Route.LOGIN, query: redirect ? { redirect: encodeURIComponent(redirect), } : undefined, } } let fromModule; router.beforeEach((to) => { if ($init) { return $init.then(() => { $init = undefined; return to.fullPath }) } console.debug(`[router] route to '${to.fullPath}'...`, to); fromModule = auth.currentModule || auth.fromModule; if (to.meta?.requiresLogin === false) { console.debug('[router] not requires user login.'); return auth.logged ? auth.load() : true } const check = checkLogin(to); if (check) return check return auth.load().catch((error) => { console.error('[router] error when loading user info!', error); const check = checkLogin(to); if (check) return check // TODO 调整方式 return { path: '/500', query: { msg: error?.message ?? error, }, } }) }); const findAuth = (auths) => { return auths.find((s) => auth.permissions[s.trim()] || auth.moduleMap[s.trim()]) }; router.afterEach(async (to, _from, failure) => { auth.fromModule = fromModule; if (failure) return if (to.meta?.requiresLogin === false || to.meta?.requiresAuth === false) { console.debug(`[auth] not requires auth access, path: ${to.fullPath}`); } else { let requires = to.meta?.requiresAuth; if (requires && typeof requires !== 'boolean') requires = typeof requires === 'string' ? requires.split(',') : requires; if ( to.path !== '/' && (requires?.length ? !findAuth(requires) : !auth.moduleMap[to.path] && !auth.moduleMap[to.matched.at(-1)?.path ?? '']) ) { console.debug(`[auth] not found module '${to.fullPath}' in user auth access.`); to.__DT_ROUTE_ERROR = '403'; } else if (to.matched?.find((route) => route?.meta.__DT_ROUTE_LOAD_FAILURED)) { to.__DT_ROUTE_ERROR = 'LOAD_FAILURED'; } } }); const jwt$1 = jwtInit ? jwt(router, getToken, setToken, jwtInit) : undefined; function _init() { auth = useAuth(); jwt$1?._init(); if (domain) { let timer; watch( () => auth.user?.username, (name) => { if (name) { // eslint-disable-next-line unicorn/no-document-cookie document.cookie = `${STORE_PREFIX}auth=${encodeURIComponent(btoa(name))}; expires=${ Date.now() + 86_400_000 }; domain=${domain}; path=/; Strict`; } else { timer !== undefined && clearInterval(timer); // eslint-disable-next-line unicorn/no-document-cookie document.cookie = `${STORE_PREFIX}auth=; expires=${ Date.now() - 60_000 }; domain=${domain}; path=/; Strict`; } }, { immediate: true, flush: 'sync' }, ); const reg = new RegExp(`(^| )${STORE_PREFIX}auth=([^;]*)(;|$)`); const checkUser = () => { const cookieUser = reg.exec(document.cookie)?.[2]; if (!cookieUser || window.atob(decodeURIComponent(cookieUser)) !== auth.user.username) { console.debug( `user is expired, user: ${auth.user.username}, cookie user: ${cookieUser}, origin: ${location.origin}, referrer: ${document.referrer}`, ); return false } }; until(computed(() => auth.user?.username)) .toBeTruthy() .then(() => { if (auth.user?.username && checkUser() === false) { auth.$reset(); promiseTimeout(30).then(() => { location.reload(); }); } else { timer = setInterval(() => { if (auth.user?.username && checkUser() !== false) return clearInterval(timer); timer = undefined; $dialog({ type: 'error', title: '账号冲突', content: '您的账号可能被其他窗口登出,即将刷新此页面。。', cancelText: false, okText: '刷 新', onOk() { auth.$reset(); return promiseTimeout(30).then(() => { location.reload(); }) }, }); }, 300); } }); } syncRef(toRef(auth, 'logged'), logged, { direction: 'ltr' }); watch( locale, async () => { console.debug('[auth] locale changed, reload user & app info.'); auth.token && auth.load(true).catch(() => { // no empty }); auth.app = (await loadAppInfo(locale.value)) ?? auth.app; }, { immediate: true }, ); } return { router, jwt: jwt$1, install(app) { if (!auth) _init(); app.config.globalProperties.$can = $can = can; app.provide('__dt_access__', can); app.use(router); }, } } function useLogin() { const auth = useAuth(); const loging = ref(false); async function login(user, load = true) { loging.value = true; try { auth.token = await auth.login(user); load && (await auth.load(true)); } finally { loging.value = false; } } const router = useRouter(); return { user: auth.user, loging, login, logout: () => { const { user, app } = auth.$state; const href = router.resolve({ name: Route.LOGOUT, query: user ? { username: user.username, appCode: app.appCode } : {}, }).href; location.replace(href); }, next() { return location.reload() }, } } function useAccess(prefix, actions) { const can = (...params) => $can(...params); can.permissions = auth.permissions; can.can = prefix ? (...actions) => can(...actions.map((action) => prefix + action)) : can; can.cans = actions?.map(prefix ? (action) => can(prefix + action) : can); return can } function lang({ locale, headerField, localeFormatter }) { let _locale; watch( locale, () => { _locale = locale.value; }, { immediate: true, flush: 'sync' }, ); return (req, next) => { if (_locale) { req.headers.set(headerField, localeFormatter(_locale)); } return next(req) } } let i18n; function initI18n(options) { const { global, install } = createI18n({ locale: options.defaultLocale, messages: options.globalMessage, }); i18n = global; const lang$2 = typeof options.http === 'object' ? lang({ ...options.http, locale: global.locale, }) : undefined; const key = `${STORE_PREFIX}core:i18n`; const version = 1; const parse = () => { const data = parseData('local', key, version, (locale) => locale && Object.values(lang$1).includes(locale)); if (data) global.locale.value = data; }; parse(); watch( global.locale, (locale, from) => { if (from === locale) return document.querySelector('html').setAttribute('lang', locale); console.debug(`[i18n] locale changed ${from || '[undefined]'} -> ${locale}.`); saveData('local', key, locale === options.defaultLocale ? undefined : locale, version); }, { immediate: true }, ); return { global, lang: lang$2, install(app) { app.use(install); }, } } let init; function setInit(init0) { init = init0; } const layzePool = []; function batchFetchDicts() { const _layzePool = [...layzePool]; layzePool.length = 0; init.fetchDicts(_layzePool.map(([dictType]) => dictType)).then( (res) => { for (const [dictType, cb] of _layzePool) { cb(res?.[dictType] ?? []); } }, (error) => { console.error('[dict]: 请求字典项数据失败:', error); for (const [_dictType, cb] of _layzePool) { cb([]); } }, ); } async function _getDict(dictType, forceCache) { if (forceCache || !hasDictType(dictType)) { if (typeof dictType !== 'string' || !dictType?.trim()) { console.error('[dict]: 字典类型错误:', dictType); return [] } removeDict(dictType); const $fetch = new Promise((resolve) => { layzePool.push([dictType, resolve]); }); if (layzePool.length === 1) { nextTick(batchFetchDicts); } registerDict(dictType, $fetch, { label: init.labelGetter, value: init.valueGetter, disabled: init.disabledGetter, }); await $fetch; } await dictMap[dictType]?.$; return unref(dictMap[dictType]?.data) ?? [] } const dictMap = {}; const dictValueMap = {}; function registerDict(dictType, dicts, { autoClean = false, cleanUseless = true, label, value, disabled } = {}) { let _dicts; let $; if (typeof dicts === 'function') { dicts = Promise.resolve(dicts(_getDict)); } if (dicts instanceof Promise) { _dicts = shallowRef([]); $ = dicts.then((dicts) => { _dicts.value = dicts; }); } else { _dicts = isRef(dicts) ? dicts : shallowRef(dicts); } const _label = typeof label === 'function' ? label : (d) => d[label ?? 'label']; const _value = typeof value === 'function' ? value : (d) => d[value ?? 'value']; const _disabled = typeof disabled === 'function' ? disabled : (d) => d[disabled ?? 'disabled']; const { locale } = useI18n(); const $dicts = computed(() => { return _dicts.value?.map((dict) => { return { label: _label(dict, locale.value), value: _value(dict), disabled: _disabled(dict), orign: dict, } }) }); dictMap[dictType] = { date: cleanUseless ? Date.now() : undefined, data: $dicts, $, }; const unregister = () => removeDict(dictType); if (autoClean) { tryOnUnmounted(unregister); } return { dictType, dicts: getDicts(dictType), unregister, } } function getDicts(dictType, hideDisabled = true, valueType = 'string') { const cache = dictMap[dictType]; cache.date = Date.now(); const dicts = shallowReactive([]); watch( cache.data, (data = []) => { dicts.length = 0; let array = valueType === 'number' ? data.map((it) => ({ ...it, value: Number(it.value) })) : data; if (hideDisabled) { array = array.filter((it) => it.disabled !== true); } dicts.push(...array); }, { immediate: true }, ); return dicts } function getDictMap(dictType, hideDisabled = false, valueType = 'string') { if (!dictValueMap[dictType + hideDisabled]) { const map = shallowReactive(new Map()); watch( getDicts(dictType, hideDisabled, valueType), (dicts) => { map.clear(); for (const dict of dicts) { map.set(dict.value, dict); } }, { immediate: true }, ); dictValueMap[dictType + hideDisabled] = map; } return dictValueMap[dictType + hideDisabled] } function hasDictType(dictType) { return !!dictMap[dictType] } function removeDict(dictType) { delete dictMap[dictType]; delete dictValueMap[dictType]; } function cleanUseless(delay = 1 * 60 * 1000) { const now = Date.now(); for (const key of Object.keys(dictMap)) { const date = dictMap[key]?.date; if (date && now - date > delay) { removeDict(key); } } } function getDict(dictType, getType, valueType, forceCache, hideDisabled, onGet) { const $fetch = _getDict(dictType, forceCache); onGet && $fetch.then(onGet); const isGetMap = getType === 'map'; let $ret; if (hasDictType(dictType)) { $ret = isGetMap ? getDictMap(dictType, hideDisabled, valueType) : getDicts(dictType, hideDisabled, valueType); } else { $ret = shallowReactive(isGetMap ? new Map() : []); } $ret.$ = $fetch; return $ret } /* eslint-disable unicorn/no-thenable */ function _useDict(dictType, getType, valueTypeOrOptions) { const valueType = typeof valueTypeOrOptions === 'string' ? valueTypeOrOptions : valueTypeOrOptions?.valueType ?? 'string'; const { forceCache = false, onGet, hideDisabled = getType === 'list', } = typeof valueTypeOrOptions === 'object' ? valueTypeOrOptions : {}; let $returns; if (isRef(dictType)) { const isGetMap = getType === 'map'; $returns = shallowReactive(isGetMap ? new Map() : []); $returns.__on = []; $returns.loading = false; watch( dictType, () => { $returns.loading = true; isGetMap ? $returns.clear() : ($returns.length = 0); if (dictType.value) { const $ret = getDict(dictType.value, getType, valueType, forceCache, hideDisabled, (dicts) => { nextTick(() => { if (isGetMap) { for (const [k, v] of $ret.entries) $returns.set(k, v); } else { $returns.push(...$ret); } onGet?.(dicts); $returns.loading = false; for (const on of $returns.__on) { try { on?.($ret); } catch (error) { console.error(error); } } }); }); } }, { immediate: true }, ); $returns.then = (on) => { $returns.__on.push(on); }; } else { $returns = getDict(dictType, getType, valueType, forceCache, hideDisabled, (dicts) => { nextTick(() => { $returns.loading = false; onGet?.(dicts); }); }); $returns.loading ??= true; $returns.then = (on) => { $returns.$.then(() => { on?.($returns); }); }; } $returns.dicts = $returns; return $returns } function useDict(dictType, valueTypeOrOptions = 'string') { return _useDict(dictType, 'list', valueTypeOrOptions) } function useDictMap(dictType, valueTypeOrOptions = 'string') { return _useDict(dictType, 'map', valueTypeOrOptions) } function initDict({ clean, ...useDictInit }) { setInit(useDictInit); if (clean?.interval !== 0) { setInterval( () => { cleanUseless(clean?.delay); }, clean?.interval ?? 2 * 60 * 10_000, ); } return { install() { // nothing }, } } function getRouteKey(key = 'path', route) { if (key === false || key === 'route') { return route.name } if (key === 'path') { return route.path } if (key === 'path&query' || key === 'route&query') { let search = route.fullPath.split('?')[1] || ''; search = search.split('#')[0]; if (key === 'path&query') { return `${route.path}?${search}` } if (key === 'route&query') { return `${route.name}?${search}` } } return key } const preferredColor = usePreferredColorScheme(); function _isDark(mode) { return mode === 'dark' || (mode === 'auto' && preferredColor.value === 'dark') } function initThemeInfo({ persistExcludesKeys = [], version = 1, Error, router, ...theme }) { return defineStore(`core:theme`, { state: () => ({ multiTab: true, mode: theme.showAppearence ? 'auto' : 'light', menuPlacement: 'sidebar', collpaseBtnPlacement: 'sidebar', logoPlacement: 'sidebar', isFixedSidebar: true, topbarDark: false, sidebarDark: false, showI18n: true, showMessage: true, showChangePwd: true, showFullscreen: true, showAppearence: true, hasSidebar: true, hasTopbar: true, hasBreadcrumb: true, hasFooter: true, floatFooter: false, footer: undefined, fullTab: false, msgInterval: 10_000, ...theme, Error: markRaw(Error), pages: [], transition: Promise.resolve(), }), getters: { isDark(state) { return _isDark(state.mode)