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:
2026-04-23 09:05:55 +08:00
parent c5911ea34b
commit 4122dfa50d
95 changed files with 9581 additions and 801 deletions

View File

@@ -1,7 +1,10 @@
import type { LastLevelRouteKey } from '@elegant-router/types';
import type { RouteMeta } from 'vue-router';
import type { ElegantConstRoute, LastLevelRouteKey } from '@elegant-router/types';
import { objectContextDomainConfigs } from '@/constants/object-context';
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
import { createStaticRoutes } from '@/router/routes';
import { request } from '../request';
import type { ServiceRequestResult } from './shared';
import { type ServiceRequestResult, safeJsonRequestConfig } from './shared';
type BackendMenuRoute = Omit<Api.Route.MenuRoute, 'id' | 'children'> & {
id: string | number;
@@ -15,6 +18,12 @@ interface BackendUserRouteDTO {
let userRoutePromise: Promise<ServiceRequestResult<BackendUserRouteDTO>> | null = null;
const staticObjectContextRouteMap = new Map<App.ObjectContext.DomainKey, ElegantConstRoute>(
createStaticRoutes()
.authRoutes.filter(route => objectContextDomainConfigs.some(config => config.domainKey === route.name))
.map(route => [route.name as App.ObjectContext.DomainKey, route])
);
export function clearUserRouteCache() {
userRoutePromise = null;
}
@@ -27,22 +36,117 @@ function normalizeMenuRoute(route: BackendMenuRoute): Api.Route.MenuRoute {
};
}
function normalizePath(path?: string | null) {
if (!path) {
return '/';
}
return path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
}
function isPathMatchedByPrefix(path: string, prefix: string) {
const normalizedPath = normalizePath(path);
const normalizedPrefix = normalizePath(prefix);
return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}/`);
}
function isTopLevelObjectContextEntryRoute(route: Api.Route.MenuRoute, config: App.ObjectContext.DomainConfig) {
const routePath = normalizePath(route.path);
return (
route.component?.startsWith('view.') &&
!route.children?.length &&
(route.name === config.entryRouteKey || routePath === normalizePath(config.entryRoutePath))
);
}
function cloneStaticRouteAsMenuRoute(route: ElegantConstRoute, idPrefix: string): Api.Route.MenuRoute {
return {
...route,
id: `${idPrefix}:${String(route.name || route.path)}`,
children: route.children?.map(child => cloneStaticRouteAsMenuRoute(child, idPrefix))
};
}
function replaceWithStaticObjectContextDomainRoute(routes: Api.Route.MenuRoute[]) {
let normalizedRoutes = [...routes];
objectContextDomainConfigs.forEach(config => {
const hasDomainRootRoute = normalizedRoutes.some(route => route.name === config.domainKey);
if (hasDomainRootRoute) {
return;
}
const domainTopLevelRoutes = normalizedRoutes.filter(route =>
config.routePathPrefixes.some(prefix => isPathMatchedByPrefix(route.path, prefix))
);
const entryRoute = domainTopLevelRoutes.find(route => isTopLevelObjectContextEntryRoute(route, config));
if (!entryRoute) {
return;
}
const staticDomainRoute = staticObjectContextRouteMap.get(config.domainKey);
if (!staticDomainRoute) {
return;
}
const wrappedDomainRoute = cloneStaticRouteAsMenuRoute(staticDomainRoute, `object-context:${config.domainKey}`);
const entryRouteIndex = normalizedRoutes.findIndex(route => route.id === entryRoute.id);
const domainRouteIds = new Set(domainTopLevelRoutes.map(route => route.id));
if (entryRoute.meta) {
const nextMeta: RouteMeta = {
title: wrappedDomainRoute.meta?.title || config.domainKey,
...(wrappedDomainRoute.meta || {})
};
if (entryRoute.meta.icon) {
nextMeta.icon = entryRoute.meta.icon;
}
if (entryRoute.meta.localIcon) {
nextMeta.localIcon = entryRoute.meta.localIcon;
}
if (entryRoute.meta.order !== undefined) {
nextMeta.order = entryRoute.meta.order;
}
wrappedDomainRoute.meta = nextMeta;
}
normalizedRoutes = normalizedRoutes.filter(route => !domainRouteIds.has(route.id));
normalizedRoutes.splice(entryRouteIndex < 0 ? normalizedRoutes.length : entryRouteIndex, 0, wrappedDomainRoute);
});
return normalizedRoutes;
}
function normalizeUserRoute(data: BackendUserRouteDTO): Api.Route.UserRoute {
return {
routes: (data.routes ?? []).map(route => normalizeMenuRoute(route)),
routes: replaceWithStaticObjectContextDomainRoute((data.routes ?? []).map(route => normalizeMenuRoute(route))),
home: (data.home || 'system_user') as LastLevelRouteKey
};
}
/** 获取常量路由 */
export function fetchGetConstantRoutes() {
return request<Api.Route.MenuRoute[]>({ url: '/route/getConstantRoutes' });
return request<Api.Route.MenuRoute[]>({
...safeJsonRequestConfig,
url: '/route/getConstantRoutes'
});
}
/** 获取用户路由 */
export async function fetchGetUserRoutes(force = false): Promise<ServiceRequestResult<Api.Route.UserRoute>> {
if (!userRoutePromise || force) {
userRoutePromise = request<BackendUserRouteDTO>({
...safeJsonRequestConfig,
url: `${SYSTEM_SERVICE_PREFIX}/auth/get-user-routes`
}).then(result => result as ServiceRequestResult<BackendUserRouteDTO>);
}