117 lines
9.2 KiB
TypeScript
117 lines
9.2 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import data from "../data/big-screen.json";
|
|
import styles from "./BigScreen.module.css";
|
|
|
|
export default function BigScreenPage() {
|
|
const [now, setNow] = useState(new Date());
|
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
const [regionMetric, setRegionMetric] = useState<"amount" | "orders">("amount");
|
|
const [toast, setToast] = useState("");
|
|
|
|
useEffect(() => {
|
|
const timer = window.setInterval(() => setNow(new Date()), 1000);
|
|
return () => window.clearInterval(timer);
|
|
}, []);
|
|
|
|
const notify = (message: string) => {
|
|
setToast(message);
|
|
window.setTimeout(() => setToast(""), 2200);
|
|
};
|
|
|
|
const enterFullscreen = async () => {
|
|
try {
|
|
if (!document.fullscreenElement) await document.documentElement.requestFullscreen();
|
|
else await document.exitFullscreen();
|
|
} catch { notify("当前预览环境不支持全屏切换"); }
|
|
};
|
|
|
|
return (
|
|
<main className={styles.screen}>
|
|
<header className={styles.header}>
|
|
<div className={styles.headerLeft}><Link href="/" aria-label="返回后台">←</Link><span>华东区域 · 实时经营</span><i className={autoRefresh ? styles.online : ""} /> <small>{autoRefresh ? "数据连接正常" : "自动刷新已暂停"}</small></div>
|
|
<div className={styles.titleBlock}><div className={styles.titleLine} /><h1>{data.title}</h1><div className={styles.titleLine} /><p>{data.subtitle}</p></div>
|
|
<div className={styles.headerRight}><button onClick={() => setAutoRefresh(!autoRefresh)}>{autoRefresh ? "暂停刷新" : "恢复刷新"}</button><button onClick={enterFullscreen}>全屏</button><div><strong>{now.toLocaleTimeString("zh-CN", { hour12: false })}</strong><small>{now.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", weekday: "short" })}</small></div></div>
|
|
</header>
|
|
|
|
<section className={styles.kpiRow} aria-label="核心经营指标">
|
|
{data.kpis.map((kpi, index) => <article key={kpi.label}><span className={`${styles.kpiIcon} ${styles[kpi.tone]}`}>{["¥", "单", "客", "价", "%"][index]}</span><div><p>{kpi.label}</p><strong>{kpi.value}</strong><small><b>{kpi.delta}</b> {kpi.unit}</small></div><i className={styles.kpiMark} /></article>)}
|
|
</section>
|
|
|
|
<section className={styles.dashboardGrid}>
|
|
<div className={styles.leftColumn}>
|
|
<Panel title="渠道贡献分布" code="CHANNEL SHARE" extra={<span className={styles.liveBadge}>今日实时</span>}>
|
|
<ChannelChart />
|
|
</Panel>
|
|
<Panel title="用户转化漏斗" code="CONVERSION FUNNEL" extra={<span className={styles.deltaGood}>转化率 +1.8%</span>}>
|
|
<FunnelChart />
|
|
</Panel>
|
|
</div>
|
|
|
|
<div className={styles.centerColumn}>
|
|
<Panel title="全时段成交趋势" code="HOURLY SALES TREND" extra={<div className={styles.legend}><span><i className={styles.cyanDot} />成交金额</span><span><i className={styles.blueDot} />订单量</span></div>} className={styles.trendPanel}>
|
|
<TrendChart />
|
|
</Panel>
|
|
<Panel title="实时订单流水" code="REAL-TIME ORDERS" extra={<button className={styles.panelButton} onClick={() => notify("已刷新实时订单")}>刷新</button>} className={styles.orderPanel}>
|
|
<RealtimeOrders />
|
|
</Panel>
|
|
</div>
|
|
|
|
<div className={styles.rightColumn}>
|
|
<Panel title="区域业绩排行" code="REGIONAL RANKING" extra={<div className={styles.metricSwitch}><button className={regionMetric === "amount" ? styles.active : ""} onClick={() => setRegionMetric("amount")}>金额</button><button className={regionMetric === "orders" ? styles.active : ""} onClick={() => setRegionMetric("orders")}>订单</button></div>}>
|
|
<RegionRanking metric={regionMetric} />
|
|
</Panel>
|
|
<Panel title="实时告警中心" code="ALERT CENTER" extra={<span className={styles.alertCount}>{data.alerts.length} 条待关注</span>}>
|
|
<AlertList />
|
|
</Panel>
|
|
</div>
|
|
</section>
|
|
|
|
<section className={styles.bottomPanel}>
|
|
<Panel title="业务品类目标达成" code="CATEGORY PERFORMANCE" extra={<span className={styles.updateText}>数据更新时间 {data.updatedAt}</span>}>
|
|
<CategoryPerformance />
|
|
</Panel>
|
|
</section>
|
|
{toast && <div className={styles.toast}><span>✓</span>{toast}</div>}
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function Panel({ title, code, extra, children, className = "" }: { title: string; code: string; extra?: React.ReactNode; children: React.ReactNode; className?: string }) {
|
|
return <section className={`${styles.panel} ${className}`}><header><div><i /><h2>{title}</h2><small>{code}</small></div>{extra}</header><div className={styles.panelBody}>{children}</div></section>;
|
|
}
|
|
|
|
function ChannelChart() {
|
|
return <div className={styles.channelChart}><div className={styles.channelTotal}><span>渠道成交额</span><strong>284.6</strong><small>万元</small><i /></div><div className={styles.channelList}>{data.channels.map((item) => <div key={item.name}><div><span className={`${styles.legendDot} ${styles[item.tone]}`} /><strong>{item.name}</strong><b>{item.amount}</b></div><span className={styles.track}><i className={styles[item.tone]} style={{ width: `${item.value}%` }} /></span><small>{item.value}%</small></div>)}</div></div>;
|
|
}
|
|
|
|
function FunnelChart() {
|
|
return <div className={styles.funnel}>{data.funnel.map((item, index) => <div key={item.name} className={styles.funnelRow}><span>{index + 1}</span><div className={styles.funnelBar} style={{ width: `${44 + item.rate * .48}%` }}><strong>{item.name}</strong><b>{item.value.toLocaleString()}</b></div><small>{item.rate}%</small>{index < data.funnel.length - 1 && <i>↓</i>}</div>)}</div>;
|
|
}
|
|
|
|
function TrendChart() {
|
|
const maxAmount = Math.max(...data.hourlyTrend.map((item) => item.amount));
|
|
const maxOrders = Math.max(...data.hourlyTrend.map((item) => item.orders));
|
|
const points = useMemo(() => data.hourlyTrend.map((item, index) => ({ x: (index / (data.hourlyTrend.length - 1)) * 100, y: 91 - (item.orders / maxOrders) * 72, item })), [maxOrders]);
|
|
return <div className={styles.trendChart} role="img" aria-label="今日每两小时成交金额和订单量趋势"><div className={styles.axisLabels}><span>100 万</span><span>75 万</span><span>50 万</span><span>25 万</span><span>0</span></div><div className={styles.plot}><div className={styles.gridLines}>{[0,1,2,3,4].map((line) => <i key={line} />)}</div><div className={styles.bars}>{data.hourlyTrend.map((item) => <div key={item.time}><span style={{ height: `${(item.amount / maxAmount) * 82}%` }} /><small>{item.time}</small></div>)}</div><div className={styles.lineLayer}>{points.map((point, index) => <div key={point.item.time} className={styles.point} style={{ left: `${point.x}%`, top: `${point.y}%` }}><i /><span>{point.item.orders.toLocaleString()} 单</span>{index < points.length - 1 && <b style={{ width: `${100 / (points.length - 1)}%` }} />}</div>)}</div></div></div>;
|
|
}
|
|
|
|
function RealtimeOrders() {
|
|
return <div className={styles.orderTable}><div className={styles.orderHead}><span>订单编号</span><span>区域</span><span>客户</span><span>成交金额</span><span>状态</span><span>时间</span></div>{data.realtimeOrders.map((order) => <div className={styles.orderRow} key={order.id}><span>{order.id}</span><span>{order.city}</span><strong>{order.customer}</strong><b>{order.amount}</b><em className={order.status === "已支付" ? styles.paid : styles.pending}>{order.status}</em><time>{order.time}</time></div>)}</div>;
|
|
}
|
|
|
|
function RegionRanking({ metric }: { metric: "amount" | "orders" }) {
|
|
const max = Math.max(...data.regions.map((item) => metric === "amount" ? item.amount : item.orders));
|
|
return <div className={styles.ranking}>{data.regions.map((region, index) => { const value = metric === "amount" ? region.amount : region.orders; return <div key={region.name}><span className={`${styles.rank} ${index < 3 ? styles[`rank${index + 1}`] : ""}`}>{index + 1}</span><strong>{region.name}</strong><div><span><i style={{ width: `${(value / max) * 100}%` }} /></span><small>{metric === "amount" ? `${region.amount.toFixed(1)} 万` : `${region.orders.toLocaleString()} 单`}</small></div><b>+{region.delta}%</b></div>; })}</div>;
|
|
}
|
|
|
|
function AlertList() {
|
|
return <div className={styles.alertList}>{data.alerts.map((alert) => <article key={alert.title}><span className={`${styles.alertIcon} ${styles[`alert_${alert.level}`]}`}>{alert.level === "high" ? "!" : alert.level === "medium" ? "△" : alert.level === "low" ? "◷" : "i"}</span><div><strong>{alert.title}</strong><p>{alert.detail}</p></div><time>{alert.time}</time></article>)}</div>;
|
|
}
|
|
|
|
function CategoryPerformance() {
|
|
return <div className={styles.categories}>{data.categories.map((category, index) => <article key={category.name}><div><span>{String(index + 1).padStart(2, "0")}</span><div><strong>{category.name}</strong><small>年度累计 {category.amount}</small></div><b className={category.actual >= category.target ? styles.targetMet : ""}>{category.actual}%</b></div><div className={styles.targetTrack}><span style={{ width: `${category.actual}%` }} /><i style={{ left: `${category.target}%` }} /><small style={{ left: `${category.target}%` }}>目标 {category.target}%</small></div></article>)}</div>;
|
|
}
|