OAFRONT6 完成前三项前端成熟化收口

这个提交包含在:
2026-05-25 13:43:02 +08:00
父节点 4a6f130b63
当前提交 7414e1ac92
共修改 7 个文件,包含 257 行新增和 174 行删除
+1
查看文件
@@ -39,6 +39,7 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/system/workspace", title: "工作区设置", group: "管理入口" },
{ path: "/people", title: "人员管理", group: "管理入口" },
{ path: "/organization", title: "组织架构", group: "管理入口" },
{ path: "/system/approval-rules", title: "审批规则", group: "管理入口" },
{ path: "/system/workflow-definitions", title: "流程定义", group: "管理入口" },
{ path: "/system/settings", title: "系统设置", group: "管理入口" },
{ path: "/system/feature-flags", title: "功能开关", group: "管理入口" },
+201 -170
查看文件
@@ -1068,176 +1068,207 @@ export function AssetRegistryPage() {
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{assetFormError}</p>
) : null}
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">资产名称</label>
<Input data-testid="asset-name" value={assetForm.name} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, name: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">资产类型</label>
<select data-testid="asset-type" className="select-std" value={assetForm.assetType} disabled={!canEditCurrent || !isCreatingAsset} onChange={(event) => setAssetForm((current) => ({ ...current, assetType: event.target.value as AssetType }))}>
{assetTypeOptions().filter((item) => item.value).map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
<p className="text-xs text-zinc-500">{typeHint(assetForm.assetType)}</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">资产状态</label>
<select data-testid="asset-status" className="select-std" value={assetForm.status} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, status: event.target.value as AssetStatus }))}>
{assetStatusOptions().filter((item) => item.value).map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">敏感级别</label>
<select data-testid="asset-sensitivity" className="select-std" value={assetForm.sensitivityLevel} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, sensitivityLevel: event.target.value as AssetSensitivityLevel }))}>
{sensitivityOptions().filter((item) => item.value).map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">资产分类</label>
<select data-testid="asset-category" className="select-std" value={assetForm.categoryId} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, categoryId: event.target.value }))}>
<option value="">请选择资产分类</option>
{(categoriesQuery.data?.items ?? []).filter((item) => item.enabled).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">法体</label>
<select data-testid="asset-legal-entity" className="select-std" value={assetForm.legalEntityId} disabled={!canEditCurrent} onChange={(event) => {
const legalEntityId = event.target.value;
setAssetForm((current) => ({
...current,
legalEntityId,
costCenterId: current.costCenterId && filteredAssetCostCenters.some((item) => item.id === current.costCenterId && item.legalEntityId === legalEntityId)
? current.costCenterId
: "",
}));
}}>
<option value="">请选择法体</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">成本中心</label>
<select data-testid="asset-cost-center" className="select-std" value={assetForm.costCenterId} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, costCenterId: event.target.value }))}>
<option value="">请选择成本中心</option>
{filteredAssetCostCenters.map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">归属人</label>
<select data-testid="asset-assignee" className="select-std" value={assetForm.assigneeEmployeeId} disabled={!canManage || !isCreatingAsset} onChange={(event) => setAssetForm((current) => ({ ...current, assigneeEmployeeId: event.target.value }))}>
<option value="">暂不分配</option>
{(assigneesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">负责人</label>
<select data-testid="asset-owner" className="select-std" value={assetForm.ownerEmployeeId} disabled={!canManage} onChange={(event) => setAssetForm((current) => ({ ...current, ownerEmployeeId: event.target.value }))}>
<option value="">未指定</option>
{(assigneesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">保管人/管理员</label>
<select data-testid="asset-keeper" className="select-std" value={assetForm.keeperEmployeeId} disabled={!canManage} onChange={(event) => setAssetForm((current) => ({ ...current, keeperEmployeeId: event.target.value }))}>
<option value="">未指定</option>
{(assigneesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">采购来源</label>
<select className="select-std" value={assetForm.purchaseRequestId} disabled={!canEditCurrent || !canReadProcurementSources || !isCreatingAsset} onChange={(event) => setAssetForm((current) => ({ ...current, purchaseRequestId: event.target.value }))}>
<option value="">无采购来源</option>
{purchaseOptions.map((item) => (
<option key={item.id} value={item.id}>
{(item.requestNo ?? item.id)} · {item.title}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">序列号</label>
<Input data-testid="asset-serial-no" value={assetForm.serialNo} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, serialNo: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">存放位置</label>
<Input data-testid="asset-location" value={assetForm.location} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, location: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">归档位置/备份位置</label>
<Input data-testid="asset-archive-location" placeholder="填归档位置摘要或离线档案号,不填 /home 或盘符完整路径" value={assetForm.archiveLocation} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, archiveLocation: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">外部引用/仓库地址</label>
<Input data-testid="asset-external-ref" placeholder="填系统入口或仓库主页,不填带 token 的链接或完整配置" value={assetForm.externalRef} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, externalRef: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">文件 hash / 校验摘要</label>
<Input data-testid="asset-source-hash" value={assetForm.sourceHash} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, sourceHash: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">历史验证编号</label>
<Input data-testid="asset-verification-no" placeholder="VER-00001" value={assetForm.verificationNo} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, verificationNo: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">最近盘点/恢复验证日期</label>
<DateTextInput value={assetForm.lastVerifiedAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, lastVerifiedAt: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">到期/续费日期</label>
<DateTextInput value={assetForm.expiresAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, expiresAt: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">启用日期</label>
<DateTextInput value={assetForm.activatedAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, activatedAt: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">退役日期</label>
<DateTextInput value={assetForm.retiredAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, retiredAt: event.target.value }))} />
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-medium text-zinc-700">保密/回收要求</label>
<textarea className="textarea-std min-h-20" placeholder="写回收要求、保管要求或脱敏说明,不写密码、密钥、原文片段" value={assetForm.confidentialityNote} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, confidentialityNote: event.target.value }))} />
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-medium text-zinc-700">扩展元数据 JSON</label>
<textarea className="textarea-std min-h-24 font-mono text-xs" placeholder='例如 {"backupPolicy":"monthly"};不要粘贴 .env、源码配置、原始 payload 或密钥' value={assetForm.metadataText} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, metadataText: event.target.value }))} />
</div>
<div className="space-y-6">
<section className="space-y-4">
<div>
<h4 className="text-sm font-semibold text-zinc-900">基础信息</h4>
<p className="text-xs text-zinc-500">维护资产主标识、类型、分类和组织归属。</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">资产名称</label>
<Input data-testid="asset-name" value={assetForm.name} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, name: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">资产类型</label>
<select data-testid="asset-type" className="select-std" value={assetForm.assetType} disabled={!canEditCurrent || !isCreatingAsset} onChange={(event) => setAssetForm((current) => ({ ...current, assetType: event.target.value as AssetType }))}>
{assetTypeOptions().filter((item) => item.value).map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
<p className="text-xs text-zinc-500">{typeHint(assetForm.assetType)}</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">资产状态</label>
<select data-testid="asset-status" className="select-std" value={assetForm.status} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, status: event.target.value as AssetStatus }))}>
{assetStatusOptions().filter((item) => item.value).map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">敏感级别</label>
<select data-testid="asset-sensitivity" className="select-std" value={assetForm.sensitivityLevel} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, sensitivityLevel: event.target.value as AssetSensitivityLevel }))}>
{sensitivityOptions().filter((item) => item.value).map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">资产分类</label>
<select data-testid="asset-category" className="select-std" value={assetForm.categoryId} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, categoryId: event.target.value }))}>
<option value="">请选择资产分类</option>
{(categoriesQuery.data?.items ?? []).filter((item) => item.enabled).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">法体</label>
<select data-testid="asset-legal-entity" className="select-std" value={assetForm.legalEntityId} disabled={!canEditCurrent} onChange={(event) => {
const legalEntityId = event.target.value;
setAssetForm((current) => ({
...current,
legalEntityId,
costCenterId: current.costCenterId && filteredAssetCostCenters.some((item) => item.id === current.costCenterId && item.legalEntityId === legalEntityId)
? current.costCenterId
: "",
}));
}}>
<option value="">请选择法体</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">成本中心</label>
<select data-testid="asset-cost-center" className="select-std" value={assetForm.costCenterId} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, costCenterId: event.target.value }))}>
<option value="">请选择成本中心</option>
{filteredAssetCostCenters.map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">采购来源</label>
<select className="select-std" value={assetForm.purchaseRequestId} disabled={!canEditCurrent || !canReadProcurementSources || !isCreatingAsset} onChange={(event) => setAssetForm((current) => ({ ...current, purchaseRequestId: event.target.value }))}>
<option value="">无采购来源</option>
{purchaseOptions.map((item) => (
<option key={item.id} value={item.id}>
{(item.requestNo ?? item.id)} · {item.title}
</option>
))}
</select>
</div>
</div>
</section>
<section className="space-y-4 border-t border-zinc-200 pt-5">
<div>
<h4 className="text-sm font-semibold text-zinc-900">责任与归档</h4>
<p className="text-xs text-zinc-500">明确领用、负责、保管、位置和生命周期日期。</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">归属人</label>
<select data-testid="asset-assignee" className="select-std" value={assetForm.assigneeEmployeeId} disabled={!canManage || !isCreatingAsset} onChange={(event) => setAssetForm((current) => ({ ...current, assigneeEmployeeId: event.target.value }))}>
<option value="">暂不分配</option>
{(assigneesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">负责人</label>
<select data-testid="asset-owner" className="select-std" value={assetForm.ownerEmployeeId} disabled={!canManage} onChange={(event) => setAssetForm((current) => ({ ...current, ownerEmployeeId: event.target.value }))}>
<option value="">未指定</option>
{(assigneesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">保管人/管理员</label>
<select data-testid="asset-keeper" className="select-std" value={assetForm.keeperEmployeeId} disabled={!canManage} onChange={(event) => setAssetForm((current) => ({ ...current, keeperEmployeeId: event.target.value }))}>
<option value="">未指定</option>
{(assigneesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">序列号</label>
<Input data-testid="asset-serial-no" value={assetForm.serialNo} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, serialNo: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">存放位置</label>
<Input data-testid="asset-location" value={assetForm.location} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, location: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">归档位置/备份位置</label>
<Input data-testid="asset-archive-location" placeholder="填归档位置摘要或离线档案号,不填 /home 或盘符完整路径" value={assetForm.archiveLocation} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, archiveLocation: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">最近盘点/恢复验证日期</label>
<DateTextInput value={assetForm.lastVerifiedAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, lastVerifiedAt: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">到期/续费日期</label>
<DateTextInput value={assetForm.expiresAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, expiresAt: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">启用日期</label>
<DateTextInput value={assetForm.activatedAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, activatedAt: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">退役日期</label>
<DateTextInput value={assetForm.retiredAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, retiredAt: event.target.value }))} />
</div>
</div>
</section>
<section className="space-y-4 border-t border-zinc-200 pt-5">
<div>
<h4 className="text-sm font-semibold text-zinc-900">电子资产安全字段</h4>
<p className="text-xs text-zinc-500">只登记必要摘要,密码、密钥、源码包、原始正文和完整路径不得进入 OA。</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">外部引用/仓库地址</label>
<Input data-testid="asset-external-ref" placeholder="填系统入口或仓库主页,不填带 token 的链接或完整配置" value={assetForm.externalRef} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, externalRef: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">文件 hash / 校验摘要</label>
<Input data-testid="asset-source-hash" value={assetForm.sourceHash} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, sourceHash: event.target.value }))} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">历史验证编号</label>
<Input data-testid="asset-verification-no" placeholder="VER-00001" value={assetForm.verificationNo} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, verificationNo: event.target.value }))} />
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-medium text-zinc-700">保密/回收要求</label>
<textarea className="textarea-std min-h-20" placeholder="写回收要求、保管要求或脱敏说明,不写密码、密钥、原文片段" value={assetForm.confidentialityNote} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, confidentialityNote: event.target.value }))} />
</div>
<details className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 md:col-span-2">
<summary className="cursor-pointer list-none text-sm font-medium text-zinc-700">
扩展元数据 JSON
<span className="ml-2 text-xs font-normal text-zinc-500">按需展开</span>
</summary>
<div className="mt-3 space-y-2">
<textarea className="textarea-std min-h-24 font-mono text-xs" placeholder='例如 {"backupPolicy":"monthly"};不要粘贴 .env、源码配置、原始 payload 或密钥' value={assetForm.metadataText} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, metadataText: event.target.value }))} />
</div>
</details>
</div>
</section>
</div>
@@ -81,6 +81,41 @@ function screenshotPath(fileName: string) {
return path.join(SCREENSHOT_DIR, `${fileName}.png`);
}
async function installStableVisualCapture(page: Page) {
await page.addInitScript(() => {
const style = document.createElement("style");
style.setAttribute("data-testid", "mobile-visual-stability-style");
style.textContent = `
*, *::before, *::after {
animation-delay: 0ms !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-delay: 0ms !important;
transition-duration: 0ms !important;
}
`;
document.documentElement.appendChild(style);
});
}
async function waitForStableMobileVisual(page: Page) {
await page.locator(".hyb-mobile-shell").waitFor({ state: "visible", timeout: 10_000 });
await page.evaluate(async () => {
await document.fonts?.ready;
});
await page.waitForFunction(() => {
const shell = document.querySelector(".hyb-mobile-shell");
if (!shell) {
return false;
}
const style = window.getComputedStyle(shell);
const rect = shell.getBoundingClientRect();
return style.opacity === "1" && rect.width > 0 && rect.height > 0;
});
await page.waitForTimeout(100);
}
function reimbursementListItem() {
return {
id: "mud6-reimbursement",
@@ -536,6 +571,7 @@ async function collectMetrics(page: Page) {
}
async function expectUnifiedMobilePage(page: Page, label: string, screenshotName: string, options: { deep: boolean }) {
await waitForStableMobileVisual(page);
await page.screenshot({ path: screenshotPath(screenshotName), fullPage: true });
const metrics = await collectMetrics(page);
expect(metrics.shellScoped, `${label} mobile shell scope`).toBe(true);
@@ -583,6 +619,7 @@ test.describe("MUD6 专用移动页面统一设计", () => {
});
test("所有专用 /mobile 页面复用统一槽位、卡片和底部导航体系", async ({ page }) => {
await installStableVisualCapture(page);
await mockApis(page);
await login(page);