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