import React from "react"; import { Card, Col, Divider, Empty, Row, Spin, Statistic, Table, Tag, theme, } from "antd"; import { ArrowDownOutlined, ArrowUpOutlined, ShopOutlined, ShoppingCartOutlined, TruckOutlined, UserOutlined, WalletOutlined, } from "@ant-design/icons"; import ReactECharts from "echarts-for-react"; import { useTranslation } from "react-i18next"; import useRequest from "@/hooks/useRequest"; import { getDashboardAnalysis } from "@/api/dashboard"; import type IDashboardAnalysis from "@/domain/iDashboard"; import { BILL_PAY_STATE_MAP, STORE_ORDER_STATUS_MAP, } from "@/domain/iStoreOrder"; const { useToken } = theme; /** 金额格式化 */ const formatMoney = (value?: number): string => "¥ " + Number(value ?? 0).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2, }); /** 空数据兜底(加载中/请求失败时页面仍可渲染) */ const EMPTY: IDashboardAnalysis = { overview: { sales: { total: 0, today: 0, week: 0, growth: null, trend7: [] }, orders: { total: 0, today: 0, week: 0, growth: null, trend7: [] }, purchase: { total: 0, today: 0, week: 0, growth: null, trend7: [] }, receivable: { bill_total: 0, received: 0, unreceived: 0 }, }, trend: [], order_status: [], category_sales: [], top_products: [], top_stores: [], recon: { bills: [], pending_payment: { count: 0, amount: 0 } }, latest_orders: [], archives: { stores: 0, products: 0, suppliers: 0, users: 0 }, }; /** 环比涨跌(红涨绿跌);上期为0无基准时显示 — */ const GrowthText: React.FC<{ growth: number | null }> = ({ growth }) => { const { token } = useToken(); if (growth === null) { return —; } const up = growth >= 0; return ( {up ? : }{" "} {Math.abs(growth).toFixed(1)}% ); }; /** 指标卡:标题 + 大数值 + 迷你趋势图 + 今日/近7天/环比 */ const MetricCard: React.FC<{ title: string; value: string; sub: React.ReactNode; growth?: number | null; chart?: React.ReactNode; }> = ({ title, value, sub, growth, chart }) => { const { token } = useToken(); return ( {title} {value} {chart} {sub} {growth !== undefined && } ); }; /** 排名徽标:前三名金/银/铜 */ const RankBadge: React.FC<{ index: number }> = ({ index }) => { const { token } = useToken(); const rank = index + 1; const colors = ["#faad14", "#8c8c8c", "#ad6800"]; if (rank <= 3) { return ( {rank} ); } return ( {rank} ); }; const Index: React.FC = () => { const { token } = useToken(); const { t } = useTranslation(); const { data, loading } = useRequest(getDashboardAnalysis); const dashboard = data ?? EMPTY; const { overview, recon, archives } = dashboard; /** 迷你图配置(指标卡右侧小图) */ const miniOption = ( seriesData: number[], type: "bar" | "line", color: string ) => ({ grid: { left: 2, right: 2, top: 6, bottom: 0 }, xAxis: { type: "category", show: false }, yAxis: { type: "value", show: false }, tooltip: { show: false }, series: [ type === "bar" ? { data: seriesData, type, barWidth: 8, itemStyle: { color, borderRadius: [3, 3, 0, 0] }, } : { data: seriesData, type, smooth: true, symbol: "none", lineStyle: { color, width: 2 }, areaStyle: { color, opacity: 0.15 }, }, ], }); /** 近30天销售趋势(金额柱 + 订单数线,双轴) */ const trendOption = { tooltip: { trigger: "axis", axisPointer: { type: "cross" }, borderWidth: 0, backgroundColor: token.colorBgElevated, textStyle: { color: token.colorText }, }, legend: { data: [t("dashboard.analysis.amountAxis"), t("dashboard.analysis.ordersAxis")], textStyle: { color: token.colorText }, }, grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true }, xAxis: { type: "category", data: dashboard.trend.map((item) => item.date.slice(5)), axisLabel: { color: token.colorTextSecondary }, axisLine: { lineStyle: { color: token.colorBorder } }, }, yAxis: [ { type: "value", name: t("dashboard.analysis.amountAxis"), nameTextStyle: { color: token.colorTextSecondary }, axisLabel: { color: token.colorTextSecondary }, splitLine: { lineStyle: { color: token.colorBorderSecondary } }, }, { type: "value", name: t("dashboard.analysis.ordersAxis"), nameTextStyle: { color: token.colorTextSecondary }, axisLabel: { color: token.colorTextSecondary }, splitLine: { show: false }, }, ], series: [ { name: t("dashboard.analysis.amountAxis"), type: "bar", data: dashboard.trend.map((item) => item.amount), barMaxWidth: 20, itemStyle: { color: token.colorPrimary, borderRadius: [4, 4, 0, 0] }, }, { name: t("dashboard.analysis.ordersAxis"), type: "line", yAxisIndex: 1, smooth: true, symbol: "none", data: dashboard.trend.map((item) => item.orders), itemStyle: { color: token.colorSuccess }, lineStyle: { width: 2 }, }, ], }; /** 订单状态分布环图(配送中用 Ant Design cyan-6) */ const statusColor: Record = { 0: token.colorTextDisabled, 1: token.colorPrimary, 2: token.colorWarning, 3: "#13c2c2", 4: token.colorSuccess, 9: token.colorError, }; const statusOption = { tooltip: { trigger: "item" }, legend: { bottom: 0, left: "center", textStyle: { color: token.colorText }, }, series: [ { name: t("dashboard.analysis.orderStatusDistribution"), type: "pie", radius: ["45%", "70%"], center: ["50%", "42%"], avoidLabelOverlap: false, itemStyle: { borderRadius: token.borderRadius, borderColor: token.colorBgContainer, borderWidth: 2, }, label: { show: false }, emphasis: { label: { show: true, fontSize: token.fontSizeLG, fontWeight: token.fontWeightStrong, }, }, labelLine: { show: false }, data: dashboard.order_status.map((item) => ({ value: item.count, name: item.name, itemStyle: { color: statusColor[item.status] ?? token.colorTextDisabled }, })), }, ], }; /** 品类销售占比环图 */ const categoryOption = { tooltip: { trigger: "item" }, legend: { bottom: 0, left: "center", textStyle: { color: token.colorText }, }, series: [ { name: t("dashboard.analysis.categorySales"), type: "pie", radius: ["40%", "68%"], center: ["50%", "42%"], avoidLabelOverlap: false, itemStyle: { borderRadius: token.borderRadius, borderColor: token.colorBgContainer, borderWidth: 2, }, label: { show: false }, emphasis: { label: { show: true, fontSize: token.fontSizeLG, fontWeight: token.fontWeightStrong, }, }, labelLine: { show: false }, data: dashboard.category_sales.map((item) => ({ value: item.amount, name: item.name, })), }, ], }; return ( {/* ===== 核心指标卡 ===== */} {t("dashboard.analysis.today")}{" "} {formatMoney(overview.sales.today)} ·{" "} {t("dashboard.analysis.thisWeek")}{" "} {formatMoney(overview.sales.week)} > } growth={overview.sales.growth} chart={ } /> {t("dashboard.analysis.today")} {overview.orders.today} ·{" "} {t("dashboard.analysis.thisWeek")} {overview.orders.week} > } growth={overview.orders.growth} chart={ } /> {t("dashboard.analysis.today")}{" "} {formatMoney(overview.purchase.today)} ·{" "} {t("dashboard.analysis.thisWeek")}{" "} {formatMoney(overview.purchase.week)} > } growth={overview.purchase.growth} chart={ } /> {t("dashboard.analysis.receivable")} {formatMoney(overview.receivable.unreceived)} {t("dashboard.analysis.totalBillAmount")}{" "} {formatMoney(overview.receivable.bill_total)} ·{" "} {t("dashboard.analysis.receivedAmount")}{" "} {formatMoney(overview.receivable.received)} {/* ===== 销售趋势 + 订单状态分布 ===== */} {dashboard.trend.length === 0 ? ( ) : ( )} {/* ===== 品类占比 + 热销商品 + 门店排行 ===== */} {dashboard.category_sales.length === 0 ? ( ) : ( )} , }, { title: t("dashboard.analysis.productName"), dataIndex: "product_name", ellipsis: true, render: (name: string, record) => ( {name} {record.product_spec} ), }, { title: t("dashboard.analysis.quantity"), key: "quantity", width: 100, render: (_, record) => `${record.quantity} ${record.unit}`, }, { title: t("dashboard.analysis.amount"), dataIndex: "amount", width: 120, render: (amount: number) => formatMoney(amount), }, ]} /> , }, { title: t("dashboard.analysis.storeName"), dataIndex: "store_name", ellipsis: true, }, { title: t("dashboard.analysis.orderCount"), dataIndex: "order_count", width: 90, }, { title: t("dashboard.analysis.amount"), dataIndex: "amount", width: 120, render: (amount: number) => formatMoney(amount), }, ]} /> {/* ===== 对账与回款 + 最新订单 ===== */} {t("dashboard.analysis.billProgress")} {recon.bills.map((bill) => ( {bill.name} {formatMoney(bill.amount)} ))} {t("dashboard.analysis.pendingPaymentReview")} {t("dashboard.analysis.pendingPaymentTip")} {formatMoney(recon.pending_payment.amount)} formatMoney(amount), }, { title: t("dashboard.analysis.status"), dataIndex: "status", width: 90, render: (status: number) => ( {STORE_ORDER_STATUS_MAP[status]?.text} ), }, ]} /> {/* ===== 基础档案 ===== */} } /> } /> } /> } /> ); }; export default Index;