diff --git a/docs/财务系统开发交付总方案.md b/docs/财务系统开发交付总方案.md index 03cfe0e..86bf672 100644 --- a/docs/财务系统开发交付总方案.md +++ b/docs/财务系统开发交付总方案.md @@ -637,7 +637,7 @@ OA 导入路线不重新判断原 OA 的业务审批结论,但财务金额、 ### 7.3 Starter 菜单、路由与页面映射 -登录后的整体框架直接使用 TDesign Starter 的左侧菜单、顶部栏、面包屑和内容区。菜单名称、顺序和路由在第一轮固定如下;详情、编辑等隐藏路由不出现在左侧菜单,但必须保留面包屑和返回路径。 +登录后的整体框架直接使用 TDesign Starter 的左侧菜单、顶部栏、页签和内容区。菜单层级已经表达业务归属,首期不在内容区显示面包屑,避免与菜单和页签重复;详情、编辑等隐藏路由必须保留安全返回路径。 | 一级菜单/入口 | 二级菜单 | 页面 | 路由 | 默认可见身份 | | --- | --- | --- | --- | --- | @@ -664,7 +664,7 @@ OA 导入路线不重新判断原 OA 的业务审批结论,但财务金额、 | 系统治理 | 权限与配置 | PAGE-21 | `/governance/settings` | 系统管理员 | | 系统治理 | 系统更新 | PAGE-22 | `/governance/update` | 系统管理员 | -本地路由表固定声明组件映射,后端只返回当前身份、权限码和数据范围,前端据此过滤菜单和按钮;后端不得下发任意组件文件路径。业务页面路由的 `meta` 至少包含 `pageId`、`pageType`、`pageTemplate`、`title`、`permission`、`requiresAuth` 和面包屑信息;出现在菜单中的路由组沿用 Starter 的 `orderNo` 排序字段,不再另造 `menuOrder`。S0 页面追溯表还要逐路由冻结本地 `componentPath`、默认入口、URL query 白名单、首个读取 operationId(没有接口时明确记 `N/A` 和原因)及证据 ID;菜单、路由和页面不得出现三份不同配置。 +本地路由表固定声明组件映射,后端只返回当前身份、权限码和数据范围,前端据此过滤菜单和按钮;后端不得下发任意组件文件路径。业务页面路由的 `meta` 至少包含 `pageId`、`pageType`、`pageTemplate`、`title`、`permission` 和 `requiresAuth`;历史 `breadcrumb` 字段可保留作路由合同和返回路径元数据,但不得渲染为可见导航。出现在菜单中的路由组沿用 Starter 的 `orderNo` 排序字段,不再另造 `menuOrder`。S0 页面追溯表还要逐路由冻结本地 `componentPath`、默认入口、URL query 白名单、首个读取 operationId(没有接口时明确记 `N/A` 和原因)及证据 ID;菜单、路由和页面不得出现三份不同配置。 同一账号切换身份时必须清空当前身份的页签、页面缓存、待办查询和按钮权限,再加载新身份菜单;不能沿用上一个身份打开的详情页。手工输入隐藏路由或无权路由时统一进入 Starter 的 403 页面,后端接口同时返回 `403` 并写审计日志。 @@ -1625,12 +1625,12 @@ Pull Request 只有同时满足以下条件才能合并:页面/操作合同完 | 区域 | 固定规则 | | --- | --- | | 登录/身份选择 | PAGE-01、PAGE-02 使用 Starter `BlankLayout`;保留 TDesign 登录视觉结构,使用甲方本地品牌素材,不显示业务侧栏 | -| 登录后框架 | PAGE-03~PAGE-22 固定使用 Starter `side` 布局:左侧菜单、顶部栏、面包屑、页签栏和内容区 | +| 登录后框架 | PAGE-03~PAGE-22 固定使用 Starter `side` 布局:左侧菜单、顶部栏、页签栏和内容区;不渲染面包屑 | | 左侧菜单 | 使用 7.3 节固定层级和顺序;支持 Starter 默认展开/收起;一级菜单图标来自 TDesign Icons,名称不得由开发人员自行缩写 | | 顶部栏 | 显示当前身份、身份切换、系统通知和用户菜单;删除代码仓库、外部帮助、语言切换、主题切换和布局设置入口 | -| 面包屑 | 以 `routes.json` 页面合同为唯一层级来源,从一级菜单到当前页面完整显示;隐藏详情路由必须显示来源列表和当前页面,任一级可安全返回 | +| 面包屑 | 首期删除可见面包屑;菜单和页签承担层级识别,隐藏详情路由通过显式返回按钮或安全返回路径返回来源列表 | | 页签栏 | 保留 Starter 路由页签;工作台固定不可关闭,详情页签显示“页面名 + 业务编号”;切换身份时清空旧身份页签和缓存 | -| 内容头 | 静态列表、工作台、台账和配置页只显示顶部面包屑,不再在内容区重复模块名、页面名或功能说明;保留读屏可识别的隐藏 `h1`,刷新、新建、导入、导出等动作进入右侧紧凑工具栏。项目详情、表单详情等动态页面可以显示业务编号、对象名称和状态,但不得重复面包屑中的静态标题 | +| 内容头 | 静态列表、工作台、台账和配置页直接进入紧凑操作栏、查询区或指标区,不显示面包屑,也不重复堆叠模块名、页面名和功能说明;保留读屏可识别的隐藏 `h1`,刷新、新建、导入、导出等动作进入右侧紧凑工具栏。项目详情、表单详情等动态页面显示业务编号、对象名称和状态,并提供明确返回路径 | | 页面滚动 | 页面主体纵向滚动;宽表格只在表格区域横向滚动;弹窗、抽屉和固定操作栏不得覆盖页面标题或分页 | 主题固定为 TDesign 浅色模式和已确认的品牌主色;首期不交付深色模式、多语言、顶部导航、混合导航或用户自定义主题。允许本地保存的只有菜单收起、非敏感表格列显示等界面偏好;身份、权限、业务记录、查询结果和敏感字段不得持久化到浏览器。 @@ -1643,7 +1643,7 @@ PAGE-01~PAGE-22 必须从以下模板组合,不允许每个开发人员各 | --- | --- | --- | --- | | 公共入口 | PAGE-01~02 | 本地品牌区 + 登录表单或三张身份卡 + 明确错误反馈 | `Form`、`Input`、`Button`、`Alert`、`Loading` | | 工作台 | PAGE-04~06 | 紧凑指标行 + 待办/异常主列表 + 常用入口 + 最近动态;下一屏内容在首屏可见 | `Statistic`、`Table`、`Tabs`、`Tag`、`Timeline`、必要的 `Card` | -| 列表/台账 | PAGE-07、09、13~16、18~20 | 合同面包屑 + 紧凑操作栏 + 查询栏 + 数据表格 + 分页;内容区不重复静态页名,详情通过路由或抽屉进入 | `Form`、`Input`、`Select`、`DateRangePicker`、`Table`、`Pagination`、`Dropdown` | +| 列表/台账 | PAGE-07、09、13~16、18~20 | 紧凑操作栏 + 查询栏 + 数据表格 + 分页;不额外渲染面包屑或重复静态页名,详情通过路由或抽屉进入 | `Form`、`Input`、`Select`、`DateRangePicker`、`Table`、`Pagination`、`Dropdown` | | 业务详情 | PAGE-08 | 项目摘要 + 状态/主操作 + 页签 + 描述信息/明细表/时间线;跨对象都有返回路径 | `Descriptions`、`Tabs`、`Table`、`Timeline`、`Tag`、`Drawer` | | 录入/导入 | PAGE-10、PAGE-12 的编辑状态 | 分组表单 + 动态明细 + 附件 + 校验结果 + 底部固定操作栏 | `Form`、`Row`、`Col`、`Input`/`MoneyInput`、仅整数 `InputNumber`、`Select`、`DatePicker`、`Upload`、`Alert` | | 审批处理 | PAGE-11、PAGE-17 | 待办列表 + 业务摘要 + 附件/校验 + 审批时间线 + 当前节点动作 | `Table`、`Descriptions`、`Steps`、`Timeline`、`Dialog`、`Textarea` | @@ -3005,6 +3005,17 @@ PAGE-16 全类回归曾暴露测试证据文件路径硬编码与应用 `finance **本轮结论**:Preview.8 具备可部署的首次安装和 32 位向导框架;生产录入安装码、数据库密码和管理员密码前必须先配置 HTTPS 反向代理或可信内网隧道,HTTP 仅用于隔离 Preview 验证。 +### 13.1.17 Preview.27 安装跳转与内容区导航收敛记录(2026-08-18) + +| 项目 | 结果 | 说明 | +| --- | --- | --- | +| 安装完成跳转 | 已修复 | 完成安装后以 `required=false` 且 `locked/ready` 状态确认服务已切换,再通过 Vue Router 进入 `/login`;不再等待永远为 `false` 的 setup `ready` 标志,也不再复用安装前缓存的 `required=true` 状态 | +| 面包屑 | 已删除可见渲染 | 菜单和路由页签已经表达层级,业务内容区不再显示“表单审批 → 表单中心”等重复导航;历史 `breadcrumb` 合同字段仅保留作元数据,不生成 DOM | +| 内容区留白 | 已收紧 | 移除面包屑占位和底部间距,页签下内容区顶部间距固定为 16px;列表、工作台和配置页直接进入操作栏/指标/查询区 | +| 自动化证据 | 通过 | TypeScript、ESLint、Stylelint、Vitest `12 files / 60 tests`、安装向导 Playwright `6/6`、全量页面可访问性与布局 Playwright `78/78`、production build/hygiene 均通过 | + +**本轮结论**:业务页面不再把菜单层级重复显示为面包屑;安装完成后会稳定进入登录页,异常重启场景也通过状态确认后自动跳转。 + ### 13.2 第三方权限和自动化负向验收 **当前状态**:`PENDING_EVIDENCE`。以下五项是发布前必须执行并归档的负向验收,不代表当前已经完成;证据包须记录代码/依赖/配置扫描、断网运行、人工登记审计断言、执行时间、版本和可复查结果。 diff --git a/frontend/e2e/accessibility.e2e.ts b/frontend/e2e/accessibility.e2e.ts index 8be377f..173ad45 100644 --- a/frontend/e2e/accessibility.e2e.ts +++ b/frontend/e2e/accessibility.e2e.ts @@ -260,13 +260,7 @@ for (const route of routes) { await expect(page.locator('h1.page-title-sr-only')).toHaveCount(1); await expect(page.locator('h1:not(.page-title-sr-only)')).toHaveCount(0); } - if (!['PAGE-01', 'PAGE-02', 'PAGE-23'].includes(route.pageId)) { - const breadcrumb = page.locator('.tdesign-breadcrumb'); - await expect(breadcrumb).toBeVisible(); - for (const label of route.breadcrumb) { - await expect(breadcrumb.getByText(label, { exact: true })).toBeVisible(); - } - } + await expect(page.locator('.tdesign-breadcrumb')).toHaveCount(0); await page.waitForLoadState('networkidle'); if (route.pageId === 'PAGE-09') { await expect(page.locator('.t-input-number__decrease, .t-input-number__increase')).toHaveCount(0); diff --git a/frontend/e2e/setup-wizard.e2e.ts b/frontend/e2e/setup-wizard.e2e.ts index 192f7cc..167c50f 100644 --- a/frontend/e2e/setup-wizard.e2e.ts +++ b/frontend/e2e/setup-wizard.e2e.ts @@ -11,7 +11,9 @@ test('first-run wizard tests MySQL, creates the administrator and stays responsi data: { required: !completed, locked: completed, - ready: completed, + // Production status is locked after setup; readiness is not the + // completion signal used by the redirect flow. + ready: false, supportedDatabaseTypes: ['MYSQL'], message: completed ? '系统已完成安装' : '请完成安装', }, @@ -69,6 +71,8 @@ test('first-run wizard tests MySQL, creates the administrator and stays responsi await expect(page.getByText('安装完成', { exact: true })).toBeVisible(); await expect(page.getByText('安装完成,系统正在切换到正式模式')).toBeVisible(); + await expect(page).toHaveURL(/\/login$/, { timeout: 15_000 }); + const layout = await page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, @@ -128,8 +132,8 @@ test('confirms completion after the setup response is interrupted by a service r await page.getByRole('button', { name: '完成安装' }).click(); await page.getByRole('button', { name: '确定', exact: true }).click(); - await expect(page.getByText('连接暂时中断,正在确认安装结果。')).toBeVisible(); await expect(page.getByText('安装完成', { exact: true })).toBeVisible(); await expect(page.getByText('安装已完成,系统正在切换到正式模式')).toBeVisible(); await expect(page.getByText('网络请求失败')).toHaveCount(0); + await expect(page).toHaveURL(/\/login$/, { timeout: 15_000 }); }); diff --git a/frontend/src/config/style.ts b/frontend/src/config/style.ts index 882a16b..d6280aa 100644 --- a/frontend/src/config/style.ts +++ b/frontend/src/config/style.ts @@ -1,7 +1,6 @@ export default { showFooter: false, isSidebarCompact: false, - showBreadcrumb: true, menuAutoCollapsed: false, mode: 'light', layout: 'side', diff --git a/frontend/src/layouts/components/LayoutContent.vue b/frontend/src/layouts/components/LayoutContent.vue index d0f2c07..73ad189 100644 --- a/frontend/src/layouts/components/LayoutContent.vue +++ b/frontend/src/layouts/components/LayoutContent.vue @@ -61,7 +61,6 @@ - @@ -81,7 +80,6 @@ import { useLocale } from '@/locales/useLocale'; import { useSettingStore, useTabsRouterStore } from '@/store'; import type { TRouterInfo, TTabRemoveOptions } from '@/types/interface'; -import LBreadcrumb from './Breadcrumb.vue'; import LContent from './Content.vue'; import LFooter from './Footer.vue'; diff --git a/frontend/src/pages/setup/index.vue b/frontend/src/pages/setup/index.vue index f731e3d..eb6e1e5 100644 --- a/frontend/src/pages/setup/index.vue +++ b/frontend/src/pages/setup/index.vue @@ -355,7 +355,7 @@ async function waitForSetupCompletion() { if (unmounted) return false; try { const status = await getSetupStatus(); - if (status.ready) return true; + if (!status.required && (status.ready || status.locked)) return true; } catch { // A short connection failure is expected while systemd restarts the service. } @@ -387,7 +387,7 @@ async function openLogin() { window.clearInterval(countdownTimer); completionMessage.value = '系统正在启动,请稍候。'; if (await waitForSetupCompletion()) { - window.location.assign('/login'); + await router.replace('/login'); return; } completionMessage.value = '系统启动时间较长,请稍后刷新页面。'; diff --git a/frontend/src/permission.ts b/frontend/src/permission.ts index d8c0be0..77d3794 100644 --- a/frontend/src/permission.ts +++ b/frontend/src/permission.ts @@ -17,7 +17,20 @@ const isPublic = (to: RouteLocationNormalized) => let setupStatusRequest: ReturnType | null = null; function loadSetupStatus() { - setupStatusRequest ||= getSetupStatus(); + if (setupStatusRequest) return setupStatusRequest; + + // Share only an in-flight request. Keeping the resolved setup state would + // send the user back to /setup after the installation marker is written. + const request = getSetupStatus(); + setupStatusRequest = request; + request.then( + () => { + if (setupStatusRequest === request) setupStatusRequest = null; + }, + () => { + if (setupStatusRequest === request) setupStatusRequest = null; + }, + ); return setupStatusRequest; } diff --git a/frontend/src/style/layout.less b/frontend/src/style/layout.less index f79d864..4aa209d 100644 --- a/frontend/src/style/layout.less +++ b/frontend/src/style/layout.less @@ -87,7 +87,7 @@ z-index: 100; } &-tabs-nav + .@{starter-prefix}-content-layout { - padding-top: var(--td-comp-paddingTB-xxl); + padding-top: var(--td-comp-paddingTB-l); } &::-webkit-scrollbar {