UNPKG

vue-app-sdk

Version:
505 lines (504 loc) 18.6 kB
"use strict"; 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); Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const index = require("../../utils/index2.cjs"); const arrayToMap = require("../../vendors/nice-fns/dist/arrayToMap.cjs"); const castFunction = require("../../vendors/nice-fns/dist/castFunction.cjs"); require("../../vendors/nice-fns/dist/compactObject.cjs"); const eachTree = require("../../vendors/nice-fns/dist/eachTree.cjs"); const findTree = require("../../vendors/nice-fns/dist/findTree.cjs"); const mapTree = require("../../vendors/nice-fns/dist/mapTree.cjs"); const parseJSON = require("../../vendors/nice-fns/dist/parseJSON.cjs"); require("../../vendors/nice-fns/dist/pascalCase.cjs"); const toArrayTree = require("../../vendors/nice-fns/dist/toArrayTree.cjs"); const toTreeArray = require("../../vendors/nice-fns/dist/toTreeArray.cjs"); const vue = require("vue"); const vueRouter = require("vue-router"); const Animation = require("../Animation/Animation.cjs"); const cloneDeep = require("../../vendors/lodash-es/cloneDeep.cjs"); const isFunction = require("../../vendors/lodash-es/isFunction.cjs"); const isString = require("../../vendors/lodash-es/isString.cjs"); const memoize = require("../../vendors/lodash-es/memoize.cjs"); 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 index.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.default(data); let result = toTreeArray.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.default(pages); if (calibrate) pages = pages.map(this.calibrateMetadata); const result = toArrayTree.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.arrayToMap(pages, { primaryKey: "id", useMap: false }); const routes = vueRouter.createRouter({ history: vueRouter.createWebHistory("/"), routes: mapTree.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.default(fn, memoizeResolver); memoizedFns.push(memoized); return memoized; }; const getFlattenPages = memoizeFn(() => { if (vue.toValue(format) === "tree") { return this.toFlattenPages(vue.toValue(pages), { childrenKey, clone: true, calibrate: true, completePath: vue.toValue(completePath) }); } const data = cloneDeep.default(vue.toValue(pages)).map(this.calibrateMetadata); if (vue.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: vue.toValue(strict), clone: true, calibrate: false, completePath: false }); }); const getVisitableTreePages = memoizeFn(() => { if (!vue.toValue(childrenFirst)) { const visitablePages2 = this.filterVisitPages( getFlattenPages(), vue.toValue(roleList), vue.toValue(strictVisit) ); return this.toTreePages(visitablePages2, { strict: vue.toValue(strict), clone: true, calibrate: false, completePath: false }); } const tree = cloneDeep.default(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, vue.toValue(roleList), vue.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.findTree(children, canVisit); return !!find; }; return mapTree.mapTree(tree.filter(filterCallback), (item) => { if (item.children) { item.children = item.children.filter(filterCallback); if (item.children.length === 0 && vue.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.arrayToMap(getVisitablePages(), { primaryKey: "id", useMap: false }); }); const getVisitableTreeLinkMap = memoizeFn(() => { const map = {}; eachTree.eachTree( getVisitableTreePages(), (item, _, __, ___, links) => { map[item.id] = links; }, { childrenKey: defaultTreeOptions.childrenKey } ); return map; }); const getVisitableMenus = memoizeFn(() => this.toMenus(getVisitablePages(), vue.toValue(strict))); const flattenPages = vue.shallowRef([]); const treePages = vue.shallowRef([]); const visitablePages = vue.shallowRef([]); const visitableTreePages = vue.shallowRef([]); const visitableMenus = vue.shallowRef([]); const visitablePageMap = vue.shallowRef({}); const visitableTreeLinkMap = vue.shallowRef( {} ); const router = this._sdk.router; const activeMenu = vue.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 (vue.isRef(pages) || isFunction.default(pages)) { vue.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 = index.syncComponentName( component, syncName === true ? page.name : castFunction.castFunction(syncName)(page.name, page) ); } return { ...extraProps, path: page.path, name: page.name, component, // 设置重定向 redirect: page.redirect ? { name: page.redirect } : void 0, // 合并元数据 meta: index.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) index.assign(query, isString.default(page.routeQuery) ? parseJSON.parseJSON(page.routeQuery) : page.routeQuery); if (page.routeParams) index.assign(params, isString.default(page.routeParams) ? parseJSON.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) index.assign(query, isString.default(menu.routeQuery) ? parseJSON.parseJSON(menu.routeQuery) : menu.routeQuery); if (menu.routeParams) index.assign(params, isString.default(menu.routeParams) ? parseJSON.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 (vue.toValue(this.options.isMobile)) { const animation = getPlugin(Animation.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 index2 = _removeRouteFns.indexOf(wrapRemoveFn); if (index2 > -1) _removeRouteFns.splice(index2, 1); } return removeFn(); } _removeRouteFns.push(wrapRemoveFn); return wrapRemoveFn; }; return () => { _removeRouteFns.length = 0; }; } } exports.PAGE_ID = PAGE_ID; exports.Page = Page;