仪表盘看起来像几张数字卡和两张表,真正的难点却在数据从哪里来、哪些请求可以并行、不同角色能看到什么,以及一个接口失败时是否拖垮全页。本文从 Vue 后台仪表盘实现拆解这些决定。

本文代码快照:Vue 后台提交
d3ee41a。主要参考manage-frontend-vue/src/pages/dashboard/DashboardPage.vue、src/api/site.ts、src/lib/permission.ts及 React 对照页。
前言:仪表盘是信息架构,不是数字装饰
一个可用的后台仪表盘应该帮助用户决定下一步做什么。管理员可能优先关注全站数据和用户管理;编辑可能关心待处理文章和评论;会员更关心个人内容,但通常不应进入管理控制台。若仪表盘只堆几张"文章数、访问量"卡片,却没有角色边界、近期工作入口或失败状态,就只是装饰页。
本文使用项目真实页面作为例子:统计卡与分类分布面向已进入仪表盘的编辑/管理员,近期文章只对有文章管理能力的角色发请求,近期评论只对有审核能力者发请求。这里要关注两件事:请求是否按角色条件启用,界面是否对每个数据块独立反馈。

一页上的几组数据来自不同 API
Vue 页面对统计、分类统计、最近文章、最近评论分别建立 query:
ts
const stats = useQuery({ queryKey: ['stats'], queryFn: getSiteStats })
const categories = useQuery({ queryKey: ['category-stats'], queryFn: getCategoryStats })
const articles = useQuery({
queryKey: ['articles', 'recent'],
queryFn: () => listAdminArticles({ sort: '-createdAt', pageSize: 5 }),
enabled: canArticles,
})
const comments = useQuery({
queryKey: ['comments', 'recent'],
queryFn: () => listAdminComments({ pageSize: 5 }),
enabled: canComments,
})
这些查询在组件初始化后可以并行执行,用户不必等文章请求完成,统计卡才开始展示。每个 query 有独立 loading/error/data 状态,也有各自的缓存身份。并行不等于盲目多发请求:enabled 将角色能力作为门槛,避免无权区域发出后台数据请求。
这个 enabled 是前端行为优化,后端仍须检查 API 权限。即使把文章 query 禁用,攻击者仍可手动发请求;不可将 query 配置当成服务器授权。

角色决定可见能力,也决定是否请求
页面从 auth store 读取当前用户,通过纯权限函数派生 canArticles 和 canComments:
ts
const canArticles = computed(() => canManageArticles(auth.user))
const canComments = computed(() => canModerateComments(auth.user))
模板使用这些值控制"写文章""处理评论"动作和对应数据区块。查询本身也使用同一能力值作为 enabled。这样能避免只隐藏区块、但页面仍请求其数据的浪费和不必要错误。
项目的管理路由已限制会员访问控制台,所以仪表盘主要面对 editor/admin;能力判定仍能明确说明文章域和评论域权限。如果未来角色细分变化,规则集中在 permission.ts,路由、菜单和数据区都可复用。角色具体能做什么以冻结契约为准,不能由仪表盘自行发明。
把统计数据映射到界面时保持语义
统计卡消费站点统计接口的文章数、评论数、活跃会员数和阅读量:
vue
<div v-for="item in [
{ label: '已发布文章', value: stats.data.value?.articleCount },
{ label: '已通过评论', value: stats.data.value?.commentCount },
{ label: '活跃会员', value: stats.data.value?.memberCount },
{ label: '累计阅读量', value: stats.data.value?.viewTotal },
]" :key="item.label">
<div>{{ item.label }}</div>
<div>{{ item.value?.toLocaleString() ?? '---' }}</div>
</div>
显示标签应与接口统计语义一致。例如"已通过评论"与"评论总数"不是同一指标,"阅读量"也不必然等于独立访客。若接口给的是 viewTotal,UI 就不能擅自称为"访客人数"。格式化数字只改变展示,不改业务含义。
分类分布目前用条形比例展示分类文章数。此类轻量图形可以先用 CSS 表达,比例上限处理在视图层;它不是项目承诺的完整分析图表,也不需要因为依赖中有 ECharts 就强行换成图表。图表适用于数据系列复杂、需要图例/坐标/交互的情况,不是仪表盘页面的必选项。
每个区块有自己的加载和空结果语义
仪表盘中分类区块有独立的 loading 状态,近期表格把 query 的 pending 状态传给 Naive DataTable。统计数据未返回时显示占位,返回零值时则显示格式化后的 0;两者不应混淆。
对于最近文章/评论,接口成功但返回空列表,通常意味着当前没有近期记录;加载失败则要提供错误提示或重试入口。若模板把 data?.list ?? [] 一概当空列表渲染,用户可能分不清"尚未加载""无数据"和"请求失败"。这是完善页面状态时应显式检查的地方,不能凭空宣称简单数据占位已满足所有错误态需求。
不同区块的加载彼此独立,可以逐块展现,不必以 Promise.all 把整页锁在一个总 loading 中。也要注意 Vue Query retry 默认值:失败 query 可能自动重试一次,界面状态需要反映请求库实际状态,而非手工又发一遍重复请求。
时间范围和数据新鲜度要遵循接口能力
如果契约只提供全站累计统计,没有时间范围参数,前端不能在图表标题上伪装成"最近 7 天"。如果后台列表端点支持排序,就可以明确用 sort: '-createdAt' 取近期文章;评论端点如果没有 sort 参数,则不能靠向 API 发送未定义字段来假设它支持。
仪表盘指标要先看 OpenAPI 能提供什么,再决定布局。如果产品需要按天趋势、活跃用户口径或比较周期,应该确认契约是否已有相应聚合接口;若没有,先评估是否需要扩展契约与后端,再设计图表。不能通过前端反复拉取所有文章再自行统计,制造数据量和权限边界问题。
缓存策略也要和指标性质匹配。全站统计默认 30 秒 fresh,用户手动刷新或返回页面时可复用近期结果。实时性要求特别高的指标需要产品与后端共同定义刷新频率,不能只将 staleTime 设为 0 就宣称具备实时分析。
仪表盘 action 应服务于下一步工作
项目页面有"写文章"和"处理待复核评论"入口,并按权限显示。它们不是为了填充标题栏,而是把摘要信息连接到实际工作流。近期文章可进入编辑,评论审核区可打开评论工作台,减少在仪表盘和业务页之间来回找路。
动作权限应与目的地路由一致:即使按钮在异常状态下被渲染,目标路由还会再执行守卫;目标 API 仍会在后端鉴权。这层层检查允许更好的体验和安全各尽其责。
React 与 Vue 的结构对照
React 版同样组合站点统计、分类树/统计、近期文章和评论等查询,并用 Can/权限函数控制入口。React Query 返回的 isLoading、data 与 Vue Query 的状态对象概念相似,模板/JSX 语法不同。Vue 版通过多个 useQuery 声明并行依赖,响应式 enabled 控制能力区请求;React 版按组件渲染与 Query hook 组织对应数据。
对照时别只比较代码行数。应核实指标相同、角色可以看到相同业务入口、使用的 API 和请求参数一致、接口失败对其他区块的影响一致。某个框架写起来更短,并不代表异步数据边界就天然更简单。
小结:仪表盘由数据契约、角色能力和工作流共同组成
仪表盘不是若干数字卡,而是并行查询、角色化请求、正确的指标语义、独立加载反馈和清楚的后续动作组合。先确认 API 提供什么,再决定展示方式;不要把 UI 隐藏误作权限,也不要把静态累计值包装成趋势分析。
下一篇进入文章列表,分析服务端筛选、选择状态、批量软删除和逐项失败反馈如何映射到 API 契约,并在部分成功时给用户清楚的结果。
延伸阅读
如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」:
🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html
📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer
