OAFRONT6 完成前三项前端成熟化收口
这个提交包含在:
@@ -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);
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户