第 3 章:工作台——数据看板
本章学习目标学会设计数据看板类页面的布局掌握统计卡片组件的实现数字动效、趋势指标集成 ECharts 实现趋势图、饼图、热力图实现待办队列Tab 切换 虚拟列表了解 Mock 数据与真实接口切换方案3.1 需求分析与布局设计工作台是用户登录后看到的第一个页面承担「数据总览」和「快速入口」两个核心职能。3.1.1 页面结构┌──────────────────────────────────────────────────┐ │ 顶部欢迎语 快捷操作按钮 │ ├──────────┬───────────────────────────────────────┤ │ │ ┌───────────────────────────────┐ │ │ 待办 │ │ 统计卡片组4 个 │ │ │ 队列 │ ├───────────────────────────────┤ │ │(左侧)│ │ 趋势图折线图 │ │ │ │ ├───────────────────────────────┤ │ │ │ │ 分类分布(饼)│ 团队效率(柱)│ │ │ │ └───────────────────────────────┘ │ └──────────┴───────────────────────────────────────┘3.1.2 布局实现使用 Ant Design 的 Row Col 栅格系统// src/pages/workbench/index.tsx import { Row, Col, Card, Typography, Space, Button } from antd; import { PlusOutlined, BellOutlined } from ant-design/icons; import StatCard from components/StatCard; import TodoList from ./components/TodoList; import TrendChart from ./components/TrendChart; import CategoryPie from ./components/CategoryPie; import TeamRanking from ./components/TeamRanking; import { useWorkbenchStore } from ./store; const { Title } Typography; export default function Workbench() { const stats useWorkbenchStore((s) s.stats); const userInfo useUserStore((s) s.userInfo); return ( div style{{ padding: 24 }} {/* 顶部欢迎区 */} Card style{{ marginBottom: 16 }} Space sizelarge style{{ width: 100%, justifyContent: space-between }} div Title level{3} style{{ margin: 0 }} 你好{userInfo?.name || 用户} /Title p style{{ color: #999, margin: 8px 0 0 }} 今天是 {new Date().toLocaleDateString(zh-CN)}祝你工作顺利 /p /div Space Button icon{BellOutlined /}消息中心/Button Button typeprimary icon{PlusOutlined /} 新建任务单 /Button /Space /Space /Card {/* 主体双栏布局 */} Row gutter{16} {/* 左侧待办队列 */} Col span{6} TodoList / /Col {/* 右侧数据看板 */} Col span{18} {/* 统计卡片 */} Row gutter{16} style{{ marginBottom: 16 }} Col span{6} StatCard title待处理 value{stats.pending} trend{12} / /Col Col span{6} StatCard title处理中 value{stats.processing} trend{-5} / /Col Col span{6} StatCard title已完成 value{stats.resolved} trend{23} / /Col Col span{6} StatCard title超时 value{stats.overdue} trend{-8} color#ff4d4f / /Col /Row {/* 趋势图 */} Card title任务趋势 style{{ marginBottom: 16 }} TrendChart / /Card {/* 饼图 柱状图 */} Row gutter{16} Col span{12} Card title分类分布 CategoryPie / /Card /Col Col span{12} Card title团队效率榜 TeamRanking / /Card /Col /Row /Col /Row /div ); }3.2 统计卡片组件StatCard统计卡片是看板最基本的元素需要支持数字动效和趋势指标。3.2.1 组件实现// src/components/StatCard/index.tsx import { useEffect, useState } from react; import { Card, Typography, Space } from antd; import { ArrowUpOutlined, ArrowDownOutlined } from ant-design/icons; const { Text } Typography; interface StatCardProps { title: string; value: number; trend?: number; // 百分比正数上升负数下降 color?: string; prefix?: string; suffix?: string; onClick?: () void; } // 数字滚动动效 function AnimatedNumber({ value, duration 800 }: { value: number; duration?: number }) { const [display, setDisplay] useState(0); useEffect(() { let startTime: number; let animationFrame: number; const animate (timestamp: number) { if (!startTime) startTime timestamp; const progress Math.min((timestamp - startTime) / duration, 1); // 缓动函数easeOutQuart const eased 1 - Math.pow(1 - progress, 4); setDisplay(Math.floor(eased * value)); if (progress 1) { animationFrame requestAnimationFrame(animate); } }; animationFrame requestAnimationFrame(animate); return () cancelAnimationFrame(animationFrame); }, [value, duration]); return {display.toLocaleString()}/; } export default function StatCard({ title, value, trend, color #1677ff, prefix , suffix , onClick, }: StatCardProps) { const isPositive (trend || 0) 0; return ( Card hoverable onClick{onClick} styles{{ body: { padding: 20 } }} Space directionvertical sizemiddle style{{ width: 100% }} Text typesecondary style{{ fontSize: 14 }} {title} /Text div span style{{ fontSize: 28, fontWeight: 600, color }} {prefix} AnimatedNumber value{value} / {suffix} /span /div {trend ! undefined ( Space size{4} {isPositive ? ( ArrowUpOutlined style{{ color: #52c41a }} / ) : ( ArrowDownOutlined style{{ color: #ff4d4f }} / )} Text style{{ color: isPositive ? #52c41a : #ff4d4f, fontSize: 12 }} {Math.abs(trend)}% 较上周 /Text /Space )} /Space /Card ); }3.2.2 设计要点数字动效用requestAnimationFrame实现平滑滚动给用户「数据在变化」的感觉趋势指标箭头 颜色 百分比一目了然hoverable卡片可点击作为快速入口跳转到对应列表可配置颜色、前缀、后缀都通过 props 传入提高复用性3.3 ECharts 图表集成ECharts 是最强大的前端图表库我们用echarts-for-react简化 React 中的使用。3.3.1 安装npminstallecharts echarts-for-react3.3.2 趋势折线图// src/pages/workbench/components/TrendChart.tsx import ReactECharts from echarts-for-react; import { useWorkbenchStore } from ../store; export default function TrendChart() { const trendData useWorkbenchStore((s) s.trendData); const option { tooltip: { trigger: axis }, legend: { data: [新建, 完成], right: 0 }, grid: { left: 40, right: 20, top: 40, bottom: 30 }, xAxis: { type: category, data: trendData.dates, boundaryGap: false, }, yAxis: { type: value }, series: [ { name: 新建, type: line, smooth: true, data: trendData.created, areaStyle: { 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) }, ], }, }, lineStyle: { color: #1677ff, width: 2 }, itemStyle: { color: #1677ff }, }, { name: 完成, type: line, smooth: true, data: trendData.resolved, areaStyle: { 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) }, ], }, }, lineStyle: { color: #52c41a, width: 2 }, itemStyle: { color: #52c41a }, }, ], }; return ReactECharts option{option} style{{ height: 280 }} /; }3.3.3 分类饼图// src/pages/workbench/components/CategoryPie.tsx import ReactECharts from echarts-for-react; import { useWorkbenchStore } from ../store; export default function CategoryPie() { const categoryData useWorkbenchStore((s) s.categoryData); const option { tooltip: { trigger: item, formatter: {b}: {c} ({d}%) }, legend: { orient: vertical, left: left }, series: [ { type: pie, radius: [40%, 70%], center: [60%, 50%], avoidLabelOverlap: true, itemStyle: { borderRadius: 6, borderColor: #fff, borderWidth: 2 }, label: { show: false }, emphasis: { label: { show: true, fontSize: 14, fontWeight: bold }, }, data: categoryData, }, ], }; return ReactECharts option{option} style{{ height: 280 }} /; }3.3.4 团队效率柱状图// src/pages/workbench/components/TeamRanking.tsx import ReactECharts from echarts-for-react; import { useWorkbenchStore } from ../store; export default function TeamRanking() { const teamData useWorkbenchStore((s) s.teamData); const option { tooltip: { trigger: axis, axisPointer: { type: shadow } }, grid: { left: 80, right: 20, top: 20, bottom: 30 }, xAxis: { type: value }, yAxis: { type: category, data: teamData.map((t) t.name), }, series: [ { type: bar, data: teamData.map((t) t.value), itemStyle: { color: { type: linear, x: 0, y: 0, x2: 1, y2: 0, colorStops: [ { offset: 0, color: #1677ff }, { offset: 1, color: #69c0ff }, ], }, borderRadius: [0, 4, 4, 0], }, barWidth: 16, label: { show: true, position: right }, }, ], }; return ReactECharts option{option} style{{ height: 280 }} /; }3.4 待办队列待办队列是工作台的核心交互区展示当前用户需要处理的任务。3.4.1 Tab 切换 列表// src/pages/workbench/components/TodoList.tsx import { useState } from react; import { Card, Tabs, List, Tag, Avatar, Badge } from antd; import { useNavigate } from react-router-dom; import dayjs from dayjs; import relativeTime from dayjs/plugin/relativeTime; import { useWorkbenchStore } from ../store; dayjs.extend(relativeTime); const tabs [ { key: pending, label: 待处理, badge: 5 }, { key: processing, label: 处理中, badge: 3 }, { key: reminder, label: 提醒, badge: 2 }, ]; export default function TodoList() { const [activeTab, setActiveTab] useState(pending); const navigate useNavigate(); const todoList useWorkbenchStore((s) s.todoList[activeTab as keyof typeof s.todoList]); const priorityColor (p: number) { const colors: Recordnumber, string { 1: red, 2: orange, 3: blue, 4: default }; return colors[p] || default; }; const priorityText (p: number) { const texts: Recordnumber, string { 1: 紧急, 2: 高, 3: 中, 4: 低 }; return texts[p] || 低; }; return ( Card title待办队列 extra{a onClick{() navigate(/ticket)}查看全部/a} styles{{ body: { padding: 0 } }} Tabs activeKey{activeTab} onChange{setActiveTab} items{tabs.map((t) ({ key: t.key, label: ( Badge count{t.badge} sizesmall offset{[4, 0]} {t.label} /Badge ), }))} style{{ padding: 0 16px }} / List dataSource{todoList} renderItem{(item) ( List.Item style{{ padding: 12px 16px, cursor: pointer }} onClick{() navigate(/ticket/${item.id})} List.Item.Meta avatar{Avatar style{{ backgroundColor: #1677ff }}{item.creator?.[0]}/Avatar} title{ div style{{ display: flex, justifyContent: space-between }} span style{{ fontWeight: 500, maxWidth: 180, overflow: hidden, textOverflow: ellipsis, whiteSpace: nowrap }} {item.title} /span Tag color{priorityColor(item.priority)} style{{ marginLeft: 8 }} {priorityText(item.priority)} /Tag /div } description{ div style{{ fontSize: 12, color: #999 }} #{item.id} · {dayjs(item.createdAt).fromNow()} /div } / /List.Item )} style{{ maxHeight: 520, overflow: auto }} / /Card ); }3.5 Mock 数据与真实接口切换在项目早期后端接口还没就绪时我们需要 Mock 数据来推进前端开发。3.5.1 Store 层的数据来源策略// src/pages/workbench/store/index.tsimport{createStore}fromstore/createStore;import{workbenchApi}fromservices/workbench;import{mockStats,mockTrendData,mockCategoryData,mockTeamData,mockTodoList}from../mock;interfaceWorkbenchState{stats:{pending:number;processing:number;resolved:number;overdue:number};trendData:{dates:string[];created:number[];resolved:number[]};categoryData:{name:string;value:number}[];teamData:{name:string;value:number}[];todoList:{pending:any[];processing:any[];reminder:any[];};loading:boolean;}constUSE_MOCKimport.meta.env.DEV;// 开发环境用 mockconstworkbenchStorecreateStoreWorkbenchState({stats:{pending:0,processing:0,resolved:0,overdue:0},trendData:{dates:[],created:[],resolved:[]},categoryData:[],teamData:[],todoList:{pending:[],processing:[],reminder:[]},loading:false,});exportconstuseWorkbenchStoreworkbenchStore.useStore;exportconstsetWorkbenchStateworkbenchStore.setState;exportconstfetchWorkbenchDataasync(){setWorkbenchState({loading:true});try{if(USE_MOCK){// 模拟请求延迟awaitnewPromise((r)setTimeout(r,500));setWorkbenchState({stats:mockStats,trendData:mockTrendData,categoryData:mockCategoryData,teamData:mockTeamData,todoList:mockTodoList,loading:false,});}else{constdataawaitworkbenchApi.getOverview();setWorkbenchState({...data,loading:false});}}catch{setWorkbenchState({loading:false});}};3.5.2 Mock 数据文件// src/pages/workbench/mock/index.tsexportconstmockStats{pending:28,processing:15,resolved:142,overdue:6,};exportconstmockTrendData{dates:[周一,周二,周三,周四,周五,周六,周日],created:[12,19,15,22,18,8,5],resolved:[10,16,18,20,15,6,4],};exportconstmockCategoryData[{name:系统问题,value:35},{name:账号权限,value:28},{name:业务咨询,value:22},{name:功能建议,value:15},{name:其他,value:10},];exportconstmockTeamData[{name:张三,value:32},{name:李四,value:28},{name:王五,value:24},{name:赵六,value:20},{name:钱七,value:16},];exportconstmockTodoList{pending:Array.from({length:8}).map((_,i)({id:TK${1000i},title:待处理任务${i1},priority:(i%4)1,creator:[张三,李四,王五,赵六][i%4],createdAt:newDate(Date.now()-i*3600000).toISOString(),})),processing:Array.from({length:5}).map((_,i)({id:TK${2000i},title:处理中任务${i1},priority:(i%3)1,creator:[张三,李四,王五][i%3],createdAt:newDate(Date.now()-i*7200000).toISOString(),})),reminder:Array.from({length:3}).map((_,i)({id:TK${3000i},title:提醒任务${i1},priority:i1,creator:[张三,李四][i%2],createdAt:newDate(Date.now()-i*1800000).toISOString(),})),};本章小结知识点关键内容布局设计Row Col 栅格系统左侧列表 右侧看板的双栏布局统计卡片requestAnimationFrame实现数字滚动动效趋势指标用颜色箭头EChartsecharts-for-react封装折线图/饼图/柱状图三种常用图表待办队列Tabs 切换、List 列表、点击跳转详情Mock 方案开发环境走 mock 数据生产环境走真实接口通过环境变量切换下一章预告我们进入核心业务模块——任务单管理学习数据模型设计、高级筛选、状态流转等复杂业务场景的实现。