gongxiangp2/components/department-dashboard/WorkflowDetailTable.tsx
2026-08-27 13:19:27 +08:00

149 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
OngoingDingWorkflowActiveTask,
OngoingDingWorkflowListItem,
} from '@/lib/ding-workflow-sync'
type WorkflowDetailTableProps<T extends OngoingDingWorkflowListItem> = {
items: T[]
tableLabel: string
mobileListLabel?: string
tasksForItem?: (item: T) => OngoingDingWorkflowActiveTask[]
}
function formatDate(value: string | null) {
if (!value) return '—'
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Asia/Shanghai',
}).format(new Date(value))
}
export function workflowNodeNames(
project: OngoingDingWorkflowListItem,
tasks = project.activeTasks,
) {
const names = Array.from(new Set(tasks.map((task) => task.nodeName).filter(Boolean)))
if (names.length > 0) return names
return project.status.trim().toUpperCase() === 'COMPLETED' ? ['流程已完结'] : ['待进入处理节点']
}
export function workflowTaskOwnerNames(tasks: OngoingDingWorkflowActiveTask[]) {
const names = Array.from(new Set(tasks.map((task) => task.assigneeName).filter(Boolean)))
return names.length > 0 ? names : ['—']
}
export function displayWorkflowValue(value: string | null | undefined) {
return value?.trim() || '—'
}
function MilestoneTime({ label, value }: { label: string; value: string | null }) {
return (
<div className="min-w-[11rem]">
<p className="font-semibold tabular-nums text-slate-800">{formatDate(value)}</p>
<p className="mt-1 text-xs leading-5 text-slate-500">{label}</p>
</div>
)
}
export default function WorkflowDetailTable<T extends OngoingDingWorkflowListItem>({
items,
tableLabel,
mobileListLabel = tableLabel,
tasksForItem = (item) => item.activeTasks,
}: WorkflowDetailTableProps<T>) {
return (
<>
<div
className="hidden max-w-full overflow-x-auto rounded-xl border border-slate-200 bg-white outline-none focus-visible:ring-2 focus-visible:ring-[#5a9ce6] xl:block"
role="region"
aria-label={tableLabel}
tabIndex={0}
>
<table className="w-full min-w-[2480px] table-fixed text-left text-sm">
<caption className="sr-only">{tableLabel},包含审批表单字段、当前节点与流程里程碑时间</caption>
<thead className="sticky top-0 z-20 bg-slate-50 text-xs font-bold text-slate-500">
<tr>
<th scope="col" className="sticky left-0 z-30 w-[320px] border-r border-slate-200 bg-slate-50 px-5 py-3.5">审批单标题</th>
<th scope="col" className="w-[190px] px-4 py-3.5">审批编号</th>
<th scope="col" className="w-[120px] px-4 py-3.5">需求人</th>
<th scope="col" className="w-[260px] px-4 py-3.5">需求部门</th>
<th scope="col" className="w-[140px] px-4 py-3.5">平台</th>
<th scope="col" className="w-[160px] px-4 py-3.5">品牌</th>
<th scope="col" className="w-[160px] px-4 py-3.5">类目</th>
<th scope="col" className="w-[160px] px-4 py-3.5">SPU</th>
<th scope="col" className="w-[120px] px-4 py-3.5">需求类型</th>
<th scope="col" className="w-[190px] px-4 py-3.5">标准出图最长时效限制</th>
<th scope="col" className="w-[220px] px-4 py-3.5">分配任务开始时间</th>
<th scope="col" className="w-[180px] px-4 py-3.5">当前节点名称</th>
<th scope="col" className="w-[160px] px-4 py-3.5">当前节点负责人</th>
<th scope="col" className="w-[220px] px-5 py-3.5">审核结束时间</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{items.map((project) => {
const tasks = tasksForItem(project)
return (
<tr key={project.id} className="group align-top hover:bg-slate-50">
<td className="sticky left-0 z-10 border-r border-slate-200 bg-white px-5 py-4 group-hover:bg-slate-50">
<p className="font-bold text-slate-950">{project.title}</p>
<p className="mt-1 text-xs text-slate-500">{project.projectType}</p>
</td>
<td className="break-all px-4 py-4 text-slate-600">{project.businessId || project.id}</td>
<td className="px-4 py-4 font-bold text-slate-800">{displayWorkflowValue(project.initiatorName)}</td>
<td className="px-4 py-4 leading-6 text-slate-700">{displayWorkflowValue(project.initiatorDepartment)}</td>
<td className="px-4 py-4 text-slate-700">{displayWorkflowValue(project.formFields.platform)}</td>
<td className="px-4 py-4 font-bold text-slate-800">{displayWorkflowValue(project.formFields.brand || project.brand)}</td>
<td className="px-4 py-4 text-slate-700">{displayWorkflowValue(project.formFields.category)}</td>
<td className="break-words px-4 py-4 text-slate-700">{displayWorkflowValue(project.formFields.spu)}</td>
<td className="px-4 py-4 font-semibold text-slate-800">{displayWorkflowValue(project.formFields.demandType)}</td>
<td className="px-4 py-4 text-slate-700">{displayWorkflowValue(project.formFields.standardOutputDeadline)}</td>
<td className="px-4 py-4"><MilestoneTime label={project.assignmentNodeLabel} value={project.assignmentStartedAt} /></td>
<td className="px-4 py-4 leading-6 text-slate-700">{workflowNodeNames(project, tasks).join(' / ')}</td>
<td className="px-4 py-4 font-semibold leading-6 text-slate-800">{workflowTaskOwnerNames(tasks).join(' / ')}</td>
<td className="px-5 py-4"><MilestoneTime label={project.reviewNodeLabel} value={project.reviewFinishedAt} /></td>
</tr>
)
})}
</tbody>
</table>
</div>
<ul className="grid gap-3 xl:hidden" aria-label={mobileListLabel}>
{items.map((project) => {
const tasks = tasksForItem(project)
return (
<li key={project.id} className="rounded-xl border border-slate-200 bg-white p-5">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h4 className="font-bold text-slate-950">{project.title}</h4>
<p className="mt-1 break-all text-xs text-slate-500">审批编号 {project.businessId || project.id}</p>
</div>
<span className="shrink-0 rounded-lg bg-blue-50 px-2.5 py-1 text-xs font-bold text-blue-700">{project.projectType}</span>
</div>
<dl className="mt-4 grid gap-x-6 gap-y-4 border-t border-slate-100 pt-4 text-sm sm:grid-cols-2 lg:grid-cols-3">
<div><dt className="text-xs text-slate-500">需求人</dt><dd className="mt-1 font-bold text-slate-800">{displayWorkflowValue(project.initiatorName)}</dd></div>
<div><dt className="text-xs text-slate-500">需求部门</dt><dd className="mt-1 font-bold leading-6 text-slate-800">{displayWorkflowValue(project.initiatorDepartment)}</dd></div>
<div><dt className="text-xs text-slate-500">平台</dt><dd className="mt-1 text-slate-700">{displayWorkflowValue(project.formFields.platform)}</dd></div>
<div><dt className="text-xs text-slate-500">品牌</dt><dd className="mt-1 font-bold text-slate-800">{displayWorkflowValue(project.formFields.brand || project.brand)}</dd></div>
<div><dt className="text-xs text-slate-500">类目</dt><dd className="mt-1 text-slate-700">{displayWorkflowValue(project.formFields.category)}</dd></div>
<div><dt className="text-xs text-slate-500">SPU</dt><dd className="mt-1 break-words text-slate-700">{displayWorkflowValue(project.formFields.spu)}</dd></div>
<div><dt className="text-xs text-slate-500">需求类型</dt><dd className="mt-1 font-bold text-slate-800">{displayWorkflowValue(project.formFields.demandType)}</dd></div>
<div><dt className="text-xs text-slate-500">标准出图最长时效限制</dt><dd className="mt-1 text-slate-700">{displayWorkflowValue(project.formFields.standardOutputDeadline)}</dd></div>
<div><dt className="text-xs text-slate-500">分配任务开始时间</dt><dd className="mt-1"><MilestoneTime label={project.assignmentNodeLabel} value={project.assignmentStartedAt} /></dd></div>
<div><dt className="text-xs text-slate-500">当前节点名称</dt><dd className="mt-1 font-bold leading-6 text-slate-800">{workflowNodeNames(project, tasks).join(' / ')}</dd></div>
<div><dt className="text-xs text-slate-500">当前节点负责人</dt><dd className="mt-1 font-bold leading-6 text-slate-800">{workflowTaskOwnerNames(tasks).join(' / ')}</dd></div>
<div><dt className="text-xs text-slate-500">审核结束时间</dt><dd className="mt-1"><MilestoneTime label={project.reviewNodeLabel} value={project.reviewFinishedAt} /></dd></div>
</dl>
</li>
)
})}
</ul>
</>
)
}