162 lines
8.4 KiB
TypeScript
162 lines
8.4 KiB
TypeScript
import Link from 'next/link'
|
||
import { ArrowLeft, GitBranch, TrendingUp } from 'lucide-react'
|
||
import DepartmentShell from './DepartmentShell'
|
||
import {
|
||
BUSINESS_TREND_BRANDS,
|
||
BUSINESS_TREND_STAGES,
|
||
BUSINESS_TREND_WEEK_LABELS,
|
||
summarizeBusinessTrend,
|
||
type BusinessTrendBrand,
|
||
} from './business-trends'
|
||
|
||
function formatAttainmentRate(rate: number | null) {
|
||
return rate === null ? '—' : `${rate.toFixed(1)}%`
|
||
}
|
||
|
||
function BusinessTrendBand({ brand }: { brand: BusinessTrendBrand }) {
|
||
const summary = summarizeBusinessTrend(brand)
|
||
const chartMax = Math.max(...brand.planned, ...brand.actual, 1)
|
||
|
||
return (
|
||
<article
|
||
aria-labelledby={`business-trend-${brand.key}`}
|
||
className="overflow-hidden rounded-xl border border-[#e2e8f0] bg-white shadow-[0_6px_18px_rgba(15,23,42,0.05)]"
|
||
>
|
||
<div className="h-1 bg-[#5a9ce6]" aria-hidden="true" />
|
||
<header className="flex flex-col gap-4 px-5 py-5 sm:flex-row sm:items-center sm:justify-between sm:px-7 sm:py-6">
|
||
<div className="flex min-w-0 items-center gap-4">
|
||
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[#edf5ff] text-[#176fc1]">
|
||
<TrendingUp className="h-5 w-5" aria-hidden="true" />
|
||
</span>
|
||
<div className="min-w-0">
|
||
<h2 id={`business-trend-${brand.key}`} className="text-xl font-bold tracking-[-0.02em] text-[#101828] sm:text-2xl">
|
||
{brand.name}
|
||
</h2>
|
||
<p className="mt-1 text-sm font-semibold text-[#176fc1]">{brand.team}负责品牌</p>
|
||
</div>
|
||
</div>
|
||
<span className="inline-flex min-h-8 items-center self-start rounded-lg bg-[#f1f6fc] px-3 text-xs font-bold text-[#5f7189] sm:self-center">
|
||
示例值 · 数据源待接入
|
||
</span>
|
||
</header>
|
||
|
||
<div className="border-t border-[#e2e9f2] lg:grid lg:grid-cols-[minmax(0,1fr)_320px]">
|
||
<figure className="min-w-0 px-5 py-6 sm:px-7 sm:py-7">
|
||
<figcaption className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||
<div>
|
||
<h3 className="text-base font-bold text-[#1b2738]">近 8 周计划 vs 实际</h3>
|
||
<p className="mt-1 text-sm text-[#66758b]">W1 至 W8 由早到近,接入后替换为真实周次。</p>
|
||
</div>
|
||
<div className="flex items-center gap-4 text-xs font-semibold text-[#5f7189]" aria-label="图例">
|
||
<span className="inline-flex items-center gap-2"><span className="h-2.5 w-2.5 rounded-sm bg-[#dce5ef]" aria-hidden="true" />计划</span>
|
||
<span className="inline-flex items-center gap-2"><span className="h-2.5 w-2.5 rounded-sm bg-[#247fd3]" aria-hidden="true" />实际</span>
|
||
</div>
|
||
</figcaption>
|
||
|
||
<div
|
||
role="img"
|
||
aria-label={`${brand.name}近8周计划与实际完成量对比`}
|
||
className="mt-6 grid grid-cols-8 gap-1.5 sm:gap-3"
|
||
>
|
||
{BUSINESS_TREND_WEEK_LABELS.map((week, index) => {
|
||
const planned = brand.planned[index] ?? 0
|
||
const actual = brand.actual[index] ?? 0
|
||
const plannedHeight = Math.max(12, Math.round((planned / chartMax) * 100))
|
||
const actualHeight = Math.max(12, Math.round((actual / chartMax) * 100))
|
||
|
||
return (
|
||
<div key={week} className="flex min-w-0 flex-col items-center">
|
||
<div className="flex h-28 items-end gap-1 sm:h-32 sm:gap-1.5" aria-hidden="true">
|
||
<span className="w-2.5 rounded-t-sm bg-[#dce5ef] sm:w-3.5" style={{ height: `${plannedHeight}%` }} />
|
||
<span className="w-2.5 rounded-t-sm bg-[#247fd3] sm:w-3.5" style={{ height: `${actualHeight}%` }} />
|
||
</div>
|
||
<p className="mt-2 text-xs font-bold text-[#4f6077]">{week}</p>
|
||
<p className="mt-0.5 text-xs tabular-nums text-[#738198]" aria-label={`${week}实际${actual}计划${planned}`}>
|
||
{actual}/{planned}
|
||
</p>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
<p className="mt-4 text-xs text-[#738198]">柱下数字为实际 / 计划,当前均为结构示例值。</p>
|
||
</figure>
|
||
|
||
<section aria-label={`${brand.name}项目结构`} className="border-t border-[#e2e9f2] lg:border-l lg:border-t-0">
|
||
<div className="flex items-center gap-3 px-5 py-4 sm:px-7 lg:px-6">
|
||
<GitBranch className="h-4 w-4 text-[#176fc1]" aria-hidden="true" />
|
||
<div>
|
||
<h3 className="text-base font-bold text-[#1b2738]">项目结构</h3>
|
||
<p className="mt-0.5 text-xs text-[#738198]">当前阶段项目数 · 示例值</p>
|
||
</div>
|
||
</div>
|
||
<dl className="grid grid-cols-2 gap-px border-t border-[#e2e9f2] bg-[#e2e9f2] sm:grid-cols-5 lg:grid-cols-1">
|
||
{BUSINESS_TREND_STAGES.map((stage) => (
|
||
<div key={stage.key} className="flex min-h-14 items-center justify-between gap-3 bg-[#f8fafd] px-5 py-3 last:col-span-2 sm:last:col-span-1 lg:px-6">
|
||
<dt className="text-sm font-semibold text-[#4f6077]">{stage.label}</dt>
|
||
<dd className="text-lg font-bold tabular-nums text-[#101828]">{brand.stages[stage.key]}</dd>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
</section>
|
||
</div>
|
||
|
||
<footer className="grid gap-px border-t border-[#e2e9f2] bg-[#e2e9f2] sm:grid-cols-[1fr_1fr_1.5fr]">
|
||
<div className="bg-[#f8fafd] px-5 py-4 sm:px-7">
|
||
<p className="text-xs font-semibold text-[#738198]">近 8 周计划总量</p>
|
||
<p className="mt-1 text-2xl font-bold tabular-nums text-[#101828]">{summary.plannedTotal}</p>
|
||
</div>
|
||
<div className="bg-[#f8fafd] px-5 py-4 sm:px-7">
|
||
<p className="text-xs font-semibold text-[#738198]">实际完成总量</p>
|
||
<p className="mt-1 text-2xl font-bold tabular-nums text-[#101828]">{summary.actualTotal}</p>
|
||
</div>
|
||
<div className="flex items-end justify-between gap-4 bg-[#edf5ff] px-5 py-4 sm:px-7">
|
||
<div>
|
||
<p className="text-xs font-bold text-[#176fc1]">示例达成率</p>
|
||
<p className="mt-1 text-xs text-[#66758b]">实际完成量 ÷ 计划数量</p>
|
||
</div>
|
||
<p className="text-4xl font-bold tabular-nums tracking-[-0.03em] text-[#101828] sm:text-5xl" aria-label={`${brand.name}示例达成率${formatAttainmentRate(summary.attainmentRate)}`}>
|
||
{formatAttainmentRate(summary.attainmentRate)}
|
||
</p>
|
||
</div>
|
||
</footer>
|
||
</article>
|
||
)
|
||
}
|
||
|
||
export default function BusinessTrendsDashboard() {
|
||
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">业务趋势</h1>
|
||
<p className="mt-3 max-w-3xl text-base leading-7 text-[#66758b] sm:text-lg">
|
||
按六个品牌查看近 8 周计划与实际完成量,并从 Brief、设计、拍摄、后期、待上线五个阶段理解当前项目结构。
|
||
</p>
|
||
</header>
|
||
|
||
<section aria-labelledby="business-trend-list-title" className="mt-6">
|
||
<div className="mb-4 flex flex-wrap items-end justify-between gap-3 rounded-lg border border-[#e2e8f0] bg-white px-6 py-5">
|
||
<div>
|
||
<h2 id="business-trend-list-title" className="text-xl font-bold tracking-[-0.02em] sm:text-2xl">六品牌近 8 周交付趋势</h2>
|
||
<p className="mt-1 text-sm text-[#66758b]">创意A:朴美、索菲亚、顾家;创意B:欧意朗、卓禾、亿家达。</p>
|
||
</div>
|
||
<p className="rounded-full bg-[#f1f6fc] px-4 py-2 text-sm font-semibold text-[#5f7189]">6 个品牌 · 8 周 · 5 个阶段</p>
|
||
</div>
|
||
|
||
<div className="grid gap-4">
|
||
{BUSINESS_TREND_BRANDS.map((brand) => <BusinessTrendBand key={brand.key} brand={brand} />)}
|
||
</div>
|
||
</section>
|
||
|
||
<Link
|
||
href="/"
|
||
className="mt-10 inline-flex min-h-11 items-center gap-2 rounded-xl bg-[#247fd3] px-4 text-sm font-bold text-white transition-colors hover:bg-[#176fc1] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#78aee9] focus-visible:ring-offset-2"
|
||
>
|
||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||
返回部门看板
|
||
</Link>
|
||
</main>
|
||
</DepartmentShell>
|
||
)
|
||
}
|