资讯动态

SSM+Vue3构建设备维修管理系统:从三层架构到前后端联调全攻略

发布时间:2026/9/27 23:03:18 来源:尧图企业网站定制
简介这份资源是一套基于SSMVue3的设备维修管理系统毕业设计完整源码包适合计算机相关专业学生用于毕设参考、课程设计或项目练手。系统围绕设备维修全流程设计覆盖设备管理、维修申请、审批流转、过程记录、维修验收与统计分析等模块后端采用Java开发前端基于Vue3与Element Plus实现B/S架构且前后端分离整体结构清晰便于二次开发与功能扩展。压缩包共2000个文件以js、md、json为主其中js文件对应前端构建资源与ECharts图表组件md文件包含项目说明与依赖文档另含sql数据库脚本和docx设计文档包体大小约129.49MB下载后可直接导入IDE进行部署。目前已有284人学习使用适合需要完整项目源码、数据库设计及前后端联调思路的读者可帮助快速理解业务模块划分与SSMVue3整合流程缩短毕设搭建周期。1. 老框架加新前端SSM与Vue3为什么还是设备维修管理系统的稳妥选型答辩老师最常问的一句话是这个系统如果只靠你模拟数据演示那它离真能跑起来还差多远设备维修管理系统被问住的点通常集中在报修单怎么流转、维修进度怎么追踪、备件费用怎么核销。用 SSM Vue3 来做后端 Spring SpringMVC MyBatis 负责把工单生命周期管清楚前端 Vue3 负责把台账和看板做得能看、能用、能演示。它适合手里已经有一点 Java 基础、想把前后端真正打通的人。难点不在框架本身而在联调时的跨域、日期序列化和动态表单行这类细节。把这些踩过一遍这个毕设就比大多数“只跑通 Demo”的同学扎实一截。2. 把职责分清楚SSM 三层与 Vue3 组合式 API 各自该管什么2.1 SSM 不是过气代码而是三层职责的教科书Spring、SpringMVC、MyBatis 这三个组件在设备维修系统里各管一段Spring 容器管业务对象维修工单的 Service 实例从哪来、生命周期多长都由它决定SpringMVC 管路由前端请求/api/repair/order/page时它负责找到对应的 Controller 方法并把参数绑进去MyBatis 管数据库访问把repair_order表映射成 Java 对象。常见做法是直接拿 SpringBoot 做整合容器pom 里引spring-boot-starter-web和mybatis-spring-boot-starter效果等同于 SSM 全家桶同时省掉一堆 XML 配置文件。但我更建议在毕设里保留手写的springmvc.xml和mybatis-config.xml哪怕只是几个关键节点。原因很实际论文里要写“框架如何工作”答辩老师也会顺着配置问下去你能答出context:component-scan扫的是哪个包、MapperScannerConfigurer注册了哪些 Mapper这一问就算过关了。这里有一个容易被带偏的方向直接套用大型后台脚手架里面塞了几十个模块和一套 RBAC 权限模型。设备维修管理系统本质是“台账 流程 统计”不需要那些东西。脚手架越大答辩时被追问盲区的概率越高。SSM 的三层结构本身就是这个题目的最佳教学载体你不需要证明自己会用很多框架而是要证明自己能把业务讲清楚。2.2 Vue3 相对 Vue2组合式 API 让逻辑不再堆在 data 里设备维修系统里最典型的页面是工单列表上面一排筛选条件中间一张表格下面一个分页。用 Vue2 写筛选条件塞 data加载方法塞 methods刷新逻辑散落在三个方法里用 Vue3 的 setup 语法糖这些逻辑可以收进一个组合函数同一个查询规则同时用在工单管理和待办提醒两个页面。// useWorkOrders.js——把工单列表的搜索、分页、刷新逻辑收进一个可复用组合函数 import { ref, reactive, onMounted } from vue import { getWorkOrderPage } from /api/repair export function useWorkOrders() { const loading ref(false) const list ref([]) const total ref(0) // 用 reactive 保存搜索条件页面切走再回来时条件不丢 const query reactive({ pageNum: 1, pageSize: 10, deviceName: , status: , urgentLevel: }) async function loadPage() { loading.value true try { const res await getWorkOrderPage(query) list.value res.data.records total.value res.data.total } finally { loading.value false } } function resetQuery() { query.pageNum 1 query.deviceName query.status query.urgentLevel loadPage() } onMounted(loadPage) return { loading, list, total, query, loadPage, resetQuery } }这段代码的关键是query用reactive保持为响应式对象表格和分页组件直接绑定它的字段。loadPage用ref控制表格的 loading 态请求结束置于finally里关闭。调用端只需要一行const { loading, list, query, loadPage, resetQuery } useWorkOrders()就能拿到全部能力多个页面复用同一套逻辑互不污染。Vue2 转 Vue3 时最容易出问题的是丢掉this。Vue2 的 methods 里全靠this.xxx访问数据Vue3 的 setup 里没有实例上下文数据和方法都通过返回值暴露给模板。第一次迁移会很不习惯但写两个页面后会明显体会到逻辑内聚了不会再出现“这个变量到底在 data 还是 computed”的纠结。2.3 技术栈选型清单最精简版的后台管理骨架前端骨架我一般控制在五个依赖以内Vue3 做视图层Vite 做构建Element Plus 提供表格、表单、弹窗这些成熟组件Pinia 存登录用户和全局状态Axios 走后端接口。图表类需求用 ECharts按需引入不要全量打包。端选型在这套系统里的职责前端Vue3 Vite组件化开发组合式 API 组织业务逻辑前端Element Plus工单表格、筛选表单、弹窗、分页组件前端Pinia存储登录用户、菜单权限、全局字典前端Axios统一请求封装携带凭证处理 code 约定后端Spring SpringMVCIOC 容器、路由分发、参数绑定后端MyBatis PageHelper数据持久化分页查询后端MySQL 8.0设备台账、工单、备件明细三张核心表可视化ECharts 5维修状态分布、报修趋势统计图这里不引入 TypeScript原因不是它不好而是毕设时间有限纯 JavaScript 的排查链路更短。网上很多“若依 Vue3 TS”脚手架下载下来第一眼看到的是一堆ts-ignore和类型报错反而把业务淹没了。Vue3 管理后台的核心不依赖 TS等你把设备维修系统跑通以后再单独学 TS 迁移会更轻松。3. 后端落地设备台账、维修工单、备件明细三张表的建表与接口设计3.1 数据模型三张核心表怎么建才不用后期返工设备维修管理系统的实体关系不复杂一台设备对应多张维修工单一张工单对应多条备件明细。如果把备件直接塞进工单表的字段里统计“哪种备件消耗最多”时就得做字符串拆分那是最难受的查询。拆成独立表以后一条聚合 SQL 就能出数据后面做图表也顺理成章。-- 设备台账表 CREATE TABLE device ( id INT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(32) NOT NULL COMMENT 设备编号, device_name VARCHAR(64) NOT NULL COMMENT 设备名称, model VARCHAR(64) DEFAULT COMMENT 型号, location VARCHAR(128) DEFAULT COMMENT 安装位置, status TINYINT DEFAULT 1 COMMENT 1正常运行 2待维修 3维修中 4报废, purchase_date DATE DEFAULT NULL COMMENT 购置日期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_device_code (device_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备台账; -- 维修工单表 CREATE TABLE repair_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(24) NOT NULL COMMENT 工单号如 WX20240506-001, device_id INT NOT NULL COMMENT 设备ID, reporter VARCHAR(32) NOT NULL COMMENT 报修人, fault_desc VARCHAR(500) NOT NULL COMMENT 故障描述, urgent_level TINYINT DEFAULT 1 COMMENT 1一般 2紧急 3特急, status TINYINT DEFAULT 0 COMMENT 0待派工 1维修中 2待验收 3已完成 4已取消, assignee VARCHAR(32) DEFAULT COMMENT 维修工, plan_end_time DATETIME DEFAULT NULL COMMENT 预计完成时间, actual_end_time DATETIME DEFAULT NULL COMMENT 实际完成时间, cost DECIMAL(10,2) DEFAULT 0.00 COMMENT 维修费用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status (status), KEY idx_device (device_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT维修工单; -- 工单备件明细表一个工单关联多条明细 CREATE TABLE repair_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL COMMENT 工单ID, part_name VARCHAR(64) NOT NULL COMMENT 备件名称, part_count INT NOT NULL DEFAULT 1 COMMENT 数量, unit_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 单价 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT维修备件明细;工单状态设计成五档而不是“已完成/未完成”两档这是评审老师会重点看的业务深度。报修人提交后是待派工维修工接单后变维修中维修完成进入待验收验收确认才置为已完成这中间每一跳都配一个时间字段。工单号用“WX 日期 三位流水”生成目的不是好看而是让工单在系统里可以直接被人类读出来现场沟通时报一个号就能定位。备件明细里没有放单价快照之外的东西因为备件价格会变工单必须保留当时的成交单价否则后续对账时单价对不上。工单表里那个cost字段是冗余的由明细行金额汇总得出这是为了方便列表页和统计页直接 SUM不用每次 JOIN 子表。数据冗余在毕设里是合理设计只要在论文里解释清楚为什么冗余。3.2 控制器到 Mapper一个标准的分页查询接口是怎么串起来的后端工程结构按entity / mapper / service / controller四层分包。设备维修系统规模小不需要引入复杂的 DDD 分层但层与层之间不能互相渗透Controller 只接收参数和返回结果Service 只处理业务逻辑Mapper 只写 SQL。以工单分页查询接口为例注意 SSM 原生注解的写法。Controller RequestMapping(/api/repair/order) public class RepairOrderController { Autowired private RepairOrderService repairOrderService; /** * 分页查询工单。deviceName、status、urgentLevel 均可为空 */ RequestMapping(/page) ResponseBody public R page(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, String deviceName, Integer status, Integer urgentLevel) { PageHelper.startPage(pageNum, pageSize); PageRepairOrderVO page repairOrderService.queryPage(deviceName, status, urgentLevel); return R.ok(page); } }这里故意用Controller ResponseBody而不是RestController是为了尊重“SSM”这个标题本身的组合方式你在 XML 配置里看到的context:component-scan扫到的就是这类控制层 Bean。PageHelper.startPage这一行有讲究它必须在紧邻要分页的查询语句之前调用中间不能插入其他数据库操作否则分页参数会被错误的查询消费掉。RepairOrderVO是专门给前端用的视图对象里面除了表字段还多一个deviceName要在 Service 层把设备和工单拼接好再返回。Service 层的重要工作是生成工单号先查当天已有工单数SELECT COUNT(*) FROM repair_order WHERE order_no LIKE WX20240506-%然后加一补零。这里有一个并发隐患两个请求同时查到同一个 count生成同样的单号。毕设里不要求做分布式锁但要在数据库层给order_no加唯一索引作为兜底真撞了就让请求重试一次这样的处理在答辩里可以主动讲出来。3.3 统一返回结构与日期格式化避免接口越写越乱前后端联调最容易乱在返回结构不统一。有的接口直接返回List有的返回Map前端每个页面写一种解包逻辑越写越累。我在所有设备维修接口里强制使用统一返回对象R。// 统一返回结构所有接口都用它包装 public class R { private int code; // 0 成功非 0 失败 private String msg; private Object data; public static R ok(Object data) { R r new R(); r.code 0; r.data data; return r; } public static R fail(String msg) { R r new R(); r.code 1; r.msg msg; return r; } // getter/setter 省略 }然后是 LocalDateTime 的序列化问题。MyBatis 从数据库查出DATETIME映射到 Java 的LocalDateTime如果没有配置 Jackson向前端返回时默认序列化成数组形式比如[2024, 5, 6, 10, 30, 0]前端拿到以后既不能直接显示也不能new Date()这是我见过最玄学的黑匣子问题之一。解决方式是在 SpringMVC 的配置里注册一个全局的 Jackson 定制器。Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer dateCustomizer() { return builder - { // 全局把 LocalDateTime 统一输出成 yyyy-MM-dd HH:mm:ss DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)); builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); }; } }这段配置解决的是接口出参的展示格式和控制层入参的解析格式。配置完以后LocalDateTime类型的字段就统一变成2024-05-06 10:30:00这种字符串前端的表格列直接渲染不需要再写过滤器。我做这类系统时习惯先把日期格式定下来再做页面否则联调阶段会花大量时间在“这个时间是数组还是字符串”的拉扯上。4. Vue3 前端落地台账表格页、动态表单行与查询条件保留4.1 用 Vite 搭 Vue3 项目并配置 SCSS 与接口转发前端项目从零初始化用 Vite 创建模板然后装齐运行时依赖。设备维修系统的前端不需要 SSR不需要微前端一个标准 SPA 就够。# 初始化 Vue3 项目 pnpm create vitelatest device-repair-web -- --template vue cd device-repair-web pnpm install # 安装运行时依赖 pnpm add element-plus axios vue-router4 pinia # 安装开发依赖sass 提供 scss 编译 pnpm add -D sass装完sass以后.vue文件里style langscss才能编译。很多人装完缺少这一步写scss直接报错属于最基础也最常翻车的环节。接着改vite.config.js这里有两个关键配置路径别名和接口转发。import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { // 让 import 路径统一用 / 开头 : fileURLToPath(new URL(./src, import.meta.url)) } }, css: { preprocessorOptions: { scss: { additionalData: use /styles/variables.scss as *; } } }, server: { host: true, // 局域网其他设备也能访问默认只监听 localhost port: 5173, proxy: { // 把 /api 开头的请求转发给后端前端代码里直接写 /api/... /api: { target: http://localhost:8080, changeOrigin: true } } } })host: true解决的是局域网访问空白问题后面避坑章节会详细展开。proxy的作用是把前端/api开头的请求转发到后端 SSM 的 8080 端口这样浏览器看到的请求始终是同源的绕开跨域限制联调阶段能少一半报错。changeOrigin: true让后端看到的请求头像是直接来自 8080避免某些后端框架对 Host 头做校验时拒绝请求。additionalData会在每个scss文件编译时自动注入公共变量比如主题色、间距变量设备维修系统里做状态标签配色会很方便。4.2 工单列表页搜索条件保留的分页表格怎么写工单列表是整个系统使用频率最高的页面它由三块组成筛选表单、数据表格、分页器。用useWorkOrders组合函数把前面写好的查询逻辑接进来模板里只做绑定。template div !-- 搜索条件输入区 -- el-form :inlinetrue el-form-item label设备名称 el-input v-modelquery.deviceName placeholder输入设备名称 clearable keyup.enterloadPage / /el-form-item el-form-item label状态 el-select v-modelquery.status clearable changeloadPage el-option label待派工 :value0 / el-option label维修中 :value1 / el-option label待验收 :value2 / el-option label已完成 :value3 / /el-select /el-form-item el-form-item el-button typeprimary clickloadPage查询/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form el-table :datalist v-loadingloading border stripe el-table-column proporderNo label工单号 width180 / el-table-column propdeviceName label设备名称 min-width160 / el-table-column propreporter label报修人 width110 / el-table-column propfaultDesc label故障描述 min-width220 show-overflow-tooltip / el-table-column propstatusName label状态 width90 / el-table-column propplanEndTime label预计完成 width170 / el-table-column label操作 width120 fixedright template #default{ row } el-button link typeprimary clickopenDetail(row)详情/el-button /template /el-table-column /el-table el-pagination layouttotal, prev, pager, next, sizes :totaltotal v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :page-sizes[10, 20, 50] current-changeloadPage / /div /template script setup import { useWorkOrders } from ./composables/useWorkOrders const { loading, list, total, query, loadPage, resetQuery } useWorkOrders() /scriptv-model:current-page和v-model:page-size是 Element Plus 2.x 配合 Vue3 的写法它让分页组件直接修改query对象里的分页参数不需要手动监听current-change再赋值这是在 Vue2 时代要用.sync修饰符实现的很多人迁移过来还在写:current-page.sync那是老写法。操作列里#default{ row }是 el-table 插槽的新语法Vue2 里写slot-scope{ row }这也是迁移时的常见改错点必须用#default加解构。“搜索条件保留”是评审老师喜欢看到的细节。上面这套写法已经保证页面内部查询和分页切换时条件不丢但刷新浏览器后条件会回到初始值。我的做法是路由切换到列表页时把query.deviceName和query.status写进 URL 的 query 参数页面初始化监听route.query并回填。这个点很小但在答辩演示时现场刷新页面搜索条件还在是很加分的实感细节。4.3 维修备件明细动态增删表单行与日期校验新建维修工单时维修工要能往表单里添加多条备件记录这就是“动态添加删除 form 表单一行数据”的场景。我的做法是用数组渲染行重点在于循环的key不能传数组下标否则删除中间行时Vue 复用组件节点会导致输入框里的值串行。template el-form refformRef :modelform :rulesrules !-- 维修备件明细动态行支持新增和删除 -- div v-for(item, index) in form.items :keyitem.rowKey classitem-row el-input v-modelitem.partName placeholder备件名称 / el-input-number v-modelitem.partCount :min1 / el-input-number v-modelitem.unitPrice :min0 :precision2 / el-button clickremoveItem(index)删除/el-button /div el-button clickaddItem添加备件/el-button /el-form /template script setup import { reactive, ref } from vue const formRef ref(null) const form reactive({ urgentLevel: 1, planEndTime: , items: [] }) // 日期校验预计完成时间不能早于当前时间 const rules { planEndTime: [ { required: true, message: 请选择预计完成时间, trigger: change }, { validator: (rule, value, callback) { if (value new Date(value).getTime() Date.now()) { callback(new Error(预计完成时间不能早于当前时间)) } else { callback() } }, trigger: change } ] } let rowKeySeed 1 function addItem() { // 用 rowKey 作为循环 key删除中间行时不会因为下标错位而重复渲染 form.items.push({ rowKey: rowKeySeed, partName: , partCount: 1, unitPrice: 0 }) } function removeItem(index) { form.items.splice(index, 1) } /script style langscss scoped .item-row { display: flex; gap: 8px; margin-bottom: 8px; } /style日期校验这里做了两层。第一层是required保证用户必须选择预计完成时间第二层是自定义validator比较时间戳拒绝早于当前时间的选择。trigger: change告诉 Element Plus 在日期选择器的值变化时触发校验而不是输入时触发避免弹窗还没选完就报错。要补充一个关联校验如果表单里还有“维修开始时间”那么结束时间的 validator 要写成闭包读取form.startTime再比较。这里故意不写死是为了防止你在不同页面里复制同一段校验逻辑却忘了修改引用的字段。动态行的单价用:precision2保证两位小数cost汇总建议放在提交方法里遍历items计算而不是每次新增删行都实时算总价减少无谓渲染。5. 避坑设备维修系统前后端联调的 5 个典型掉坑现场5.1 局域网打不开 Vite 页面先在 vite.config 里开 host现象pnpm run dev启动后本机能访问但同一局域网的另一台电脑或手机打开http://192.168.x.x:5173是空白页控制台报连接失败。原因Vite 默认监听localhost也就是只绑定了127.0.0.1外部设备自然连不上。这是 Vite 的设计默认值不是系统 bug。解决在vite.config.js的server节点里加一行host: true它会自动监听0.0.0.0。注意不要手写死成某个 IP因为办公环境的局域网 IP 是 DHCP 分配的手写死换个网络就失效。同样的原理后端 SSM 如果部署在同一台机器上SpringBoot 内嵌 Tomcat 默认监听所有网卡不用额外处理。5.2 跨域报错与预检请求接口能通但浏览器拦了现象前端里填了http://localhost:8080/api/...全路径去请求浏览器控制台报Access-Control-Allow-Origin缺失部分 POST 请求出现两次第一次是 OPTIONS。原因前端 5173 端口访问后端 8080 端口属于跨源请求。浏览器对带自定义头或 JSON 的请求会先发 OPTIONS 预检后端没有处理这个预检方法请求就断在这里。解决最省事的方式是用 Vite 的接口转发让浏览器看起来永远是同源请求也就是 4.1 节里proxy配置做的事情。如果后端接口需要被其他系统直接调用再考虑在后端加 CORS 过滤器注意三个点允许的源要写具体地址不能写*要允许OPTIONS方法要允许携带凭证头。配置完以后可以在本地用 curl 带Origin头验证响应里有没有正确的Access-Control-Allow-*比反复刷新浏览器排查快得多。5.3 登录后 Session 丢失axios 要带上 cookie现象登录接口调用成功后端也把用户写进了 Session但紧接着查询工单列表的请求返回 401前端拦截器弹出“未登录”明明用户已经在系统里。原因跨域环境下axios默认不会携带 Cookie。后端给第一个请求创建的JSESSIONID浏览器没有存下来后续请求对后端来说都是新会话。这是前后端分离最典型的登录态丢失问题和密码对不对完全没关系。解决创建 axios 实例时设置withCredentials: true并且在拦截器里统一处理 401 状态避免每个请求都单独写一遍跳转逻辑注意要避免拦截器里无限循环跳转加上一个重定向标记判断。5.4 数据库字段 repair_status 映射成 null现象数据库里repair_order表的repair_status字段有值但页面表格里状态列是空白打印查询结果发现status是null。原因MyBatis 默认把数据库列名照搬给 Java 属性repair_status对应不上repairStatus于是赋值失败。这个问题在 SSM 手写 XML 配置时最容易出现因为驼峰映射默认是关闭的。解决在全局 MyBatis 配置里开启mapUnderscoreToCamelCase这样repair_status会自动映射到repairStatus不需要为每个字段写别名。如果你用了 SpringBoot 整合就在application.yml里配置开启如果手写mybatis-config.xml就在settings里配置。开启后重启再做一次查询就能确认。5.5 上传工单附件时 on-success 不触发现象用el-upload上传设备维修照片后端返回了{code:0,data:...}接口明明是通的但前端绑定的on-success回调就是不执行上传状态停滞。原因Element Plus 的on-success判断依据是 HTTP 状态码为2xx且响应体可被 JSON 解析。很多后端上传接口成功返回 200但响应内容是空的或者返回了text/html类型的错误页前端解析不到 JSON自然不触发成功回调。解决让后端的上传接口统一返回R结构同时前端不要依赖on-success里的事件对象取 URL而是从response.data里拿文件访问路径。另一个细节before-upload钩子里如果return false上传会被中断这个钩子只适合做文件类型和大小校验不要在这里做异步校验否则上传行为会变得不可预测。上传成功后把返回的文件路径放进表单的attachment字段提交工单时一起入库。6. 让毕设多一句可讲的亮点用聚合 SQL 和 ECharts 做维修统计仪表盘6.1 最近 30 天工单状态分布一条 SQL 出仪表盘数据设备维修管理系统如果只有增删改查答辩时很容易被一句话问住“你的系统除了录入数据还做了什么”一个稳妥的回答方向是统计看板维修状态分布、报修趋势、备件消耗排行。统计的数据来源可以让后端直接返回聚合结果比如最近 30 天各状态工单数量。SELECT SUM(CASE WHEN status 0 THEN 1 ELSE 0 END) AS pending, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS repairing, SUM(CASE WHEN status 2 THEN 1 ELSE 0 END) AS checking, SUM(CASE WHEN status 3 THEN 1 ELSE 0 END) AS finished, COUNT(*) AS total FROM repair_order WHERE create_time DATE_SUB(NOW(), INTERVAL 30 DAY)这条 SQL 比在 Java 里循环统计靠谱得多数据库一次聚合就把四个状态的数量算完。用DATE_SUB(NOW(), INTERVAL 30 DAY)做时间窗口是为了让看板展示的是“最近一个月”的数据而不是全量数据这个口径在答辩时也要讲得出来为什么取 30 天——因为备件采购和维保考核通常按月做。6.2 ECharts 在 Vue3 里的两种挂法与数据接入ECharts 5 在 Vue3 里的接入很简单关键是不要每次渲染都重新初始化实例。常见做法是在onMounted里echarts.init用watch监听后端返回的数据变化时调用setOption。import * as echarts from echarts import { ref, onMounted, watch } from vue import { getStatusOverview } from /api/statistics const chartRef ref(null) const chartData ref({ pending: 0, repairing: 0, checking: 0, finished: 0, total: 0 }) onMounted(async () { const res await getStatusOverview() chartData.value res.data // 图表实例只需要创建一次后续数据变化走 setOption const chart echarts.init(chartRef.value) chart.setOption({ series: [{ type: pie, radius: [40%, 70%], data: [ { name: 待派工, value: chartData.value.pending }, { name: 维修中, value: chartData.value.repairing }, { name: 待验收, value: chartData.value.checking }, { name: 已完成, value: chartData.value.finished } ] }] }) })radius: [40%, 70%]是环形图的经典写法外环 70%、内环 40%中心留出来放总数。echarts.init必须在 DOM 节点已经挂载之后调用所以放在onMounted里而不是setup里这是初学者最容易踩的顺序问题。如果页面里还有“报修趋势折线图”就再写一个watch监听数据到达后重新setOption。这个统计模块我前后做了两版第一版表格页弄好了图第二版才意识到统计图的布局和路由菜单是分开设计的结果把图表硬塞进工单列表页底部交互很别扭。后来把统计页独立成菜单图表数据由单独的统计接口提供页面结构清爽很多。再做这类系统我会先把统计页面的路由和布局想好再动手写图表代码这个习惯帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑