实现 B-2b 统一报销工作台迁移(reimbursement-form-shared-component-migration)

这个提交包含在:
2026-09-02 07:35:01 +08:00
父节点 6bca91a360
当前提交 abdd250325
共修改 38 个文件,包含 1850 行新增和 2723 行删除
+95
查看文件
@@ -564,6 +564,75 @@ function scanNavigationTitleCoverage(file, source) {
}
}
const reimbursementWorkspaceShellFiles = new Set([
"client/src/components/reimbursements/ReimbursementDesktopWorkspaceShell.tsx",
"client/src/components/reimbursements/ReimbursementMobileWorkspaceShell.tsx",
]);
function scanReimbursementWorkspaceShellBoundary(file, source) {
const normalized = normalizeFindingFile(file);
if (!reimbursementWorkspaceShellFiles.has(normalized)) {
return;
}
const patterns = [
{ regex: /\buseQuery\b/g, evidence: "useQuery" },
{ regex: /\buseMutation\b/g, evidence: "useMutation" },
{ regex: /(?:from\s+["'][^"']*\/api\/|\bapiClient\b|\bapi\.(?:get|post|put|patch|delete)\b)/g, evidence: "API client" },
{ regex: /\bfetch\s*\(/g, evidence: "fetch" },
{ regex: /\b(?:hasPermission|roleCodes?|permissionCodes?)\b/g, evidence: "permission/role" },
{ regex: /\b(?:workflowStatus|reimbursementStatus)\b/g, evidence: "business status" },
{ regex: /\bmode\s*(?:={2,3}|!==?)\s*["']/g, evidence: "mode comparison" },
{ regex: /\bisMobile\b/g, evidence: "isMobile branch" },
];
for (const pattern of patterns) {
let match = pattern.regex.exec(source);
while (match) {
addFinding({
rule: "architecture.reimbursement_workspace_shell_boundary",
severity: "error",
file,
line: lineNumber(source, match.index),
message: "报销工作台布局壳只能组合已计算模型和能力,不得查询 API、判断角色/业务模式或用移动端分支推导行为。",
evidence: pattern.evidence,
});
match = pattern.regex.exec(source);
}
}
}
function scanLegacyWorkflowReimbursementBranch(file, source) {
const normalized = normalizeFindingFile(file);
if (!["client/src/pages/WorkflowTaskDetailPage.tsx", "client/src/pages/MobileApprovalsPage.tsx"].includes(normalized)) {
return;
}
const patterns = [
/\bReimbursementItemEvidenceList\b/g,
/\b(?:isReimbursementTask|isSelectedReimbursement|reimbursementDecisionReady|reimbursementEvidence|reimbursementDetailQuery)\b/g,
/\bbuildReimbursementReturnReference\b/g,
/\bselectedTask\?*\.reimbursement\b/g,
/data-testid=["']reimbursement-approval-(?:evidence|checks)["']/g,
/(?:提交时检查|当前票据检查|打开完整原始表单)/g,
];
for (const regex of patterns) {
let match = regex.exec(source);
while (match) {
addFinding({
rule: "architecture.no_legacy_workflow_reimbursement_branch",
severity: "error",
file,
line: lineNumber(source, match.index),
message: "通用审批详情不得保留报销证据、检查或决策就绪分支;报销只能在授权分派层进入工作台适配器。",
evidence: match[0],
});
match = regex.exec(source);
}
}
}
function scanFileSource(file, source) {
scanForbiddenCopy(file, source);
scanCompetitorReferenceCopy(file, source);
@@ -580,6 +649,8 @@ function scanFileSource(file, source) {
scanReimbursementRawCodeDisplay(file, source);
scanDuplicateBusinessIdentifierDisplay(file, source);
scanNavigationTitleCoverage(file, source);
scanReimbursementWorkspaceShellBoundary(file, source);
scanLegacyWorkflowReimbursementBranch(file, source);
}
function scanFile(file) {
@@ -628,6 +699,30 @@ function runSelfTest() {
expectedRules: [],
expectedSuppressedRules: ["data.no_uuid_primary_display"],
},
{
file: "client/src/components/reimbursements/ReimbursementDesktopWorkspaceShell.tsx",
source: `
import { useQuery } from "@tanstack/react-query";
export function ReimbursementDesktopWorkspaceShell({ mode, roleCodes }) {
const query = useQuery({ queryKey: ["bad"] });
void fetch("/api/reimbursements/bad");
return mode === "approval" && roleCodes ? <div>{query.data}</div> : null;
}
`,
expectedRules: ["architecture.reimbursement_workspace_shell_boundary"],
expectedSuppressedRules: [],
},
{
file: "client/src/pages/WorkflowTaskDetailPage.tsx",
source: `
import { ReimbursementItemEvidenceList } from "../components/reimbursements/ReimbursementItemEvidenceList";
export function Fixture({ selectedTask }) {
return selectedTask?.reimbursement ? <><ReimbursementItemEvidenceList evidence={reimbursementEvidence} />提交时检查</> : null;
}
`,
expectedRules: ["architecture.no_legacy_workflow_reimbursement_branch"],
expectedSuppressedRules: [],
},
];
for (const testCase of cases) {