vue-app-sdk
Version:
505 lines (504 loc) • 18.6 kB
JavaScript
"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;