简介:本资源是一套完整的疫情防控管理系统毕业设计源码案例,面向Java全栈初学者与高校计算机专业毕业生,聚焦信息化管理场景下的疫情数据采集、统计与可视化需求。项目采用Spring Boot构建后端服务,Vue实现响应式前端界面,MySQL+MyBatis完成数据持久化,覆盖前后端分离开发全流程,适合作为课程设计、毕设参考或技术栈整合实践。压缩包共85个文件,包含31个Java业务逻辑类、13个MyBatis映射XML、7个Vue组件及7个JS交互脚本,辅以SQL建表语句、配置文件(yml/properties)、运行说明文档(txt/md)及系统效果图(jpg/png),结构清晰、开箱即用。资源大小仅1016KB,轻量易部署,已有122人学习下载。读者可直接导入IDEA运行,获取完整可执行系统、数据库初始化脚本、功能需求说明及多张界面效果截图,快速掌握企业级Web应用的工程组织方式与典型模块实现逻辑。
1. 这不是又一个“毕设管理系统”,而是 Spring Boot + Vue 工程化落地的典型切口
很多同学拿到“基于 Spring Boot + Vue 的疫情防控管理系统”这类毕业设计题目时,第一反应是:找源码、改数据库、换 logo、调接口、打包交差。但真正拉开差距的,从来不是功能是否“能跑”,而是系统如何支撑真实业务逻辑闭环——比如人员轨迹关联风险等级的动态计算、多级审核流程的状态机驱动、核酸数据批量校验与异常告警、前端权限随角色实时收敛、部署后静态资源路径不崩、跨域配置在 Nginx 和开发代理间无缝切换。这不是教科书里的 CRUD 演示,而是把 Spring Boot 的自动装配、事务边界、参数校验、统一响应体,和 Vue 的 Composition API、路由守卫、Pinia 状态持久化、Axios 请求拦截器,拧成一股能应对真实业务压力的工程流。适合正在用 Vue 3 + Spring Boot 2.7.x / 3.2.x 搭建前后端分离项目的开发者,尤其当你发现vue-router路由守卫拿不到登录态、@RequestBody接收嵌套对象报 400、application.yml里spring.profiles.active=prod切换后日志不输出、或者npm run build后访问/login返回 404 —— 这些都不是“环境问题”,而是架构层设计缺位的信号。
2. 从零构建可运行的最小闭环:Spring Boot 后端骨架与核心接口契约
一个真正能交付的疫情防控管理系统,后端不能只暴露几个@RestController方法。它必须具备清晰的分层结构、可验证的数据契约、可追踪的请求链路,以及面向运维的可观测性基础。我们以 Spring Boot 2.7.18(LTS)为基准,避免使用 Spring Boot 3.x 的 Jakarta EE 9+ 带来的兼容性陷阱(如javax.validation包迁移),同时兼顾主流 IDE(IntelliJ IDEA 2023.3)的项目识别能力。
2.1 初始化工程与关键依赖选型逻辑
使用 Spring Initializr(https://start.spring.io)生成基础项目,勾选以下模块:
- Spring Web:提供 RESTful 接口能力,底层基于 Tomcat 9.0.x(Spring Boot 2.7 默认)
- Spring Data JPA:替代 MyBatis 的轻量选择,配合 H2 内存数据库快速验证逻辑(生产环境再替换为 MySQL 8.0+)
- Validation:用于
@Valid校验@RequestBody,比手动if判断更符合契约精神 - Lombok:消除样板代码,
@Data+@Builder+@NoArgsConstructor组合覆盖实体类 90% 冗余 - Actuator:暴露
/actuator/health、/actuator/metrics等端点,为后续 Nginx 健康检查或 Prometheus 采集打基础
提示:不要勾选
Spring Security初期就引入。先让/api/user/list不带鉴权返回 JSON,再逐步叠加 JWT 认证。过早引入安全框架容易掩盖接口设计缺陷,比如UserVO是否包含敏感字段、password字段是否被@JsonIgnore正确屏蔽。
生成后的pom.xml中需显式声明 Lombok 版本(避免与 Spring Boot 内置版本冲突):
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> <version>1.18.30</version> </dependency>2.2 定义疫情防控领域核心实体与 DTO 分离策略
疫情防控场景下,Person(人员)、TestRecord(核酸记录)、RiskArea(风险区域)、ReviewTask(审核任务)四类实体构成主干。关键在于严格区分 Entity、DTO、VO:
Person(Entity):对应数据库表,含id,name,idCard,phone,currentAddress,createdAtPersonDTO(DTO):用于 Controller 层接收前端 POST 数据,仅含name,idCard,phone,currentAddress,且@NotBlank校验PersonVO(VO):用于返回给前端,不含idCard(脱敏),增加riskLevel: String(根据地址匹配风险区域动态计算)
// src/main/java/com/example/covid/entity/Person.java @Entity @Table(name = "t_person") @Data @NoArgsConstructor public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Column(unique = true) // 身份证号唯一索引 private String idCard; private String phone; private String currentAddress; private LocalDateTime createdAt; }// src/main/java/com/example/covid/dto/PersonDTO.java @Data public class PersonDTO { @NotBlank(message = "姓名不能为空") private String name; @NotBlank(message = "身份证号不能为空") @Pattern(regexp = "^\\d{17}[\\dXx]$", message = "身份证格式不正确") private String idCard; @NotBlank(message = "手机号不能为空") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phone; @NotBlank(message = "当前住址不能为空") private String currentAddress; }2.3 实现带业务语义的 REST 接口与统一响应体
避免直接return personRepository.findAll(),而是封装为Result<T>统一结构:
// src/main/java/com/example/covid/common/Result.java @Data @AllArgsConstructor @NoArgsConstructor public class Result<T> { private Integer code; // 200 成功,400 参数错误,500 服务器异常 private String msg; private T data; public static <T> Result<T> success(T data) { return new Result<>(200, "操作成功", data); } public static <T> Result<T> fail(String msg) { return new Result<>(500, msg, null); } }Controller 层使用@Valid校验 DTO,并捕获全局异常:
// src/main/java/com/example/covid/controller/PersonController.java @RestController @RequestMapping("/api/person") @Slf4j public class PersonController { @Autowired private PersonService personService; @PostMapping public Result<PersonVO> create(@Valid @RequestBody PersonDTO dto) { try { PersonVO vo = personService.create(dto); return Result.success(vo); } catch (Exception e) { log.error("创建人员失败", e); return Result.fail("系统繁忙,请稍后重试"); } } @GetMapping("/list") public Result<List<PersonVO>> list() { return Result.success(personService.listAll()); } }注意:
@Valid触发校验失败时,默认返回400 Bad Request和原始错误信息(如Field error in object 'personDTO' on field 'idCard': rejected value [123]; codes [Pattern.personDTO.idCard,Pattern.idCard,Pattern.java.lang.String,Pattern]; arguments [org.springframework.validation.beanvalidation.SpringValidatorAdapter$SpringConstraintValidatorContext@3a7e6b2c,]; default message [身份证格式不正确])。这不利于前端解析。需配置全局异常处理器统一转换:
// src/main/java/com/example/covid/exception/GlobalExceptionHandler.java @ControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public Result<String> handleValidationExceptions(MethodArgumentNotValidException ex) { String defaultMessage = ex.getBindingResult().getFieldError().getDefaultMessage(); return Result.fail("参数错误:" + defaultMessage); } }3. Vue 3 前端工程搭建与疫情防控业务组件化实现
Vue 部分不能停留在vue create后改App.vue。疫情防控系统的交互复杂度远超普通后台,需从路由设计、状态管理、API 封装、权限控制四个维度构建可维护结构。我们采用 Vue 3.4.21 + Vite 5.2.12 + Pinia 2.1.7 + Element Plus 2.3.12(兼容 Vue 3)组合,避开 Vue 2 的 Options API 陷阱。
3.1 Vite 项目初始化与关键配置项说明
执行命令初始化:
npm create vue@latest # 选择:✔ Project name: … covid-vue # ✔ Add TypeScript? … No / Yes → 选 Yes(类型安全对疫情数据字段至关重要) # ✔ Add JSX Support? … No # ✔ Add Vue Router for Single Page Application routing? … Yes(必须) # ✔ Add Pinia for state management? … Yes(替代 Vuex,更轻量) # ✔ Add Vitest for Unit Testing? … No(毕设阶段可跳过) # ✔ Add an ESLint configuration? … Yes(推荐 Prettier + Standard) # ✔ Add Cypress for End-to-End testing? … No生成后,修改vite.config.ts关键配置:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, 'src') // 支持 @/components 导入 } }, server: { port: 8080, open: true, proxy: { '/api': { target: 'http://localhost:8081', // Spring Boot 默认端口 changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') // 去掉 /api 前缀,后端无需处理 } } } })提示:
proxy配置仅在开发环境生效。生产环境需通过 Nginx 反向代理,因此vite.config.ts中的 proxy 不会影响最终打包产物。务必在vue.config.js(若使用 Vue CLI)或vite.config.ts中明确区分 dev/prod 行为。
3.2 基于 Vue Router 的权限路由守卫与动态菜单生成
疫情防控系统需按角色展示不同菜单:管理员看到「人员管理」「审核中心」「风险区域维护」;网格员只看到「我的辖区」「核酸上报」;居民只能查看「个人健康码」「核酸记录」。路由结构设计如下:
// src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' import Layout from '@/layout/index.vue' // 通用布局组件 import Login from '@/views/Login.vue' import Home from '@/views/Home.vue' import PersonList from '@/views/person/PersonList.vue' import ReviewList from '@/views/review/ReviewList.vue' import RiskArea from '@/views/risk/RiskArea.vue' const routes = [ { path: '/login', name: 'Login', component: Login, meta: { requiresAuth: false } // 不需要登录 }, { path: '/', name: 'Layout', component: Layout, meta: { requiresAuth: true }, // 需要登录 children: [ { path: '', redirect: '/home' }, { path: 'home', name: 'Home', component: Home, meta: { title: '首页' } }, { path: 'person', name: 'PersonList', component: PersonList, meta: { title: '人员管理', roles: ['admin', 'grid'] } }, { path: 'review', name: 'ReviewList', component: ReviewList, meta: { title: '审核中心', roles: ['admin'] } }, { path: 'risk', name: 'RiskArea', component: RiskArea, meta: { title: '风险区域', roles: ['admin'] } } ] } ] const router = createRouter({ history: createWebHistory(), routes }) // 全局前置守卫:检查登录态 & 权限 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.requiresAuth && token) { const userRoles = JSON.parse(localStorage.getItem('user') || '{}').roles || [] if (to.meta.roles && !to.meta.roles.some(role => userRoles.includes(role))) { next('/403') // 无权限页面 } else { next() } } else { next() } }) export default router3.3 使用 Pinia 管理全局状态与 Axios 请求封装
创建src/stores/user.ts存储用户信息与 token:
// src/stores/user.ts import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('user') || '{}'), roles: [] as string[] }), actions: { setToken(token: string) { this.token = token localStorage.setItem('token', token) }, setUserInfo(userInfo: any) { this.userInfo = userInfo this.roles = userInfo.roles || [] localStorage.setItem('user', JSON.stringify(userInfo)) }, clear() { this.token = '' this.userInfo = {} this.roles = [] localStorage.removeItem('token') localStorage.removeItem('user') } } })封装src/utils/request.ts,集成请求拦截、响应拦截、错误统一处理:
// src/utils/request.ts import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' const request = axios.create({ baseURL: '/api', // 与 vite.config.ts 中 proxy 保持一致 timeout: 10000 }) // 请求拦截:添加 token request.interceptors.request.use( config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }, error => Promise.reject(error) ) // 响应拦截:统一处理业务错误码 request.interceptors.response.use( response => { const { code, msg, data } = response.data if (code === 200) { return data } else { ElMessage.error(msg || '请求失败') return Promise.reject(new Error(msg)) } }, error => { if (error.response?.status === 401) { const userStore = useUserStore() userStore.clear() window.location.href = '/login' } else if (error.response?.status === 403) { ElMessage.error('权限不足') } else { ElMessage.error('网络错误,请检查连接') } return Promise.reject(error) } ) export default request注意:
baseURL: '/api'是关键。它让所有request.get('/person/list')实际请求http://localhost:8080/api/person/list,而 Vite 的 proxy 会将/api/**转发到http://localhost:8081/**。这样前端代码无需硬编码后端地址,也避免了 CORS 问题。
3.4 实现「核酸记录上报」业务组件与表单校验联动
以src/views/test/TestUpload.vue为例,展示如何用 Composition API + Element Plus 实现带文件上传、日期校验、地理位置选择的复杂表单:
<!-- src/views/test/TestUpload.vue --> <template> <el-card> <template #header> <div class="card-header">核酸记录上报</div> </template> <el-form :model="form" :rules="rules" ref="formRef" label-width="120px"> <el-form-item label="检测时间" prop="testTime"> <el-date-picker v-model="form.testTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择检测时间" /> </el-form-item> <el-form-item label="检测机构" prop="institution"> <el-input v-model="form.institution" placeholder="请输入检测机构名称" /> </el-form-item> <el-form-item label="检测结果" prop="result"> <el-select v-model="form.result" placeholder="请选择结果"> <el-option label="阴性" value="negative" /> <el-option label="阳性" value="positive" /> <el-option label="待检测" value="pending" /> </el-select> </el-form-item> <el-form-item label="上传报告" prop="reportFile"> <el-upload class="upload-demo" action="#" :http-request="handleUpload" :show-file-list="false" :limit="1" > <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip">仅支持 JPG/PNG/PDF,大小不超过 5MB</div> </el-upload> <div v-if="form.reportUrl" class="mt-2"> <el-link :href="form.reportUrl" target="_blank">已上传:{{ form.reportName }}</el-link> </div> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm">提交</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> </el-card> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import { ElMessage } from 'element-plus' import request from '@/utils/request' const formRef = ref() const form = reactive({ testTime: '', institution: '', result: '', reportUrl: '', reportName: '' }) const rules = { testTime: [{ required: true, message: '请选择检测时间', trigger: 'change' }], institution: [{ required: true, message: '请输入检测机构', trigger: 'blur' }], result: [{ required: true, message: '请选择检测结果', trigger: 'change' }] } const handleUpload = (params: any) => { const file = params.file const reader = new FileReader() reader.onload = (e) => { const base64 = e.target?.result as string form.reportUrl = base64 form.reportName = file.name } reader.readAsDataURL(file) } const submitForm = () => { formRef.value.validate((valid: boolean) => { if (valid) { request.post('/test/upload', form) .then(() => { ElMessage.success('上报成功') resetForm() }) .catch(() => { ElMessage.error('上报失败') }) } }) } const resetForm = () => { form.testTime = '' form.institution = '' form.result = '' form.reportUrl = '' form.reportName = '' } </script>提示:此处
handleUpload使用FileReader读取本地文件转 Base64,适用于小文件(<5MB)。生产环境应对接后端文件上传接口,返回 URL 后存入form.reportUrl。request.post('/test/upload', form)中form包含reportUrl(Base64 字符串),后端需解码并保存为文件。
4. 前后端联调关键路径与 Spring Boot + Vue 部署实操
完成单侧开发只是开始,真正的难点在于联调时的请求链路打通、跨域策略适配、静态资源路径修正、以及生产环境部署的路径一致性。很多“毕设源码”跑不起来,根源不在代码,而在这些工程细节。
4.1 联调必查的 5 个断点与验证命令
| 断点位置 | 验证方式 | 常见失败现象 | 解决方案 |
|---|---|---|---|
| 后端接口是否可达 | curl -X GET http://localhost:8081/api/person/list | Connection refused | 检查 Spring Boot 是否启动(Application started in X.XXX seconds日志)、端口是否被占用(netstat -ano | findstr :8081) |
| 前端代理是否生效 | 浏览器 Network Tab 查看/api/person/list请求 URL | 显示http://localhost:8080/api/person/list(未代理) | 检查vite.config.ts中server.proxy配置是否正确,重启 Vite 服务 |
| JWT Token 是否携带 | Network Tab → Headers → Authorization | 无Authorization: Bearer xxx | 检查request.interceptors.request.use是否执行,localStorage.getItem('token')是否存在 |
| 后端返回是否为 Result 结构 | Network Tab → Response | 返回原始 JSON(非{code:200,msg:"ok",data:[...]}) | 检查 Controller 方法是否返回Result.success(...),而非直接return list |
| Vue 路由是否匹配 | 浏览器地址栏输入/person | 显示空白页或 404 | 检查router.beforeEach是否阻止跳转,PersonList.vue文件路径是否与routes中component路径一致 |
4.2 生产环境部署:Nginx 配置与 Spring Boot 静态资源路径修正
开发时 Vue 通过vite preview或npm run serve运行,Spring Boot 独立运行。生产环境需合并部署,常见两种模式:
模式一:Nginx 反向代理(推荐)
Vue 打包产物放 Nginxhtml/目录,Spring Boot 仍运行在 8081 端口,Nginx 将/api/**转发给 Spring Boot,其他请求返回 Vue 静态文件。模式二:Spring Boot 内嵌 Vue(简化运维)
将npm run build生成的dist/目录复制到 Spring Bootsrc/main/resources/static/,Spring Boot 启动后自动托管http://localhost:8081/。
我们采用模式一,因其更符合前后端分离原则,且便于独立升级。
Nginx 配置 (/etc/nginx/conf.d/covid.conf):
upstream backend { server 127.0.0.1:8081; } server { listen 80; server_name localhost; # Vue 静态资源 location / { root /var/www/covid-vue/dist; try_files $uri $uri/ /index.html; } # API 接口转发 location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 防止 favicon.ico 404 location = /favicon.ico { log_not_found off; access_log off; } }注意:
try_files $uri $uri/ /index.html;是关键。它确保 Vue Router 的 History 模式在刷新页面时不会返回 404。例如访问/person时,Nginx 先找dist/person/index.html(不存在),再找dist/person(不存在),最后 fallback 到dist/index.html,由 Vue Router 解析路由。
Spring Boot 需关闭默认静态资源路径冲突(如果启用内嵌模式):
# application-prod.yml spring: web: resources: static-locations: classpath:/static/ mvc: static-path-pattern: /static/**但模式一无需修改 Spring Boot,只需确保其application.yml中server.port=8081且不暴露/static/**路径。
4.3 Vue 打包后路径问题的终极解决方案:public 目录与 BASE_URL
npm run build后,Vue 默认生成相对路径引用(如<script src="/assets/index-xxx.js">)。若部署在子路径(如http://example.com/covid/),需配置BASE_URL。
在vite.config.ts中设置:
export default defineConfig({ base: '/covid/', // 对应 Nginx location /covid/ // ... 其他配置 })同时,在index.html中确保:
<head> <base href="/covid/"> </head>提示:
base配置必须与 Nginx 的location路径完全一致。若 Nginx 配置location /covid/ { root /var/www/covid-vue/dist; },则base必须为/covid/。否则 CSS/JS 加载 404。
5. 疫情防控业务特性的深度落地:风险等级动态计算与多级审核状态机
毕业设计常止步于“增删改查”,但真实疫情防控系统的核心价值在于业务规则引擎的嵌入。Spring Boot + Vue 的组合,恰恰提供了在后端做复杂计算、前端做状态可视化的能力。我们以“人员风险等级动态计算”和“审核任务状态流转”为例,展示如何让系统真正“活”起来。
5.1 基于地理围栏的风险等级计算:Spring Boot 实现
风险等级(低风险/中风险/高风险)不存储在Person表中,而是根据currentAddress实时匹配RiskArea表中的areaName和level字段。为提升性能,使用ConcurrentHashMap缓存风险区域数据(避免每次查询 DB):
// src/main/java/com/example/covid/service/RiskAreaService.java @Service @Slf4j public class RiskAreaService { @Autowired private RiskAreaRepository riskAreaRepository; // 缓存:areaName -> level private final Map<String, String> areaLevelCache = new ConcurrentHashMap<>(); @PostConstruct public void initCache() { List<RiskArea> areas = riskAreaRepository.findAll(); areas.forEach(area -> areaLevelCache.put(area.getAreaName(), area.getLevel())); log.info("风险区域缓存加载完成,共 {} 条", areaLevelCache.size()); } public String getRiskLevelByAddress(String address) { // 简化版:精确匹配区域名(实际可用 GeoHash 或行政区划树) return areaLevelCache.getOrDefault(address, "低风险"); } }PersonService中调用:
// src/main/java/com/example/covid/service/PersonService.java @Service public class PersonService { @Autowired private PersonRepository personRepository; @Autowired private RiskAreaService riskAreaService; public PersonVO create(PersonDTO dto) { Person person = new Person(); BeanUtils.copyProperties(dto, person); person.setCreatedAt(LocalDateTime.now()); Person saved = personRepository.save(person); // 动态计算风险等级 String riskLevel = riskAreaService.getRiskLevelByAddress(dto.getCurrentAddress()); PersonVO vo = new PersonVO(); vo.setId(saved.getId()); vo.setName(saved.getName()); vo.setPhone(saved.getPhone()); vo.setCurrentAddress(saved.getCurrentAddress()); vo.setRiskLevel(riskLevel); // 不存库,只返回 vo.setCreatedAt(saved.getCreatedAt()); return vo; } }5.2 多级审核状态机:用枚举 + 状态流转表驱动业务
审核任务(ReviewTask)需经历:DRAFT(草稿)→SUBMITTED(已提交)→REVIEWING(审核中)→APPROVED(通过)或REJECTED(驳回)。硬编码if-else易出错,改用状态机模式:
// src/main/java/com/example/covid/enums/ReviewStatus.java public enum ReviewStatus { DRAFT("草稿"), SUBMITTED("已提交"), REVIEWING("审核中"), APPROVED("已通过"), REJECTED("已驳回"); private final String desc; ReviewStatus(String desc) { this.desc = desc; } public String getDesc() { return desc; } }定义合法状态流转规则(防止非法跳转):
// src/main/java/com/example/covid/config/ReviewStateMachine.java @Component public class ReviewStateMachine { // key: 当前状态, value: 允许跳转到的状态列表 private static final Map<ReviewStatus, Set<ReviewStatus>> TRANSITIONS = Map.of( ReviewStatus.DRAFT, Set.of(ReviewStatus.SUBMITTED), ReviewStatus.SUBMITTED, Set.of(ReviewStatus.REVIEWING), ReviewStatus.REVIEWING, Set.of(ReviewStatus.APPROVED, ReviewStatus.REJECTED) ); public static boolean canTransition(ReviewStatus from, ReviewStatus to) { return TRANSITIONS.getOrDefault(from, Collections.emptySet()).contains(to); } }Controller 中校验:
@PostMapping("/review/{id}/approve") public Result<String> approve(@PathVariable Long id) { ReviewTask task = reviewTaskRepository.findById(id).orElseThrow(); if (!ReviewStateMachine.canTransition(task.getStatus(), ReviewStatus.APPROVED)) { return Result.fail("当前状态不允许通过操作"); } task.setStatus(ReviewStatus.APPROVED); task.setApprovedAt(LocalDateTime.now()); reviewTaskRepository.save(task); return Result.success("审核通过"); }5.3 Vue 前端状态可视化:使用 ElTag 与状态颜色映射表
在ReviewList.vue中,将状态码转为带颜色的标签:
<template> <el-table :data="tableData"> <!-- ... --> <el-table-column prop="status" label="状态" width="120"> <template #default="{ row }"> <el-tag :type="getStatusType(row.status)" disable-transitions> {{ getStatusText(row.status) }} </el-tag> </template> </el-table-column> </el-table> </template> <script setup lang="ts"> const getStatusText = (status: string) => { const map: Record<string, string> = { DRAFT: '草稿', SUBMITTED: '已提交', REVIEWING: '审核中', APPROVED: '已通过', REJECTED: '已驳回' } return map[status] || status } const getStatusType = (status: string) => { const map: Record<string, string> = { DRAFT: 'info', SUBMITTED: 'warning', REVIEWING: 'primary', APPROVED: 'success', REJECTED: 'danger' } return map[status] || 'info' } </script>提示:状态文本与颜色映射应与后端
ReviewStatus枚举严格一致。前端不应自行定义新状态,所有状态变更必须经后端 API 触发并返回最新状态,保证数据源头唯一。
本文还有配套的精品资源,点击获取