feat(product): 新增产品管理模块与字典组件功能
- 新增产品管理相关路由和页面(dashboard、list、requirement、setting) - 实现产品基础信息编辑弹窗组件(base-info-dialog.vue) - 添加运行时字典功能(dict-select、dict-text、dict-tag组件) - 集成字典管理store和API调用 - 规范ID类型定义为string避免精度丢失问题 - 完善国际化资源文件支持中英文对照 - 新增对象上下文业务域入口页导航实现说明 - 添加Vue DevTools浮动入口注释说明 - 统一权限控制支持全局和对象作用域区分 - 规范分页查询参数类型定义与使用方式
This commit is contained in:
@@ -2,8 +2,8 @@ import { effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
|
||||
import { breakpointsTailwind, useBreakpoints, useEventListener, useTitle } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import { router } from '@/router';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { getGlobalRouter } from '@/router/instance';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { $t, setLocale } from '@/locales';
|
||||
import { setDayjsLocale } from '@/locales/dayjs';
|
||||
@@ -63,7 +63,7 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
|
||||
|
||||
/** Update document title by locale */
|
||||
function updateDocumentTitleByLocale() {
|
||||
const { i18nKey, title } = router.currentRoute.value.meta;
|
||||
const { i18nKey, title } = getGlobalRouter().currentRoute.value.meta;
|
||||
|
||||
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import { useRouterPush } from '@/hooks/common/router';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { $t } from '@/locales';
|
||||
import { useDictStore } from '../dict';
|
||||
import { useRouteStore } from '../route';
|
||||
import { useTabStore } from '../tab';
|
||||
import { useObjectContextStore } from '../object-context';
|
||||
import { clearAuthStorage, getToken } from './shared';
|
||||
|
||||
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
@@ -16,6 +18,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const authStore = useAuthStore();
|
||||
const routeStore = useRouteStore();
|
||||
const tabStore = useTabStore();
|
||||
const dictStore = useDictStore();
|
||||
const objectContextStore = useObjectContextStore();
|
||||
const { toLogin, redirectFromLogin } = useRouterPush(false);
|
||||
const { loading: loginLoading, startLoading, endLoading } = useLoading();
|
||||
|
||||
@@ -46,6 +50,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
clearAuthStorage();
|
||||
|
||||
authStore.$reset();
|
||||
dictStore.resetDictCache();
|
||||
objectContextStore.$reset();
|
||||
|
||||
if (!route.meta.constant) {
|
||||
await toLogin();
|
||||
@@ -138,6 +144,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const pass = await getUserInfo();
|
||||
|
||||
if (pass) {
|
||||
await dictStore.initDictCache(true);
|
||||
|
||||
token.value = loginToken.token;
|
||||
|
||||
return true;
|
||||
|
||||
210
src/store/modules/dict/index.ts
Normal file
210
src/store/modules/dict/index.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchGetFrontendDictCache } from '@/service/api';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
|
||||
type DictValue = string | number | null | undefined;
|
||||
type DictFilterOptions = {
|
||||
onlyEnabled?: boolean;
|
||||
};
|
||||
type DictLabelOptions = DictFilterOptions & {
|
||||
fallback?: string;
|
||||
};
|
||||
type DictLabelsOptions = DictLabelOptions & {
|
||||
separator?: string;
|
||||
};
|
||||
|
||||
function sortDictData(list: Api.Dict.DictData[]) {
|
||||
return list.slice().sort((left, right) => left.sort - right.sort || left.label.localeCompare(right.label, 'zh-CN'));
|
||||
}
|
||||
|
||||
function normalizeFrontendDictData(
|
||||
dictType: string,
|
||||
list: Api.Dict.FrontendDictData[],
|
||||
dictIndex: number
|
||||
): Api.Dict.DictData[] {
|
||||
const normalizedList = list.map((item, itemIndex) => ({
|
||||
id: -((dictIndex + 1) * 100000 + itemIndex + 1),
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
dictType: item.dictType || dictType,
|
||||
sort: item.sort,
|
||||
status: item.status ?? 0,
|
||||
remark: null,
|
||||
createTime: 0
|
||||
}));
|
||||
|
||||
return sortDictData(normalizedList);
|
||||
}
|
||||
|
||||
function normalizeFrontendDictCache(cache: Api.Dict.FrontendDictCache) {
|
||||
const entries = Object.entries(cache);
|
||||
|
||||
return Object.fromEntries(
|
||||
entries.map(([dictType, list], index) => [dictType, normalizeFrontendDictData(dictType, list, index)])
|
||||
);
|
||||
}
|
||||
|
||||
function applyDictTypeAliases(dictDataMap: Record<string, Api.Dict.DictData[]>) {
|
||||
const nextDictDataMap = { ...dictDataMap };
|
||||
|
||||
// 兼容后端尚未切换完成的过渡期:旧编码仍返回时,前端统一映射到新编码。
|
||||
if (!nextDictDataMap[RDMS_OBJECT_DIRECTION_DICT_CODE] && nextDictDataMap[RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE]) {
|
||||
nextDictDataMap[RDMS_OBJECT_DIRECTION_DICT_CODE] = nextDictDataMap[RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE].map(
|
||||
item => ({
|
||||
...item,
|
||||
dictType: RDMS_OBJECT_DIRECTION_DICT_CODE
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return nextDictDataMap;
|
||||
}
|
||||
|
||||
function createRuntimeDictTypes(dictDataMap: Record<string, Api.Dict.DictData[]>) {
|
||||
return Object.keys(dictDataMap).map((dictType, index) => ({
|
||||
id: -(index + 1),
|
||||
name: dictType,
|
||||
type: dictType,
|
||||
status: 0 as const,
|
||||
remark: null,
|
||||
createTime: 0
|
||||
}));
|
||||
}
|
||||
|
||||
function findDictItem(list: Api.Dict.DictData[], value?: DictValue) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return list.find(item => item.value === String(value));
|
||||
}
|
||||
|
||||
export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const dictTypes = ref<Api.Dict.DictType[]>([]);
|
||||
const dictDataMap = ref<Record<string, Api.Dict.DictData[]>>({});
|
||||
const loadedAt = ref<number | null>(null);
|
||||
|
||||
let initPromise: Promise<boolean> | null = null;
|
||||
|
||||
function resetDictCache() {
|
||||
dictTypes.value = [];
|
||||
dictDataMap.value = {};
|
||||
loadedAt.value = null;
|
||||
initialized.value = false;
|
||||
initPromise = null;
|
||||
}
|
||||
|
||||
async function initDictCache(force = false) {
|
||||
if (initialized.value && !force) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (initPromise && !force) {
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
resetDictCache();
|
||||
}
|
||||
|
||||
initPromise = (async () => {
|
||||
loading.value = true;
|
||||
|
||||
const result = await fetchGetFrontendDictCache();
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (result.error) {
|
||||
initPromise = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedDictDataMap = applyDictTypeAliases(normalizeFrontendDictCache(result.data || {}));
|
||||
|
||||
dictTypes.value = createRuntimeDictTypes(normalizedDictDataMap);
|
||||
dictDataMap.value = normalizedDictDataMap;
|
||||
loadedAt.value = Date.now();
|
||||
initialized.value = true;
|
||||
initPromise = null;
|
||||
|
||||
return true;
|
||||
})();
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
function getDictData(dictType: string, onlyEnabled = false) {
|
||||
if (!dictType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const list = dictDataMap.value[dictType] || [];
|
||||
|
||||
if (!onlyEnabled) {
|
||||
return list;
|
||||
}
|
||||
|
||||
return list.filter(item => item.status === 0);
|
||||
}
|
||||
|
||||
function getDictOptions(dictType: string, onlyEnabled = true) {
|
||||
return getDictData(dictType, onlyEnabled).map(item => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
}));
|
||||
}
|
||||
|
||||
function getDictItem(dictType: string, value?: DictValue, options: DictFilterOptions = {}) {
|
||||
return findDictItem(getDictData(dictType, options.onlyEnabled), value);
|
||||
}
|
||||
|
||||
function getDictLabel(dictType: string, value?: DictValue, options: DictLabelOptions = {}) {
|
||||
const { fallback = '--', onlyEnabled = false } = options;
|
||||
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const matched = getDictItem(dictType, value, { onlyEnabled });
|
||||
|
||||
return matched?.label || String(value);
|
||||
}
|
||||
|
||||
function getDictLabels(dictType: string, values?: Array<DictValue> | null, options: DictLabelsOptions = {}) {
|
||||
const { fallback = '--', separator = ' / ', onlyEnabled = false } = options;
|
||||
|
||||
if (!values?.length) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const labels = values
|
||||
.filter(value => value !== null && value !== undefined && value !== '')
|
||||
.map(value => getDictLabel(dictType, value, { fallback: String(value), onlyEnabled }));
|
||||
|
||||
return labels.length ? labels.join(separator) : fallback;
|
||||
}
|
||||
|
||||
function hasDictValue(dictType: string, value?: DictValue, options: DictFilterOptions = {}) {
|
||||
return Boolean(getDictItem(dictType, value, options));
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
initialized,
|
||||
dictTypes,
|
||||
dictDataMap,
|
||||
loadedAt,
|
||||
initDictCache,
|
||||
resetDictCache,
|
||||
getDictData,
|
||||
getDictOptions,
|
||||
getDictItem,
|
||||
getDictLabel,
|
||||
getDictLabels,
|
||||
hasDictValue
|
||||
};
|
||||
});
|
||||
403
src/store/modules/object-context/index.ts
Normal file
403
src/store/modules/object-context/index.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import type { LocationQueryRaw, RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
|
||||
import { defineStore } from 'pinia';
|
||||
import type { ElegantConstRoute } from '@elegant-router/types';
|
||||
import {
|
||||
OBJECT_CONTEXT_QUERY_KEY,
|
||||
getObjectContextDomainConfigByPath,
|
||||
isObjectContextEntryPath
|
||||
} from '@/constants/object-context';
|
||||
import { fetchGetObjectContext } from '@/service/api/object-context';
|
||||
import { $t } from '@/locales';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { useRouteStore } from '../route';
|
||||
|
||||
function createEmptyState(): App.ObjectContext.State {
|
||||
return {
|
||||
domainKey: '',
|
||||
objectType: '',
|
||||
objectId: '',
|
||||
objectName: '',
|
||||
objectSummary: null,
|
||||
contextScopedMenus: [],
|
||||
buttonCodes: [],
|
||||
defaultRouteKey: '',
|
||||
defaultRoutePath: '',
|
||||
isReady: false
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePath(path: string) {
|
||||
if (!path) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
|
||||
}
|
||||
|
||||
function isRouteMatchedByPrefix(path: string, prefix: string) {
|
||||
const normalizedPath = normalizePath(path);
|
||||
const normalizedPrefix = normalizePath(prefix);
|
||||
|
||||
return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}/`);
|
||||
}
|
||||
|
||||
function findDomainRootRoute(
|
||||
routes: ElegantConstRoute[],
|
||||
config: App.ObjectContext.DomainConfig
|
||||
): ElegantConstRoute | null {
|
||||
for (const route of routes) {
|
||||
if (config.routePathPrefixes.some(prefix => isRouteMatchedByPrefix(route.path, prefix))) {
|
||||
return route;
|
||||
}
|
||||
|
||||
if (route.children?.length) {
|
||||
const matchedChild = findDomainRootRoute(route.children, config);
|
||||
|
||||
if (matchedChild) {
|
||||
return matchedChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isEntryRoute(route: ElegantConstRoute, config: App.ObjectContext.DomainConfig) {
|
||||
return route.name === config.entryRouteKey || normalizePath(route.path) === normalizePath(config.entryRoutePath);
|
||||
}
|
||||
|
||||
function getContextMenuLabel(route: ElegantConstRoute) {
|
||||
const routeName = String(route.name || route.path);
|
||||
|
||||
return route.meta?.i18nKey ? $t(route.meta.i18nKey) : String(route.meta?.title || routeName);
|
||||
}
|
||||
|
||||
type ContextRouteLookupItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
routeKey: string | null;
|
||||
routePath: string | null;
|
||||
};
|
||||
|
||||
function createContextRouteLookup(
|
||||
routes: ElegantConstRoute[],
|
||||
lookup = new Map<string, ContextRouteLookupItem>()
|
||||
): Map<string, ContextRouteLookupItem> {
|
||||
routes.forEach(route => {
|
||||
const routeName = route.name ? String(route.name) : '';
|
||||
const routePath = route.path ? String(route.path) : '';
|
||||
const item: ContextRouteLookupItem = {
|
||||
key: routeName || routePath,
|
||||
label: getContextMenuLabel(route),
|
||||
routeKey: routeName || null,
|
||||
routePath: routePath || null
|
||||
};
|
||||
|
||||
if (routeName) {
|
||||
lookup.set(routeName, item);
|
||||
}
|
||||
|
||||
if (routePath) {
|
||||
lookup.set(routePath, item);
|
||||
}
|
||||
|
||||
route.children?.forEach(child => {
|
||||
createContextRouteLookup([child], lookup);
|
||||
});
|
||||
});
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function enrichContextMenu(
|
||||
menu: App.ObjectContext.Menu,
|
||||
routeLookup: Map<string, ContextRouteLookupItem>
|
||||
): App.ObjectContext.Menu {
|
||||
const matchedRoute =
|
||||
routeLookup.get(String(menu.routeKey || '')) ||
|
||||
routeLookup.get(String(menu.routePath || '')) ||
|
||||
routeLookup.get(menu.key);
|
||||
|
||||
return {
|
||||
key: matchedRoute?.key || menu.key,
|
||||
label: menu.label || matchedRoute?.label || menu.key,
|
||||
routeKey: menu.routeKey || matchedRoute?.routeKey || null,
|
||||
routePath: menu.routePath || matchedRoute?.routePath || null,
|
||||
children: menu.children?.map(child => enrichContextMenu(child, routeLookup)) || []
|
||||
};
|
||||
}
|
||||
|
||||
function getLeafRoutes(routes: ElegantConstRoute[]): ElegantConstRoute[] {
|
||||
return routes.flatMap(route => {
|
||||
if (route.children?.length) {
|
||||
return getLeafRoutes(route.children);
|
||||
}
|
||||
|
||||
return [route];
|
||||
});
|
||||
}
|
||||
|
||||
export const useObjectContextStore = defineStore(SetupStoreId.ObjectContext, () => {
|
||||
const routeStore = useRouteStore();
|
||||
const domainKey = ref<App.ObjectContext.DomainKey>('');
|
||||
const objectType = ref<App.ObjectContext.ObjectType>('');
|
||||
const objectId = ref('');
|
||||
const objectName = ref('');
|
||||
const objectSummary = ref<App.ObjectContext.Summary | null>(null);
|
||||
const contextScopedMenus = ref<App.ObjectContext.Menu[]>([]);
|
||||
const buttonCodes = ref<string[]>([]);
|
||||
const defaultRouteKey = ref('');
|
||||
const defaultRoutePath = ref('');
|
||||
const isReady = ref(false);
|
||||
|
||||
const hasContext = computed(() => isReady.value && Boolean(domainKey.value) && Boolean(objectId.value));
|
||||
|
||||
function patchState(state: App.ObjectContext.State) {
|
||||
domainKey.value = state.domainKey;
|
||||
objectType.value = state.objectType;
|
||||
objectId.value = state.objectId;
|
||||
objectName.value = state.objectName;
|
||||
objectSummary.value = state.objectSummary;
|
||||
contextScopedMenus.value = state.contextScopedMenus;
|
||||
buttonCodes.value = state.buttonCodes;
|
||||
defaultRouteKey.value = state.defaultRouteKey;
|
||||
defaultRoutePath.value = state.defaultRoutePath;
|
||||
isReady.value = state.isReady;
|
||||
}
|
||||
|
||||
function clearContext() {
|
||||
patchState(createEmptyState());
|
||||
}
|
||||
|
||||
function resolveDefaultRoute(
|
||||
config: App.ObjectContext.DomainConfig,
|
||||
domainRoutes: ElegantConstRoute[],
|
||||
context: Api.ObjectContext.ContextInfo
|
||||
) {
|
||||
const leafRoutes = getLeafRoutes(domainRoutes);
|
||||
const defaultRouteByKey = (routeKey?: string | null) => leafRoutes.find(route => route.name === routeKey);
|
||||
const defaultRouteByPath = (routePath?: string | null) =>
|
||||
leafRoutes.find(route => normalizePath(route.path) === normalizePath(routePath || ''));
|
||||
|
||||
const matchedContextByKey = defaultRouteByKey(context.defaultRouteKey);
|
||||
|
||||
if (matchedContextByKey?.name && matchedContextByKey.path) {
|
||||
return {
|
||||
defaultRouteKey: String(matchedContextByKey.name),
|
||||
defaultRoutePath: matchedContextByKey.path
|
||||
};
|
||||
}
|
||||
|
||||
const matchedContextByPath = defaultRouteByPath(context.defaultRoutePath);
|
||||
|
||||
if (matchedContextByPath?.name && matchedContextByPath.path) {
|
||||
return {
|
||||
defaultRouteKey: String(matchedContextByPath.name),
|
||||
defaultRoutePath: matchedContextByPath.path
|
||||
};
|
||||
}
|
||||
|
||||
const matchedFallbackByKey = defaultRouteByKey(config.fallbackDefaultRouteKey);
|
||||
|
||||
if (matchedFallbackByKey?.name && matchedFallbackByKey.path) {
|
||||
return {
|
||||
defaultRouteKey: String(matchedFallbackByKey.name),
|
||||
defaultRoutePath: matchedFallbackByKey.path
|
||||
};
|
||||
}
|
||||
|
||||
const matchedFallbackByPath = defaultRouteByPath(config.fallbackDefaultRoutePath);
|
||||
|
||||
if (matchedFallbackByPath?.name && matchedFallbackByPath.path) {
|
||||
return {
|
||||
defaultRouteKey: String(matchedFallbackByPath.name),
|
||||
defaultRoutePath: matchedFallbackByPath.path
|
||||
};
|
||||
}
|
||||
|
||||
const [firstLeafRoute] = leafRoutes;
|
||||
|
||||
if (firstLeafRoute?.name && firstLeafRoute.path) {
|
||||
return {
|
||||
defaultRouteKey: String(firstLeafRoute.name),
|
||||
defaultRoutePath: firstLeafRoute.path
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
defaultRouteKey: context.defaultRouteKey || config.fallbackDefaultRouteKey,
|
||||
defaultRoutePath: context.defaultRoutePath || config.fallbackDefaultRoutePath
|
||||
};
|
||||
}
|
||||
|
||||
function applyContext(config: App.ObjectContext.DomainConfig, context: Api.ObjectContext.ContextInfo) {
|
||||
const domainRootRoute = findDomainRootRoute(routeStore.authRoutes, config);
|
||||
const domainRoutes: ElegantConstRoute[] =
|
||||
domainRootRoute?.children?.filter((route: ElegantConstRoute) => !isEntryRoute(route, config)) || [];
|
||||
const routeLookup = createContextRouteLookup(domainRoutes);
|
||||
// 对象上下文菜单以接口返回为准,前端只补全跳转所需的本地路由信息。
|
||||
const contextMenus = context.contextScopedMenus.map(menu => enrichContextMenu(menu, routeLookup));
|
||||
const resolvedDefaultRoute = resolveDefaultRoute(config, domainRoutes, context);
|
||||
|
||||
patchState({
|
||||
...context,
|
||||
contextScopedMenus: contextMenus,
|
||||
defaultRouteKey: resolvedDefaultRoute.defaultRouteKey,
|
||||
defaultRoutePath: resolvedDefaultRoute.defaultRoutePath,
|
||||
isReady: true
|
||||
});
|
||||
}
|
||||
|
||||
function getObjectIdFromRoute(route: Pick<RouteLocationNormalized, 'query'>) {
|
||||
const routeObjectId = route.query?.[OBJECT_CONTEXT_QUERY_KEY];
|
||||
|
||||
if (Array.isArray(routeObjectId)) {
|
||||
return String(routeObjectId[0] || '');
|
||||
}
|
||||
|
||||
if (routeObjectId === null || routeObjectId === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(routeObjectId);
|
||||
}
|
||||
|
||||
function getContextQuery(targetObjectId = objectId.value): LocationQueryRaw {
|
||||
if (!targetObjectId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
[OBJECT_CONTEXT_QUERY_KEY]: targetObjectId
|
||||
};
|
||||
}
|
||||
|
||||
function createEntryLocation(config: App.ObjectContext.DomainConfig): RouteLocationRaw {
|
||||
return {
|
||||
path: config.entryRoutePath
|
||||
};
|
||||
}
|
||||
|
||||
function createDefaultLocation(config: App.ObjectContext.DomainConfig, targetObjectId: string): RouteLocationRaw {
|
||||
const query = getContextQuery(targetObjectId);
|
||||
|
||||
if (defaultRouteKey.value) {
|
||||
return {
|
||||
name: defaultRouteKey.value,
|
||||
query
|
||||
};
|
||||
}
|
||||
|
||||
if (defaultRoutePath.value) {
|
||||
return {
|
||||
path: defaultRoutePath.value,
|
||||
query
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
path: config.fallbackDefaultRoutePath,
|
||||
query
|
||||
};
|
||||
}
|
||||
|
||||
async function enterContext(config: App.ObjectContext.DomainConfig, targetObjectId: string) {
|
||||
const result = await fetchGetObjectContext(config, targetObjectId);
|
||||
|
||||
if (!result.error && result.data) {
|
||||
applyContext(config, result.data);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function switchContext(config: App.ObjectContext.DomainConfig, targetObjectId: string) {
|
||||
return enterContext(config, targetObjectId);
|
||||
}
|
||||
|
||||
async function ensureContextByRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw | null> {
|
||||
const domainConfig = getObjectContextDomainConfigByPath(to.path);
|
||||
|
||||
if (!domainConfig) {
|
||||
if (hasContext.value) {
|
||||
clearContext();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const routeObjectId = getObjectIdFromRoute(to);
|
||||
|
||||
if (!routeObjectId) {
|
||||
clearContext();
|
||||
|
||||
if (isObjectContextEntryPath(to.path, domainConfig)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createEntryLocation(domainConfig);
|
||||
}
|
||||
|
||||
const isSameContext =
|
||||
hasContext.value && domainKey.value === domainConfig.domainKey && objectId.value === routeObjectId;
|
||||
|
||||
if (!isSameContext) {
|
||||
const { error } = await enterContext(domainConfig, routeObjectId);
|
||||
|
||||
if (error) {
|
||||
clearContext();
|
||||
return createEntryLocation(domainConfig);
|
||||
}
|
||||
}
|
||||
|
||||
if (isObjectContextEntryPath(to.path, domainConfig)) {
|
||||
return createDefaultLocation(domainConfig, routeObjectId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMenuRouteLocation(
|
||||
menu: App.ObjectContext.Menu,
|
||||
targetObjectId = objectId.value
|
||||
): RouteLocationRaw | null {
|
||||
const query = getContextQuery(targetObjectId);
|
||||
|
||||
if (menu.routeKey) {
|
||||
return {
|
||||
name: menu.routeKey,
|
||||
query
|
||||
};
|
||||
}
|
||||
|
||||
if (menu.routePath) {
|
||||
return {
|
||||
path: menu.routePath,
|
||||
query
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
domainKey,
|
||||
objectType,
|
||||
objectId,
|
||||
objectName,
|
||||
objectSummary,
|
||||
contextScopedMenus,
|
||||
buttonCodes,
|
||||
defaultRouteKey,
|
||||
defaultRoutePath,
|
||||
isReady,
|
||||
hasContext,
|
||||
clearContext,
|
||||
enterContext,
|
||||
switchContext,
|
||||
ensureContextByRoute,
|
||||
getContextQuery,
|
||||
getMenuRouteLocation
|
||||
};
|
||||
});
|
||||
@@ -3,13 +3,12 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import type { CustomRoute, ElegantConstRoute, LastLevelRouteKey, RouteKey, RouteMap } from '@elegant-router/types';
|
||||
import { router } from '@/router';
|
||||
import { fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { createStaticRoutes, getAuthVueRoutes } from '@/router/routes';
|
||||
import { ROOT_ROUTE } from '@/router/routes/builtin';
|
||||
import { getRouteName, getRoutePath } from '@/router/elegant/transform';
|
||||
import { getGlobalRouter } from '@/router/instance';
|
||||
import { useAuthStore } from '../auth';
|
||||
import { useDictStore } from '../dict';
|
||||
import { useTabStore } from '../tab';
|
||||
import {
|
||||
filterAuthRoutesByRoles,
|
||||
@@ -23,8 +22,27 @@ import {
|
||||
updateLocaleOfGlobalMenus
|
||||
} from './shared';
|
||||
|
||||
type RouteModule = typeof import('@/router/routes');
|
||||
|
||||
async function loadRouteModule(): Promise<RouteModule> {
|
||||
return import('@/router/routes');
|
||||
}
|
||||
|
||||
function createRootRoute(redirect: string): CustomRoute {
|
||||
return {
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect,
|
||||
meta: {
|
||||
title: 'root',
|
||||
constant: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
const authStore = useAuthStore();
|
||||
const dictStore = useDictStore();
|
||||
const tabStore = useTabStore();
|
||||
const { bool: isInitConstantRoute, setBool: setIsInitConstantRoute } = useBoolean();
|
||||
const { bool: isInitAuthRoute, setBool: setIsInitAuthRoute } = useBoolean();
|
||||
@@ -117,7 +135,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
* @param routeKey
|
||||
*/
|
||||
async function resetRouteCache(routeKey?: RouteKey) {
|
||||
const routeName = routeKey || (router.currentRoute.value.name as RouteKey);
|
||||
const routeName = routeKey || (getGlobalRouter().currentRoute.value.name as RouteKey);
|
||||
|
||||
excludeCacheRoutes.value.push(routeName);
|
||||
|
||||
@@ -127,7 +145,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
/** 全局面包屑 */
|
||||
const breadcrumbs = computed(() => getBreadcrumbsByRoute(router.currentRoute.value, menus.value));
|
||||
const breadcrumbs = computed(() => getBreadcrumbsByRoute(getGlobalRouter().currentRoute.value, menus.value));
|
||||
|
||||
/** 重置 store */
|
||||
async function resetStore() {
|
||||
@@ -151,11 +169,12 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
async function initConstantRoute() {
|
||||
if (isInitConstantRoute.value) return;
|
||||
|
||||
const { createStaticRoutes } = await loadRouteModule();
|
||||
const staticRoute = createStaticRoutes();
|
||||
|
||||
addConstantRoutes(staticRoute.constantRoutes);
|
||||
|
||||
handleConstantAndAuthRoutes();
|
||||
await handleConstantAndAuthRoutes();
|
||||
|
||||
setIsInitConstantRoute(true);
|
||||
|
||||
@@ -169,8 +188,10 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
await authStore.initUserInfo();
|
||||
}
|
||||
|
||||
await dictStore.initDictCache();
|
||||
|
||||
if (authRouteMode.value === 'static') {
|
||||
initStaticAuthRoute();
|
||||
await initStaticAuthRoute();
|
||||
} else {
|
||||
await initDynamicAuthRoute();
|
||||
}
|
||||
@@ -179,7 +200,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
/** 初始化静态权限路由 */
|
||||
function initStaticAuthRoute() {
|
||||
async function initStaticAuthRoute() {
|
||||
const { createStaticRoutes } = await loadRouteModule();
|
||||
const { authRoutes: staticAuthRoutes } = createStaticRoutes();
|
||||
|
||||
if (authStore.isStaticSuper) {
|
||||
@@ -190,7 +212,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
addAuthRoutes(filteredAuthRoutes);
|
||||
}
|
||||
|
||||
handleConstantAndAuthRoutes();
|
||||
await handleConstantAndAuthRoutes();
|
||||
|
||||
setIsInitAuthRoute(true);
|
||||
}
|
||||
@@ -204,11 +226,11 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
|
||||
addAuthRoutes(routes);
|
||||
|
||||
handleConstantAndAuthRoutes();
|
||||
await handleConstantAndAuthRoutes();
|
||||
|
||||
setRouteHome(home);
|
||||
|
||||
handleUpdateRootRouteRedirect(home);
|
||||
await handleUpdateRootRouteRedirect(home);
|
||||
|
||||
setIsInitAuthRoute(true);
|
||||
} else {
|
||||
@@ -218,7 +240,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
/** 统一处理常量路由和权限路由 */
|
||||
function handleConstantAndAuthRoutes() {
|
||||
async function handleConstantAndAuthRoutes() {
|
||||
const { getAuthVueRoutes } = await loadRouteModule();
|
||||
const allRoutes = [...constantRoutes.value, ...authRoutes.value];
|
||||
|
||||
const sortRoutes = sortRoutesByOrder(allRoutes);
|
||||
@@ -241,7 +264,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
*/
|
||||
function addRoutesToVueRouter(routes: RouteRecordRaw[]) {
|
||||
routes.forEach(route => {
|
||||
const removeFn = router.addRoute(route);
|
||||
const removeFn = getGlobalRouter().addRoute(route);
|
||||
addRemoveRouteFn(removeFn);
|
||||
});
|
||||
}
|
||||
@@ -260,11 +283,13 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
*
|
||||
* @param redirectKey 重定向目标路由 key
|
||||
*/
|
||||
function handleUpdateRootRouteRedirect(redirectKey: LastLevelRouteKey) {
|
||||
async function handleUpdateRootRouteRedirect(redirectKey: LastLevelRouteKey) {
|
||||
const redirect = getRoutePath(redirectKey);
|
||||
|
||||
if (redirect) {
|
||||
const rootRoute: CustomRoute = { ...ROOT_ROUTE, redirect };
|
||||
const { getAuthVueRoutes } = await loadRouteModule();
|
||||
const rootRoute = createRootRoute(redirect);
|
||||
const router = getGlobalRouter();
|
||||
|
||||
router.removeRoute(rootRoute.name);
|
||||
|
||||
@@ -287,6 +312,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
if (authRouteMode.value === 'static') {
|
||||
const { createStaticRoutes } = await loadRouteModule();
|
||||
const { authRoutes: staticAuthRoutes } = createStaticRoutes();
|
||||
return isRouteExistByRouteName(routeName, staticAuthRoutes);
|
||||
}
|
||||
@@ -316,6 +342,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
return {
|
||||
resetStore,
|
||||
routeHome,
|
||||
authRoutes,
|
||||
menus,
|
||||
searchMenus,
|
||||
updateGlobalMenusByLocale,
|
||||
|
||||
@@ -2,10 +2,10 @@ import { computed, ref } from 'vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import type { RouteKey } from '@elegant-router/types';
|
||||
import { router } from '@/router';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { getGlobalRouter } from '@/router/instance';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { useThemeStore } from '../theme';
|
||||
import {
|
||||
@@ -35,7 +35,7 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
|
||||
/** Init home tab */
|
||||
function initHomeTab() {
|
||||
homeTab.value = getDefaultHomeTab(router, routeStore.routeHome);
|
||||
homeTab.value = getDefaultHomeTab(getGlobalRouter(), routeStore.routeHome);
|
||||
}
|
||||
|
||||
/** Get all tabs */
|
||||
@@ -62,7 +62,7 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
const storageTabs = localStg.get('globalTabs');
|
||||
|
||||
if (themeStore.tab.cache && storageTabs) {
|
||||
const extractedTabs = extractTabsByAllRoutes(router, storageTabs);
|
||||
const extractedTabs = extractTabsByAllRoutes(getGlobalRouter(), storageTabs);
|
||||
tabs.value = updateTabsByI18nKey(extractedTabs);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user