资讯动态

第 9 章:数据统计与分析

发布时间:2026/9/30 8:30:13 来源:尧图企业网站定制
本章学习目标理解统计类页面的架构模式掌握多维度筛选器设计学会统计卡片与条形图的实现实现分类热度分析与团队效率排行榜掌握数据导出功能学会空状态与加载态设计9.1 统计类页面的架构模式数据统计页面是后台系统的「驾驶舱」让管理者快速了解业务状况。这类页面有一套通用的架构模式。9.1.1 典型结构┌─────────────────────────────────────────────────────┐ │ 筛选区时间范围 分类 部门 人员 状态 导出按钮 │ ├─────────────────────────────────────────────────────┤ │ 统计卡片行4-6 个核心指标 │ ├─────────────────────────────────────────────────────┤ │ ┌──────────────────┬────────────────────────────┐ │ │ │ 趋势图折线 │ 分类分布饼图/柱图 │ │ │ └──────────────────┴────────────────────────────┘ │ ├─────────────────────────────────────────────────────┤ │ 排行榜 / 明细表 │ └─────────────────────────────────────────────────────┘9.1.2 设计原则自上而下从宏观到微观先看总览再看明细可交互每个图表都可以点击下钻查看详情可导出支持导出 Excel/PDF方便做汇报有默认值进入页面就有数据默认展示最近 7 天/30 天9.2 多维度筛选器设计统计页面的筛选条件通常很多需要合理组织避免页面拥挤。9.2.1 展开/收起模式当筛选条件超过 4 个时默认只显示常用的其余折叠起来// src/pages/statistics/components/FilterBar.tsx import { useState } from react; import { Form, DatePicker, Select, Button, Row, Col, Space } from antd; import { SearchOutlined, ReloadOutlined, DownOutlined, UpOutlined, ExportOutlined } from ant-design/icons; import dayjs, { Dayjs } from dayjs; export interface StatisticsFilters { dateRange: [Dayjs, Dayjs]; categoryId?: string; departmentId?: string; assigneeId?: string; status?: string; priority?: number; sourceType?: string; } interface Props { onSearch: (filters: StatisticsFilters) void; onExport: () void; loading?: boolean; } export default function FilterBar({ onSearch, onExport, loading }: Props) { const [form] Form.useForm(); const [expanded, setExpanded] useState(false); const handleSearch () { const values form.getFieldsValue(); onSearch(values); }; const handleReset () { form.resetFields(); handleSearch(); }; return ( Form form{form} layoutvertical initialValues{{ dateRange: [dayjs().subtract(30, day), dayjs()], }} Row gutter{16} Col span{6} Form.Item label时间范围 namedateRange DatePicker.RangePicker style{{ width: 100% }} / /Form.Item /Col Col span{6} Form.Item label分类 namecategoryId Select placeholder全部分类 allowClear Select.Option valuecat1系统问题/Select.Option Select.Option valuecat2账号权限/Select.Option Select.Option valuecat3业务咨询/Select.Option /Select /Form.Item /Col Col span{6} Form.Item label部门 namedepartmentId Select placeholder全部部门 allowClear showSearch optionFilterProplabel {/* 部门列表从接口获取 */} /Select /Form.Item /Col Col span{6} Form.Item label处理人 nameassigneeId Select placeholder全部处理人 allowClear showSearch optionFilterProplabel {/* 人员列表从接口获取 */} /Select /Form.Item /Col /Row {expanded ( Row gutter{16} Col span{6} Form.Item label状态 namestatus Select placeholder全部状态 allowClear Select.Option valuepending待处理/Select.Option Select.Option valueprocessing处理中/Select.Option Select.Option valueresolved已解决/Select.Option Select.Option valueclosed已关闭/Select.Option /Select /Form.Item /Col Col span{6} Form.Item label优先级 namepriority Select placeholder全部优先级 allowClear Select.Option value{1}紧急/Select.Option Select.Option value{2}高/Select.Option Select.Option value{3}中/Select.Option Select.Option value{4}低/Select.Option /Select /Form.Item /Col Col span{6} Form.Item label来源类型 namesourceType Select placeholder全部来源 allowClear Select.Option valueweb网页/Select.Option Select.Option valueemail邮件/Select.Option Select.Option valuephone电话/Select.Option Select.Option valueapiAPI/Select.Option /Select /Form.Item /Col /Row )} div style{{ display: flex, justifyContent: flex-end }} Space Button typelink onClick{() setExpanded(!expanded)} {expanded ? 收起 UpOutlined // : 展开 DownOutlined //} /Button Button icon{ReloadOutlined /} onClick{handleReset}重置/Button Button icon{ExportOutlined /} onClick{onExport}导出/Button Button typeprimary icon{SearchOutlined /} onClick{handleSearch} loading{loading} 查询 /Button /Space /div /Form ); }9.3 统计卡片与趋势图9.3.1 统计卡片行统计卡片是数据看板的第一视觉焦点展示核心指标。// src/pages/statistics/components/StatsCards.tsx import { Row, Col, Card, Space, Typography } from antd; import { FileTextOutlined, CheckCircleOutlined, ClockCircleOutlined, WarningOutlined, ArrowUpOutlined, ArrowDownOutlined, } from ant-design/icons; import StatCard from components/StatCard; const { Text } Typography; interface StatsData { total: number; resolved: number; pending: number; overdue: number; totalTrend: number; resolvedTrend: number; pendingTrend: number; overdueTrend: number; } interface Props { data: StatsData; loading?: boolean; } export default function StatsCards({ data, loading }: Props) { return ( Row gutter{16} Col span{6} StatCard title总任务数 value{data.total} trend{data.totalTrend} color#1677ff icon{FileTextOutlined style{{ fontSize: 24 }} /} / /Col Col span{6} StatCard title已完成 value{data.resolved} trend{data.resolvedTrend} color#52c41a icon{CheckCircleOutlined style{{ fontSize: 24 }} /} / /Col Col span{6} StatCard title待处理 value{data.pending} trend{data.pendingTrend} color#faad14 icon{ClockCircleOutlined style{{ fontSize: 24 }} /} / /Col Col span{6} StatCard title超时 value{data.overdue} trend{data.overdueTrend} color#ff4d4f icon{WarningOutlined style{{ fontSize: 24 }} /} / /Col /Row ); }9.3.2 趋势图// src/pages/statistics/components/TrendChart.tsx import ReactECharts from echarts-for-react; import { Card, Radio, Space } from antd; import { useState } from react; interface TrendData { dates: string[]; created: number[]; resolved: number[]; } interface Props { data: TrendData; loading?: boolean; } export default function TrendChart({ data, loading }: Props) { const [type, setType] useStateline | bar(line); const option { tooltip: { trigger: axis }, legend: { data: [新建, 完成], right: 80 }, grid: { left: 40, right: 20, top: 40, bottom: 30 }, xAxis: { type: category, data: data.dates, boundaryGap: type bar }, yAxis: { type: value }, series: [ { name: 新建, type, data: data.created, smooth: true, itemStyle: { color: #1677ff }, areaStyle: type line ? { color: { type: linear, x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: rgba(22,119,255,0.3) }, { offset: 1, color: rgba(22,119,255,0.05) }, ], }, } : undefined, barWidth: type bar ? 12 : undefined, }, { name: 完成, type, data: data.resolved, smooth: true, itemStyle: { color: #52c41a }, areaStyle: type line ? { color: { type: linear, x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: rgba(82,196,26,0.3) }, { offset: 1, color: rgba(82,196,26,0.05) }, ], }, } : undefined, barWidth: type bar ? 12 : undefined, }, ], }; return ( Card title任务趋势 extra{ Radio.Group sizesmall value{type} onChange{(e) setType(e.target.value)} Radio.Button valueline折线图/Radio.Button Radio.Button valuebar柱状图/Radio.Button /Radio.Group } loading{loading} ReactECharts option{option} style{{ height: 320 }} / /Card ); }9.4 分类热度分析用横向柱状图展示各个分类的任务数量分布一目了然。// src/pages/statistics/components/CategoryHeatmap.tsx import ReactECharts from echarts-for-react; import { Card } from antd; interface CategoryItem { name: string; value: number; } interface Props { data: CategoryItem[]; loading?: boolean; } export default function CategoryHeatmap({ data, loading }: Props) { const sorted [...data].sort((a, b) a.value - b.value); const option { tooltip: { trigger: axis, axisPointer: { type: shadow } }, grid: { left: 100, right: 40, top: 20, bottom: 30 }, xAxis: { type: value }, yAxis: { type: category, data: sorted.map((d) d.name), axisLabel: { width: 80, overflow: truncate }, }, series: [ { type: bar, data: sorted.map((d) d.value), itemStyle: { color: { type: linear, x: 0, y: 0, x2: 1, y2: 0, colorStops: [ { offset: 0, color: #69c0ff }, { offset: 1, color: #1677ff }, ], }, borderRadius: [0, 4, 4, 0], }, barWidth: 16, label: { show: true, position: right, formatter: {c} }, }, ], }; return ( Card title分类热度 loading{loading} ReactECharts option{option} style{{ height: 360 }} / /Card ); }9.5 团队效率排行榜展示处理人的任务完成量排名激励团队成员。// src/pages/statistics/components/TeamRanking.tsx import { Card, List, Avatar, Tag, Space, Typography } from antd; import { TrophyOutlined } from ant-design/icons; const { Text } Typography; interface RankingItem { rank: number; name: string; avatar?: string; resolvedCount: number; avgDuration: number; // 平均处理时长小时 satisfaction: number; // 满意度 0-100 } interface Props { data: RankingItem[]; loading?: boolean; } const rankColors: Recordnumber, string { 1: #faad14, 2: #ad6800, 3: #d4b106, }; export default function TeamRanking({ data, loading }: Props) { return ( Card title{ Space TrophyOutlined style{{ color: #faad14 }} / span团队效率榜/span /Space } loading{loading} List dataSource{data} renderItem{(item) ( List.Item List.Item.Meta avatar{ div style{{ display: flex, alignItems: center, gap: 12 }} Text strong style{ { width: 24, textAlign: center, color: rankColors[item.rank] || #999, fontSize: item.rank 3 ? 18 : 14, } {item.rank} /Text Avatar{item.name[0]}/Avatar /div } title{item.name} description{ Space sizelarge Text typesecondary style{{ fontSize: 12 }} 完成 {item.resolvedCount} 单 /Text Text typesecondary style{{ fontSize: 12 }} 平均 {item.avgDuration}h /Text Tag colorgreen style{{ margin: 0 }} 满意度 {item.satisfaction}% /Tag /Space } / /List.Item )} / /Card ); }9.6 数据导出功能统计数据导出是非常高频的需求。我们提供导出 Excel 的功能。9.6.1 前端导出方案对于简单的报表可以直接在前端用xlsx库导出npminstallxlsx file-savernpminstall-Dtypes/file-saver// src/utils/exportExcel.tsimport*asXLSXfromxlsx;import{saveAs}fromfile-saver;importdayjsfromdayjs;exportinterfaceExportColumnT{key:keyofT|string;title:string;format?:(value:any,row:T)string|number;}exportfunctionexportToExcelT(data:T[],columns:ExportColumnT[],filenameexport){constexportDatadata.map((row){constobj:Recordstring,any{};columns.forEach((col){constkeyString(col.key);constvalue(rowasany)[col.key];obj[col.title]col.format?col.format(value,row):value??;});returnobj;});constwsXLSX.utils.json_to_sheet(exportData);constwbXLSX.utils.book_new();XLSX.utils.book_append_sheet(wb,ws,Sheet1);constexcelBufferXLSX.write(wb,{bookType:xlsx,type:array});constblobnewBlob([excelBuffer],{type:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet});saveAs(blob,${filename}_${dayjs().format(YYYYMMDD_HHmmss)}.xlsx);}9.6.2 后端导出方案如果数据量大建议由后端生成文件前端只触发下载// 触发后端导出asyncfunctionexportStatistics(filters:any){constresponseawaitrequest.get(/api/v1/statistics/export,{params:filters,responseType:blob,});consturlwindow.URL.createObjectURL(newBlob([response]));constlinkdocument.createElement(a);link.hrefurl;link.downloadstatistics_${Date.now()}.xlsx;link.click();window.URL.revokeObjectURL(url);}9.7 空状态与加载态设计统计页面经常会出现「没有数据」或「正在加载」的情况需要优雅处理。9.7.1 加载态首次加载用骨架屏Skeleton占位避免布局跳动刷新数据在图表区域加 loading 遮罩按钮 loading查询/导出按钮显示加载状态9.7.2 空状态当筛选结果为空时不要只显示一个光秃秃的 Empty 组件要引导用户下一步操作import { Empty, Button, Space } from antd; function EmptyStats({ onReset }: { onReset: () void }) { return ( div style{{ padding: 60px 0, textAlign: center }} Empty description暂无数据 Space Button onClick{onReset}重置筛选条件/Button Button typeprimary导出空报表/Button /Space /Empty /div ); }9.8 统计页主组件把上面的组件组合成完整的统计页面// src/pages/statistics/index.tsx import { useState } from react; import { Row, Col, Card, Space } from antd; import FilterBar, { StatisticsFilters } from ./components/FilterBar; import StatsCards from ./components/StatsCards; import TrendChart from ./components/TrendChart; import CategoryHeatmap from ./components/CategoryHeatmap; import TeamRanking from ./components/TeamRanking; import { statisticsApi } from services/statistics; export default function StatisticsPage() { const [loading, setLoading] useState(false); const [stats, setStats] useStateany({ total: 0, resolved: 0, pending: 0, overdue: 0, totalTrend: 0, resolvedTrend: 0, pendingTrend: 0, overdueTrend: 0, }); const [trendData, setTrendData] useStateany({ dates: [], created: [], resolved: [] }); const [categoryData, setCategoryData] useStateany[]([]); const [rankingData, setRankingData] useStateany[]([]); const fetchData async (filters: StatisticsFilters) { setLoading(true); try { const [overview, trend, category, ranking] await Promise.all([ statisticsApi.getOverview(filters), statisticsApi.getTrend(filters), statisticsApi.getCategoryStats(filters), statisticsApi.getTeamRanking(filters), ]); setStats(overview); setTrendData(trend); setCategoryData(category); setRankingData(ranking); } finally { setLoading(false); } }; const handleExport () { // 调用导出接口 statisticsApi.exportData({}); }; return ( div style{{ padding: 24 }} Card style{{ marginBottom: 16 }} FilterBar onSearch{fetchData} onExport{handleExport} loading{loading} / /Card Space directionvertical sizemiddle style{{ width: 100% }} StatsCards data{stats} loading{loading} / TrendChart data{trendData} loading{loading} / Row gutter{16} Col span{14} CategoryHeatmap data{categoryData} loading{loading} / /Col Col span{10} TeamRanking data{rankingData} loading{loading} / /Col /Row /Space /div ); }本章小结知识点关键内容架构模式筛选区 统计卡片 趋势图 分类分布 排行榜自上而下从宏观到微观多维度筛选展开/收起模式常用条件默认展示高级条件折叠统计卡片数字动效 趋势指标 图标视觉冲击力强趋势图折线/柱状切换支持双系列对比新建 vs 完成分类热度横向柱状图带渐变填充和数值标签团队排行前三名高亮展示完成量、时长、满意度数据导出前端 xlsx 轻量导出 / 后端生成文件下载按需选择空状态Empty 操作按钮引导用户下一步下一章预告我们来学习项目的部署与工程化包括构建优化、Docker 容器化、Nginx 配置和性能监控。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑