431 lines
27 KiB
TypeScript
431 lines
27 KiB
TypeScript
import Link from 'next/link'
|
||
import {
|
||
ArrowRight,
|
||
CalendarDays,
|
||
CheckCircle2,
|
||
Clock3,
|
||
Database,
|
||
ExternalLink,
|
||
RotateCcw,
|
||
Search,
|
||
} from 'lucide-react'
|
||
import type {
|
||
FirstPassReviewProjectListItem,
|
||
FirstPassReviewProjectPage,
|
||
} from '@/lib/work-review-service'
|
||
import type {
|
||
MonthlyLaunchInsights,
|
||
OnTimeDeliveryInsights,
|
||
OngoingDingWorkflowInsights,
|
||
TeamWorkloadInsights,
|
||
} from '@/lib/ding-workflow-sync'
|
||
import DepartmentShell from './DepartmentShell'
|
||
import MonthlyLaunchesInsights from './MonthlyLaunchesInsights'
|
||
import OngoingProjectsInsights from './OngoingProjectsInsights'
|
||
import OnTimeDeliveryInsightsView from './OnTimeDeliveryInsights'
|
||
import OnTimeDeliveryRate from './OnTimeDeliveryRate'
|
||
import TeamWorkloadInsightsView from './TeamWorkloadInsights'
|
||
import type { CoreMetric } from './core-metrics'
|
||
import { formatMonthlyLaunchPeriod } from './monthly-launch-period'
|
||
|
||
type CoreMetricDetailProps = {
|
||
metric: CoreMetric
|
||
query?: string
|
||
projectInsights?: OngoingDingWorkflowInsights
|
||
monthlyLaunches?: MonthlyLaunchInsights
|
||
onTimeDelivery?: OnTimeDeliveryInsights
|
||
firstPassPage?: FirstPassReviewProjectPage
|
||
teamWorkload?: TeamWorkloadInsights
|
||
}
|
||
|
||
function formatDate(value: string | null, includeTime = false) {
|
||
if (!value) return '—'
|
||
return new Intl.DateTimeFormat('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
...(includeTime ? { hour: '2-digit', minute: '2-digit', hour12: false } : {}),
|
||
timeZone: 'Asia/Shanghai',
|
||
}).format(new Date(value))
|
||
}
|
||
|
||
function buildPageHref(metricKey: string, page: number, query?: string) {
|
||
const search = new URLSearchParams()
|
||
if (page > 1) search.set('page', String(page))
|
||
if (query) search.set('q', query)
|
||
const suffix = search.toString()
|
||
return `/workspaces/dashboard/core-metrics/${metricKey}${suffix ? `?${suffix}` : ''}`
|
||
}
|
||
|
||
function formatRate(rate: number | null) {
|
||
if (rate === null) return '—'
|
||
return `${Number.isInteger(rate) ? rate.toFixed(0) : rate.toFixed(1)}%`
|
||
}
|
||
|
||
const CURRENT_REVIEW_STATUS_LABELS: Record<string, string> = {
|
||
approved: '当前已通过',
|
||
pending_review: '当前评审中',
|
||
revision_required: '当前待修改',
|
||
}
|
||
|
||
const REVIEW_ROLE_LABELS: Record<string, string> = {
|
||
manager: '主管',
|
||
final_reviewer: '终审',
|
||
}
|
||
|
||
const REVIEW_DECISION_LABELS: Record<string, string> = {
|
||
approved: '通过',
|
||
revision_required: '退修',
|
||
}
|
||
|
||
function getFirstVersionReviewerSummary(project: FirstPassReviewProjectListItem) {
|
||
if (!project.firstVersionReviews?.length) return '待评审'
|
||
return project.firstVersionReviews.map((review) => (
|
||
`${review.reviewerName}(${REVIEW_ROLE_LABELS[review.reviewerRole] || '评审'} · ${REVIEW_DECISION_LABELS[review.decision] || '待核对'})`
|
||
)).join('、')
|
||
}
|
||
|
||
function FirstPassStatusBadge({ status }: { status: FirstPassReviewProjectListItem['firstVersionStatus'] }) {
|
||
if (status === 'approved') {
|
||
return (
|
||
<span className="inline-flex min-h-7 items-center gap-1.5 rounded-lg bg-emerald-50 px-2.5 text-xs font-bold text-emerald-800">
|
||
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />首轮通过
|
||
</span>
|
||
)
|
||
}
|
||
if (status === 'revision_required') {
|
||
return (
|
||
<span className="inline-flex min-h-7 items-center gap-1.5 rounded-lg bg-amber-50 px-2.5 text-xs font-bold text-amber-800">
|
||
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" />首轮未通过
|
||
</span>
|
||
)
|
||
}
|
||
if (status === 'pending_review') {
|
||
return (
|
||
<span className="inline-flex min-h-7 items-center gap-1.5 rounded-lg bg-blue-50 px-2.5 text-xs font-bold text-blue-700">
|
||
<Clock3 className="h-3.5 w-3.5" aria-hidden="true" />首轮评审中
|
||
</span>
|
||
)
|
||
}
|
||
return (
|
||
<span className="inline-flex min-h-7 items-center rounded-lg bg-slate-100 px-2.5 text-xs font-bold text-slate-600">
|
||
状态待核对
|
||
</span>
|
||
)
|
||
}
|
||
|
||
function FirstPassProjectList({ metric, query, firstPassPage }: Required<Pick<CoreMetricDetailProps, 'metric' | 'firstPassPage'>> & Pick<CoreMetricDetailProps, 'query'>) {
|
||
const { items, pagination, summary, submitterRates } = firstPassPage
|
||
|
||
return (
|
||
<div className="mt-10">
|
||
<section aria-labelledby="first-pass-submitter-rates-title">
|
||
<h2 id="first-pass-submitter-rates-title" className="text-xl font-bold tracking-[-0.02em] sm:text-2xl">各提交人的首轮通过率</h2>
|
||
<p className="mt-1 max-w-3xl text-sm leading-6 text-slate-600">
|
||
按提交人汇总 V1 审核通过作品数 ÷ 当前已通过作品数,按通过率从高到低排列。
|
||
</p>
|
||
|
||
{submitterRates.length === 0 ? (
|
||
<div role="status" className="mt-5 rounded-lg border border-[#e2e8f0] bg-white px-5 py-10 text-center text-sm text-slate-600">
|
||
暂无可统计的提交人数据。
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="mt-5 hidden overflow-x-auto rounded-lg border border-[#e2e8f0] bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#78aee9] focus-visible:ring-offset-2 md:block" tabIndex={0} aria-label="各提交人首轮通过率横向滚动表格">
|
||
<table className="min-w-[720px] w-full table-fixed text-left text-sm">
|
||
<caption className="sr-only">各提交人的首轮通过率,按通过率从高到低排列</caption>
|
||
<thead className="bg-slate-50 text-xs font-bold text-slate-500">
|
||
<tr>
|
||
<th scope="col" className="w-[32%] px-5 py-3.5">提交人</th>
|
||
<th scope="col" className="w-[20%] px-4 py-3.5">首轮通过率</th>
|
||
<th scope="col" className="w-[16%] px-4 py-3.5">V1 通过</th>
|
||
<th scope="col" className="w-[16%] px-4 py-3.5">当前已通过</th>
|
||
<th scope="col" className="w-[16%] px-5 py-3.5">V1 作品数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-200">
|
||
{submitterRates.map((submitter) => (
|
||
<tr key={submitter.submitterName} className="transition-colors hover:bg-slate-50">
|
||
<th scope="row" className="break-words px-5 py-4 font-bold text-slate-950">{submitter.submitterName}</th>
|
||
<td className="px-4 py-4 text-lg font-bold tabular-nums text-[#176fc1]">{formatRate(submitter.rate)}</td>
|
||
<td className="px-4 py-4 tabular-nums text-slate-700">{submitter.firstPassApproved}</td>
|
||
<td className="px-4 py-4 tabular-nums text-slate-700">{submitter.totalApproved}</td>
|
||
<td className="px-5 py-4 tabular-nums text-slate-700">{submitter.totalUploads}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<ul className="mt-5 divide-y divide-slate-200 overflow-hidden rounded-lg border border-[#e2e8f0] bg-white md:hidden" aria-label="各提交人首轮通过率列表">
|
||
{submitterRates.map((submitter) => (
|
||
<li key={submitter.submitterName} className="p-5">
|
||
<div className="flex min-w-0 items-start justify-between gap-4">
|
||
<h3 className="min-w-0 break-words font-bold text-slate-950">{submitter.submitterName}</h3>
|
||
<p className="shrink-0 text-lg font-bold tabular-nums text-[#176fc1]">{formatRate(submitter.rate)}</p>
|
||
</div>
|
||
<p className="mt-2 text-xs tabular-nums leading-5 text-slate-500">
|
||
V1 通过 {submitter.firstPassApproved} · 当前已通过 {submitter.totalApproved} · V1 作品 {submitter.totalUploads}
|
||
</p>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</>
|
||
)}
|
||
</section>
|
||
|
||
<section aria-labelledby="first-pass-list-title" className="mt-10">
|
||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||
<div>
|
||
<h2 id="first-pass-list-title" className="text-xl font-bold tracking-[-0.02em] sm:text-2xl">所有作品明细</h2>
|
||
<p className="mt-1 text-sm leading-6 text-slate-600">
|
||
展示作品过稿后台全部已上传且包含 V1 的作品;每个作品只取 V1 审核结果,评审中、首轮未通过和待核对状态均会列出。
|
||
</p>
|
||
</div>
|
||
<a
|
||
href="https://sc.jiansun.vip/work-review"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="inline-flex min-h-10 items-center gap-2 self-start rounded-xl bg-white px-3 text-sm font-bold text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 lg:self-auto"
|
||
>
|
||
打开作品过稿数据源<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||
</a>
|
||
</div>
|
||
|
||
<div className="mt-5 rounded-lg border border-[#e2e8f0] bg-white px-5 py-4">
|
||
<p className="text-xs font-bold text-slate-500">统计公式</p>
|
||
<p className="mt-1 text-sm font-bold text-slate-900">
|
||
首轮通过率 = V1 审核通过作品数 ÷ 所有已通过作品数 × 100%
|
||
</p>
|
||
<p className="mt-2 text-xs tabular-nums text-slate-500">
|
||
当前口径:{summary.firstPassApproved} ÷ {summary.totalApproved} = {formatRate(summary.rate)};共读取 {summary.totalUploads} 个作品
|
||
</p>
|
||
</div>
|
||
|
||
<div className="mt-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||
<div className="max-w-xl flex-1">
|
||
<form className="flex gap-2" action={`/workspaces/dashboard/core-metrics/${metric.key}`} method="get" role="search">
|
||
<label htmlFor="first-pass-query" className="sr-only">搜索作品、提交人或负责人</label>
|
||
<div className="relative min-w-0 flex-1">
|
||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" aria-hidden="true" />
|
||
<input
|
||
id="first-pass-query"
|
||
name="q"
|
||
type="search"
|
||
defaultValue={query}
|
||
aria-describedby="first-pass-search-note"
|
||
placeholder="搜索作品、提交人或负责人"
|
||
className="min-h-11 w-full rounded-xl border border-slate-300 bg-white py-2 pl-9 pr-3 text-sm text-slate-950 outline-none transition-colors placeholder:text-slate-400 focus:border-slate-500 focus:ring-2 focus:ring-slate-300"
|
||
/>
|
||
</div>
|
||
<button type="submit" className="min-h-11 shrink-0 rounded-xl bg-[#176fc1] px-4 text-sm font-bold text-white transition-colors hover:bg-[#125b9d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#78aee9] focus-visible:ring-offset-2">
|
||
查询
|
||
</button>
|
||
</form>
|
||
<p id="first-pass-search-note" className="mt-2 text-xs text-slate-500">搜索只筛选下方明细,不改变上方完整统计口径。</p>
|
||
</div>
|
||
<p className="text-sm font-semibold tabular-nums text-slate-600">共 {pagination.total} 个作品</p>
|
||
</div>
|
||
|
||
{items.length === 0 ? (
|
||
<div role="status" className="mt-6 rounded-lg border border-[#e2e8f0] bg-white px-5 py-14 text-center">
|
||
<Database className="mx-auto h-8 w-8 text-slate-400" aria-hidden="true" />
|
||
<h3 className="mt-4 text-base font-bold text-slate-900">
|
||
{query ? '没有匹配的作品' : '当前没有可统计的 V1 作品'}
|
||
</h3>
|
||
<p className="mx-auto mt-2 max-w-xl text-sm leading-6 text-slate-600">
|
||
{query ? '请调整关键词后重新查询。' : '作品上传至作品过稿后,本页会读取 V1 审核状态并自动纳入统计。'}
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="mt-6 hidden overflow-x-auto rounded-lg border border-[#e2e8f0] bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#78aee9] focus-visible:ring-offset-2 md:block" tabIndex={0} aria-label="所有作品明细横向滚动表格">
|
||
<table className="min-w-[1400px] w-full table-fixed text-left text-sm">
|
||
<caption className="sr-only">所有作品的 V1 首轮审核数据表</caption>
|
||
<thead className="bg-slate-50 text-xs font-bold text-slate-500">
|
||
<tr>
|
||
<th scope="col" className="w-[260px] px-5 py-3.5">作品</th>
|
||
<th scope="col" className="w-[170px] px-4 py-3.5">首轮结果</th>
|
||
<th scope="col" className="w-[120px] px-4 py-3.5">当前状态</th>
|
||
<th scope="col" className="w-[100px] px-4 py-3.5">提交人</th>
|
||
<th scope="col" className="w-[110px] px-4 py-3.5">主管</th>
|
||
<th scope="col" className="w-[110px] px-4 py-3.5">终审</th>
|
||
<th scope="col" className="w-[230px] px-4 py-3.5">V1 评审</th>
|
||
<th scope="col" className="w-[150px] px-4 py-3.5">V1 上传</th>
|
||
<th scope="col" className="w-[150px] px-4 py-3.5">V1 完成</th>
|
||
<th scope="col" className="w-[130px] px-5 py-3.5">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-200">
|
||
{items.map((project) => (
|
||
<tr key={project.id} className="align-top transition-colors hover:bg-slate-50">
|
||
<td className="px-5 py-4">
|
||
<p className="truncate font-bold text-slate-950">{project.title}</p>
|
||
{project.brand && <p className="mt-1 text-xs font-semibold text-slate-600">品牌:{project.brand}</p>}
|
||
{project.description && <p className="mt-1 truncate text-xs text-slate-600">{project.description}</p>}
|
||
<p className="mt-1 text-xs text-slate-500">作品日期 {formatDate(project.workDate)} · 当前第 {project.latestVersion} 版</p>
|
||
</td>
|
||
<td className="px-4 py-4"><FirstPassStatusBadge status={project.firstVersionStatus} /></td>
|
||
<td className="px-4 py-4 font-medium text-slate-700">{CURRENT_REVIEW_STATUS_LABELS[project.currentStatus] || '当前状态待核对'}</td>
|
||
<td className="truncate px-4 py-4 font-medium text-slate-700">{project.submitterName}</td>
|
||
<td className="truncate px-4 py-4 font-medium text-slate-700">{project.managerName}</td>
|
||
<td className="truncate px-4 py-4 font-medium text-slate-700">{project.finalReviewerName}</td>
|
||
<td className="px-4 py-4 text-xs leading-5 text-slate-600">{getFirstVersionReviewerSummary(project)}</td>
|
||
<td className="whitespace-nowrap px-4 py-4 tabular-nums text-slate-600">{formatDate(project.firstVersionSubmittedAt, true)}</td>
|
||
<td className="whitespace-nowrap px-4 py-4 tabular-nums text-slate-600">{formatDate(project.firstVersionCompletedAt, true)}</td>
|
||
<td className="px-5 py-4">
|
||
<Link href={`/work-review?project=${encodeURIComponent(project.id)}`} aria-label={`查看${project.title}过稿记录`} className="inline-flex min-h-9 items-center gap-1 rounded-lg px-2 font-bold text-[#b4231c] hover:bg-[#fff0ee] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#d92d20]">
|
||
查看过稿记录<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
|
||
</Link>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<ul className="mt-6 divide-y divide-slate-200 overflow-hidden rounded-lg border border-[#e2e8f0] bg-white md:hidden" aria-label="所有作品的 V1 首轮审核列表">
|
||
{items.map((project) => (
|
||
<li key={project.id} className="p-5">
|
||
<h3 className="break-words font-bold text-slate-950">{project.title}</h3>
|
||
{project.brand && <p className="mt-1 text-sm font-semibold text-slate-600">品牌:{project.brand}</p>}
|
||
{project.description && <p className="mt-1 text-sm leading-6 text-slate-600">{project.description}</p>}
|
||
<div className="mt-3"><FirstPassStatusBadge status={project.firstVersionStatus} /></div>
|
||
<dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||
<div><dt className="text-xs text-slate-500">当前状态</dt><dd className="mt-1 font-semibold text-slate-800">{CURRENT_REVIEW_STATUS_LABELS[project.currentStatus] || '待核对'}</dd></div>
|
||
<div><dt className="text-xs text-slate-500">当前版本</dt><dd className="mt-1 font-semibold tabular-nums text-slate-800">V{project.latestVersion}</dd></div>
|
||
<div><dt className="text-xs text-slate-500">提交人</dt><dd className="mt-1 font-semibold text-slate-800">{project.submitterName}</dd></div>
|
||
<div><dt className="text-xs text-slate-500">作品日期</dt><dd className="mt-1 tabular-nums text-slate-700">{formatDate(project.workDate)}</dd></div>
|
||
<div><dt className="text-xs text-slate-500">主管</dt><dd className="mt-1 font-semibold text-slate-800">{project.managerName}</dd></div>
|
||
<div><dt className="text-xs text-slate-500">终审</dt><dd className="mt-1 font-semibold text-slate-800">{project.finalReviewerName}</dd></div>
|
||
<div><dt className="text-xs text-slate-500">V1 上传</dt><dd className="mt-1 tabular-nums text-slate-700">{formatDate(project.firstVersionSubmittedAt, true)}</dd></div>
|
||
<div><dt className="text-xs text-slate-500">V1 完成</dt><dd className="mt-1 tabular-nums text-slate-700">{formatDate(project.firstVersionCompletedAt, true)}</dd></div>
|
||
</dl>
|
||
<p className="mt-4 text-xs leading-5 text-slate-600">V1 评审:{getFirstVersionReviewerSummary(project)}</p>
|
||
<Link href={`/work-review?project=${encodeURIComponent(project.id)}`} aria-label={`查看${project.title}过稿记录`} className="mt-4 inline-flex min-h-10 items-center gap-1 rounded-xl bg-slate-100 px-3 text-sm font-bold text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400">
|
||
查看过稿记录<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</>
|
||
)}
|
||
|
||
{pagination.totalPages > 1 && (
|
||
<nav aria-label="所有作品的 V1 首轮审核分页" className="mt-5 flex items-center justify-between gap-4 text-sm">
|
||
<p className="text-slate-500">第 {pagination.page} / {pagination.totalPages} 页</p>
|
||
<div className="flex gap-2">
|
||
{pagination.page > 1 && <Link className="inline-flex min-h-10 items-center rounded-xl bg-white px-3 font-bold text-slate-700 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400" href={buildPageHref(metric.key, pagination.page - 1, query)}>上一页</Link>}
|
||
{pagination.page < pagination.totalPages && <Link className="inline-flex min-h-10 items-center rounded-xl bg-white px-3 font-bold text-slate-700 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400" href={buildPageHref(metric.key, pagination.page + 1, query)}>下一页</Link>}
|
||
</div>
|
||
</nav>
|
||
)}
|
||
</section>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function PendingDataSource({ metric }: { metric: CoreMetric }) {
|
||
return (
|
||
<section aria-labelledby="detail-fields-title" className="mt-10">
|
||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||
<div>
|
||
<h2 id="detail-fields-title" className="text-xl font-bold tracking-[-0.02em] sm:text-2xl">明细框架</h2>
|
||
<p className="mt-1 text-sm leading-6 text-slate-600">底层业务表与公式尚未接入,当前不展示虚构记录。</p>
|
||
</div>
|
||
<span className="rounded-lg bg-amber-50 px-3 py-2 text-xs font-bold text-amber-800">数据源待接入</span>
|
||
</div>
|
||
<div role="status" className="mt-6 overflow-hidden rounded-lg border border-[#e2e8f0] bg-white">
|
||
<div className="border-b border-slate-200 px-5 py-4">
|
||
<h3 className="font-bold text-slate-950">计划展示字段</h3>
|
||
</div>
|
||
<div className="flex flex-wrap gap-2 p-5">
|
||
{metric.plannedFields.map((field) => <span key={field} className="rounded-lg bg-slate-100 px-3 py-2 text-sm font-semibold text-slate-700">{field}</span>)}
|
||
</div>
|
||
<p className="border-t border-slate-200 px-5 py-8 text-center text-sm leading-6 text-slate-500">
|
||
数据源、字段映射和统计公式确认后,本页将自动列出对应业务明细。
|
||
</p>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export default function CoreMetricDetail({ metric, query, projectInsights, monthlyLaunches, onTimeDelivery, firstPassPage, teamWorkload }: CoreMetricDetailProps) {
|
||
const hasRealProjectSource = metric.key === 'ongoing-projects' && projectInsights
|
||
const hasMonthlyLaunchSource = metric.key === 'monthly-launches' && monthlyLaunches
|
||
const hasFirstPassSource = metric.key === 'first-pass-rate' && firstPassPage
|
||
const hasOnTimeDeliverySource = metric.key === 'on-time-delivery' && onTimeDelivery
|
||
const hasTeamWorkloadSource = metric.key === 'team-workload' && teamWorkload
|
||
const monthlyLaunchPeriod = hasMonthlyLaunchSource
|
||
? formatMonthlyLaunchPeriod(monthlyLaunches.summary.monthStart, monthlyLaunches.summary.monthEnd)
|
||
: ''
|
||
const onTimeDeliveryPeriod = hasOnTimeDeliverySource
|
||
? (onTimeDelivery.availableMonths?.length
|
||
? formatMonthlyLaunchPeriod(onTimeDelivery.summary.monthStart, onTimeDelivery.summary.monthEnd)
|
||
: '本月')
|
||
: ''
|
||
|
||
return (
|
||
<DepartmentShell>
|
||
<main className="mx-auto max-w-[1600px] px-4 pb-16 pt-7 sm:px-6 lg:px-8">
|
||
<header className="border-b border-[#dfe4ea] pb-6">
|
||
<h1 className="text-balance text-3xl font-bold tracking-[-0.03em] sm:text-4xl">{metric.label}</h1>
|
||
<p className="mt-3 max-w-3xl text-base leading-7 text-[#66758b] sm:text-lg">{metric.detailDescription}</p>
|
||
</header>
|
||
|
||
<section aria-label="指标口径" className="mt-6 grid overflow-hidden rounded-lg border border-[#e2e8f0] bg-white sm:grid-cols-3">
|
||
<div className="border-b border-slate-200 p-5 sm:border-b-0 sm:border-r">
|
||
<p className="text-xs font-bold text-slate-500">{hasFirstPassSource ? '当前首轮通过率' : hasRealProjectSource ? '当前未完结流程' : hasMonthlyLaunchSource ? `${monthlyLaunchPeriod}已完结 / 新品流程总数` : hasOnTimeDeliverySource ? `${onTimeDeliveryPeriod}按时交付率` : hasTeamWorkloadSource ? '品牌中心平均负荷' : '总览示例值'}</p>
|
||
{hasOnTimeDeliverySource && onTimeDelivery.summary.rate !== null
|
||
? <OnTimeDeliveryRate rate={onTimeDelivery.summary.rate} contextLabel="按时交付当前值" size="summary" />
|
||
: <p className="mt-2 text-2xl font-bold tabular-nums">{hasFirstPassSource ? formatRate(firstPassPage.summary.rate) : hasRealProjectSource ? projectInsights.total : hasMonthlyLaunchSource ? `${monthlyLaunches.summary.completed}/${monthlyLaunches.summary.total}` : hasOnTimeDeliverySource ? '—' : hasTeamWorkloadSource ? formatRate(teamWorkload.summary.averageLoadPercent) : metric.exampleValue}</p>}
|
||
</div>
|
||
<div className="border-b border-slate-200 p-5 sm:border-b-0 sm:border-r">
|
||
<p className="text-xs font-bold text-slate-500">明细数据</p>
|
||
<p className="mt-2 text-base font-bold">{hasFirstPassSource ? `V1通过 ${firstPassPage.summary.firstPassApproved} / 已通过 ${firstPassPage.summary.totalApproved}` : hasRealProjectSource ? `${projectInsights.total} 条流程快照` : hasMonthlyLaunchSource ? `${monthlyLaunchPeriod}已完结 ${monthlyLaunches.summary.completed} / 当前进行中 ${monthlyLaunches.summary.ongoing}` : hasOnTimeDeliverySource ? `未超时审核节点完成 ${onTimeDelivery.summary.onTime} / 审核节点完成 ${onTimeDelivery.summary.total}` : hasTeamWorkloadSource ? `${teamWorkload.summary.busyPersonnel} 人有进行中流程 / 共 ${teamWorkload.summary.personnelCount} 人` : '等待业务数据源'}</p>
|
||
</div>
|
||
<div className="p-5">
|
||
<p className="text-xs font-bold text-slate-500">同步状态</p>
|
||
<p className="mt-2 inline-flex items-center gap-2 text-base font-bold">
|
||
<CalendarDays className="h-4 w-4 text-slate-500" aria-hidden="true" />
|
||
{hasRealProjectSource
|
||
? projectInsights.sync.lastSyncedAt
|
||
? `每小时增量同步 · ${formatDate(projectInsights.sync.lastSyncedAt, true)}`
|
||
: projectInsights.sync.syncStatus === 'error' ? '首轮同步失败' : '等待首轮同步'
|
||
: hasMonthlyLaunchSource
|
||
? monthlyLaunches.summary.lastSyncedAt
|
||
? `每小时增量同步 · ${formatDate(monthlyLaunches.summary.lastSyncedAt, true)}`
|
||
: monthlyLaunches.summary.syncStatus === 'error' ? '首轮同步失败' : '等待首轮同步'
|
||
: hasOnTimeDeliverySource
|
||
? onTimeDelivery.summary.lastSyncedAt
|
||
? `每小时增量同步 · ${formatDate(onTimeDelivery.summary.lastSyncedAt, true)}`
|
||
: onTimeDelivery.summary.syncStatus === 'error' ? '首轮同步失败' : '等待首轮同步'
|
||
: hasFirstPassSource
|
||
? firstPassPage.summary.lastSyncedAt
|
||
? `每小时增量同步 · ${formatDate(firstPassPage.summary.lastSyncedAt, true)}`
|
||
: firstPassPage.summary.syncStatus === 'error' ? '增量同步失败' : '等待首轮同步'
|
||
: hasTeamWorkloadSource
|
||
? teamWorkload.summary.lastSyncedAt
|
||
? `每小时增量同步 · ${formatDate(teamWorkload.summary.lastSyncedAt, true)}`
|
||
: teamWorkload.summary.syncStatus === 'error' ? '同步失败' : '等待首轮同步'
|
||
: '每日同步待接入'}
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
{hasRealProjectSource
|
||
? <OngoingProjectsInsights insights={projectInsights} />
|
||
: hasMonthlyLaunchSource
|
||
? <MonthlyLaunchesInsights insights={monthlyLaunches} />
|
||
: hasOnTimeDeliverySource
|
||
? <OnTimeDeliveryInsightsView insights={onTimeDelivery} />
|
||
: hasFirstPassSource
|
||
? <FirstPassProjectList metric={metric} query={query} firstPassPage={firstPassPage} />
|
||
: hasTeamWorkloadSource
|
||
? <TeamWorkloadInsightsView insights={teamWorkload} />
|
||
: <PendingDataSource metric={metric} />}
|
||
</main>
|
||
</DepartmentShell>
|
||
)
|
||
}
|