gongxiangp2/components/department-dashboard/BusinessTrendsDashboard.tsx
2026-08-19 12:08:38 +08:00

162 lines
8.4 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.

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