Compare commits

...

1 Commits

Author SHA1 Message Date
jingchuan
f53a956df9 fix(oa-board): refine overview and detail tabs 2026-06-15 17:49:44 +08:00
2 changed files with 275 additions and 158 deletions

View File

@ -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>
)

View File

@ -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,