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>