Compare commits
1 Commits
main
...
codex/xuxi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53a956df9 |
@ -1,13 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { AlertTriangle, CheckCircle2, Clock3, Layers3, LoaderCircle } from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { AlertTriangle, CheckCircle2, Clock3, Layers3, LoaderCircle, Rows3, Table2 } from 'lucide-react'
|
||||
import type {
|
||||
OaBoardDashboardDTO,
|
||||
OaBoardNodeAssessmentDTO,
|
||||
OaBoardProjectListItemDTO,
|
||||
} from '@/server/services/oa-board.service'
|
||||
|
||||
type OaBoardTabKey = 'overview' | 'detail'
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
@ -69,6 +71,34 @@ function FlowChip({
|
||||
)
|
||||
}
|
||||
|
||||
function TabButton({
|
||||
label,
|
||||
active,
|
||||
icon,
|
||||
onClick,
|
||||
}: {
|
||||
label: string
|
||||
active: boolean
|
||||
icon: React.ReactNode
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={[
|
||||
'inline-flex items-center gap-2 rounded-apple-lg border px-3 py-2 text-xs font-apple-medium transition-colors',
|
||||
active
|
||||
? 'border-[color:var(--color-blue)] bg-[color:color-mix(in_srgb,var(--color-blue)_10%,white)] text-apple-label-primary'
|
||||
: 'border-apple-separator bg-apple-bg-secondary text-apple-label-secondary hover:bg-apple-bg-primary',
|
||||
].join(' ')}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectRow({
|
||||
item,
|
||||
index,
|
||||
@ -90,12 +120,21 @@ function ProjectRow({
|
||||
>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-secondary">{index + 1}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.styleImageLabel || '-'}
|
||||
{item.styleImages[0]?.url ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={item.styleImages[0].url}
|
||||
alt={item.styleImages[0].alt}
|
||||
className="h-14 w-14 rounded-apple-md border border-apple-separator object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-apple-md border border-apple-separator bg-apple-bg-secondary text-[10px] text-apple-label-tertiary">
|
||||
无图片
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
<div className="font-apple-medium text-apple-label-primary">
|
||||
{item.formSummary.openingCode || item.openingCode || item.boardKey}
|
||||
</div>
|
||||
<div className="font-apple-medium text-apple-label-primary">{item.formSummary.openingCode || '-'}</div>
|
||||
<div className="mt-1 text-apple-label-secondary">{item.title}</div>
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
@ -110,27 +149,15 @@ function ProjectRow({
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.currentNodeDisplay || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.demandType || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.developmentPlan || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.productType || item.productType || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.plannedSalesBrands || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.productPlanner || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.spuOwner || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
{item.formSummary.purchaseOwner || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.demandType || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.developmentPlan || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.productType || item.productType || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.plannedSalesBrands || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.productPlanner || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.spuOwner || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.purchaseOwner || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.pricingOwner || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">{item.formSummary.spu || item.spu || '-'}</td>
|
||||
<td className="px-3 py-3 align-top text-[11px] text-apple-label-primary">
|
||||
<div>目标:{item.overallTargetHours ?? '-'}h</div>
|
||||
<div className="mt-1">实际:{item.overallActualHours ?? '-'}h</div>
|
||||
@ -200,8 +227,13 @@ export default function OaBoardClient({
|
||||
loadErrorMessage?: string | null
|
||||
}) {
|
||||
const { summary, items } = dashboard
|
||||
const [activeTab, setActiveTab] = useState<OaBoardTabKey>('overview')
|
||||
const [selectedBoardKey, setSelectedBoardKey] = useState(items[0]?.boardKey ?? '')
|
||||
const selectedItem = items.find((item) => item.boardKey === selectedBoardKey) ?? items[0] ?? null
|
||||
|
||||
const selectedItem = useMemo(
|
||||
() => items.find((item) => item.boardKey === selectedBoardKey) ?? items[0] ?? null,
|
||||
[items, selectedBoardKey],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#eef2f5] px-4 py-4">
|
||||
@ -223,7 +255,7 @@ export default function OaBoardClient({
|
||||
<div>
|
||||
<h1 className="text-base font-apple-semibold text-apple-label-primary">OA 新品流程看板</h1>
|
||||
<p className="mt-1 text-xs leading-5 text-apple-label-secondary">
|
||||
直接读取企划库流程数据,集中查看新品开款、审批进度和节点考核时效。今天这版先完成列表和节点详情联看雏形。
|
||||
直接读取企划库流程数据,集中查看新品开款、审批进度和节点考核时效。页面分成总览与流程明细两个页签,方便先看整体,再看节点。
|
||||
</p>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2 rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2 text-xs text-apple-label-secondary">
|
||||
@ -233,154 +265,173 @@ export default function OaBoardClient({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<StatCard label="项目总数" value={summary.totalProjects} description="当前命中的新品项目总数。" />
|
||||
<StatCard label="流程中" value={summary.runningProjects} description="至少有一个子流程仍处于进行中的项目。" tone="warning" />
|
||||
<StatCard label="已完成" value={summary.completedProjects} description="核心流程已全部走完的项目。" tone="success" />
|
||||
<StatCard label="超时项目" value={summary.timeoutProjects} description="按产品类型总目标时效判断已超时的项目。" tone="danger" />
|
||||
</section>
|
||||
<div className="flex items-center gap-2">
|
||||
<TabButton
|
||||
label="总览"
|
||||
active={activeTab === 'overview'}
|
||||
icon={<Table2 className="h-3.5 w-3.5" />}
|
||||
onClick={() => setActiveTab('overview')}
|
||||
/>
|
||||
<TabButton
|
||||
label="流程明细"
|
||||
active={activeTab === 'detail'}
|
||||
icon={<Rows3 className="h-3.5 w-3.5" />}
|
||||
onClick={() => setActiveTab('detail')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs">
|
||||
<div className="flex items-start justify-between gap-3 border-b border-apple-separator px-4 py-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-apple-semibold text-apple-label-primary">项目列表</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-apple-label-secondary">
|
||||
列表字段对齐附件三口径,选中项目后,下方会联动展示该项目的节点考核详情。
|
||||
</p>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2 rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2 text-xs text-apple-label-secondary">
|
||||
<Layers3 className="h-3.5 w-3.5" />
|
||||
<span>{items.length} 个项目</span>
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'overview' ? (
|
||||
<>
|
||||
<section className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<StatCard label="项目总数" value={summary.totalProjects} description="当前命中的新品项目总数。" />
|
||||
<StatCard label="流程中" value={summary.runningProjects} description="至少有一个子流程仍处于进行中的项目。" tone="warning" />
|
||||
<StatCard label="已完成" value={summary.completedProjects} description="核心流程已全部走完的项目。" tone="success" />
|
||||
<StatCard label="超时项目" value={summary.timeoutProjects} description="按产品类型总目标时效判断已超时的项目。" tone="danger" />
|
||||
</section>
|
||||
|
||||
<div className="max-h-[720px] overflow-auto">
|
||||
<table className="min-w-[2680px] table-fixed text-left text-[11px]">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr className="border-b border-apple-separator">
|
||||
<th className="w-[60px] px-3 py-2.5 font-apple-medium">序号</th>
|
||||
<th className="w-[90px] px-3 py-2.5 font-apple-medium">款式图片</th>
|
||||
<th className="w-[220px] px-3 py-2.5 font-apple-medium">开款编号</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">总流程状态</th>
|
||||
<th className="w-[160px] px-3 py-2.5 font-apple-medium">流程审批阶段</th>
|
||||
<th className="w-[140px] px-3 py-2.5 font-apple-medium">当前负责人</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">当前审批节点</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">需求类型</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">开发方案</th>
|
||||
<th className="w-[200px] px-3 py-2.5 font-apple-medium">产品类型</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">计划售卖品牌</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">商品企划</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">SPU负责人</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">采购负责人</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">限价负责人</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">SPU</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">总时效</th>
|
||||
<th className="w-[320px] px-3 py-2.5 font-apple-medium">流程片段</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={18} className="px-4 py-12 text-center text-xs text-apple-label-secondary">
|
||||
<div className="mx-auto flex max-w-md flex-col items-center gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-apple-label-tertiary" />
|
||||
<p>当前还没有拿到可展示的企划库流程数据。先检查企划库连接与节点/时效规则,再刷新页面。</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
items.map((item, index) => (
|
||||
<ProjectRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
index={index}
|
||||
selected={selectedItem?.boardKey === item.boardKey}
|
||||
onSelect={setSelectedBoardKey}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs">
|
||||
<div className="flex items-start justify-between gap-3 border-b border-apple-separator px-4 py-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-apple-semibold text-apple-label-primary">节点考核详情</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-apple-label-secondary">
|
||||
选中一个项目后,按节点查看目标时效、实际用时和超时情况。今天先做成单页联看的监控雏形。
|
||||
</p>
|
||||
</div>
|
||||
{selectedItem ? (
|
||||
<div className="inline-flex items-center gap-2 rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2 text-xs text-apple-label-secondary">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
<span>{selectedItem.formSummary.openingCode || selectedItem.boardKey}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!selectedItem ? (
|
||||
<div className="px-4 py-12 text-center text-xs text-apple-label-secondary">
|
||||
暂无可查看的项目节点明细。
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-3 border-b border-apple-separator px-4 py-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<DetailField label="当前审批阶段" value={selectedItem.currentApprovalStageName} />
|
||||
<DetailField label="当前负责人" value={selectedItem.formSummary.currentOwnerDisplay} />
|
||||
<DetailField label="当前审批节点" value={selectedItem.formSummary.currentNodeDisplay} />
|
||||
<DetailField label="产品类型" value={selectedItem.formSummary.productType || selectedItem.productType} />
|
||||
<DetailField label="计划售卖品牌" value={selectedItem.formSummary.plannedSalesBrands} />
|
||||
<DetailField label="流程链接" value={selectedItem.formSummary.workflowLink} />
|
||||
<section className="overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs">
|
||||
<div className="flex items-start justify-between gap-3 border-b border-apple-separator px-4 py-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-apple-semibold text-apple-label-primary">项目列表</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-apple-label-secondary">
|
||||
列表字段对齐附件三口径,选中项目后可切到“流程明细”页签查看节点考核详情。
|
||||
</p>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2 rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2 text-xs text-apple-label-secondary">
|
||||
<Layers3 className="h-3.5 w-3.5" />
|
||||
<span>{items.length} 个项目</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 border-b border-apple-separator px-4 py-4 md:grid-cols-3 xl:grid-cols-6">
|
||||
<StatCard label="节点总数" value={selectedItem.nodeAssessments.length} description="当前项目已识别到的节点考核行。" />
|
||||
<StatCard label="超时节点" value={selectedItem.nodeAssessments.filter((node) => node.isOvertime).length} description="目标时效已被超过的节点数量。" tone="danger" />
|
||||
<StatCard label="项目目标时效" value={selectedItem.overallTargetHours ?? '-'} description="按产品类型配置的总目标时效。" />
|
||||
<StatCard label="项目实际用时" value={selectedItem.overallActualHours ?? '-'} description="当前项目累计实际用时。" tone="warning" />
|
||||
<StatCard label="总超时小时" value={selectedItem.overallTimeoutHours ?? '-'} description="超过项目总目标时效的小时数。" tone={selectedItem.overallTimeoutFlag ? 'danger' : 'success'} />
|
||||
<StatCard label="总流程状态" value={selectedItem.formSummary.totalFlowStatusLabel || '-'} description="当前项目总流程的状态摘要。" />
|
||||
</div>
|
||||
|
||||
<div className="max-h-[520px] overflow-auto">
|
||||
<table className="min-w-[1760px] table-fixed text-left text-[11px]">
|
||||
<div className="max-h-[720px] overflow-auto">
|
||||
<table className="min-w-[2520px] table-fixed text-left text-[11px]">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr className="border-b border-apple-separator">
|
||||
<th className="w-[60px] px-3 py-2.5 font-apple-medium">序号</th>
|
||||
<th className="w-[160px] px-3 py-2.5 font-apple-medium">流程</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">节点名称</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">审批阶段</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">责任部门</th>
|
||||
<th className="w-[140px] px-3 py-2.5 font-apple-medium">负责人</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">目标时效</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">实际用时</th>
|
||||
<th className="w-[100px] px-3 py-2.5 font-apple-medium">是否超时</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">超时小时</th>
|
||||
<th className="w-[100px] px-3 py-2.5 font-apple-medium">状态</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">开始时间</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">完成时间</th>
|
||||
<th className="w-[90px] px-3 py-2.5 font-apple-medium">款式图片</th>
|
||||
<th className="w-[220px] px-3 py-2.5 font-apple-medium">开款编号</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">总流程状态</th>
|
||||
<th className="w-[160px] px-3 py-2.5 font-apple-medium">流程审批阶段</th>
|
||||
<th className="w-[140px] px-3 py-2.5 font-apple-medium">当前负责人</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">当前审批节点</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">需求类型</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">开发方案</th>
|
||||
<th className="w-[200px] px-3 py-2.5 font-apple-medium">产品类型</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">计划售卖品牌</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">商品企划</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">SPU负责人</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">采购负责人</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">限价负责人</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">SPU</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">总时效</th>
|
||||
<th className="w-[320px] px-3 py-2.5 font-apple-medium">流程片段</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedItem.nodeAssessments.length === 0 ? (
|
||||
{items.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={13} className="px-4 py-12 text-center text-xs text-apple-label-secondary">
|
||||
当前项目还没有可展示的节点考核数据。
|
||||
<td colSpan={18} className="px-4 py-12 text-center text-xs text-apple-label-secondary">
|
||||
<div className="mx-auto flex max-w-md flex-col items-center gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-apple-label-tertiary" />
|
||||
<p>当前还没有拿到可展示的企划库流程数据。先检查企划库连接与节点/时效规则,再刷新页面。</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
selectedItem.nodeAssessments.map((node, index) => (
|
||||
<NodeAssessmentRow key={`${node.taskId}:${index}`} item={node} index={index} />
|
||||
items.map((item, index) => (
|
||||
<ProjectRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
index={index}
|
||||
selected={selectedItem?.boardKey === item.boardKey}
|
||||
onSelect={setSelectedBoardKey}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<section className="overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs">
|
||||
<div className="flex items-start justify-between gap-3 border-b border-apple-separator px-4 py-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-apple-semibold text-apple-label-primary">节点考核详情</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-apple-label-secondary">
|
||||
选中一个项目后,按节点查看目标时效、实际用时和超时情况。今天先做成单页联看的监控雏形。
|
||||
</p>
|
||||
</div>
|
||||
{selectedItem ? (
|
||||
<div className="inline-flex items-center gap-2 rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 py-2 text-xs text-apple-label-secondary">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
<span>{selectedItem.formSummary.openingCode || selectedItem.boardKey}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!selectedItem ? (
|
||||
<div className="px-4 py-12 text-center text-xs text-apple-label-secondary">
|
||||
暂无可查看的项目节点明细。
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-3 border-b border-apple-separator px-4 py-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<DetailField label="当前审批阶段" value={selectedItem.currentApprovalStageName} />
|
||||
<DetailField label="当前负责人" value={selectedItem.formSummary.currentOwnerDisplay} />
|
||||
<DetailField label="当前审批节点" value={selectedItem.formSummary.currentNodeDisplay} />
|
||||
<DetailField label="产品类型" value={selectedItem.formSummary.productType || selectedItem.productType} />
|
||||
<DetailField label="计划售卖品牌" value={selectedItem.formSummary.plannedSalesBrands} />
|
||||
<DetailField label="流程链接" value={selectedItem.formSummary.workflowLink} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 border-b border-apple-separator px-4 py-4 md:grid-cols-3 xl:grid-cols-6">
|
||||
<StatCard label="节点总数" value={selectedItem.nodeAssessments.length} description="当前项目已识别到的节点考核行。" />
|
||||
<StatCard label="超时节点" value={selectedItem.nodeAssessments.filter((node) => node.isOvertime).length} description="目标时效已被超过的节点数量。" tone="danger" />
|
||||
<StatCard label="项目目标时效" value={selectedItem.overallTargetHours ?? '-'} description="按产品类型配置的总目标时效。" />
|
||||
<StatCard label="项目实际用时" value={selectedItem.overallActualHours ?? '-'} description="当前项目累计实际用时。" tone="warning" />
|
||||
<StatCard label="总超时小时" value={selectedItem.overallTimeoutHours ?? '-'} description="超过项目总目标时效的小时数。" tone={selectedItem.overallTimeoutFlag ? 'danger' : 'success'} />
|
||||
<StatCard label="总流程状态" value={selectedItem.formSummary.totalFlowStatusLabel || '-'} description="当前项目总流程的状态摘要。" />
|
||||
</div>
|
||||
|
||||
<div className="max-h-[520px] overflow-auto">
|
||||
<table className="min-w-[1760px] table-fixed text-left text-[11px]">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr className="border-b border-apple-separator">
|
||||
<th className="w-[60px] px-3 py-2.5 font-apple-medium">序号</th>
|
||||
<th className="w-[160px] px-3 py-2.5 font-apple-medium">流程</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">节点名称</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">审批阶段</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">责任部门</th>
|
||||
<th className="w-[140px] px-3 py-2.5 font-apple-medium">负责人</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">目标时效</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">实际用时</th>
|
||||
<th className="w-[100px] px-3 py-2.5 font-apple-medium">是否超时</th>
|
||||
<th className="w-[120px] px-3 py-2.5 font-apple-medium">超时小时</th>
|
||||
<th className="w-[100px] px-3 py-2.5 font-apple-medium">状态</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">开始时间</th>
|
||||
<th className="w-[180px] px-3 py-2.5 font-apple-medium">完成时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedItem.nodeAssessments.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={13} className="px-4 py-12 text-center text-xs text-apple-label-secondary">
|
||||
当前项目还没有可展示的节点考核数据。
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
selectedItem.nodeAssessments.map((node, index) => (
|
||||
<NodeAssessmentRow key={`${node.taskId}:${index}`} item={node} index={index} />
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -27,6 +27,11 @@ export interface OaBoardFlowChipDTO {
|
||||
usedWorkflowFallback: boolean
|
||||
}
|
||||
|
||||
export interface OaBoardFormPreviewImageDTO {
|
||||
url: string
|
||||
alt: string
|
||||
}
|
||||
|
||||
export interface OaBoardNodeAssessmentDTO {
|
||||
flowType: string
|
||||
flowLabel: string
|
||||
@ -71,6 +76,7 @@ export interface OaBoardProjectListItemDTO {
|
||||
overallTimeoutFlag: boolean
|
||||
originatorDeptName: string | null
|
||||
lastSourceUpdatedAt: string | null
|
||||
styleImages: OaBoardFormPreviewImageDTO[]
|
||||
formSummary: {
|
||||
styleImageLabel: string
|
||||
openingCode: string
|
||||
@ -250,11 +256,70 @@ function readFormSummary(value: unknown): OaBoardProjectListItemDTO['formSummary
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeImageList(value: unknown): OaBoardFormPreviewImageDTO[] {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return []
|
||||
|
||||
if (trimmed.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return { url: item, alt: '款式图片' }
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
const record = item as Record<string, unknown>
|
||||
const url = typeof record.url === 'string' ? record.url : ''
|
||||
if (!url) return null
|
||||
return {
|
||||
url,
|
||||
alt: typeof record.alt === 'string' ? record.alt : '款式图片',
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((item): item is OaBoardFormPreviewImageDTO => Boolean(item))
|
||||
}
|
||||
} catch {
|
||||
return [{ url: trimmed, alt: '款式图片' }]
|
||||
}
|
||||
}
|
||||
|
||||
return [{ url: trimmed, alt: '款式图片' }]
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return { url: item, alt: '款式图片' }
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
const record = item as Record<string, unknown>
|
||||
const url = typeof record.url === 'string' ? record.url : ''
|
||||
if (!url) return null
|
||||
return {
|
||||
url,
|
||||
alt: typeof record.alt === 'string' ? record.alt : '款式图片',
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((item): item is OaBoardFormPreviewImageDTO => Boolean(item))
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
type PreviewProjectRecord = Awaited<ReturnType<typeof queryOaBoardPreview>>['projectGraphs'][number]
|
||||
|
||||
function toProjectListItem(record: PreviewProjectRecord): OaBoardProjectListItemDTO {
|
||||
const metadata = isRecord(record.metadata) ? record.metadata : null
|
||||
const formSummary = readFormSummary(metadata?.formSummary)
|
||||
const styleImages = normalizeImageList(metadata?.styleImages)
|
||||
const nodeAssessments = record.flowInstances
|
||||
.flatMap((flowInstance) =>
|
||||
flowInstance.taskRecords
|
||||
@ -317,6 +382,7 @@ function toProjectListItem(record: PreviewProjectRecord): OaBoardProjectListItem
|
||||
overallTimeoutFlag: record.overallTimeoutFlag,
|
||||
originatorDeptName: record.originatorDeptName,
|
||||
lastSourceUpdatedAt: toIsoString(record.lastSourceUpdatedAt),
|
||||
styleImages,
|
||||
formSummary,
|
||||
flowChips: record.flowInstances.map((flowInstance) => ({
|
||||
flowType: flowInstance.flowType,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user