diff --git a/AGENTS.md b/AGENTS.md index 68b4727..79dd483 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,9 @@ - 外科手术式修改:只改与任务直接相关的文件和代码行,不重构无关模块,不调整无关格式或注释。 - 保持现有风格:遵循仓库已有包结构、分层方式、命名和写法,不按个人偏好重写。 - 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。 +- 页面边距约定:业务页面根节点默认跟随布局主内容区 `el-main` 的 `15px` 边距,不再额外叠加页面级外边距;如需特殊边距,必须先有明确的视觉参照页面或业务原因。 +- 表格样式约定:业务表格优先复用仓库现有 `table-main`、`card`、`table-header` 结构,参照 `dictdata` 页面;表格卡片内部默认不再额外堆叠页面标题、说明文案或自定义装饰区,表头左侧用于主操作、次操作和危险批量操作,右侧用于刷新、列设置、搜索等工具按钮;不要在单页里重复自定义表格卡片边框、表头按钮布局和表头配色,除非有明确视觉参照或业务原因。 +- 按钮样式约定:业务页面按钮参照 `dictdata` 页面;表头主操作使用 `type="primary"`,表头次操作使用 `type="primary" plain`,危险批量操作使用 `type="danger" plain`,表格行内操作统一使用 `link` 风格并优先保持 `primary` 语义与图标一致性;弹窗底部保持“取消”默认按钮、“主确认”使用 `primary`,同级辅助执行按钮使用 `primary plain`。 - 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性、界面影响范围和启动链路检查进行验证。 - 除非用户明确要求,否则不执行 `npm run build`、`npm run build-w`、`electron-builder`、加密打包或其他重型构建命令;通常只做静态检查、必要的代码阅读和轻量验证。 @@ -35,6 +38,8 @@ ## 代码风格与命名规范 前端格式化规则定义在 `frontend/.prettierrc`:4 空格缩进、单引号、不写分号、单行 120 字符、LF 换行。Lint 规则基于 Vue 3 与 TypeScript。 +文件编码规范:所有新增或修改的源码、脚本、配置、文档统一使用 UTF-8 编码(无 BOM)和 LF 换行,不要保存为 GBK、ANSI 或其他本地编码,避免再次出现乱码。 + 请遵循现有命名方式: - 页面或路由目录通常使用 `index.vue`,例如 `views/home/` - 通用组件使用 PascalCase,例如 `HomeToolCard.vue` diff --git a/frontend/src/layouts/components/Tabs/components/MoreButton.vue b/frontend/src/layouts/components/Tabs/components/MoreButton.vue index ef785f5..d98544f 100644 --- a/frontend/src/layouts/components/Tabs/components/MoreButton.vue +++ b/frontend/src/layouts/components/Tabs/components/MoreButton.vue @@ -7,31 +7,31 @@ - {{ $t('tabs.refresh') }} + {{ t('tabs.refresh') }} - {{ $t('tabs.maximize') }} + {{ t('tabs.maximize') }} - {{ $t('tabs.closeCurrent') }} + {{ t('tabs.closeCurrent') }} - + - {{ $t('tabs.closeLeft') }} + {{ t('tabs.closeLeft') }} - + - {{ $t('tabs.closeRight') }} + {{ t('tabs.closeRight') }} - + - {{ $t('tabs.closeOther') }} + {{ t('tabs.closeOther') }} - {{ $t('tabs.closeAll') }} + {{ t('tabs.closeAll') }} @@ -39,7 +39,8 @@ diff --git a/frontend/src/routers/modules/dynamicRouter.ts b/frontend/src/routers/modules/dynamicRouter.ts index 2706f44..acc1a12 100644 --- a/frontend/src/routers/modules/dynamicRouter.ts +++ b/frontend/src/routers/modules/dynamicRouter.ts @@ -5,8 +5,9 @@ import { ElNotification } from 'element-plus' import { useUserStore } from '@/stores/modules/user' import { useAuthStore } from '@/stores/modules/auth' -// 引入 views 文件夹下所有 vue 文件 const modules = import.meta.glob('@/views/**/*.vue') +const VIEWS_ALIAS_PREFIX = '@/views' +const VIEWS_SRC_PREFIX = '/src/views' const STATIC_ROUTE_NAMES = new Set([ 'layout', 'login', @@ -14,6 +15,7 @@ const STATIC_ROUTE_NAMES = new Set([ 'tools', 'toolWaveform', 'toolMmsMapping', + 'toolAddData', 'systemMonitor', 'diskMonitor', '403', @@ -24,7 +26,7 @@ const STATIC_ROUTE_NAMES = new Set([ let isInitializing = false /** - * 清除已有的动态路由 + * 清除已有的动态路由,避免重复注入。 */ const clearDynamicRoutes = () => { const routes = router.getRoutes() @@ -36,13 +38,19 @@ const clearDynamicRoutes = () => { } /** - * 根据菜单 component 路径查找对应的页面模块。 - * 兼容两种仓库写法: - * 1. /foo/bar.vue - * 2. /foo/bar/index.vue + * 统一菜单 component 配置格式,只允许映射到 views 目录。 */ -const resolveComponentModule = (path: string) => { - let normalizedPath = path.trim() +const normalizeComponentPath = (path: string) => { + let normalizedPath = path.trim().replace(/\\/g, '/') + + if (normalizedPath.startsWith(VIEWS_ALIAS_PREFIX)) { + normalizedPath = normalizedPath.slice(VIEWS_ALIAS_PREFIX.length) + } else if (normalizedPath.startsWith(VIEWS_SRC_PREFIX)) { + normalizedPath = normalizedPath.slice(VIEWS_SRC_PREFIX.length) + } else if (normalizedPath.startsWith('src/views')) { + normalizedPath = normalizedPath.slice('src/views'.length) + } + if (!normalizedPath.startsWith('/')) { normalizedPath = '/' + normalizedPath } @@ -53,13 +61,31 @@ const resolveComponentModule = (path: string) => { normalizedPath = normalizedPath.slice(0, -1) } - const candidatePaths = [`/src/views${normalizedPath}.vue`, `/src/views${normalizedPath}/index.vue`] + return normalizedPath +} + +/** + * 根据菜单 component 路径查找对应页面模块。 + * 兼容菜单资源里常见的多种写法,避免因为前缀或 index 差异导致误判。 + */ +const resolveComponentModule = (path: string) => { + const normalizedPath = normalizeComponentPath(path) + const viewPath = normalizedPath.endsWith('/index') ? normalizedPath.slice(0, -'/index'.length) : normalizedPath + const candidatePaths = Array.from( + new Set([ + `${VIEWS_ALIAS_PREFIX}${normalizedPath}.vue`, + `${VIEWS_ALIAS_PREFIX}${normalizedPath}/index.vue`, + `${VIEWS_ALIAS_PREFIX}${viewPath}/index.vue`, + `${VIEWS_SRC_PREFIX}${normalizedPath}.vue`, + `${VIEWS_SRC_PREFIX}${normalizedPath}/index.vue`, + `${VIEWS_SRC_PREFIX}${viewPath}/index.vue` + ]) + ) for (const candidatePath of candidatePaths) { - const moduleLoader = modules[candidatePath] - if (moduleLoader) { + if (candidatePath in modules) { return { - moduleLoader, + moduleLoader: modules[candidatePath], resolvedPath: candidatePath } } @@ -72,7 +98,7 @@ const resolveComponentModule = (path: string) => { } /** - * @description 初始化动态路由 + * 初始化动态路由。 */ export const initDynamicRouter = async () => { if (isInitializing) return Promise.reject(new Error('Dynamic router initialization in progress')) @@ -83,15 +109,13 @@ export const initDynamicRouter = async () => { const unresolvedRoutes: Array<{ name?: string; path?: string; component?: string; candidates: string[] }> = [] try { - // 1. 获取菜单列表 && 按钮权限列表 await authStore.getAuthMenuList() await authStore.getAuthButtonList() - // 2. 判断当前用户有没有菜单权限 if (!authStore.authMenuListGet.length) { ElNotification({ title: '无权限访问', - message: '当前账号无任何菜单权限,请联系系统管理员!', + message: '当前账号无任何菜单权限,请联系系统管理员', type: 'warning', duration: 3000 }) @@ -102,21 +126,17 @@ export const initDynamicRouter = async () => { return Promise.reject('No permission') } - // 3. 清理之前的动态路由 clearDynamicRoutes() - // 4. 添加动态路由 for (const item of authStore.flatMenuListGet) { - // 删除 children 避免冗余嵌套 if (item.children) delete item.children - // 处理组件映射 + // 动态菜单组件必须先映射成真实页面模块,否则 addRoute 后会直接落到 404。 if (item.component && typeof item.component === 'string') { const { moduleLoader, resolvedPath } = resolveComponentModule(item.component) if (moduleLoader) { item.component = moduleLoader } else { - // 动态路由组件一旦解析失败,对应菜单会落入 404,这里必须打印清楚候选路径。 unresolvedRoutes.push({ name: item.name, path: item.path, @@ -127,7 +147,6 @@ export const initDynamicRouter = async () => { } } - // 类型守卫:确保满足 RouteRecordRaw 接口要求 if ( typeof item.path === 'string' && (typeof item.component === 'function' || typeof item.redirect === 'string') @@ -147,7 +166,6 @@ export const initDynamicRouter = async () => { console.error('[dynamic-router] unresolved route components', unresolvedRoutes) } } catch (error) { - // 当按钮 || 菜单请求出错时,重定向到登陆页 userStore.setAccessToken('') userStore.setRefreshToken('') userStore.setExp(0) diff --git a/frontend/src/routers/modules/staticRouter.ts b/frontend/src/routers/modules/staticRouter.ts index e72598d..a1dc9e5 100644 --- a/frontend/src/routers/modules/staticRouter.ts +++ b/frontend/src/routers/modules/staticRouter.ts @@ -54,11 +54,19 @@ export const staticRouter: RouteRecordRaw[] = [ path: '/tools/mmsMapping', name: 'toolMmsMapping', alias: ['/tools/mmsmapping', '/tools/mms-mapping'], - component: () => import('@/views/tools/mmsmapping/index.vue'), + component: () => import('@/views/tools/mmsMapping/index.vue'), meta: { title: 'MMS 映射' } }, + { + path: '/tools/addData', + name: 'toolAddData', + component: () => import('@/views/tools/addData/index.vue'), + meta: { + title: '模拟数据' + } + }, { path: '/403', name: '403', @@ -96,6 +104,10 @@ export const staticRouter: RouteRecordRaw[] = [ name: 'diskMonitor', component: () => import('@/views/systemMonitor/diskMonitor/index.vue'), meta: { + // 磁盘监控页复用系统监控主标签 + activeMenu: '/systemMonitor', + hideTab: true, + parentPath: '/systemMonitor', title: '磁盘监控' } }, diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts index 1c3cc4c..ad90521 100644 --- a/frontend/src/types/global.d.ts +++ b/frontend/src/types/global.d.ts @@ -12,6 +12,8 @@ declare namespace Menu { icon: string; title: string; activeMenu?: string; + hideTab?: boolean; + parentPath?: string; isLink?: string; isHide: boolean; isFull: boolean; diff --git a/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorJobDetailDrawer.vue b/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorJobDetailDrawer.vue index f555723..1cd3781 100644 --- a/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorJobDetailDrawer.vue +++ b/frontend/src/views/systemMonitor/diskMonitor/components/DiskMonitorJobDetailDrawer.vue @@ -1,5 +1,6 @@