Add business intelligence big screen template
This commit is contained in:
14
app/bigscreen/BigScreen.module.css
Normal file
14
app/bigscreen/BigScreen.module.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.screen{--bg:#07111f;--panel:#0c1b2d;--panel2:#0f2238;--line:#1d3a57;--lineBright:#2e668e;--text:#dcecff;--muted:#7791aa;--cyan:#28d7e5;--blue:#3f8cff;--purple:#8b6cf1;--green:#35c98a;--orange:#f1a33c;min-width:1180px;min-height:100vh;padding:12px 16px 16px;overflow:hidden;background:var(--bg);color:var(--text);font-family:"DIN Alternate","Microsoft YaHei",system-ui,sans-serif}
|
||||||
|
.header{height:64px;display:grid;grid-template-columns:1fr 1.35fr 1fr;align-items:center;gap:18px;border-bottom:1px solid #16324d}.headerLeft{display:flex;align-items:center;gap:7px;color:#7791aa;font-size:10px}.headerLeft a{width:27px;height:27px;display:grid;place-items:center;border:1px solid #244762;color:#8eb6d5;text-decoration:none}.headerLeft i{width:6px;height:6px;margin-left:4px;border-radius:50%;background:#53697e}.headerLeft i.online{background:var(--green);box-shadow:0 0 10px rgba(53,201,138,.6)}.headerLeft small{font-size:9px}.titleBlock{position:relative;text-align:center}.titleBlock h1{display:inline-block;margin:0 16px;color:#eaf6ff;font-size:24px;letter-spacing:3px;text-shadow:0 0 16px rgba(40,215,229,.25)}.titleBlock p{margin:4px 0 0;color:#4f7897;font-size:8px;letter-spacing:3px}.titleLine{display:inline-block;width:54px;height:1px;background:#2d668d;vertical-align:middle}.titleLine::after{content:"";display:block;width:6px;height:6px;margin-top:-3px;border:1px solid #3d88ad;transform:rotate(45deg)}.titleLine:nth-child(3)::after{margin-left:auto}.headerRight{display:flex;align-items:center;justify-content:flex-end;gap:6px}.headerRight button{height:28px;padding:0 9px;border:1px solid #244761;background:#0c1b2d;color:#86a5bd;cursor:pointer;font-size:8px}.headerRight>div{display:flex;flex-direction:column;align-items:flex-end;gap:2px;margin-left:7px}.headerRight strong{color:#d8edff;font-size:16px;letter-spacing:1px}.headerRight small{color:#68849b;font-size:7px}
|
||||||
|
.kpiRow{height:100px;display:grid;grid-template-columns:repeat(5,1fr);gap:10px;padding:10px 0}.kpiRow article{position:relative;display:flex;align-items:center;gap:12px;padding:10px 13px;border:1px solid #163650;background:#0b192a;overflow:hidden}.kpiIcon{width:39px;height:39px;display:grid;place-items:center;flex:0 0 auto;border:1px solid currentColor;background:#10263b;font-size:13px;font-weight:800}.cyan{color:var(--cyan)!important;background:#0c2a35!important}.blue{color:var(--blue)!important;background:#102646!important}.purple{color:var(--purple)!important;background:#211d3f!important}.green{color:var(--green)!important;background:#102d29!important}.orange{color:var(--orange)!important;background:#31271d!important}.kpiRow article>div{display:flex;flex-direction:column;gap:2px;min-width:0}.kpiRow p{margin:0;color:#7190aa;font-size:8px}.kpiRow strong{overflow:hidden;color:#e8f4ff;font-size:18px;letter-spacing:.5px;text-overflow:ellipsis;white-space:nowrap}.kpiRow small{color:#607f98;font-size:7px}.kpiRow small b{margin-right:4px;color:#35c98a}.kpiMark{position:absolute;right:-18px;bottom:-24px;width:58px;height:58px;border:1px solid #173956;transform:rotate(45deg)}
|
||||||
|
.dashboardGrid{height:calc(100vh - 64px - 100px - 176px);min-height:480px;display:grid;grid-template-columns:25% 48% 27%;gap:10px}.leftColumn,.centerColumn,.rightColumn{min-width:0;display:grid;grid-template-rows:1fr 1fr;gap:10px}.centerColumn{grid-template-rows:1.34fr .66fr}.panel{position:relative;min-height:0;display:flex;flex-direction:column;border:1px solid #183955;background:#0b192a;box-shadow:inset 0 0 24px rgba(6,15,27,.2)}.panel::before,.panel::after{content:"";position:absolute;width:13px;height:13px;border-color:#3b789b;pointer-events:none}.panel::before{top:-1px;left:-1px;border-top:2px solid #3b789b;border-left:2px solid #3b789b}.panel::after{right:-1px;bottom:-1px;border-right:2px solid #3b789b;border-bottom:2px solid #3b789b}.panel>header{height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 11px;border-bottom:1px solid #15344f;background:#0d1e31}.panel>header>div:first-child{display:flex;align-items:center;gap:7px}.panel>header i{width:3px;height:13px;background:var(--cyan)}.panel h2{margin:0;color:#cfe5f6;font-size:10px;letter-spacing:.4px}.panel>header small{color:#496b85;font-size:6px;letter-spacing:.7px}.panelBody{min-height:0;flex:1;overflow:hidden}.liveBadge,.alertCount,.updateText,.deltaGood{color:#5f859f;font-size:7px}.liveBadge::before{content:"";display:inline-block;width:5px;height:5px;margin-right:5px;border-radius:50%;background:var(--cyan)}.deltaGood{color:#35c98a}.panelButton{height:23px;padding:0 7px;border:1px solid #26506c;background:#10253a;color:#7fa4bd;font-size:7px}.legend{display:flex;gap:9px;color:#6c8ba3;font-size:7px}.legend span{display:flex;align-items:center;gap:4px}.legend i{width:7px;height:7px;border-radius:1px}.cyanDot{background:var(--cyan)!important}.blueDot{background:var(--blue)!important}
|
||||||
|
.channelChart{height:100%;display:grid;grid-template-columns:38% 62%;align-items:center;padding:9px}.channelTotal{position:relative;width:108px;height:108px;display:flex;flex-direction:column;align-items:center;justify-content:center;margin:auto;border:10px solid #17354d;border-top-color:var(--cyan);border-right-color:var(--blue);border-radius:50%}.channelTotal::after{content:"";position:absolute;inset:6px;border:1px solid #27536d;border-radius:50%}.channelTotal span,.channelTotal small{position:relative;z-index:2;color:#64839c;font-size:7px}.channelTotal strong{position:relative;z-index:2;color:#e7f5ff;font-size:18px}.channelTotal i{position:absolute;right:-7px;top:9px;width:7px;height:7px;border:2px solid #0b192a;border-radius:50%;background:var(--cyan)}.channelList{display:flex;flex-direction:column;gap:7px}.channelList>div{position:relative}.channelList>div>div{display:flex;align-items:center;gap:5px;margin-bottom:3px}.legendDot{width:6px;height:6px;border-radius:1px}.channelList strong{flex:1;color:#9eb7ca;font-size:7px;font-weight:500}.channelList b{color:#cce3f4;font-size:7px}.track{width:calc(100% - 30px);height:3px;display:block;background:#173047}.track i{display:block;height:100%}.channelList>div>small{position:absolute;right:0;bottom:-1px;color:#5f819a;font-size:6px}
|
||||||
|
.funnel{height:100%;display:flex;flex-direction:column;justify-content:center;gap:4px;padding:8px 15px}.funnelRow{position:relative;display:flex;align-items:center;justify-content:center;min-height:30px}.funnelRow>span{position:absolute;left:0;width:17px;height:17px;display:grid;place-items:center;border:1px solid #285474;color:#6f99b6;font-size:6px}.funnelBar{height:28px;display:flex;align-items:center;justify-content:space-between;padding:0 8px;border-left:2px solid var(--cyan);background:#102a40;clip-path:polygon(6% 0,94% 0,100% 100%,0 100%)}.funnelBar strong{color:#9ec2d8;font-size:7px}.funnelBar b{color:#e3f2fc;font-size:8px}.funnelRow>small{position:absolute;right:0;color:#5d829b;font-size:7px}.funnelRow>i{position:absolute;bottom:-7px;color:#2d5c7a;font-size:8px;font-style:normal}
|
||||||
|
.trendPanel .panelBody{padding:4px 9px 7px}.trendChart{height:100%;display:grid;grid-template-columns:35px 1fr;padding-top:6px}.axisLabels{display:flex;flex-direction:column;justify-content:space-between;padding:0 5px 18px 0;color:#496c85;font-size:6px;text-align:right}.plot{position:relative;min-width:0}.gridLines{position:absolute;inset:0 0 18px;display:flex;flex-direction:column;justify-content:space-between}.gridLines i{height:1px;background:#17334a}.bars{position:absolute;inset:0 0 0;display:flex;align-items:flex-end;justify-content:space-around;padding-bottom:18px}.bars>div{position:relative;width:6%;height:100%;display:flex;align-items:flex-end;justify-content:center}.bars>div>span{width:65%;display:block;background:#14436a;border-top:2px solid var(--cyan)}.bars small{position:absolute;bottom:-15px;color:#496b83;font-size:5px;white-space:nowrap}.lineLayer{position:absolute;inset:0 2.8% 18px}.point{position:absolute;width:7px;height:7px;margin:-3px 0 0 -3px;border:2px solid #081321;border-radius:50%;background:var(--blue);box-shadow:0 0 8px rgba(63,140,255,.55)}.point>span{display:none;position:absolute;bottom:10px;left:50%;z-index:3;padding:4px 6px;border:1px solid #295676;background:#10253a;color:#bbd7e9;font-size:6px;white-space:nowrap;transform:translateX(-50%)}.point:hover>span{display:block}.point>b{position:absolute;top:2px;left:3px;height:2px;background:var(--blue);transform:rotate(-7deg);transform-origin:left center}
|
||||||
|
.orderPanel .panelBody{padding:0 8px}.orderTable{height:100%;display:flex;flex-direction:column}.orderHead,.orderRow{display:grid;grid-template-columns:1.1fr .6fr 1.1fr .9fr .7fr .8fr;align-items:center;gap:5px}.orderHead{height:24px;color:#55748d;font-size:6px}.orderRow{min-height:24px;border-top:1px solid #142f47;color:#718da3;font-size:6px}.orderRow strong{color:#99b4c8;font-weight:500}.orderRow b{color:#d3e7f5}.orderRow em{width:fit-content;padding:2px 4px;border-radius:2px;font-size:5px;font-style:normal}.paid{background:#12352f;color:#49ca93}.pending{background:#392b1b;color:#e9a145}.orderRow time{color:#55758e}
|
||||||
|
.metricSwitch{display:flex;border:1px solid #214760}.metricSwitch button{height:21px;padding:0 7px;border:0;background:#0b192a;color:#5e7f98;font-size:6px}.metricSwitch button.active{background:#1b5276;color:#bce5f4}.ranking{height:100%;display:flex;flex-direction:column;justify-content:space-around;padding:7px 11px}.ranking>div{display:grid;grid-template-columns:18px 30px 1fr 35px;align-items:center;gap:6px;min-height:26px}.rank{width:15px;height:15px;display:grid;place-items:center;border:1px solid #28516d;color:#64869f;font-size:6px}.rank1{border-color:#e9ad47;color:#f3c56f}.rank2{border-color:#7798ad;color:#a8c2d3}.rank3{border-color:#aa7349;color:#ce9368}.ranking>div>strong{color:#8eabc0;font-size:7px}.ranking>div>div{display:grid;grid-template-columns:1fr 42px;align-items:center;gap:5px}.ranking>div>div>span{height:4px;display:block;background:#173149}.ranking>div>div i{display:block;height:100%;background:var(--blue)}.ranking small{color:#9ab5c8;font-size:6px}.ranking>div>b{color:#35c98a;font-size:6px;text-align:right}
|
||||||
|
.alertList{height:100%;display:flex;flex-direction:column;justify-content:space-around;padding:5px 9px}.alertList article{display:flex;align-items:center;gap:8px;min-height:39px;padding:4px 0;border-bottom:1px solid #142e45}.alertList article:last-child{border-bottom:0}.alertIcon{width:25px;height:25px;display:grid;place-items:center;flex:0 0 auto;border:1px solid;border-radius:50%;font-size:8px;font-weight:700}.alert_high{border-color:#c74e4e;color:#f16d6d;background:#321b21}.alert_medium{border-color:#ba7932;color:#efaa57;background:#31251b}.alert_low{border-color:#7964c5;color:#a38ced;background:#221f3a}.alert_info{border-color:#267d73;color:#48c7b8;background:#122e31}.alertList article>div{min-width:0;flex:1}.alertList strong{display:block;overflow:hidden;color:#9eb6c8;font-size:7px;text-overflow:ellipsis;white-space:nowrap}.alertList p{margin:3px 0 0;color:#5d7a91;font-size:6px}.alertList time{color:#4e7088;font-size:6px}
|
||||||
|
.bottomPanel{height:166px;padding-top:10px}.bottomPanel>.panel{height:100%}.bottomPanel .panelBody{padding:7px 12px}.categories{height:100%;display:grid;grid-template-columns:repeat(5,1fr);gap:15px}.categories article{display:flex;flex-direction:column;justify-content:center}.categories article>div:first-child{display:flex;align-items:center;gap:7px}.categories article>div:first-child>span{color:#315b78;font-size:12px}.categories article>div>div{flex:1;display:flex;flex-direction:column;gap:2px}.categories strong{color:#a7c1d4;font-size:8px}.categories small{color:#57778f;font-size:6px}.categories article>div>b{color:#e5f3fc;font-size:13px}.categories article>div>b.targetMet{color:var(--green)}.targetTrack{position:relative;height:5px;margin:12px 0 13px;background:#173149}.targetTrack>span{display:block;height:100%;background:var(--cyan)}.targetTrack>i{position:absolute;top:-4px;width:1px;height:13px;background:#f1a33c}.targetTrack>small{position:absolute;top:10px;color:#6e879a;font-size:5px;transform:translateX(-50%)}
|
||||||
|
.toast{position:fixed;right:18px;bottom:17px;z-index:90;display:flex;align-items:center;gap:7px;padding:9px 12px;border:1px solid #28516d;background:#10253a;color:#aed1e5;font-size:7px}.toast span{width:17px;height:17px;display:grid;place-items:center;border:1px solid #2d9970;border-radius:50%;color:#49ca93}
|
||||||
|
@media(max-width:1450px){.screen{padding:8px 10px 10px}.header{height:58px}.kpiRow{height:90px}.dashboardGrid{height:calc(100vh - 58px - 90px - 156px)}.bottomPanel{height:146px}.titleBlock h1{font-size:20px}.kpiRow strong{font-size:15px}.channelTotal{width:92px;height:92px}.panel>header{height:32px}}
|
||||||
|
@media(prefers-reduced-motion:reduce){.online{box-shadow:none!important}}
|
||||||
7
app/bigscreen/README.md
Normal file
7
app/bigscreen/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 数据大屏模板
|
||||||
|
|
||||||
|
访问 `/bigscreen` 查看 16:9 经营数据大屏。所有展示数据来自 `app/data/big-screen.json`。
|
||||||
|
|
||||||
|
大屏包含核心指标、分时成交趋势、渠道贡献、转化漏斗、区域排名、实时订单、经营告警和品类目标达成。页面提供实时时钟、刷新状态和浏览器全屏切换。
|
||||||
|
|
||||||
|
接入真实业务数据时,可保留现有数据结构,将 JSON 导入替换为接口或实时消息数据源。
|
||||||
116
app/bigscreen/page.tsx
Normal file
116
app/bigscreen/page.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"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>;
|
||||||
|
}
|
||||||
68
app/data/big-screen.json
Normal file
68
app/data/big-screen.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"title": "华东区域经营数据中心",
|
||||||
|
"subtitle": "BUSINESS INTELLIGENCE COMMAND CENTER",
|
||||||
|
"updatedAt": "2026-08-07 15:30:00",
|
||||||
|
"kpis": [
|
||||||
|
{ "label": "今日成交额", "value": "¥ 2,846,920", "delta": "+18.6%", "unit": "较昨日", "tone": "cyan" },
|
||||||
|
{ "label": "有效订单", "value": "18,642", "delta": "+8.2%", "unit": "完成率 92.4%", "tone": "blue" },
|
||||||
|
{ "label": "活跃客户", "value": "6,829", "delta": "+12.5%", "unit": "新增 486", "tone": "purple" },
|
||||||
|
{ "label": "客单价", "value": "¥ 1,527", "delta": "+3.1%", "unit": "目标 ¥1,480", "tone": "green" },
|
||||||
|
{ "label": "退款率", "value": "1.26%", "delta": "-0.18%", "unit": "低于预警线", "tone": "orange" }
|
||||||
|
],
|
||||||
|
"hourlyTrend": [
|
||||||
|
{ "time": "00:00", "amount": 12, "orders": 180, "conversion": 2.1 },
|
||||||
|
{ "time": "02:00", "amount": 8, "orders": 126, "conversion": 1.8 },
|
||||||
|
{ "time": "04:00", "amount": 6, "orders": 98, "conversion": 1.6 },
|
||||||
|
{ "time": "06:00", "amount": 15, "orders": 230, "conversion": 2.4 },
|
||||||
|
{ "time": "08:00", "amount": 42, "orders": 680, "conversion": 3.8 },
|
||||||
|
{ "time": "10:00", "amount": 68, "orders": 1080, "conversion": 4.7 },
|
||||||
|
{ "time": "12:00", "amount": 57, "orders": 910, "conversion": 4.2 },
|
||||||
|
{ "time": "14:00", "amount": 78, "orders": 1260, "conversion": 5.1 },
|
||||||
|
{ "time": "16:00", "amount": 92, "orders": 1480, "conversion": 5.6 },
|
||||||
|
{ "time": "18:00", "amount": 84, "orders": 1370, "conversion": 5.2 },
|
||||||
|
{ "time": "20:00", "amount": 100, "orders": 1680, "conversion": 6.1 },
|
||||||
|
{ "time": "22:00", "amount": 71, "orders": 1150, "conversion": 4.8 }
|
||||||
|
],
|
||||||
|
"channels": [
|
||||||
|
{ "name": "自然访问", "value": 38, "amount": "108.2 万", "tone": "cyan" },
|
||||||
|
{ "name": "付费推广", "value": 27, "amount": "76.8 万", "tone": "blue" },
|
||||||
|
{ "name": "私域触达", "value": 19, "amount": "54.1 万", "tone": "purple" },
|
||||||
|
{ "name": "合作渠道", "value": 11, "amount": "31.3 万", "tone": "green" },
|
||||||
|
{ "name": "其他来源", "value": 5, "amount": "14.2 万", "tone": "orange" }
|
||||||
|
],
|
||||||
|
"funnel": [
|
||||||
|
{ "name": "访问用户", "value": 128600, "rate": 100 },
|
||||||
|
{ "name": "商品浏览", "value": 86420, "rate": 67.2 },
|
||||||
|
{ "name": "加入购物车", "value": 32980, "rate": 25.6 },
|
||||||
|
{ "name": "提交订单", "value": 20180, "rate": 15.7 },
|
||||||
|
{ "name": "支付成功", "value": 18642, "rate": 14.5 }
|
||||||
|
],
|
||||||
|
"regions": [
|
||||||
|
{ "name": "上海", "amount": 684.2, "orders": 4286, "delta": 19.4 },
|
||||||
|
{ "name": "杭州", "amount": 536.8, "orders": 3652, "delta": 16.8 },
|
||||||
|
{ "name": "南京", "amount": 418.5, "orders": 2940, "delta": 12.7 },
|
||||||
|
{ "name": "苏州", "amount": 362.4, "orders": 2516, "delta": 10.5 },
|
||||||
|
{ "name": "宁波", "amount": 286.9, "orders": 1982, "delta": 8.3 },
|
||||||
|
{ "name": "合肥", "amount": 214.6, "orders": 1540, "delta": 7.1 }
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
{ "name": "企业服务", "target": 92, "actual": 88, "amount": "892 万" },
|
||||||
|
{ "name": "智能硬件", "target": 86, "actual": 74, "amount": "746 万" },
|
||||||
|
{ "name": "办公协同", "target": 78, "actual": 81, "amount": "623 万" },
|
||||||
|
{ "name": "数据产品", "target": 70, "actual": 67, "amount": "518 万" },
|
||||||
|
{ "name": "技术服务", "target": 64, "actual": 59, "amount": "406 万" }
|
||||||
|
],
|
||||||
|
"alerts": [
|
||||||
|
{ "level": "high", "title": "上海仓库存低于安全水位", "detail": "SKU-A238 剩余 126 件", "time": "15:28" },
|
||||||
|
{ "level": "medium", "title": "杭州区域退款率短时上升", "detail": "近 30 分钟达到 2.16%", "time": "15:22" },
|
||||||
|
{ "level": "low", "title": "付费渠道预算使用达到 80%", "detail": "建议关注晚高峰投放额度", "time": "15:08" },
|
||||||
|
{ "level": "info", "title": "南京区域完成本月阶段目标", "detail": "目标完成率 102.4%", "time": "14:56" }
|
||||||
|
],
|
||||||
|
"realtimeOrders": [
|
||||||
|
{ "id": "ORD-92861", "city": "上海", "customer": "启辰科技", "amount": "¥ 28,600", "status": "已支付", "time": "15:30:18" },
|
||||||
|
{ "id": "ORD-92860", "city": "杭州", "customer": "云帆智造", "amount": "¥ 16,840", "status": "已支付", "time": "15:29:42" },
|
||||||
|
{ "id": "ORD-92859", "city": "苏州", "customer": "格新生物", "amount": "¥ 9,260", "status": "待确认", "time": "15:29:08" },
|
||||||
|
{ "id": "ORD-92858", "city": "南京", "customer": "北辰咨询", "amount": "¥ 12,580", "status": "已支付", "time": "15:28:36" },
|
||||||
|
{ "id": "ORD-92857", "city": "宁波", "customer": "海岳贸易", "amount": "¥ 7,920", "status": "已支付", "time": "15:27:51" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,9 +1 @@
|
|||||||
.launcherStack { position:fixed; right:26px; bottom:26px; z-index:45; display:flex; flex-direction:column; gap:9px; }
|
.launcherStack{position:fixed;right:26px;bottom:26px;z-index:45;display:flex;flex-direction:column;gap:8px}.launcher{width:208px;min-height:55px;display:flex;align-items:center;gap:10px;padding:8px 11px;border:1px solid;border-radius:10px;box-shadow:0 13px 28px rgba(30,49,86,.2);color:#fff;text-decoration:none;transition:transform .18s ease,background .18s ease}.launcher:hover{transform:translateY(-2px)}.approval{border-color:#295fcf;background:#3276f6}.approval:hover{background:#215ecb}.worklist{border-color:#273a59;background:#17263d}.worklist:hover{background:#223655}.kanban{border-color:#47734f;background:#28613e}.kanban:hover{background:#1d5032}.bigscreen{border-color:#216c7c;background:#0d5365}.bigscreen:hover{background:#094757}.launcherIcon{width:33px;height:33px;display:grid;place-items:center;flex:0 0 auto;border-radius:8px;background:rgba(255,255,255,.14);font-size:15px}.launcher>span:nth-child(2){display:flex;flex:1;flex-direction:column;gap:3px}.launcher strong{font-size:10px}.launcher small{color:rgba(255,255,255,.7);font-size:8px}.launcher b{font-size:14px}@media(max-width:620px){.launcherStack{right:14px;bottom:14px;gap:6px}.launcher{width:48px;min-height:48px;padding:8px;border-radius:50%}.launcher>span:nth-child(2),.launcher b{display:none}.launcherIcon{width:30px;height:30px;background:transparent}}
|
||||||
.launcher { width:208px; min-height:58px; display:flex; align-items:center; gap:10px; padding:8px 11px; border:1px solid; border-radius:11px; box-shadow:0 14px 30px rgba(30,49,86,.21); color:#fff; text-decoration:none; transition:transform .18s ease,background .18s ease; }
|
|
||||||
.launcher:hover { transform:translateY(-2px); }
|
|
||||||
.approval { border-color:#295fcf; background:#3276f6; }.approval:hover{background:#215ecb}
|
|
||||||
.worklist { border-color:#273a59; background:#17263d; }.worklist:hover{background:#223655}
|
|
||||||
.kanban { border-color:#47734f; background:#28613e; }.kanban:hover{background:#1d5032}
|
|
||||||
.launcherIcon { width:34px; height:34px; display:grid; place-items:center; flex:0 0 auto; border-radius:8px; background:rgba(255,255,255,.14); font-size:16px; }
|
|
||||||
.launcher>span:nth-child(2){display:flex;flex:1;flex-direction:column;gap:3px}.launcher strong{font-size:11px}.launcher small{color:rgba(255,255,255,.7);font-size:8px}.launcher b{font-size:15px}
|
|
||||||
@media(max-width:620px){.launcherStack{right:14px;bottom:14px;gap:7px}.launcher{width:50px;min-height:50px;padding:8px;border-radius:50%}.launcher>span:nth-child(2),.launcher b{display:none}.launcherIcon{width:32px;height:32px;background:transparent}}
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ export default function Home() {
|
|||||||
<>
|
<>
|
||||||
<AdminApp />
|
<AdminApp />
|
||||||
<nav className={styles.launcherStack} aria-label="模板功能入口">
|
<nav className={styles.launcherStack} aria-label="模板功能入口">
|
||||||
|
<Link className={`${styles.launcher} ${styles.bigscreen}`} href="/bigscreen">
|
||||||
|
<span className={styles.launcherIcon}>▥</span>
|
||||||
|
<span><strong>数据大屏</strong><small>经营指挥中心</small></span>
|
||||||
|
<b>→</b>
|
||||||
|
</Link>
|
||||||
<Link className={`${styles.launcher} ${styles.kanban}`} href="/kanban">
|
<Link className={`${styles.launcher} ${styles.kanban}`} href="/kanban">
|
||||||
<span className={styles.launcherIcon}>▦</span>
|
<span className={styles.launcherIcon}>▦</span>
|
||||||
<span><strong>泳道看板</strong><small>拖拽变更卡片状态</small></span>
|
<span><strong>泳道看板</strong><small>拖拽变更卡片状态</small></span>
|
||||||
|
|||||||
Reference in New Issue
Block a user