gongxiangp2/components/department-dashboard/CoreMetricsDashboard.tsx
2026-08-31 14:38:23 +08:00

301 lines
16 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.

'use client'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import {
ArrowRight,
CalendarClock,
} from 'lucide-react'
import type { FirstPassReviewSummary } from '@/lib/work-review-metrics'
import type {
MonthlyLaunchSummary,
OnTimeDeliverySummary,
OngoingDingWorkflowSummary,
} from '@/lib/ding-workflow-sync'
import DepartmentShell from './DepartmentShell'
import { CORE_METRICS, getCoreMetricHref } from './core-metrics'
import OnTimeDeliveryRate from './OnTimeDeliveryRate'
type FirstPassState =
| { status: 'loading'; data: null }
| { status: 'success'; data: FirstPassReviewSummary }
| { status: 'error'; data: null }
type OngoingWorkflowState =
| { status: 'loading'; data: null }
| { status: 'success'; data: OngoingDingWorkflowSummary }
| { status: 'error'; data: null }
type MonthlyLaunchState =
| { status: 'loading'; data: null }
| { status: 'success'; data: MonthlyLaunchSummary }
| { status: 'error'; data: null }
type OnTimeDeliveryState =
| { status: 'loading'; data: null }
| { status: 'success'; data: OnTimeDeliverySummary }
| { status: 'error'; data: null }
function formatRate(rate: number | null) {
if (rate === null) return '—'
return `${Number.isInteger(rate) ? rate.toFixed(0) : rate.toFixed(1)}%`
}
function formatSyncTime(value: string | null) {
if (!value) return '等待首轮同步'
return new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Asia/Shanghai',
}).format(new Date(value))
}
export default function CoreMetricsDashboard() {
const [firstPass, setFirstPass] = useState<FirstPassState>({ status: 'loading', data: null })
const [ongoingWorkflows, setOngoingWorkflows] = useState<OngoingWorkflowState>({ status: 'loading', data: null })
const [monthlyLaunches, setMonthlyLaunches] = useState<MonthlyLaunchState>({ status: 'loading', data: null })
const [onTimeDelivery, setOnTimeDelivery] = useState<OnTimeDeliveryState>({ status: 'loading', data: null })
useEffect(() => {
const controller = new AbortController()
async function loadFirstPassSummary() {
try {
const response = await fetch('/api/department-dashboard/core-metrics/first-pass', {
cache: 'no-store',
signal: controller.signal,
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload?.success || !payload.data) throw new Error('指标读取失败')
setFirstPass({ status: 'success', data: payload.data })
} catch (error) {
if ((error as Error).name !== 'AbortError') setFirstPass({ status: 'error', data: null })
}
}
async function loadOngoingWorkflowSummary() {
try {
const response = await fetch('/api/department-dashboard/core-metrics/ongoing-projects', {
cache: 'no-store',
signal: controller.signal,
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload?.success || !payload.data) throw new Error('指标读取失败')
setOngoingWorkflows({ status: 'success', data: payload.data })
} catch (error) {
if ((error as Error).name !== 'AbortError') setOngoingWorkflows({ status: 'error', data: null })
}
}
async function loadMonthlyLaunchSummary() {
try {
const response = await fetch('/api/department-dashboard/core-metrics/monthly-launches', {
cache: 'no-store',
signal: controller.signal,
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload?.success || !payload.data) throw new Error('指标读取失败')
setMonthlyLaunches({ status: 'success', data: payload.data })
} catch (error) {
if ((error as Error).name !== 'AbortError') setMonthlyLaunches({ status: 'error', data: null })
}
}
async function loadOnTimeDeliverySummary() {
try {
const response = await fetch('/api/department-dashboard/core-metrics/on-time-delivery', {
cache: 'no-store',
signal: controller.signal,
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload?.success || !payload.data) throw new Error('指标读取失败')
setOnTimeDelivery({ status: 'success', data: payload.data })
} catch (error) {
if ((error as Error).name !== 'AbortError') setOnTimeDelivery({ status: 'error', data: null })
}
}
void loadFirstPassSummary()
void loadOngoingWorkflowSummary()
void loadMonthlyLaunchSummary()
void loadOnTimeDeliverySummary()
return () => controller.abort()
}, [])
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 id="core-metrics-title" className="max-w-4xl text-balance text-3xl font-bold tracking-[-0.03em] sm:text-4xl">
核心指标
</h1>
<p className="mt-3 max-w-3xl text-base leading-7 text-[#66758b] sm:text-lg">
集中查看项目进度、交付质量、团队负荷与风险状态,为部门日常经营提供统一入口。
</p>
</header>
<section aria-labelledby="metrics-heading" className="mt-6">
<div className="mb-0 flex flex-wrap items-end justify-between gap-3 rounded-t-xl border border-b-0 border-[#e2e8f0] bg-white px-6 py-5">
<div>
<h2 id="metrics-heading" className="text-xl font-bold tracking-[-0.02em] sm:text-2xl">六项核心经营指标</h2>
<p className="mt-1 text-sm text-[#66758b]">每个分区已预留独立公式与同步状态。</p>
</div>
<p className="rounded-full bg-[#f1f6fc] px-4 py-2 text-sm font-semibold text-[#5f7189]">6 个指标分区</p>
</div>
<div
aria-describedby="metrics-disclaimer"
className="relative grid gap-px overflow-hidden rounded-b-xl border border-[#e2e8f0] bg-[#e2e9f2] pt-1 shadow-[0_6px_18px_rgba(15,23,42,0.05)] sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-6"
>
<span className="absolute inset-x-0 top-0 h-1 bg-[#5a9ce6]" aria-hidden="true" />
{CORE_METRICS.map((metric) => {
const Icon = metric.icon
const isOngoingWorkflow = metric.key === 'ongoing-projects'
const isMonthlyLaunch = metric.key === 'monthly-launches'
const isFirstPass = metric.key === 'first-pass-rate'
const isOnTimeDelivery = metric.key === 'on-time-delivery'
const badge = isOngoingWorkflow
? ongoingWorkflows.status === 'success'
? ongoingWorkflows.data.syncStatus === 'ready'
? '每小时同步'
: ongoingWorkflows.data.syncStatus === 'syncing'
? '正在同步'
: ongoingWorkflows.data.syncStatus === 'error'
? '同步异常'
: '等待首轮同步'
: ongoingWorkflows.status === 'error' ? '读取失败' : '正在读取'
: isMonthlyLaunch
? monthlyLaunches.status === 'success'
? monthlyLaunches.data.syncStatus === 'ready'
? '每小时同步'
: monthlyLaunches.data.syncStatus === 'syncing'
? '正在同步'
: monthlyLaunches.data.syncStatus === 'error'
? '同步异常'
: '等待首轮同步'
: monthlyLaunches.status === 'error' ? '读取失败' : '正在读取'
: isOnTimeDelivery
? onTimeDelivery.status === 'success'
? onTimeDelivery.data.syncStatus === 'ready'
? '每小时同步'
: onTimeDelivery.data.syncStatus === 'syncing'
? '正在同步'
: onTimeDelivery.data.syncStatus === 'error'
? '同步异常'
: '等待首轮同步'
: onTimeDelivery.status === 'error' ? '读取失败' : '正在读取'
: isFirstPass
? firstPass.status === 'success'
? firstPass.data.syncStatus === 'ready' ? '每小时同步' : firstPass.data.syncStatus === 'error' ? '同步异常' : '正在同步'
: firstPass.status === 'error' ? '读取失败' : '正在读取'
: '示例值'
const value = isOngoingWorkflow
? ongoingWorkflows.status === 'success' ? String(ongoingWorkflows.data.total) : '—'
: isMonthlyLaunch
? monthlyLaunches.status === 'success' ? `${monthlyLaunches.data.completed}/${monthlyLaunches.data.total}` : '—'
: isOnTimeDelivery
? onTimeDelivery.status === 'success' ? formatRate(onTimeDelivery.data.rate) : '—'
: isFirstPass
? firstPass.status === 'success' ? formatRate(firstPass.data.rate) : '—'
: metric.exampleValue
return (
<Link
key={metric.key}
href={getCoreMetricHref(metric.key)}
aria-label={isFirstPass ? '查看所有作品明细' : `查看${metric.label}明细`}
className="group flex min-h-48 flex-col bg-[#f8fafd] p-5 transition-colors hover:bg-white focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#5a9ce6] sm:min-h-56 sm:p-6"
>
<div className="flex items-start justify-between gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#edf5ff] text-[#176fc1]">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<span className="rounded-lg bg-white px-2.5 py-1 text-xs font-bold text-[#5f7189] shadow-[0_4px_12px_rgba(67,93,126,0.06)]">
{badge}
</span>
</div>
<h3 className="mt-6 text-base font-bold text-[#4f6077]">{metric.label}</h3>
<div aria-live={isFirstPass || isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery ? 'polite' : undefined} aria-atomic={isFirstPass || isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery ? 'true' : undefined}>
{isOnTimeDelivery && onTimeDelivery.status === 'success' && onTimeDelivery.data.rate !== null
? <OnTimeDeliveryRate rate={onTimeDelivery.data.rate} contextLabel={`${metric.label}当前值`} />
: (
<p className="mt-2 text-3xl font-bold tracking-[-0.03em] text-[#101828] sm:text-4xl" aria-label={`${metric.label}${isFirstPass || isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery ? '当前值' : '示例值'}${value}`}>
{value}
</p>
)}
{isFirstPass && firstPass.status === 'success' && (
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
V1通过 {firstPass.data.firstPassApproved} / 已通过 {firstPass.data.totalApproved}
</p>
)}
{isFirstPass && firstPass.status === 'success' && firstPass.data.lastSyncedAt && (
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
最近同步 {formatSyncTime(firstPass.data.lastSyncedAt)}
</p>
)}
{isFirstPass && firstPass.status === 'loading' && (
<p className="mt-2 text-xs font-semibold text-slate-500">正在连接作品过稿</p>
)}
{isOngoingWorkflow && ongoingWorkflows.status === 'success' && (
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
最近同步 {formatSyncTime(ongoingWorkflows.data.lastSyncedAt)}
</p>
)}
{isOngoingWorkflow && ongoingWorkflows.status === 'loading' && (
<p className="mt-2 text-xs font-semibold text-slate-500">正在读取本地流程快照</p>
)}
{isMonthlyLaunch && monthlyLaunches.status === 'success' && (
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
本月已完结 {monthlyLaunches.data.completed} · 当前进行中 {monthlyLaunches.data.ongoing}
</p>
)}
{isMonthlyLaunch && monthlyLaunches.status === 'loading' && (
<p className="mt-2 text-xs font-semibold text-slate-500">正在读取本地流程快照</p>
)}
{isOnTimeDelivery && onTimeDelivery.status === 'success' && (
<p className="mt-2 text-xs font-semibold tabular-nums text-slate-500">
未超时 {onTimeDelivery.data.onTime} / 本月完结 {onTimeDelivery.data.total}
</p>
)}
{isOnTimeDelivery && onTimeDelivery.status === 'loading' && (
<p className="mt-2 text-xs font-semibold text-slate-500">正在计算本月交付时效</p>
)}
</div>
<p className="mt-3 text-sm leading-6 text-[#66758b]">{metric.description}</p>
<div className="mt-auto flex flex-wrap items-center justify-between gap-x-4 gap-y-2 pt-6 text-xs font-semibold">
<span className="flex flex-wrap items-center gap-x-3 gap-y-1 text-slate-500">
<span className="inline-flex items-center gap-1.5">
<CalendarClock className="h-3.5 w-3.5" aria-hidden="true" />
{isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery
? (isOngoingWorkflow ? ongoingWorkflows.status : isMonthlyLaunch ? monthlyLaunches.status : onTimeDelivery.status) === 'error' ? '刷新页面重试' : '每小时增量同步'
: isFirstPass ? (firstPass.status === 'error' ? '刷新页面重试' : '每小时增量同步') : '每日同步'}
</span>
<span aria-hidden="true">·</span>
<span>{isOngoingWorkflow || isMonthlyLaunch || isOnTimeDelivery
? (isOngoingWorkflow ? ongoingWorkflows.status : isMonthlyLaunch ? monthlyLaunches.status : onTimeDelivery.status) === 'error' ? '数据未加载' : '钉钉流程快照'
: isFirstPass ? (firstPass.status === 'error' ? '数据未加载' : '作品过稿后台') : '公式待接入'}</span>
</span>
<span className="inline-flex items-center gap-1 text-[#176fc1]">
{isFirstPass ? '查看所有作品明细' : '查看明细'}
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</div>
</Link>
)
})}
</div>
<p id="metrics-disclaimer" className="mt-3 text-xs leading-5 text-[#66758b]">
“进行中项目”和“本月上新”每小时读取钉钉审批流程三表快照,“首轮通过”读取作品过稿后台 V1 审核结果并每小时增量同步;其余示例值将在对应数据源与公式确认后替换。
</p>
</section>
</main>
</DepartmentShell>
)
}