From 4b14c925b698b476f91e9ebaaae82cdb709c4dcd Mon Sep 17 00:00:00 2001 From: shunli Date: Thu, 14 May 2026 00:52:59 +0800 Subject: [PATCH] =?UTF-8?q?DG9=20=E7=BB=9F=E4=B8=80=E5=85=A8=E7=AB=99=20UI?= =?UTF-8?q?=20=E5=B8=83=E5=B1=80=E6=A0=B7=E6=9D=BF=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 关联 Issue: DG9-0..DG9-12 --- .agents/skills/hyb-oa-delivery/SKILL.md | 6 +- .../src/components/patterns/ListWorkspace.tsx | 94 +++++++++++- client/src/pages/ApOperationsPage.tsx | 12 +- client/src/pages/ApprovalRulesPage.tsx | 17 +-- client/src/pages/AssetRegistryPage.tsx | 24 +-- client/src/pages/InvoicePoolPage.tsx | 8 +- client/src/pages/LeaveRequestPage.tsx | 9 +- client/src/pages/MasterDataPage.tsx | 44 +++--- client/src/pages/MobileApprovalsPage.tsx | 118 +++++++++------ client/src/pages/MobileNotificationsPage.tsx | 96 ++++++------ client/src/pages/MobilePortalPage.tsx | 66 ++++---- .../pages/MobileReimbursementDetailPage.tsx | 2 +- client/src/pages/PaymentBatchesPage.tsx | 16 +- client/src/pages/ProjectsPage.tsx | 9 +- client/src/pages/PurchaseRequestsPage.tsx | 39 ++--- client/src/pages/ReimbursementDetailPage.tsx | 51 +++---- client/src/pages/ReimbursementsPage.tsx | 9 +- client/src/pages/SystemSettingsPage.tsx | 10 +- client/src/pages/WorkflowDefinitionsPage.tsx | 14 +- client/src/pages/WorkflowTaskDetailPage.tsx | 142 ++++++++---------- client/src/pages/WorkflowTasksPage.tsx | 116 ++++++++------ client/src/styles/mobile-touch.css | 77 ++++++++++ client/tests/e2e/project-wide-visual.spec.ts | 8 +- .../reimbursement-workflow-progress.spec.ts | 102 ++++++++----- docs/Agent开发交付实践.md | 7 + docs/产品设计规则.md | 18 +++ docs/文档事实源状态矩阵.md | 3 +- docs/文档索引.md | 6 +- ...6-05-13_23-30-00-dg9-ui-layout-rollout.csv | 14 ++ scripts/qa/check-ui-design-rules.mjs | 43 ++++++ 30 files changed, 743 insertions(+), 437 deletions(-) create mode 100644 issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv diff --git a/.agents/skills/hyb-oa-delivery/SKILL.md b/.agents/skills/hyb-oa-delivery/SKILL.md index 4b39fcf1..01d195ba 100644 --- a/.agents/skills/hyb-oa-delivery/SKILL.md +++ b/.agents/skills/hyb-oa-delivery/SKILL.md @@ -13,7 +13,7 @@ Read only what is relevant: - Always read `AGENTS.md` and the newest relevant `issues/*.csv`. - After R5, first check the main R5/R6 contract status before planning trial-run or follow-up work; R5 must remain the completed gate and R6 must have its own Issue CSV before expanding scope. -- For UI/page work, PC/mobile/WeCom experience, approval UX, or domestic OA benchmarking, read `docs/产品设计规则.md` and `references/visual-qa.md`. DG8 makes Feishu workbench/approval the single main UI/layout reference, with Hyinda branding and project rules taking priority. Other products are only function/process/control references. Mobile page work must treat `docs/产品设计规则.md` as the source of truth for the unified mobile shell, icon grid, card lists, deep Push pages, sticky actions, safe-area, and cross-viewport matrix. 移动页必须先对齐该设计事实源,再进入实现或验证。 +- For UI/page work, PC/mobile/WeCom experience, approval UX, or domestic OA benchmarking, read `docs/产品设计规则.md` and `references/visual-qa.md`. DG8 makes Feishu workbench/approval the single main UI/layout reference, with Hyinda branding and project rules taking priority. DG9 adds the A/B/C page layering and shared workbench/mobile samples; choose the DG9 sample before designing or coding a page. Other products are only function/process/control references. Mobile page work must treat `docs/产品设计规则.md` as the source of truth for the unified mobile shell, icon grid, card lists, deep Push pages, sticky actions, safe-area, and cross-viewport matrix. 移动页必须先对齐该设计事实源,再进入实现或验证。 - For approval workflow, roles, self-approval, notification, deeplink, reminder, or escalation work, read `docs/需求-统一审批中心规约.md`. - For reimbursement, invoice, OCR, duplicate invoice, or attachment work, read `docs/报销发票附件规则.md` and `references/reimbursement.md`. - For local/WeCom/public trial-run, OCR runtime, Caddy, or DB drift work, read `docs/试运行与自动化验证手册.md` and `references/trialrun.md`. @@ -37,10 +37,10 @@ Default behavior: 2. Identify or create the current Issue CSV before non-trivial work; after R5, distinguish R6 preparation from actual trial-run execution. 3. State the implementation approach before editing code; if already approved, proceed. 4. Keep changes scoped to the current Issue. -5. Prefer existing project patterns, official SDKs, and the main UI/layout hierarchy in `docs/产品设计规则.md`; do not mix multiple competitor visual systems or copy competitor flows/text directly. +5. Prefer existing project patterns, official SDKs, and the main UI/layout hierarchy in `docs/产品设计规则.md`; for page work, classify the page into DG9 A/B/C layering and reuse the matching workbench/mobile sample before adding local layout code. Do not mix multiple competitor visual systems or copy competitor flows/text directly. 6. Verify with the narrowest relevant automated tests, then expand by risk. 7. Use Playwright screenshots or visual metrics for PC/mobile/WeCom UI changes. New or changed mobile pages must join the unified responsive system and the 320/360/375/390/393/414/430px 统一视觉矩阵 rather than adding page-specific layout fixes. -8. Dedicated `/mobile` pages must use the shared mobile slot/card/list/sticky-action system from `docs/产品设计规则.md`: fixed icon/avatar/status slots, `minmax(0, 1fr)` copy slots, `mobile-card`/mobile list cards, mobile tokens, and no desktop `card-std p-4` as a mobile main container. +8. Dedicated `/mobile` pages must use the shared mobile slot/card/list/sticky-action system from `docs/产品设计规则.md`: fixed icon/avatar/status slots, `minmax(0, 1fr)` copy slots, `mobile-card`/mobile list cards, `mobile-field-row`, mobile tokens, and no desktop `card-std p-4/p-5/p-6` as a mobile main container. 9. Mobile homepage, approval, notification, and business cards must follow the display dedupe rules in `docs/产品设计规则.md`: one pending/progress summary per page, notifications as event stream with direct target links, bottom badges as counts only, and one visible business identifier/status per card or row. 10. Update Issue CSV statuses only after the stated `Test_Method` passes. 11. Commit in Chinese batches with Issue IDs. diff --git a/client/src/components/patterns/ListWorkspace.tsx b/client/src/components/patterns/ListWorkspace.tsx index 844cbfbb..45b32937 100644 --- a/client/src/components/patterns/ListWorkspace.tsx +++ b/client/src/components/patterns/ListWorkspace.tsx @@ -1,22 +1,104 @@ import type { ReactNode } from "react"; import { cn } from "../../lib/utils"; -export function ListWorkspace({ className, children }: { className?: string; children: ReactNode }) { +type WorkspaceChildrenProps = { + className?: string; + children: ReactNode; +}; + +type WorkspaceHeaderProps = { + eyebrow?: string; + title: string; + subtitle?: ReactNode; + meta?: ReactNode; + actions?: ReactNode; + className?: string; +}; + +export function ListWorkspace({ className, children }: WorkspaceChildrenProps) { return
{children}
; } -export function FilterBar({ className, children }: { className?: string; children: ReactNode }) { +export function WorkspaceHeader({ eyebrow, title, subtitle, meta, actions, className }: WorkspaceHeaderProps) { + return ( +
+
+ {eyebrow ?
{eyebrow}
: null} +

{title}

+ {subtitle ?
{subtitle}
: null} + {meta ?
{meta}
: null} +
+ {actions ?
{actions}
: null} +
+ ); +} + +export function FilterBar({ className, children }: WorkspaceChildrenProps) { return
{children}
; } -export function StatStrip({ className, children }: { className?: string; children: ReactNode }) { - return
{children}
; +export function FilterGrid({ className, children }: WorkspaceChildrenProps) { + return
{children}
; } -export function ActionToolbar({ className, children }: { className?: string; children: ReactNode }) { +export function StatStrip({ + className, + columns = 3, + children, +}: WorkspaceChildrenProps & { columns?: 2 | 3 | 4 }) { + const columnClass = { + 2: "md:grid-cols-2", + 3: "md:grid-cols-3", + 4: "md:grid-cols-2 xl:grid-cols-4", + }[columns]; + return
{children}
; +} + +export function WorkspaceSection({ + title, + description, + actions, + className, + children, +}: WorkspaceChildrenProps & { + title?: string; + description?: ReactNode; + actions?: ReactNode; +}) { + return ( +
+ {title || description || actions ? ( +
+
+ {title ?

{title}

: null} + {description ?
{description}
: null} +
+ {actions ?
{actions}
: null} +
+ ) : null} + {children} +
+ ); +} + +export function ActionToolbar({ className, children }: WorkspaceChildrenProps) { return
{children}
; } -export function PrimaryTable({ className, children }: { className?: string; children: ReactNode }) { +export function PrimaryTable({ className, children }: WorkspaceChildrenProps) { return
{children}
; } + +export function WorkspaceEmptyState({ + className, + children = "暂无数据", +}: { + className?: string; + children?: ReactNode; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/client/src/pages/ApOperationsPage.tsx b/client/src/pages/ApOperationsPage.tsx index 5b0af12b..5671e9d8 100644 --- a/client/src/pages/ApOperationsPage.tsx +++ b/client/src/pages/ApOperationsPage.tsx @@ -41,7 +41,7 @@ import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; import { ConfirmDialog } from "../components/common/ConfirmDialog"; -import { PageHeader } from "../components/common/PageHeader"; +import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { Button } from "../components/ui/button"; import { Dialog, @@ -605,12 +605,12 @@ export function ApOperationsPage() { }; if (summaryQuery.isLoading && paymentBatchesQuery.isLoading && riskFlagsQuery.isLoading && financeLedgerQuery.isLoading) { - return
加载财务工作台...
; + return
加载财务工作台...
; } return ( -
- +
-
+
setActiveTab(value as OperationsTab)}> 财务台账 {financeLedgerQuery.data?.total ?? 0} @@ -1630,6 +1630,6 @@ export function ApOperationsPage() { await executeMutation.mutateAsync(retryTarget.batchId); }} /> -
+ ); } diff --git a/client/src/pages/ApprovalRulesPage.tsx b/client/src/pages/ApprovalRulesPage.tsx index b7bcb402..0d2b5377 100644 --- a/client/src/pages/ApprovalRulesPage.tsx +++ b/client/src/pages/ApprovalRulesPage.tsx @@ -10,7 +10,7 @@ import { type ApprovalRoleCode, type ApprovalRule, } from "../api/approvalRules"; -import { PageHeader } from "../components/common/PageHeader"; +import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { Button } from "../components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../components/ui/dialog"; import { Input } from "../components/ui/input"; @@ -144,8 +144,8 @@ export function ApprovalRulesPage() { }, [editOpen, editRule]); return ( -
- + ) : null} -
+

新建审批规则

@@ -286,7 +286,7 @@ export function ApprovalRulesPage() {
-
+

规则列表

{listQuery.isLoading ? : null} @@ -304,7 +304,7 @@ export function ApprovalRulesPage() { ) : null} {!listQuery.isLoading && !listQuery.isError ? ( -
+ @@ -371,7 +371,7 @@ export function ApprovalRulesPage() { )}
-
+ ) : null}
@@ -516,7 +516,6 @@ export function ApprovalRulesPage() { -
+ ); } - diff --git a/client/src/pages/AssetRegistryPage.tsx b/client/src/pages/AssetRegistryPage.tsx index 6287fd25..9da76f46 100644 --- a/client/src/pages/AssetRegistryPage.tsx +++ b/client/src/pages/AssetRegistryPage.tsx @@ -27,10 +27,10 @@ import { fetchCostCenters, fetchDocumentCategories, fetchLegalEntities } from ". import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission, hasPermission } from "../auth/permissions"; -import { PageHeader } from "../components/common/PageHeader"; import { AiSuggestionCard } from "../components/ai/AiSuggestionCard"; import { StatusBadge } from "../components/common/StatusBadge"; import { TableSkeleton } from "../components/common/TableSkeleton"; +import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { SectionPanel } from "../components/patterns/SectionPanel"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; @@ -578,8 +578,8 @@ export function AssetRegistryPage() { }; return ( -
- +
-
+

资产分类

{canManage ? ( @@ -669,7 +669,7 @@ export function AssetRegistryPage() {
暂无资产分类
)}
-
+ @@ -691,13 +691,13 @@ export function AssetRegistryPage() { ))}
-
+ ) : null}
-
+
仅停用
-
+ {canManage ? ( -
+

新建法体

@@ -377,10 +377,11 @@ export function MasterDataPage() {
) : null} -
+

法体列表

{legalEntitiesQuery.isLoading ? : null} {!legalEntitiesQuery.isLoading ? ( + @@ -431,12 +432,13 @@ export function MasterDataPage() { )}
+
) : null}
-
+
-
+ {canManage ? ( -
+

新建单据分类

@@ -693,7 +697,7 @@ export function MasterDataPage() {
) : null} -
+

单据分类列表

{documentCategoriesQuery.isLoading ? : null} {!documentCategoriesQuery.isLoading ? ( @@ -790,6 +794,6 @@ export function MasterDataPage() { } }} /> -
+ ); } diff --git a/client/src/pages/MobileApprovalsPage.tsx b/client/src/pages/MobileApprovalsPage.tsx index 23e83240..282deabc 100644 --- a/client/src/pages/MobileApprovalsPage.tsx +++ b/client/src/pages/MobileApprovalsPage.tsx @@ -999,11 +999,11 @@ export function MobileApprovalsPage() { ) : null} {!isPushDetail && !canAccessRemoteApprovals ? ( -
-
-
-

我发起的进度

-

报销、请假、采购进度

+
+
+
+

我发起的进度

+

报销、请假、采购进度

{employeeProgressCards.length} 条
@@ -1028,7 +1028,7 @@ export function MobileApprovalsPage() {
暂无申请进度
) : null} -
+
{employeeProgressCards.map((item) => ( -
+
-
{item.title}
- {item.subtitle ?
{item.subtitle}
: null} +
{item.title}
+ {item.subtitle ?
{item.subtitle}
: null}
- + + +
))} @@ -1051,13 +1053,13 @@ export function MobileApprovalsPage() { ) : null} {!isPushDetail && canAccessRemoteApprovals ? ( -
-
-
-

{listTitle}

-

{filteredTaskCards.length} 条

+
+
+
+

{listTitle}

+

{filteredTaskCards.length} 条

- {isOnline ? "在线" : "离线"} + {isOnline ? "在线" : "离线"}
@@ -1195,7 +1197,7 @@ export function MobileApprovalsPage() {

) : null} -
+
{filteredTaskCards.map((task) => { const isSelected = selectedTaskId === task.id; const isBatchSelected = selectedBatchTaskIds.includes(task.id); @@ -1204,7 +1206,7 @@ export function MobileApprovalsPage() { return (
@@ -1221,12 +1223,14 @@ export function MobileApprovalsPage() { data-testid={`mobile-approvals-task-${task.id}`} onClick={() => toggleBatchTask(task.id)} > -
-
{task.title}
- +
+
{task.title}
+ + +
-
{task.subtitle}
-
+
{task.subtitle}
+
{joinBusinessMeta([task.applicantName ? `申请人:${task.applicantName}` : null, task.metric ? `金额/天数:${task.metric}` : null]) || "暂无摘要"}
{task.isCached ?
来源:离线缓存
: null} @@ -1245,12 +1249,14 @@ export function MobileApprovalsPage() { onClick={() => handleTaskSelection(task.id)} type="button" > -
-
{task.title}
- +
+
{task.title}
+ + +
-
{task.subtitle}
-
+
{task.subtitle}
+
{joinBusinessMeta([task.applicantName ? `申请人:${task.applicantName}` : null, task.metric ? `金额/天数:${task.metric}` : null]) || "暂无摘要"}
{task.isCached ?
来源:离线缓存
: null} @@ -1262,8 +1268,8 @@ export function MobileApprovalsPage() { ) : null} {isPushDetail ? ( -
-

处理

+
+

处理

{!selectedTaskId ?

请先选择任务。

: null} @@ -1286,12 +1292,14 @@ export function MobileApprovalsPage() { {selectedTask ? ( <>
-
+
-
{selectedDetailTitle}
-
{joinBusinessMeta([`节点:${selectedTask.task.stepName}`, `创建:${formatDateTime(selectedTask.task.createdAt)}`])}
+
{selectedDetailTitle}
+
{joinBusinessMeta([`节点:${selectedTask.task.stepName}`, `创建:${formatDateTime(selectedTask.task.createdAt)}`])}
- + + +
@@ -1343,10 +1351,10 @@ export function MobileApprovalsPage() { ) : null}
-
-
-

处理历史

- {selectedTask.history.length} 条 +
+
+

处理历史

+ {selectedTask.history.length} 条
{selectedTask.history.length ? ( selectedTask.history.map((item) => ( @@ -1381,7 +1389,7 @@ export function MobileApprovalsPage() {

离线仅保存通过 / 驳回草稿,联网同步成功后才会正式提交。

) : null} -
+
- } - /> +
+ 未读 {listQuery.data?.unreadCount ?? 0}} + actions={ + + } + /> -
-
+
+
-
-
+
-
+

采购单列表

共 {listQuery.data?.total ?? 0} 条 @@ -952,7 +955,7 @@ export function PurchaseRequestsPage() {
暂无采购申请
)}
-
+ @@ -1002,7 +1005,7 @@ export function PurchaseRequestsPage() { )}
-
+ ) : null} @@ -1014,7 +1017,7 @@ export function PurchaseRequestsPage() { />
-
+
{!selectedId ? (
请先在左侧选择采购单,或新建一张采购草稿。 @@ -1096,7 +1099,7 @@ export function PurchaseRequestsPage() {
附件
-
{detail.attachments.length} 个
+
{detailAttachments.length} 个
@@ -1116,12 +1119,12 @@ export function PurchaseRequestsPage() {
- + - {detail.approvalHistory.length ? ( + {detailApprovalHistory.length ? (
- {detail.approvalHistory.map((item) => ( + {detailApprovalHistory.map((item) => (
@@ -1515,8 +1518,8 @@ export function PurchaseRequestsPage() {
- {detail.attachments.length ? ( - detail.attachments.map((attachment) => ( + {detailAttachments.length ? ( + detailAttachments.map((attachment) => (
{attachment.originalName}
{attachment.mimeType} / {attachment.fileSize} bytes
@@ -1563,8 +1566,8 @@ export function PurchaseRequestsPage() { - {detail.attachments.length ? ( - detail.attachments.map((attachment) => ( + {detailAttachments.length ? ( + detailAttachments.map((attachment) => (
{attachment.originalName}
@@ -1754,6 +1757,6 @@ export function PurchaseRequestsPage() { ) : null}
-
+ ); } diff --git a/client/src/pages/ReimbursementDetailPage.tsx b/client/src/pages/ReimbursementDetailPage.tsx index 1d80cc6a..b698d8c8 100644 --- a/client/src/pages/ReimbursementDetailPage.tsx +++ b/client/src/pages/ReimbursementDetailPage.tsx @@ -22,7 +22,6 @@ import { withdrawReimbursement, linkReimbursementProject, type ReimbursementItem, - type ReimbursementStatus, } from "../api/reimbursements"; import { remindWorkflowTask } from "../api/workflow"; import { @@ -51,6 +50,8 @@ import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { DetailSideMeta } from "../components/patterns/DetailSideMeta"; import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero"; +import { StatStrip } from "../components/patterns/ListWorkspace"; +import { MetricCard } from "../components/patterns/MetricCard"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { SectionPanel } from "../components/patterns/SectionPanel"; import { @@ -73,15 +74,6 @@ import { buildPrecheckDialogMessage } from "../lib/precheck"; type ItemEditorState = { mode: "create" } | { mode: "edit"; itemId: string } | null; type SelectedInvoiceMap = Record; -const REIMBURSEMENT_STATUS_LABEL: Record = { - draft: "草稿", - reviewing: "审批中", - approved: "已通过", - paid: "已支付", - rejected: "已驳回", - cancelled: "已取消", -}; - function formatDateTime(value: string | null) { if (!value) return "-"; return value.slice(0, 19).replace("T", " "); @@ -99,10 +91,6 @@ function buildActionErrorMessage(error: unknown, fallbackMessage: string) { return fallbackMessage; } -function reimbursementStatusLabel(status: ReimbursementStatus) { - return REIMBURSEMENT_STATUS_LABEL[status] ?? "未知状态"; -} - function workflowActionLabel(action: string) { const labels: Record = { start: "提交", @@ -1009,27 +997,24 @@ export function ReimbursementDetailPage() {
-
-
-
当前状态
-
-
-
-
关联项目
-
{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
-
-
-
明细数
-
{detail.items.length}
-
-
-
发票
-
{detail.invoices.length}
-
-
+ + + {detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"} + + } + /> + + + {isPaidReimbursement ? "已付款" : "未付款"}} + /> +
-
状态:{reimbursementStatusLabel(detail.status)}
报销编号:{displayCode}
报销日期:{detail.reimbursementDate ?? "-"}
金额:{detail.totalAmount ?? "-"}
diff --git a/client/src/pages/ReimbursementsPage.tsx b/client/src/pages/ReimbursementsPage.tsx index ad7ffa42..d2ab8247 100644 --- a/client/src/pages/ReimbursementsPage.tsx +++ b/client/src/pages/ReimbursementsPage.tsx @@ -25,11 +25,10 @@ import { hasPermission } from "../auth/permissions"; import { toast } from "sonner"; import { downloadBlob } from "../lib/download"; import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay"; -import { PageHeader } from "../components/common/PageHeader"; import { Pagination } from "../components/common/Pagination"; import { StatusBadge } from "../components/common/StatusBadge"; import { TableSkeleton } from "../components/common/TableSkeleton"; -import { FilterBar, ListWorkspace, PrimaryTable } from "../components/patterns/ListWorkspace"; +import { FilterBar, ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { MetricCard } from "../components/patterns/MetricCard"; import { Button } from "../components/ui/button"; import { @@ -362,7 +361,7 @@ export function ReimbursementsPage() { return ( - ) : null} -
+ 0 ? "warning" : "neutral"} /> -
+
diff --git a/client/src/pages/SystemSettingsPage.tsx b/client/src/pages/SystemSettingsPage.tsx index c924ea9f..85c7d418 100644 --- a/client/src/pages/SystemSettingsPage.tsx +++ b/client/src/pages/SystemSettingsPage.tsx @@ -1,5 +1,5 @@ import { Link } from "react-router-dom"; -import { PageHeader } from "../components/common/PageHeader"; +import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace"; const ITEMS = [ { to: "/system/workspace", title: "工作区设置", description: "调整小团队模式的导航与模块显隐。" }, @@ -14,17 +14,17 @@ const ITEMS = [ export function SystemSettingsPage() { return ( -
- + +
{ITEMS.map((item) => ( - +
{item.title}
{item.description}
))}
-
+ ); } diff --git a/client/src/pages/WorkflowDefinitionsPage.tsx b/client/src/pages/WorkflowDefinitionsPage.tsx index af5f76a9..c445d898 100644 --- a/client/src/pages/WorkflowDefinitionsPage.tsx +++ b/client/src/pages/WorkflowDefinitionsPage.tsx @@ -8,7 +8,7 @@ import { updateWorkflowDefinition, type WorkflowDefinitionEditorModel, } from "../api/workflowDefinitions"; -import { PageHeader } from "../components/common/PageHeader"; +import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; @@ -79,10 +79,10 @@ export function WorkflowDefinitionsPage() { }); return ( -
- + +
-
+
updateSearchParams({ entityType: event.target.value as WorkflowEntityFilter, page: 1 })} @@ -252,7 +260,8 @@ export function WorkflowTasksPage() { ))}
-
+ + {query.isLoading ? : null} @@ -270,24 +279,36 @@ export function WorkflowTasksPage() { {!query.isLoading && !query.isError ? ( <> -
+
{taskItems.length ? ( taskItems.map((task) => ( -
-
+
+
-
{displayTitle(task)}
-
+
{displayTitle(task)}
+
{entityLabel(task.workflowInstance?.entityType)} · {displayReference(task)}
- +
-
-
申请人:{displayApplicant(task)}
-
金额/天数:{displayMetric(task)}
-
当前节点:{task.stepName}
-
业务状态:{businessStatusLabel(task.businessSummary?.businessStatus)}
+
+
+ 申请人 + {displayApplicant(task)} +
+
+ 金额/天数 + {displayMetric(task)} +
+
+ 当前节点 + {task.stepName} +
+
+ 业务状态 + {businessStatusLabel(task.businessSummary?.businessStatus)} +
{status === "pending" && (task.allowedActions ?? []).includes("approve") ? ( @@ -317,13 +338,11 @@ export function WorkflowTasksPage() {
)) ) : ( -
- 暂无数据 -
+ )}
-
+ @@ -381,14 +400,14 @@ export function WorkflowTasksPage() { )) ) : ( - + 暂无数据 )}
-
+ ) : null} @@ -398,7 +417,6 @@ export function WorkflowTasksPage() { onPageChange={(p) => updateSearchParams({ page: p })} testIdPrefix="workflow-tasks" /> -
-
+ ); } diff --git a/client/src/styles/mobile-touch.css b/client/src/styles/mobile-touch.css index 08fa8f67..4e1d58ed 100644 --- a/client/src/styles/mobile-touch.css +++ b/client/src/styles/mobile-touch.css @@ -222,6 +222,72 @@ padding: var(--mobile-card-padding); } +.mobile-page-stack { + display: grid; + gap: var(--mobile-card-gap); + min-width: 0; +} + +.mobile-section-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 0.75rem; + min-width: 0; +} + +.mobile-section-title { + min-width: 0; + color: rgb(24 24 27); + font-size: 0.95rem; + font-weight: 700; + line-height: 1.35rem; +} + +.mobile-section-meta { + flex: none; + color: rgb(113 113 122); + font-size: 0.75rem; + line-height: 1rem; +} + +.mobile-list-card__header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: start; + min-width: 0; +} + +.mobile-list-card__title { + min-width: 0; + overflow: hidden; + color: rgb(24 24 27); + font-size: 0.9rem; + font-weight: 700; + line-height: 1.35rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mobile-list-card__subtitle { + min-width: 0; + overflow: hidden; + color: rgb(113 113 122); + font-size: 0.75rem; + line-height: 1.05rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mobile-status-slot { + flex: none; + max-width: 42%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .mobile-subcard { min-width: 0; border: 1px solid rgb(228 228 231); @@ -299,6 +365,17 @@ padding: 0.75rem var(--mobile-page-x) max(0.75rem, var(--mobile-safe-area-bottom)); } +.mobile-sticky-action-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr)); + gap: 0.5rem; + min-width: 0; +} + +.mobile-sticky-action-grid > * { + min-height: 46px; +} + @media (max-width: 359px) { :root { --mobile-page-x: 8px; diff --git a/client/tests/e2e/project-wide-visual.spec.ts b/client/tests/e2e/project-wide-visual.spec.ts index 5414742d..3a8abd8d 100644 --- a/client/tests/e2e/project-wide-visual.spec.ts +++ b/client/tests/e2e/project-wide-visual.spec.ts @@ -1,5 +1,5 @@ import { execSync } from "node:child_process"; -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { expect, test, type Browser, type Page } from "@playwright/test"; @@ -30,7 +30,11 @@ type PreparedRoutes = { }; const ROOT_DIR = path.resolve(process.cwd(), ".."); -const MATRIX_PATH = path.join(ROOT_DIR, "docs/trialrun/project-wide-flow-matrix.csv"); +const MATRIX_PATH_CANDIDATES = [ + path.join(ROOT_DIR, "docs/trialrun/项目级流程覆盖矩阵.csv"), + path.join(ROOT_DIR, "docs/trialrun/project-wide-flow-matrix.csv"), +]; +const MATRIX_PATH = MATRIX_PATH_CANDIDATES.find((candidate) => existsSync(candidate)) ?? MATRIX_PATH_CANDIDATES[0]; const ISSUE_ID = process.env.QA_ISSUE_ID?.trim() || "project-wide-visual"; const BASE_URL = process.env.E2E_BASE_URL?.trim() || null; const SCREENSHOT_DIR = resolveRootPath( diff --git a/client/tests/e2e/reimbursement-workflow-progress.spec.ts b/client/tests/e2e/reimbursement-workflow-progress.spec.ts index d1f338e2..e089c5ea 100644 --- a/client/tests/e2e/reimbursement-workflow-progress.spec.ts +++ b/client/tests/e2e/reimbursement-workflow-progress.spec.ts @@ -1,43 +1,77 @@ +import type { APIRequestContext } from "@playwright/test"; + import { test, expect } from "./helpers/fixtures"; -import { EMPLOYEE, login, logout } from "./helpers/auth"; +import { EMPLOYEE, createAuthenticatedApiClient, login, logout } from "./helpers/auth"; import { openNavLink } from "./helpers/navigation"; +async function firstEnabledExpenseTypeId(api: APIRequestContext) { + const response = await api.get("/api/expense-types?enabled=true"); + const payload = await response.json() as { data?: { items?: Array<{ id: string; enabled?: boolean }> } }; + const item = payload.data?.items?.find((candidate) => candidate.enabled !== false) ?? payload.data?.items?.[0]; + if (!response.ok() || !item?.id) { + throw new Error(`无法读取费用类型:HTTP ${response.status()}`); + } + return item.id; +} + +async function createReimbursementItem(api: APIRequestContext, reimbursementId: string) { + const expenseTypeId = await firstEnabledExpenseTypeId(api); + const response = await api.post(`/api/reimbursements/${reimbursementId}/items`, { + data: { + expenseTypeId, + expenseDate: "2026-05-13", + amount: "168.30", + description: "E2E 报销审批进度明细", + }, + }); + if (!response.ok()) { + throw new Error(`创建报销明细失败:HTTP ${response.status()}`); + } +} + test("E2E 报销审批进度:提交后详情页展示当前步骤进度", async ({ page, reimbursementDetail }) => { + const apiClient = await createAuthenticatedApiClient(EMPLOYEE); await login(page, EMPLOYEE); - await (await openNavLink(page, "我的报销")).click(); - await expect(page).toHaveURL(/\/reimbursements/); - await expect(page.getByTestId("reimbursements-create")).toBeVisible(); + try { + await (await openNavLink(page, "我的报销")).click(); + await expect(page).toHaveURL(/\/reimbursements/); + await expect(page.getByTestId("reimbursements-create")).toBeVisible(); - const createResponsePromise = page.waitForResponse((response) => { - const url = new URL(response.url()); - return response.request().method() === "POST" && url.pathname === "/api/reimbursements" && response.ok(); - }); - await page.getByTestId("reimbursements-create").click(); - await page.getByRole("menuitem", { name: "空白报销" }).click(); - const createPayload = (await (await createResponsePromise).json()) as { data?: { id?: string } }; - const reimbursementId = createPayload.data?.id; - if (!reimbursementId) { - throw new Error(`创建报销单后未返回 id: ${JSON.stringify(createPayload)}`); + const createResponsePromise = page.waitForResponse((response) => { + const url = new URL(response.url()); + return response.request().method() === "POST" && url.pathname === "/api/reimbursements" && response.ok(); + }); + await page.getByTestId("reimbursements-create").click(); + await page.getByRole("menuitem", { name: "空白报销" }).click(); + const createPayload = (await (await createResponsePromise).json()) as { data?: { id?: string } }; + const reimbursementId = createPayload.data?.id; + if (!reimbursementId) { + throw new Error(`创建报销单后未返回 id: ${JSON.stringify(createPayload)}`); + } + + await createReimbursementItem(apiClient.api, reimbursementId); + + await page.goto(`/reimbursements/${reimbursementId}`); + await reimbursementDetail.assertLoaded(); + + const marker = `MOCK_E2E_${Date.now()}_${Math.random().toString(16).slice(2)}`; + await reimbursementDetail.uploadInvoicePdf( + `progress-${Date.now()}.pdf`, + Buffer.from(`%PDF-1.4\n${marker}\n%%EOF\n`, "utf8"), + ); + + await page.goto(`/reimbursements/${reimbursementId}`); + await reimbursementDetail.assertLoaded(); + await reimbursementDetail.submitReimbursement(); + + await expect(page.getByText(/reviewing|审批中/).first()).toBeVisible({ timeout: 30_000 }); + await expect(reimbursementDetail.workflowProgress).toBeVisible({ timeout: 30_000 }); + await expect(reimbursementDetail.workflowProgress).toContainText("当前节点"); + await expect(reimbursementDetail.workflowProgress).toContainText(/第 1 步|第 2 步|第 3 步/); + + await logout(page, EMPLOYEE.name); + } finally { + await apiClient.dispose(); } - - await page.goto(`/reimbursements/${reimbursementId}`); - await reimbursementDetail.assertLoaded(); - - const marker = `MOCK_E2E_${Date.now()}_${Math.random().toString(16).slice(2)}`; - await reimbursementDetail.uploadInvoicePdf( - `progress-${Date.now()}.pdf`, - Buffer.from(`%PDF-1.4\n${marker}\n%%EOF\n`, "utf8"), - ); - - await page.goto(`/reimbursements/${reimbursementId}`); - await reimbursementDetail.assertLoaded(); - await reimbursementDetail.submitReimbursement(); - - await expect(page.getByText(/状态:(reviewing|审批中)/)).toBeVisible({ timeout: 30_000 }); - await expect(reimbursementDetail.workflowProgress).toBeVisible({ timeout: 30_000 }); - await expect(reimbursementDetail.workflowProgress).toContainText("当前节点"); - await expect(reimbursementDetail.workflowProgress).toContainText(/第 1 步|第 2 步|第 3 步/); - - await logout(page, EMPLOYEE.name); }); diff --git a/docs/Agent开发交付实践.md b/docs/Agent开发交付实践.md index 4990491a..cac03ef9 100644 --- a/docs/Agent开发交付实践.md +++ b/docs/Agent开发交付实践.md @@ -82,6 +82,13 @@ DG8 之后,页面视觉和布局治理只有一个主 UI 参考:飞书工作 - 审批:待办聚合、明确状态、可追溯、可催办、超时提醒和效率统计。AP6 v1 已提供催办、待办摘要和超时提醒 job;后续审批/通知/催办开发必须先核对统一审批中心规约,复用既有接口、审计和通知语义,不另造平行链路。 - 企业微信:入口、身份、个人应用消息和 deeplink,不替代 OA 正式审批。 +DG9 之后,页面任务必须先按 `docs/产品设计规则.md` 的 A/B/C 页面分层选样板,再开始设计或编码: + +- A 类核心流程先对齐审批中心、移动审批/进度、报销详情、首页和通知样板。 +- B 类业务运营按 A 类样板迁移,但不得顺手新增移动采购创建、恢复冻结差旅主线或改变财务/OCR/项目接口。 +- C 类管理配置保持中后台高密度布局;只在授权管理或审计场景展示必要内部编号。 +- PC 页面优先复用 `WorkspaceHeader`、指标条、`FilterBar`、主表格和分页;移动页面优先复用移动槽位、`mobile-list-card`、`mobile-field-row` 和吸底操作。 + 可自动化的体验检查先自动化,例如 Playwright PC、移动、企业微信 UA 和 DOM 指标;真实企业微信登录、手机应用消息到达、真实通知权限和财务制度决策必须保留人工确认记录。 移动页治理必须收敛到同一套事实源和组件体系: diff --git a/docs/产品设计规则.md b/docs/产品设计规则.md index a0d18d78..af6d4717 100644 --- a/docs/产品设计规则.md +++ b/docs/产品设计规则.md @@ -41,6 +41,24 @@ DG8 之后,hyb-oa 的唯一主 UI/布局参考是飞书工作台/飞书审批 - 详情页是业务事实源,PC 按摘要、关键字段、附件/发票、审批流和日志分区;移动深层 Push 页隐藏底部 Tab,使用顶部返回、状态和吸底操作。 - 竞品资料只用于抽象原则;不得复制飞书、钉钉、企业微信、泛微、每刻、合思、WeUI 或 Ant Design 的页面文案、视觉细节和专有交互。 +## DG9 页面分层与样板 + +DG9 之后,页面整改以“共享样板先行、核心流程优先、低频配置后置”为执行口径。DG9 只承接 UI/布局统一,不替代 WF8 审批通知验收、不替代 UX8 真实反馈池、不替代 BIZ8 财务/OCR/项目业务验收,也不代表已启动部署。 + +页面分层如下: + +- A 类核心流程:审批中心、审批/进度移动页、报销列表、报销详情、移动报销详情、首页、通知中心。这些页面优先使用 DG9 工作台骨架和移动槽位,是后续页面整改的样板。 +- B 类业务运营:票据池、AP 台账、付款批次、请假、采购、资产、项目、项目详情等。按 A 类样板迁移,但不新增移动采购创建、不恢复冻结差旅主线、不改变 OCR、付款、导出或项目关联接口。 +- C 类管理配置:用户、组织、主数据、审批规则、工作流定义、系统设置、Feature Flag、审计等。保持中后台高密度布局,允许在授权管理场景展示必要内部编号或审计字段,但不改变权限模型、审批规则语义或路由。 + +DG9 样板规则: + +- PC 列表页使用统一工作台骨架:`WorkspaceHeader`、指标条、`FilterBar`、主表格、分页和批量/导出操作区。 +- PC 详情页使用统一事实源骨架:顶部摘要、关键字段、材料/附件/发票、审批流、日志和主操作区;同一状态或业务编号不得在同一区域重复展示。 +- 移动页使用统一移动槽位:`mobile-card`、`mobile-list-card`、固定状态槽、`mobile-field-row`、安全区和吸底操作,不把桌面 `card-std p-4/p-5/p-6` 主容器回流到专用 `/mobile` 页面。 +- 通知仍是事件流,首页只保留一处待办或进度摘要;企业微信继续只承担入口、免登录、个人应用消息和 deeplink。 +- 后续页面任务必须先判断页面属于 A/B/C 哪一层,再选择 DG9 样板,不再临时混用多个竞品视觉风格。 + 参考资料: - 钉钉审批:https://page.dingtalk.com/wow/dingtalk/act/approval-web diff --git a/docs/文档事实源状态矩阵.md b/docs/文档事实源状态矩阵.md index aa4fa4ac..f69b60aa 100644 --- a/docs/文档事实源状态矩阵.md +++ b/docs/文档事实源状态矩阵.md @@ -29,6 +29,7 @@ | --- | --- | --- | --- | | `GOV8` | `issues/2026-05-13_16-31-09-gov8-system-governance.csv` | `EVIDENCE` | 连续型 Issue 治理、文档台账、流程模板和审计脚本,已收口 | | `DG8` | `issues/2026-05-13_22-56-49-design-ui-governance.csv` | `EVIDENCE` | 设计治理主参考系统收口;飞书工作台/飞书审批为唯一主 UI/布局参考,不代表页面已整改 | +| `DG9` | `issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv` | `CURRENT` | 全站 UI/布局样板迁移;A/B/C 页面分层、共享工作台骨架和移动槽位,不替代 WF8/UX8/BIZ8 | | `R7E` | `issues/2026-05-13_17-11-44-r7e-human-trialrun.csv` | `CURRENT` | 真实扩大试运行人工确认和执行;R7E-1..7 完成前不得启动真实 R7 | | `ORG8` | `issues/2026-05-13_18-23-51-org8-master-data-permission.csv` | `EVIDENCE` | 组织账号和权限主数据治理已阶段收口;本地代表性角色验收通过,不替代 R7E 真实人工试运行 | | `WF8` | `issues/2026-05-13_18-23-52-wf8-approval-notification.csv` | `CURRENT` | 审批与通知增强;移动审批历史展示方案批准后继续执行 | @@ -45,7 +46,7 @@ | `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8` | `audit-docs` | none | 2026-05-13 | `audit-docs --check-registry` | medium | 判断文档权威性 | | `PRD-MVP` | `docs/海音达OA-MVP需求规格说明书.md` | 需求事实源 | PRD 主事实源 | `CURRENT` | product | `REQ8,P2T,GOV8` | Issue CSV | `docs/requirements-specification.md` | 2026-05-13 | `rg` | high | 定义做什么、不做什么和验收口径 | | `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T` | `rg` | none | 2026-05-13 | `rg` | high | 记录已拍板产品边界 | -| `DESIGN-UI` | `docs/产品设计规则.md` | 设计事实源 | PC/移动/企业微信设计事实源 | `CURRENT` | design/frontend | `MIA7,MIF7,GOV8,DG8` | Playwright/视觉审查/DG8 | none | 2026-05-13 | `rg` | high | 页面改动必须读取;DG8 后飞书为主 UI/布局参考 | +| `DESIGN-UI` | `docs/产品设计规则.md` | 设计事实源 | PC/移动/企业微信设计事实源 | `CURRENT` | design/frontend | `MIA7,MIF7,GOV8,DG8,DG9` | Playwright/视觉审查/DG8/DG9 | none | 2026-05-13 | `rg` | high | 页面改动必须读取;DG8 后飞书为主 UI/布局参考;DG9 后按页面分层和样板选型 | | `DELIVERY` | `docs/开发交付与变更管理规约.md` | 开发治理 | 开发交付事实源 | `CURRENT` | dev/qa | `GOV8` | `rg` | none | 2026-05-13 | `rg` | high | Issue、review、commit 和交付规则 | | `QA-GATE` | `docs/测试规约与回归门禁.md` | 测试验收 | 测试和回归门禁事实源 | `CURRENT` | qa | `QAF8,GOV8` | 回归脚本/Playwright | none | 2026-05-13 | `rg` | high | 测试计划和 DONE 门禁 | | `AGENT-PRACTICE` | `docs/Agent开发交付实践.md` | Agent 治理 | Agent 协作事实源 | `CURRENT` | dev | `GOV8,DG8` | Skill 校验 | none | 2026-05-13 | `rg` | medium | Agent 协作开发实践和设计主参考层级 | diff --git a/docs/文档索引.md b/docs/文档索引.md index 29044d96..ec02e230 100644 --- a/docs/文档索引.md +++ b/docs/文档索引.md @@ -19,13 +19,14 @@ ## 最近事实源链 -R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8。 +R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9。 - R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。 - ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。 - QG6 已完成 QA 治理升级。 - MIA7 已完成移动信息架构、视觉和流程审查;MIF7-1..7 与 MIF7T 已完成本地整改、公网部署、公网匿名复检和带凭据 live smoke/OCR 复检。 - DG8 已完成设计治理主参考系统收口:飞书工作台/飞书审批是唯一主 UI/布局参考;本结论只更新设计事实源,不代表已做页面整改。 +- DG9 承接 DG8 后的页面整改:按 A/B/C 页面分层和 DG9 样板统一 PC、移动和企业微信入口布局;不替代 WF8、UX8 或 BIZ8 业务验收。 - R7/OPS7/DOC7 是准备材料,不代表已上线或已启动真实 R7。 ## 当前活跃治理流 @@ -36,6 +37,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M | --- | --- | --- | --- | --- | | `GOV8` | `issues/2026-05-13_16-31-09-gov8-system-governance.csv` | 已收口 | 文档分类、台账、流程模板、审计脚本、需求冻结、变更控制 | 不改业务代码、不部署 | | `DG8` | `issues/2026-05-13_22-56-49-design-ui-governance.csv` | 已收口:设计事实源主参考系统更新 | 主 UI/布局参考层级、外部参考降级、Agent/Skill 设计入口、设计检查门禁 | 不改业务页面、不部署、不接入飞书 | +| `DG9` | `issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv` | 当前执行:全站 UI/布局样板迁移 | A/B/C 页面分层、共享工作台骨架、移动槽位、审批/报销/首页/通知样板、设计门禁 | 不改 server、不改 API/schema/权限/状态机、不部署 | | `R7E` | `issues/2026-05-13_17-11-44-r7e-human-trialrun.csv` | 当前执行:等待人工确认 | 人工确认、真实试运行、每日问题收口、验收结论 | 不新增 P2 能力 | | `ORG8` | `issues/2026-05-13_18-23-51-org8-master-data-permission.csv` | 已收口:本地代表性角色验收通过 | 组织、账号、直属主管、停用、角色、企业微信绑定、权限边界 | 不做审批流程增强;不替代 R7E 真实人工试运行 | | `WF8` | `issues/2026-05-13_18-23-52-wf8-approval-notification.csv` | 当前执行:待移动审批历史方案批准 | 转审、通知、催办、邮件通道决策、三渠道验收 | 不做复杂 BPM 或审批设计器 | @@ -62,7 +64,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M | --- | --- | | [海音达OA-MVP需求规格说明书](./海音达OA-MVP需求规格说明书.md) | PRD 主事实源 | | [文档事实源状态矩阵](./文档事实源状态矩阵.md) | 文档分类、删除恢复和冲突规则 | -| [产品设计规则](./产品设计规则.md) | PC、移动、企业微信 WebView 设计事实源;DG8 后飞书工作台/飞书审批为唯一主 UI/布局参考 | +| [产品设计规则](./产品设计规则.md) | PC、移动、企业微信 WebView 设计事实源;DG8 后飞书工作台/飞书审批为唯一主 UI/布局参考;DG9 后按页面分层和样板选型 | | [产品口径](./产品口径.md) | 已拍板产品边界和业务规则 | | [需求 - 统一审批中心规约](./需求-统一审批中心规约.md) | 审批、催办、超时提醒、审批效率 | | [报销发票附件规则](./报销发票附件规则.md) | 报销、发票、OCR、附件细则 | diff --git a/issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv b/issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv new file mode 100644 index 00000000..1f673778 --- /dev/null +++ b/issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv @@ -0,0 +1,14 @@ +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +DG9-0,DG9 总合同建档,建立 DG9 全站 UI/布局统一迁移合同,承接 DG8 后的页面整改,不追加到 DG8/WF8/UX8/BIZ8 旧 CSV。,1) CSV 列完整并校验通过;2) 范围限定 client/src、client/tests、scripts/qa、docs、.agents/skills、issues;3) 不改 server;4) 不部署。,流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv;人工核对 Files 不含 server。,python3|manual,DONE,DONE,DONE,issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv,[],回滚路径:删除本 CSV 和本轮 DG9 变更;不影响运行环境。2026-05-13 CSV 校验通过,Files 不含 server。 +DG9-1,全站页面审计与分层,按 DG8 主 UI/布局参考层级审计当前 PC、移动和企业微信可达页面,形成 A/B/C 分层,避免把冻结或低频能力拉入主线整改。,1) 文档索引或事实源矩阵登记 DG9;2) A/B/C 页面分层写入设计事实源;3) 明确 WF8/UX8/BIZ8 未关闭项不被 DG9 替代;4) 不新增业务能力。,流程级验证:rg -n 'DG9|A 类核心流程|B 类业务运营|C 类管理配置|不替代 WF8|不替代 UX8|不替代 BIZ8' docs/产品设计规则.md docs/文档索引.md docs/文档事实源状态矩阵.md。,rg|manual,DONE,DONE,DONE,docs/产品设计规则.md|docs/文档索引.md|docs/文档事实源状态矩阵.md,"[""DG9-0""]",2026-05-13 rg 验证 DG9、A/B/C 分层和不替代 WF8/UX8/BIZ8 均已登记;只做事实源分层。 +DG9-2,共享 PC 移动布局组件契约,复用并扩展现有 ListWorkspace、MetricCard、PageScaffold、移动 card/token,形成 PC 工作台和移动槽位的共享布局契约,不重建第二套 UI 系统。,1) PC 列表页可使用统一标题、指标、筛选、表格和空状态骨架;2) 移动页保留统一 shell、槽位、卡片、安全区和吸底操作;3) 不改变业务数据接口;4) typecheck/build 通过。,流程级验证:cd client && npm run typecheck && npm run build;人工核对共享组件不引入 server/API 依赖。,typescript|vite|manual,DONE,DONE,DONE,client/src/components/patterns|client/src/styles/mobile-touch.css,"[""DG9-1""]",2026-05-13 typecheck/build 通过;复用 ListWorkspace/MetricCard/PageScaffold 与移动 token,无 server/API 依赖。 +DG9-3,PC 审批中心样板,将 WorkflowTasksPage 迁移为高密度工作台样板,统一摘要、筛选、表格和操作区,同时保留现有 testid、审批行为和查询参数。,1) 审批中心使用统一工作台骨架;2) 待办、已办、业务类型筛选和分页行为不变;3) 快速通过和查看详情入口保留;4) workflow 相关 Playwright 或类型构建通过。,流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/workflow-quick-approve.spec.ts --workers=1 --reporter=list。,typescript|vite|playwright,DONE,DONE,DONE,client/src/pages/WorkflowTasksPage.tsx,"[""DG9-2""]",2026-05-13 workflow-quick-approve 通过;本地后端需以 APP_ORIGIN=http://127.0.0.1:5173 启动以通过 refresh origin 校验。 +DG9-4,PC 移动详情页样板,按摘要、关键字段、材料/附件/发票、审批流、日志和操作区统一审批详情、报销详情、移动报销详情和移动采购详情。,1) PC 详情页分区清楚且高密度;2) 移动深层详情隐藏底部 Tab 并使用统一返回、状态和吸底操作;3) 业务编号和状态不重复;4) 报销/审批详情回归通过。,流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/reimbursement-workflow-progress.spec.ts --workers=1 --reporter=list。,typescript|vite|playwright,DONE,DONE,DONE,client/src/pages/*DetailPage.tsx|client/src/pages/reimbursement-detail,"[""DG9-2"",""DG9-3""]",2026-05-13 reimbursement-workflow-progress 通过;测试数据补一条报销明细以符合当前发票绑定规则,页面状态按 DG9 标签断言。 +DG9-5,移动审批进度样板,将 MobileApprovalsPage 按员工进度、审批人待办、深层 Push 详情和吸底操作统一,不改离线队列、转审或历史逻辑。,1) 员工和审批人视角仍按角色裁剪;2) 审批/进度卡片使用统一移动槽位;3) 深层详情不显示底部 Tab;4) 移动矩阵关键用例通过。,流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r6-mobile-unified-design-rollout.spec.ts --workers=1 --reporter=list。,typescript|vite|playwright,DONE,DONE,DONE,client/src/pages/MobileApprovalsPage.tsx|client/src/components/mobile,"[""DG9-2"",""DG9-4""]",2026-05-13 dg7-mobile-information-dedupe 与 r6-mobile-unified-design-rollout 共 12 passed;代表性真机未执行,记录 PAUSED,不宣称真机完成。 +DG9-6,首页与通知迁移,统一 PC 首页、移动首页和通知中心为工作台摘要、事件流和 deeplink 直达结构,避免待办重复展示。,1) 首页只保留一处待办或进度摘要;2) 通知中心作为事件流,不展示审批动作副本;3) 点击通知仍直达业务详情或审批详情;4) 移动和企业微信入口不误入 PC 壳。,流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts tests/e2e/r103-cross-page-ui-consistency.spec.ts --workers=1 --reporter=list。,typescript|vite|playwright,DONE,DONE,DONE,client/src/pages/PortalHomePage.tsx|client/src/pages/MobilePortalPage.tsx|client/src/pages/MobileNotificationsPage.tsx|client/src/pages/NotificationsPage.tsx,"[""DG9-5""]",2026-05-13 r6-wecom-mobile-rendering-hardening 8 passed,r103-cross-page-ui-consistency 3 passed;不改企业微信登录/通知/deeplink 契约。 +DG9-7,报销票据财务迁移,统一报销列表、票据池、AP 台账、付款批次和财务摘要的列表、筛选、状态标签和操作密度。,1) 报销列表继续支持草稿、新建、删除、批量审批和导出;2) 票据和财务页不暴露内部术语到普通用户主视图;3) 财务敏感入口权限不变;4) 报销和财务页面构建通过。,流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/reimbursement-workflow-progress.spec.ts tests/e2e/biz8-ap-ledger-project.spec.ts --workers=1 --reporter=list。,typescript|vite|playwright,DONE,DONE,DONE,client/src/pages/ReimbursementsPage.tsx|client/src/pages/InvoicePoolPage.tsx|client/src/pages/ApOperationsPage.tsx|client/src/pages/PaymentBatchesPage.tsx,"[""DG9-4""]",2026-05-13 reimbursement-workflow-progress 与 biz8-ap-ledger-project 通过;不改 OCR、付款、导出或项目关联 API。 +DG9-8,请假采购资产项目页迁移,将现有可达的请假、采购、资产和项目页统一到 DG9 卡片、表格、详情和状态标签规则,不新增移动采购创建,不恢复冻结差旅主线。,1) 请假、采购、资产和项目页使用统一工作台或业务列表骨架;2) 移动采购仍只保留已实现边界;3) 不新增后端字段或路由;4) 相关页面构建通过。,流程级验证:cd client && npm run typecheck && npm run build;人工按请假、采购、资产、项目入口核对布局和边界。,typescript|vite|manual,DONE,DONE,DONE,client/src/pages/LeaveRequestPage.tsx|client/src/pages/PurchaseRequestsPage.tsx|client/src/pages/AssetRegistryPage.tsx|client/src/pages/ProjectsPage.tsx,"[""DG9-4""]",2026-05-13 typecheck/build 与 project-wide-visual 通过;代码核对未新增移动采购创建,差旅仍不恢复主线。 +DG9-9,管理配置页迁移,将用户、组织、审批规则、工作流定义、主数据和系统设置等配置页统一为中后台高密度布局,不改变权限。,1) 配置页使用统一标题、筛选、表格和操作区语义;2) 管理页面仍可展示必要内部 ID 或审计字段;3) 权限和路由不变;4) 构建通过。,流程级验证:cd client && npm run typecheck && npm run build;人工以管理员入口核对用户、组织、审批规则、工作流定义和主数据页面。,typescript|vite|manual,DONE,DONE,DONE,client/src/pages/*Management*|client/src/pages/ApprovalRulesPage.tsx|client/src/pages/WorkflowDefinitionsPage.tsx|client/src/pages/MasterDataPage.tsx|client/src/pages/SystemSettingsPage.tsx,"[""DG9-2""]",2026-05-13 typecheck/build 与 project-wide-visual 通过;代码核对仅做配置页布局密度统一,不改权限模型。 +DG9-10,设计检查与视觉门禁,扩展设计检查脚本和 Playwright 视觉门禁,覆盖竞品文案、移动黑色主控件、移动桌面容器回流和 DG9 样板关键页面。,1) check-ui-design-rules 可解析;2) self-test 通过;3) strict 模式 0 findings 或有明确记录;4) 视觉用例覆盖 DG9 核心页面。,流程级验证:node --check scripts/qa/check-ui-design-rules.mjs;node scripts/qa/check-ui-design-rules.mjs --self-test;node scripts/qa/check-ui-design-rules.mjs --strict;cd client && npx playwright test tests/e2e/project-wide-visual.spec.ts --workers=1 --reporter=list。,node|playwright,DONE,DONE,DONE,scripts/qa/check-ui-design-rules.mjs|client/tests/e2e,"[""DG9-5"",""DG9-7"",""DG9-8"",""DG9-9""]",2026-05-13 node --check/self-test/strict 通过,strict 0 findings;project-wide-visual 1 passed。 +DG9-11,文档 Skill 事实源同步,同步产品设计规则、文档索引、事实源矩阵和 hyb-oa Skill,记录 DG9 样板与迁移状态,要求后续页面先按 DG9 样板选型。,1) 产品设计规则记录 DG9 页面分层和样板;2) 文档索引和事实源矩阵登记 DG9;3) Skill 提醒后续页面先按 DG9 样板选型;4) Skill 校验通过。,流程级验证:python3 /home/shunli/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/hyb-oa-delivery;rg -n 'DG9|页面分层|样板|工作台骨架' docs/产品设计规则.md docs/文档索引.md docs/文档事实源状态矩阵.md .agents/skills/hyb-oa-delivery/SKILL.md。,python3|rg,DONE,DONE,DONE,docs/产品设计规则.md|docs/文档索引.md|docs/文档事实源状态矩阵.md|.agents/skills/hyb-oa-delivery/SKILL.md,"[""DG9-10""]",2026-05-13 Skill quick_validate 通过;产品设计规则、文档索引、事实源矩阵和 Skill 已同步 DG9 样板选型。 +DG9-12,总回归与阶段收口,完成 DG9 状态回填、自动化/人工验证摘要、中文提交并明确未部署。,1) CSV 状态真实回填;2) validate_issues_csv 通过;3) audit-docs registry 校验通过或记录既有 warning;4) git diff --check 通过;5) 中文 commit 引用 DG9;6) 最终说明未部署。,流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv;node scripts/qa/audit-docs.mjs --check-registry;git diff --check;人工确认未修改 server 且未部署。,python3|node|git|manual,DONE,DONE,DONE,issues/2026-05-13_23-30-00-dg9-ui-layout-rollout.csv|client/src|client/tests|docs|scripts/qa,"[""DG9-11""]",2026-05-13 validate_issues_csv、audit-docs、git diff --check 通过;audit-docs 保留既有 unregistered docs files: 24 warning;未部署。 diff --git a/scripts/qa/check-ui-design-rules.mjs b/scripts/qa/check-ui-design-rules.mjs index 1d10d30e..753d0bd5 100644 --- a/scripts/qa/check-ui-design-rules.mjs +++ b/scripts/qa/check-ui-design-rules.mjs @@ -339,6 +339,38 @@ function scanMobilePcShell(file, source) { } } +function scanMobileDesktopContainers(file, source) { + if (!isMobileSourceFile(file)) { + return; + } + + const patterns = [ + { + regex: /\bcard-std\b[^"'`]{0,80}\bp-[45]\b/g, + evidence: "card-std p-4/p-5", + }, + { + regex: /\bcard-std\b[^"'`]{0,80}\bp-6\b/g, + evidence: "card-std p-6", + }, + ]; + + for (const pattern of patterns) { + let match = pattern.regex.exec(source); + while (match) { + addFinding({ + rule: "mobile.no_desktop_card_container", + severity: "error", + file, + line: lineNumber(source, match.index), + message: "专用 /mobile 页面不应把桌面 card-std 大容器作为主内容,应使用 mobile-card、mobile-list-card 或移动表单分组。", + evidence: match[0], + }); + match = pattern.regex.exec(source); + } + } +} + function scanRawBusinessIdentifiers(file, source) { const normalized = toPosix(file); if (!/Reimbursement|Invoice|Leave|Purchase|Travel/.test(path.basename(normalized))) { @@ -542,6 +574,7 @@ function scanFileSource(file, source) { scanMobileCapturePdfInputs(file, source); scanMobileDesktopTables(file, source); scanMobilePcShell(file, source); + scanMobileDesktopContainers(file, source); scanUuidPrimaryDisplay(file, source); scanRawBusinessIdentifiers(file, source); scanReimbursementRawCodeDisplay(file, source); @@ -575,6 +608,16 @@ function runSelfTest() { ], expectedSuppressedRules: [], }, + { + file: "client/src/pages/MobileDesktopCardFixture.tsx", + source: ` + export function MobileDesktopCardFixture() { + return
内容
; + } + `, + expectedRules: ["mobile.no_desktop_card_container"], + expectedSuppressedRules: [], + }, { file: "client/src/pages/AuditLogsPage.tsx", source: `