UNPKG

vue-app-sdk

Version:
505 lines (504 loc) 18.1 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { assign, syncComponentName } from "../../utils/index2.js"; import { arrayToMap } from "../../vendors/nice-fns/dist/arrayToMap.js"; import { castFunction } from "../../vendors/nice-fns/dist/castFunction.js"; import "../../vendors/nice-fns/dist/compactObject.js"; import { eachTree } from "../../vendors/nice-fns/dist/eachTree.js"; import { findTree } from "../../vendors/nice-fns/dist/findTree.js"; import { mapTree } from "../../vendors/nice-fns/dist/mapTree.js"; import { parseJSON } from "../../vendors/nice-fns/dist/parseJSON.js"; import "../../vendors/nice-fns/dist/pascalCase.js"; import { toArrayTree } from "../../vendors/nice-fns/dist/toArrayTree.js"; import { toTreeArray } from "../../vendors/nice-fns/dist/toTreeArray.js"; import { shallowRef, computed, isRef, watch, toValue } from "vue"; import { createRouter, createWebHistory } from "vue-router"; import { ANIMATION_ID } from "../Animation/Animation.js"; import cloneDeep from "../../vendors/lodash-es/cloneDeep.js"; import isFunction from "../../vendors/lodash-es/isFunction.js"; import isString from "../../vendors/lodash-es/isString.js"; import memoize from "../../vendors/lodash-es/memoize.js"; const PAGE_ID = Symbol("page"); const RESET_FLAG = Symbol("reset flag"); const defaultCalibrateMetadata = (data) => data; const defaultAllowLink = (menu) => !!menu.link; const defaultTreeOptions = { key: "id", parentKey: "parentId", childrenKey: "children" }; class Page { constructor(options) { __publicField(this, "id", PAGE_ID); /** * AppSDK 实例 */ __publicField(this, "_sdk"); /** * 移除路由函数列表 */ __publicField(this, "_removeRouteFns", []); /** * 已打开的窗口实例 */ __publicField(this, "openedWindow", null); /** * 重置路由列表为初始创建路由器时传入的列表 */ __publicField(this, "resetRoutes", () => { this._removeRouteFns.splice(0).forEach((fn) => fn(RESET_FLAG)); }); /** * 获取路由上的页面元数据(浅拷贝) * @param data `route` 或 `route.meta` */ __publicField(this, "getPageMetadata", (data) => { const meta = data.meta || data || {}; return assign({}, meta._metadata); }); /** * 将树形元数据列表扁平化为符合 `PageMetadata` 的页面元数据列表 * @param data 树形元数据列表 * @param options 配置项 * @returns 扁平化的页面元数据列表 */ __publicField(this, "toFlattenPages", (data, options = {}) => { const { childrenKey = defaultTreeOptions.childrenKey, clone, calibrate, completePath } = options; if (clone) data = cloneDeep(data); let result = toTreeArray(data, { childrenKey, dropKeys: [childrenKey] }); if (calibrate) result = result.map(this.calibrateMetadata); if (completePath) { this.toTreePages(result, { strict: false, clone: false, calibrate: false, completePath: true }); result.forEach((item) => { delete item.children; }); } return result; }); /** * 将元数据列表转为符合 `PageMetaWithChildren` 的树形页面元数据列表 * @param data 元数据列表 * @param options 配置项 * @param options.strict 严格模式,开启后将移除父子关联不存在的数据 * @param options.clone 是否克隆源数据,避免转换时影响到源数据 * @param options.calibrate 是否校准元数据,数据格式不符合 `PageMetadata` 时可以设为 `true` * @param options.completePath 是否补全路径,设为 `true` 后会对 `path` 进行拼接补全,可以在 `addRoute()` 时不受父子级限制 * @returns 树形页面元数据列表 */ __publicField(this, "toTreePages", (data, options = {}) => { const { strict = false, clone, calibrate, completePath } = options; let pages = data; if (clone) pages = cloneDeep(pages); if (calibrate) pages = pages.map(this.calibrateMetadata); const result = toArrayTree(pages, { strict, orderBy: ["index", "asc"], removeEmptyChildrenKey: true, ...defaultTreeOptions }); if (completePath) { result.forEach((item) => { if (!item.path.startsWith("/")) item.path = `/${item.path}`; }); const map = arrayToMap(pages, { primaryKey: "id", useMap: false }); const routes = createRouter({ history: createWebHistory("/"), routes: mapTree(result, (item) => { return { name: item.name, path: item.path, children: item.children, meta: { _metadata: item } }; }) }).getRoutes(); routes.forEach((route) => { map[this.getPageMetadata(route).id].path = route.path; }); } return result; }); /** * 父子关联页面元数据列表转为树形菜单列表,返回深克隆数据,与源数据不共用内存引用 * @param pages 页面元数据列表 * @param strict 严格模式 * @returns 树形菜单元数据列表 */ __publicField(this, "toMenus", (pages, strict) => { return this.toTreePages( pages.filter((item) => item.isMenu), { strict, clone: true, calibrate: false, completePath: false } ); }); /** * 指定角色列表是否可以访问页面 * @param page 页面元数据 * @param roleList 角色列表 * @param strict 严格模式 * @returns 是否可以访问页面 */ __publicField(this, "canVisitPage", (page, roleList, strict) => { if (roleList === "*") return true; const pageRoleList = page.roleList || []; if (strict && (roleList.length === 0 || pageRoleList.length === 0)) return false; return pageRoleList === "*" || pageRoleList.some((role) => roleList.includes(role)); }); /** * 过滤指定角色可以访问的页面元数据列表 * @param pages 页面元数据列表 * @param roleList 角色列表 * @param strict 严格模式 * @returns 可以访问的页面元数据列表 */ __publicField(this, "filterVisitPages", (pages, roleList, strict) => { if (roleList === "*") return pages; if (strict && roleList.length === 0) return []; return pages.filter((item) => this.canVisitPage(item, roleList, strict)); }); /** * 创建不同场景下的页面元数据状态 * @param pages 页面元数据列表 * @param options 配置项 */ __publicField(this, "createStates", (pages, options) => { const { format, childrenKey = defaultTreeOptions.childrenKey, strict = false, roleList = [], strictVisit = true, childrenFirst = true, completePath = true, resolveActiveMenu = (route) => { const metadata = this.getPageMetadata(route); return metadata.activeMenu || metadata.name || ""; } } = options; const memoizeResolver = () => memoizeResolver.id; memoizeResolver.id = -1; const memoizedFns = []; const memoizeFn = (fn) => { const memoized = memoize(fn, memoizeResolver); memoizedFns.push(memoized); return memoized; }; const getFlattenPages = memoizeFn(() => { if (toValue(format) === "tree") { return this.toFlattenPages(toValue(pages), { childrenKey, clone: true, calibrate: true, completePath: toValue(completePath) }); } const data = cloneDeep(toValue(pages)).map(this.calibrateMetadata); if (toValue(completePath)) { this.toTreePages(data, { strict: false, clone: false, calibrate: false, completePath: true }); data.forEach((item) => { delete item.children; }); } return data; }); const getTreePages = memoizeFn(() => { return this.toTreePages(getFlattenPages(), { strict: toValue(strict), clone: true, calibrate: false, completePath: false }); }); const getVisitableTreePages = memoizeFn(() => { if (!toValue(childrenFirst)) { const visitablePages2 = this.filterVisitPages( getFlattenPages(), toValue(roleList), toValue(strictVisit) ); return this.toTreePages(visitablePages2, { strict: toValue(strict), clone: true, calibrate: false, completePath: false }); } const tree = cloneDeep(getTreePages()); const visitCache = /* @__PURE__ */ new Map(); const canVisit = (item) => { const { id } = item; const cacheValue = visitCache.get(id); if (cacheValue != null) return cacheValue; const valid = this.canVisitPage(item, toValue(roleList), toValue(strictVisit)); visitCache.set(id, valid); return valid; }; const filterCallback = (item) => { const children = item.children || []; if (children.length === 0) return canVisit(item); const find = findTree(children, canVisit); return !!find; }; return mapTree(tree.filter(filterCallback), (item) => { if (item.children) { item.children = item.children.filter(filterCallback); if (item.children.length === 0 && toValue(strict)) delete item.children; } return item; }); }); const getVisitablePages = memoizeFn(() => { return this.toFlattenPages(getVisitableTreePages(), { childrenKey: defaultTreeOptions.childrenKey, clone: true, calibrate: false, completePath: false }); }); const getVisitablePageMap = memoizeFn(() => { return arrayToMap(getVisitablePages(), { primaryKey: "id", useMap: false }); }); const getVisitableTreeLinkMap = memoizeFn(() => { const map = {}; eachTree( getVisitableTreePages(), (item, _, __, ___, links) => { map[item.id] = links; }, { childrenKey: defaultTreeOptions.childrenKey } ); return map; }); const getVisitableMenus = memoizeFn(() => this.toMenus(getVisitablePages(), toValue(strict))); const flattenPages = shallowRef([]); const treePages = shallowRef([]); const visitablePages = shallowRef([]); const visitableTreePages = shallowRef([]); const visitableMenus = shallowRef([]); const visitablePageMap = shallowRef({}); const visitableTreeLinkMap = shallowRef( {} ); const router = this._sdk.router; const activeMenu = computed(() => { const route = router.currentRoute.value; return resolveActiveMenu(route, { flattenPages: flattenPages.value, treePages: treePages.value, visitablePages: visitablePages.value, visitableTreePages: visitableTreePages.value, visitableMenus: visitableMenus.value, visitablePageMap: visitablePageMap.value, visitableTreeLinkMap: visitableTreeLinkMap.value }); }); if (isRef(pages) || isFunction(pages)) { watch(pages, update, { immediate: true, flush: "sync", deep: true }); } else { update(); } function update() { memoizedFns.forEach((fn) => fn.cache.delete(memoizeResolver.id++)); flattenPages.value = getFlattenPages(); treePages.value = getTreePages(); visitablePages.value = getVisitablePages(); visitableTreePages.value = getVisitableTreePages(); visitableMenus.value = getVisitableMenus(); visitablePageMap.value = getVisitablePageMap(); visitableTreeLinkMap.value = getVisitableTreeLinkMap(); } return { /** * 扁平化的页面元数据列表 */ flattenPages, /** * 树形页面元数据列表 */ treePages, /** * 可访问的树形页面元数据列表 */ visitableTreePages, /** * 可访问的页面元数据列表 */ visitablePages, /** * 可访问的页面元数据映射 */ visitablePageMap, /** * 可访问的树形页面元数据链路映射 */ visitableTreeLinkMap, /** * 可访问的菜单页面元数据列表 */ visitableMenus, /** * 激活的菜单页面标识 */ activeMenu }; }); /** * 页面元数据转为路由配置 * @param page 页面元数据 * @param extraProps 路由额外参数 * @param syncName 同步组件 `name` 为路由 `name`,可以更好的配合 `KeepAlive` 组件缓存 * @returns 路由配置 */ __publicField(this, "pageToRoute", (page, extraProps = {}, syncName = true) => { let component = page.file ? this.options.resolveComponent(page.file, page) : void 0; if (component && syncName) { component = syncComponentName( component, syncName === true ? page.name : castFunction(syncName)(page.name, page) ); } return { ...extraProps, path: page.path, name: page.name, component, // 设置重定向 redirect: page.redirect ? { name: page.redirect } : void 0, // 合并元数据 meta: assign({}, extraProps.meta, { _metadata: page }) }; }); /** * 获取页面元数据设置的外链地址 * @param page 页面元数据 * @param selfLink 获取自身的外链地址(等同于 `page.link=true`) * @returns 外链地址 * * @example * ```ts * resolveLink({ path: '/test', name: 'test', link: true }) * // => '/test' * * resolveLink({ path: '/test', name: 'test', link: '//www.test.com' }) * // => '//www.test.com' * * resolveLink({ path: '/test', name: 'test' }) * // => '' * * resolveLink({ path: '/test/:id', name: 'test', routeParams: { id: 1 } }, true) * // => '/test/1' * ``` */ __publicField(this, "resolveLink", (page, selfLink = false) => { if (page.link === true || selfLink) { const { router } = this._sdk; const query = {}; const params = {}; if (page.routeQuery) assign(query, isString(page.routeQuery) ? parseJSON(page.routeQuery) : page.routeQuery); if (page.routeParams) assign(params, isString(page.routeParams) ? parseJSON(page.routeParams) : page.routeParams); return router.resolve({ name: page.name, query, params }).href; } return page.link || ""; }); /** * 处理菜单点击,设置 `link` 时将会在客户端新标签页打开指定路由页或网站, * 在 `options.isMobile` 设为 `true` 时将禁用动画插件(若存在)的导航动画并使用 `router.replace` 跳转 * @param menu 页面元数据 * @param singleWindow 跳转外链时若存在已打开的外链窗口是否重复使用进行跳转 */ __publicField(this, "handleMenuClick", (menu, singleWindow = this.options.linkSingleWindow) => { const { router, getPlugin } = this._sdk; const query = {}; const params = {}; if (menu.routeQuery) assign(query, isString(menu.routeQuery) ? parseJSON(menu.routeQuery) : menu.routeQuery); if (menu.routeParams) assign(params, isString(menu.routeParams) ? parseJSON(menu.routeParams) : menu.routeParams); const to = { name: menu.name, query, params }; if (this.allowLink(menu)) { const hasOpened = this.openedWindow && !this.openedWindow.closed; const win = singleWindow && hasOpened ? this.openedWindow : window; const url = this.resolveLink(menu); if (singleWindow && this.openedWindow && !this.openedWindow.closed) this.openedWindow.location.replace(url); else this.openedWindow = win.open(url, "_blank"); return; } if (menu.file || menu.redirect) { if (toValue(this.options.isMobile)) { const animation = getPlugin(ANIMATION_ID); if (animation) animation.disable(); router.replace(to); } else { router.push(to); } } }); this.options = options; } /** * 校准页面元数据 * @readonly */ get calibrateMetadata() { return this.options.calibrateMetadata || defaultCalibrateMetadata; } /** * 是否允许跳转外链 * @readonly */ get allowLink() { return this.options.allowLink || defaultAllowLink; } install(sdk) { this._sdk = sdk; const { router } = sdk; const { _removeRouteFns } = this; const { addRoute } = router; router.addRoute = function(...args) { const removeFn = addRoute.apply(this, args); function wrapRemoveFn(flag) { if (flag !== RESET_FLAG) { const index = _removeRouteFns.indexOf(wrapRemoveFn); if (index > -1) _removeRouteFns.splice(index, 1); } return removeFn(); } _removeRouteFns.push(wrapRemoveFn); return wrapRemoveFn; }; return () => { _removeRouteFns.length = 0; }; } } export { PAGE_ID, Page };