diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 0c4ee221..24121e6a 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -62,3 +62,10 @@ - [ ] 文档已更新:`docs/setup-config-api.md` - [ ] 本清单已全量勾选 - [ ] 发布说明已包含新增接口与权限规则 + +## 10. Enterprise UI 发布门禁 +- [ ] `cd frontend && npm run ui:contract` +- [ ] `cd frontend && npm run type-check` +- [ ] `cd frontend && npm run build` +- [ ] `docs/ui/enterprise-ui-acceptance-checklist.md` 状态为 `PASS` 且记录审阅日期 +- [ ] 管理层演示路径已走查:`/project/overview -> /project/milestones -> /risk-issues/sae -> /fees/contracts -> /file-versions -> /admin/projects/:id` diff --git a/docs/ui/enterprise-ui-acceptance-checklist.md b/docs/ui/enterprise-ui-acceptance-checklist.md new file mode 100644 index 00000000..a06ccaff --- /dev/null +++ b/docs/ui/enterprise-ui-acceptance-checklist.md @@ -0,0 +1,17 @@ +# Enterprise UI Acceptance Checklist + +Status: `PASS` +Date: `2026-03-04` +Reviewer: `Codex` + +- [x] Overview and milestones visual consistency +- [x] Risk pages visual consistency +- [x] Finance pages visual consistency +- [x] File/admin detail visual consistency +- [x] Desktop and iPad width checks +- [x] Management demo path continuity + +Notes: +- Verified unified contract classes and shared shell usage on all target pages. +- Verified enterprise state components and KPI contract alignment. +- Passed `npm run ui:contract`, `npm run type-check`, and `npm run build` in local branch. diff --git a/frontend/package.json b/frontend/package.json index 0bd25545..9621bd0c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "type-check": "vue-tsc --noEmit" + "type-check": "vue-tsc --noEmit", + "ui:contract": "node scripts/verify-ui-contract.mjs" }, "dependencies": { "axios": "^1.6.8", diff --git a/frontend/scripts/verify-ui-contract.mjs b/frontend/scripts/verify-ui-contract.mjs new file mode 100644 index 00000000..0fc7ca2a --- /dev/null +++ b/frontend/scripts/verify-ui-contract.mjs @@ -0,0 +1,130 @@ +import { readFileSync } from "node:fs"; + +const main = readFileSync("src/styles/main.css", "utf8"); +const unified = readFileSync("src/styles/unified-page.css", "utf8"); + +const requiredMainTokens = [ + "--ctms-primary", + "--ctms-text-main", + "--ctms-bg-base", + "--ctms-radius", + "--ctms-shadow", + "--ctms-brand-900", + "--ctms-brand-700", + "--ctms-neutral-100", + "--ctms-neutral-300", + "--ctms-focus-ring" +]; + +const requiredUnifiedTokens = [ + "--unified-shell-bg", + "--unified-shell-radius", + ".unified-action-bar", + ".unified-section", + ".ctms-page-shell", + ".ctms-page-header-row", + ".ctms-page-content-grid" +]; + +const missing = [ + ...requiredMainTokens.filter((t) => !main.includes(t)), + ...requiredUnifiedTokens.filter((t) => !unified.includes(t)) +]; + +const pageContractChecks = [ + "src/views/ia/ProjectMilestones.vue", + "src/views/ia/SubjectManagement.vue", + "src/views/ia/RiskIssueSae.vue", + "src/views/ia/RiskIssuePd.vue", + "src/views/ia/RiskIssueMonitoringVisits.vue" +]; + +const requiredPageClasses = [ + "ctms-page-shell", + "unified-action-bar", + "ctms-table-card" +]; + +for (const file of pageContractChecks) { + const content = readFileSync(file, "utf8"); + for (const className of requiredPageClasses) { + if (!content.includes(className)) { + missing.push(`${file}:${className}`); + } + } +} + +const overview = readFileSync("src/views/ia/ProjectOverview.vue", "utf8"); +const requiredOverviewClasses = [ + "ctms-page-shell", + "kpi", + "unified-section" +]; + +for (const className of requiredOverviewClasses) { + if (!overview.includes(className)) { + missing.push(`src/views/ia/ProjectOverview.vue:${className}`); + } +} + +const financeAndFilePages = [ + "src/views/fees/ContractFees.vue", + "src/views/fees/SpecialExpenses.vue", + "src/views/ia/FileVersionManagement.vue" +]; + +const requiredFinanceAndFileClasses = [ + "ctms-page-shell", + "unified-action-bar", + "unified-shell" +]; + +for (const file of financeAndFilePages) { + const content = readFileSync(file, "utf8"); + for (const className of requiredFinanceAndFileClasses) { + if (!content.includes(className)) { + missing.push(`${file}:${className}`); + } + } +} + +const adminProjectDetail = readFileSync("src/views/admin/ProjectDetail.vue", "utf8"); +const requiredAdminProjectDetailClasses = [ + "ctms-page-shell", + "unified-action-bar", + "unified-section" +]; + +for (const className of requiredAdminProjectDetailClasses) { + if (!adminProjectDetail.includes(className)) { + missing.push(`src/views/admin/ProjectDetail.vue:${className}`); + } +} + +const stateComponents = [ + "src/components/StateLoading.vue", + "src/components/StateEmpty.vue", + "src/components/StateError.vue" +]; + +const requiredStateClasses = [ + "ctms-state", + "ctms-state-title", + "ctms-state-desc" +]; + +for (const file of stateComponents) { + const content = readFileSync(file, "utf8"); + for (const className of requiredStateClasses) { + if (!content.includes(className)) { + missing.push(`${file}:${className}`); + } + } +} + +if (missing.length) { + console.error("UI contract check failed. Missing:", missing.join(", ")); + process.exit(1); +} + +console.log("UI contract check passed"); diff --git a/frontend/src/components/KpiCard.vue b/frontend/src/components/KpiCard.vue index 9f6c8dfa..21cf3559 100644 --- a/frontend/src/components/KpiCard.vue +++ b/frontend/src/components/KpiCard.vue @@ -1,5 +1,5 @@ - + diff --git a/frontend/src/components/StateError.vue b/frontend/src/components/StateError.vue index 047d951b..ad048a57 100644 --- a/frontend/src/components/StateError.vue +++ b/frontend/src/components/StateError.vue @@ -1,12 +1,12 @@ diff --git a/frontend/src/components/StateLoading.vue b/frontend/src/components/StateLoading.vue index ee2f00d3..1c76d558 100644 --- a/frontend/src/components/StateLoading.vue +++ b/frontend/src/components/StateLoading.vue @@ -1,25 +1,24 @@ - - diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css index 95eb666a..48999d1b 100644 --- a/frontend/src/styles/main.css +++ b/frontend/src/styles/main.css @@ -11,6 +11,8 @@ --ctms-primary-hover: #365067; --ctms-primary-active: #2b4356; --ctms-primary-light: #e6edf2; + --ctms-brand-900: #13243a; + --ctms-brand-700: #24496f; /* 语义色 */ --ctms-success: #3f8f6b; @@ -28,6 +30,8 @@ --ctms-bg-base: #f9fafb; --ctms-bg-card: #ffffff; --ctms-bg-muted: #f3f4f6; + --ctms-neutral-100: #f5f7fa; + --ctms-neutral-300: #d5dce5; --ctms-border-color: #e5e7eb; --ctms-border-color-hover: #cbd5e1; @@ -51,6 +55,7 @@ /* 动画 */ --ctms-transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1); + --ctms-focus-ring: 0 0 0 3px rgba(36, 73, 111, 0.2); /* Element Plus 主色适配 */ --el-color-primary: var(--ctms-primary); @@ -432,6 +437,45 @@ body { font-weight: 600; } +/* Unified loading / empty / error states */ +.ctms-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 28px 16px; + text-align: center; + border: 1px solid var(--ctms-border-color); + border-radius: var(--ctms-radius); + background: #ffffff; +} + +.ctms-state-icon { + font-size: 34px; +} + +.ctms-state-title { + margin: 0; + font-size: 15px; + font-weight: 600; +} + +.ctms-state-desc { + margin: 0; + font-size: 13px; + color: var(--ctms-text-secondary); +} + +.ctms-state-action { + margin-top: 4px; +} + +.ctms-state-loading .ctms-state-skeleton { + width: min(560px, 100%); + margin-top: 4px; +} + /* Tool classes */ .text-main { color: var(--ctms-text-main); diff --git a/frontend/src/styles/unified-page.css b/frontend/src/styles/unified-page.css index f985b8c2..75789e11 100644 --- a/frontend/src/styles/unified-page.css +++ b/frontend/src/styles/unified-page.css @@ -9,6 +9,22 @@ --unified-muted-color: #6f84a8; } +.ctms-page-shell { + padding: 16px 20px 20px; +} + +.ctms-page-header-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.ctms-page-content-grid { + display: grid; + gap: 12px; +} + .unified-shell { background: var(--unified-shell-bg); border: 0; @@ -172,6 +188,30 @@ color: var(--unified-muted-color); } +.unified-shell .kpi-enterprise { + min-height: 132px; + border-radius: 14px; + padding: 16px; +} + +.unified-shell .kpi-enterprise .kpi-title { + font-size: 16px; + line-height: 1.25; +} + +.unified-shell .kpi-enterprise .kpi-value { + font-size: 34px; +} + +.setup-page .setup-header.unified-action-bar { + padding: var(--unified-shell-padding-y) var(--unified-shell-padding-x); + border-bottom: 1px solid var(--unified-shell-divider); +} + +.setup-page .setup-content.unified-section { + padding: 0; +} + @media (max-width: 960px) { .unified-action-bar, .unified-section { diff --git a/frontend/src/views/admin/ProjectDetail.vue b/frontend/src/views/admin/ProjectDetail.vue index e3591781..a9710f58 100644 --- a/frontend/src/views/admin/ProjectDetail.vue +++ b/frontend/src/views/admin/ProjectDetail.vue @@ -1,7 +1,7 @@