资讯动态

基于SSM+Vue的工单管理系统:从部署实战到二次开发

发布时间:2026/10/19 14:34:48 来源:尧图企业网站定制
简介基于 Java SSM 框架Spring、SpringMVC、MyBatis与 Vue.js、MySQL 8.0 构建的工单管理系统完整毕业设计源码包面向需完成毕业设计、课程设计或期末大作业的计算机相关专业学生可直接运行无需改动。项目已通过导师指导并获得高分评价前后端代码、数据库脚本与技术工具一应俱全。压缩包共含 1001 个文件以 Java 业务逻辑代码104 个、JSP 及 HTML 页面53 个 25 个、CSS 样式125 个、JS 脚本242 个、SQL 数据库脚本2 个为主体另含 PNG、GIF、XML 等辅助文件整包仅 10.46MB。系统涵盖工单创建、派发、处理、关闭的完整流程前端配置多套主题样式与动态图表展示管理便捷、功能齐全。目前已有 177 人浏览学习对需快速掌握 SSM 与 Vue 整合开发或完成类似管理系统的同学具有较高参考价值。1. 工单管理系统是什么毕业设计项目能跑通就能上手改某公司的客服部之前处理研发支持请求靠的是微信群接单加 Excel 登记。群里消息一刷工单停在谁那里、处理到哪一步、有没有超时全靠人工翻聊天记录。后来换上了这套基于 Java SSM Vue MySQL 的工单管理系统每一条请求都变成一张有编号、有状态、有操作日志的工单创建、派单、处理、归档、统计走完整个闭环。作为一份高分毕业设计交付包它的源码、数据库脚本、前端页面和部署配置都打包在一起下载后导入数据库、改一下连接配置、部署到 Tomcat 就能跑不需要再补功能。适合两类人一类是做课程设计或期末大作业、需要一个完整可演示项目的学生另一类是想拆一份真实 SSM Vue 全栈工程、搞清楚前后端数据流动逻辑的开发者。项目不花哨但覆盖了管理系统最典型的功能骨架。2. 技术选型与工程结构SSMVue的分工边界在哪2.1 为什么这套组合在毕设和课设里最稳先讲清楚技术栈分工。Spring 负责 Bean 管理、事务和 AOPSpringMVC 负责路由、参数绑定和返回值处理MyBatis 把 SQL 独立到 XML 文件Java 方法只调用接口不手写 JDBC 连接。前端用 Vue 接管页面数据绑定和组件交互通过 axios 调用后端接口管理后台的视觉风格则由 Bootstrap、Layui 这类成熟样式库撑起来。我拆开资源包后看到的 pink-violet.css、yellow-green.css、bootstrap.min.css、layui.css、animate.css、ueditor.css 正好印证了这一点——界面不是从零手搓 CSS而是基于现成后台模板二次拼装。这个选择蛮聪明外观经得起截图代码结构又不会像框架项目那样包了一层又一层自动配置答辩时容易展开讲。数据库用的是 MySQL 8.0开发工具是 IDEA构建走 Maven部署环境是 Tomcat 7.x 或 8.x。有人会质疑现在都是 Spring Boot 的天下了为什么还抱着 SSM 不放这个问题的回答本身就是这类项目的价值。SSM 的配置全部是显式 XML一个请求从 DispatcherServlet 进 Controller、再进 Service、再进 Mapper每一步都能在配置文件里找到对应关系。答辩被问到一个请求怎么走通的直接打开 web.xml 和 spring-mvc.xml 就能顺着链路讲完全不是黑匣子。Spring Boot 自动配置虽然省事但对学生来说很容易变成加个注解就能跑具体发生了什么说不清。高校 Java Web 课程多数还停留在 Servlet/JSP 或 SSM 阶段用这套技术栈做出来的项目跟课堂教学衔接也最自然。2.2 工程目录后端分层和前端资源放在哪拆解打包好的工程通常会看到 Maven 单工程结构后端在 src/main/java 下按 controller、service、mapper、entity 分包前端源码放在 frontend 目录构建后的静态资源复制进 webapp/static。这种结构的好处是一份工程里既能改 Vue 源码又不需要在运行环境单独起 Node 服务Tomcat 直接托管编译后的前端文件。workorder-system/ ├── pom.xml ├── sql/ │ ├── workorder_db.sql │ └── init_data.sql ├── frontend/ │ ├── src/ │ │ ├── views/ # 页面创建工单、工单列表、处理页、统计看板 │ │ ├── components/ # 表格、状态标签、派单弹窗 │ │ ├── api/ # axios 接口统一封装 │ │ ├── router/ # 前端路由 │ │ └── utils/ ├── src/main/java/com/example/workorder/ │ ├── controller/ # 接收请求返回统一 Result │ ├── service/ # 业务规则编号生成、状态流转 │ ├── mapper/ # MyBatis 接口方法 │ └── entity/ # 数据库表对应的实体类 ├── src/main/resources/ │ ├── mapper/ # Mapper XMLSQL 集中管理 │ ├── jdbc.properties │ ├── spring-mvc.xml │ ├── spring-mybatis.xml │ └── web.xml └── src/main/webapp/ ├── static/ │ ├── css/ │ ├── js/ │ └── images/ └── WEB-INF/web.xml目录横跨三层前端 Vue 源码只在开发时用改完前端要执行 npm run build把产物同步到 webapp/static 下后端才能访问到controller 层不写业务逻辑状态流转和编号生成都在 serviceresources 下的 Mapper XML 跟 Java 接口同名MyBatis 启动时按包名自动扫描绑定。如果启动后报找不到 Mapper 方法先检查 XML 文件是否真的被编译进 target/classes再看 namespace 是否跟接口全限定名一致。2.3 配置层数据源、事务和静态资源的三个关键点配置里最容易出问题的是数据源。jdbc.properties 的常见配置是四件套driver、url、username、password。MySQL 8.0 下driver 必须写 com.mysql.cj.jdbc.Driverurl 里要带 serverTimezone 和 useSSL 参数否则启动时连接数据库大概率抛时区异常或 SSL 警告。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/workorder?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password123456这段配置里useSSLfalse 是关闭连接加密本机调试能省掉证书配置的麻烦serverTimezoneAsia/Shanghai 是为 JDBC 指定时区MySQL 时区如果不是标准的 CST不写这个参数会直接连接失败characterEncodingutf8 保证中文不乱码。对应的 spring-mybatis.xml 里用扫包方式注册 Mapper 接口再配置事务管理器把 create、changeStatus 这类写操作纳入事务避免工单主表插入成功、日志表插入失败导致数据不一致。SpringMVC 配置里的第二件大事是静态资源放行。如果 web.xml 中 DispatcherServlet 的 url-pattern 写成 /它会接管所有请求static 下的 css 和 js 也会进 Controller页面样式会全部丢失。解决方式是在 spring-mvc.xml 里开启默认 Servlet 处理具体配置放在第五章排查部分展开。第三件大事是事务粒度SSM 里通常用 DataSourceTransactionManager 配合 tx:advice 声明式事务把插入工单 插入日志绑定成一个原子操作单独 commit 其中任何一步都会留下脏数据。3. 核心模块拆解一张工单从创建到归档的完整链路3.1 前端提交表单数据怎么组装和后端对齐创建工单是系统的入口。页面上用户选择工单类型、填写标题、优先级和描述Vue 组件里的 data 维护一个 form 对象字段名跟后端实体严格对应。点击提交时先做前端校验再通过 axios 发 POST 请求代码如下// frontend/src/views/workorder/CreateWorkOrder.vue结构简化 createWorkOrder() { if (!this.form.title || !this.form.typeId) { this.$message.warning(标题和工单类型必填); return; } axios.post(/api/workOrder/create, { title: this.form.title, typeId: this.form.typeId, priority: this.form.priority, description: this.form.description, creatorId: this.currentUser.id }).then(res { if (res.data.code 200) { this.$router.push(/workOrder/list); } else { this.$message.error(res.data.msg); } }); }这个请求里title 是工单标题typeId 对应工单类型表的主键priority 用 1/2/3 表示低/中/高description 是富文本内容creatorId 是当前登录用户的 ID。接口前缀统一是 /api/workOrder跟前端页面路由区分开后面要做权限拦截时也更方便。前端校验只做非空检查后端 Service 还会再做一遍参数校验两层校验缺一不可——直接绕过页面用 Postman 调接口的情况经常发生。3.2 后端接收Controller 只做参数接收和结果包装Controller 层的任务是把请求参数转给 Service再把返回结果包装成统一格式。常见写法是这样的Controller RequestMapping(/api/workOrder) public class WorkOrderController { Autowired private WorkOrderService workOrderService; ResponseBody RequestMapping(value /create, method RequestMethod.POST) public Result create(RequestBody WorkOrder workOrder) { try { workOrderService.create(workOrder); return Result.success(创建成功); } catch (Exception e) { return Result.error(e.getMessage()); } } }注意 RequestBody 表示请求体是 JSONSpringMVC 会按字段名自动映射到 WorkOrder 实体。如果前端传的参数名跟实体字段对不上这里不会报错只是实体对应字段是 null等插库的时候才会暴露。所以我一般建议前后端字段名保持完全一致省去中间转换层。3.3 Service 层工单编号和初始状态的生成规则工单编号一般用WO 年月日 四位自增序列的规则同一分钟内创建多条也不会撞号。Service 的 create 方法里做了四件事补全编号、补全初始状态、补全社会时间、写入一条创建日志。public void create(WorkOrder workOrder) { // 1. 按当天已有工单数生成序列号 Integer todayCount workOrderMapper.selectTodayCount(new Date()); String orderNo WO DateUtils.format(new Date(), yyyyMMdd) String.format(%04d, todayCount 1); workOrder.setOrderNo(orderNo); // 2. 初始状态固定为 1待派单 workOrder.setStatus(1); workOrder.setCreateTime(new Date()); workOrder.setUpdateTime(new Date()); workOrderMapper.insert(workOrder); // 3. 记录日志 workOrderLogMapper.insertLog(workOrder.getId(), workOrder.getCreatorId(), 创建工单 workOrder.getTitle()); }selectTodayCount 是按日期统计当天已创建的工单数而不是查全表最大 ID 再加一这样即使用户删了最后一条工单编号也不会复用。状态用整数 1/2/3/4/5 存储分别对应待派单、已派单、处理中、已完成、已退回。这种 tinyint 字段比 varchar 省空间排序和分组更快Java 侧用一个枚举类就能对应过来。日志记录是唯一只增不改的表答辩时讲数据一致性这段是最能体现工程经验的地方。3.4 列表查询分页、状态筛选和关键字搜索的组合工单列表是系统最高频的页面管理员每天都要在这里看待派单有几条、处理中有几条、总共有几条。分页用 PageHelper 插件状态筛选和关键字搜索靠 MyBatis 动态 SQL 拼装select idselectPage resultTypeWorkOrder SELECT id, order_no, title, priority, status, creator_id, handler_id, create_time, update_time FROM work_order where if teststatus ! null and status ! AND status #{status} /if if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR order_no LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY create_time DESC /select这里 status 传空就看全部传 2 就只看已派单keyword 支持按单号和标题模糊搜索。注意列表 SQL 没有把 description 字段查出来因为富文本内容体积大列表根本用不到等进详情页再单独查这个习惯能明显降低列表接口的响应时间。ORDER BY create_time DESC 必须有否则分页后每一页的数据顺序会不稳定翻页时出现重复或漏数据非常难排查。3.5 状态流转派单、处理、退回由谁在改状态处理工单时最常见的操作是管理员把待派单的工单指派给处理人处理人开始处理处理完成提交管理员确认关闭。中间可能还有退回。状态流转我一般收敛到 Service 的一个方法里统一做前置状态校验public void changeStatus(Integer orderId, Integer fromStatus, Integer toStatus, Integer operatorId, String remark) { WorkOrder order workOrderMapper.selectById(orderId); if (order null) { throw new RuntimeException(工单不存在); } // 校验数据库当前状态和前端传来的前置状态一致 if (order.getStatus() ! fromStatus) { throw new RuntimeException(当前状态不允许该操作); } order.setStatus(toStatus); order.setUpdateTime(new Date()); workOrderMapper.update(order); workOrderLogMapper.insertLog(orderId, operatorId, remark); }有人会问为什么前端要传 fromStatus而不是后端自己判断因为同一张工单可能被多人同时操作管理员点了派单处理人也点了开始处理后提交的请求如果不校验前置状态就可能把工单状态覆盖成错误的值。前端的按钮每次操作只能在一个状态下显示但后端不能信前端必须拿数据库里的当前状态跟请求里的 fromStatus 比对。这个防跳转校验不需要引入状态机框架一个 if 就能挡住大部分误操作是整张表数据不乱的核心。3.6 详情页与日志回显一次请求带回关联数据处理工单时处理人要看到工单基本信息和历史处理记录。历史记录来自 work_order_log 表前端进入详情页时同时请求详情和日志。常见的做法是后端在查询详情的接口里把日志列表直接塞进返回对象减少一次网络往返loadDetail() { axios.get(/api/workOrder/detail, { params: { id: this.orderId } }).then(res { this.order res.data.data; this.orderLogs res.data.data.logs; }); }后端组装 WorkOrder 对象时用一个关联查询把 logs 字段填上。这里有个细节详情 SQL 必须把 description 查出来列表 SQL 不查两者的 select 字段不一样所以要写两个 Mapper 方法不能共用一个查询。如果你发现详情页富文本内容空白大概率是共用了列表查询把 description 漏掉了。4. 数据库设计拆解表结构、状态编码与索引约定4.1 工单主表字段取舍与建表要点工单主表是整套系统的核心字段设计要能支撑创建、列表、派单、处理、统计这五个页面。我按最常见的设计整理出关键字段建表脚本如下CREATE TABLE work_order ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 工单编号, title varchar(200) NOT NULL COMMENT 工单标题, type_id int NOT NULL COMMENT 工单类型ID, priority tinyint NOT NULL DEFAULT 2 COMMENT 优先级 1低 2中 3高, status tinyint NOT NULL DEFAULT 1 COMMENT 状态 1待派单 2已派单 3处理中 4已完成 5已退回, creator_id int NOT NULL COMMENT 创建人ID, handler_id int DEFAULT NULL COMMENT 处理人ID, description text COMMENT 工单描述, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_status_create_time (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工单表;这里有三个细节值得说。order_no 建唯一索引业务上保证单号不重复idx_status_create_time 是联合索引因为列表页最常见的查询是按状态筛选 按创建时间倒序这个联合索引能直接覆盖不用回表查文件排序description 用 text 类型存富文本但列表页不要 select *否则每条工单都要多传几 KB 的文字内容网络开销翻好几倍。4.2 用户表与日志表权限和追踪怎么落地用户表负责登录和权限识别密码不能用明文存储。通常结构是下面这样CREATE TABLE sys_user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(64) NOT NULL COMMENT 加密后的密码, real_name varchar(50) DEFAULT NULL, role tinyint NOT NULL DEFAULT 2 COMMENT 1管理员 2普通用户 3处理人, status tinyint NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;username 唯一索引保证账号不重复role 字段把用户分成管理员、普通用户、处理人三类。密码建议用 MD5 加盐或更安全的散列算法直接用明文存在数据库一旦数据库文件泄露所有账号都会暴露。日志表记录每一次状态变化和关键操作字段是 id、work_order_id、operator_id、remark、create_time。这张表只增不改不删是审计追踪的唯一来源。工单类型表更简单id、name、sort 三列足够后续想加故障类型需求类型直接在管理页面里插数据不用动代码。4.3 表关系为什么不做物理外键表与表之间的关系是工单表通过 creator_id 和 handler_id 指向用户表通过 type_id 指向类型表日志表通过 work_order_id 指向工单表。但我没有建物理外键原因是业务系统里物理外键会让插入删除变得很重而且用户一旦被删除工单历史会跟着出问题。更稳的做法是只建普通索引由 Service 层在删除前先检查是否有工单引用。这个选择跟生产环境里很多系统的做法一致逻辑外键保证灵活物理外键留给强一致场景。4.4 统计报表一条 SQL 撑起看板看板页最常见的是各状态工单数和本周新增趋势两个统计。前者直接按状态分组SELECT status, COUNT(*) AS cnt FROM work_order GROUP BY status;后端拿到结果是 {1: 3, 2: 5, 3: 2, 4: 7}映射到前端就是四个状态卡片。日期趋势统计用 DATE_FORMAT 按天聚合SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS cnt FROM work_order WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY day;这类 SQL 写清楚后看板页不接图表库也能用表格展示。如果要换成柱状图或折线图把查询结果转成图表的 categories 和 data 数组即可。统计接口要单独缓存因为列表每翻一页就会查一次没必要让看板的聚合查询也跟着跑。4.5 实体映射配置驼峰转换是无声的坑Java 实体类字段用驼峰命名数据库字段用下划线命名比如 createTime 对应 create_time。如果 mybatis-config.xml 没有开启驼峰映射MyBatis 默认不会自动转换查询结果里这些字段会全是 null而不是报错排查起来非常头疼。正确的配置是加一行设置settings setting namemapUnderscoreToCamelCase valuetrue/ /settings开启后resultType 直接写实体类就行不用为每张表手写 resultMap。时间字段类型要注意Java 侧用 Date前端拿到的是毫秒时间戳需要在 axios 响应拦截器统一格式化或者后端直接返回格式化后的字符串。数据库统一用 datetime 而不是 timestamp这样时区和 2038 年的坑都能避开。5. 部署运行与踩坑排查从拿到包到浏览器看到登录页5.1 本地环境配置和启动步骤拿到源码后的部署步骤很固定我按 Windows IDEA 的场景走一遍。先装 JDK8、Maven、MySQL 8.0、Navicat、Tomcat 8。环境变量要确认JAVA_HOME 指向 JDK 目录MAVEN_HOME 指向 Maven 目录并且 PATH 里加入 bin。Tomcat 不用配环境变量IDEA 里能指定路径就行。接下来用 Navicat 新建一个 utf8mb4 的数据库然后在查询窗口运行 sql 目录下的 workorder_db.sql 和 init_data.sql导入表结构和初始账号数据。导入完成后检查 jdbc.properties# src/main/resources/jdbc.properties jdbc.urljdbc:mysql://localhost:3306/workorder?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password改成你的数据库密码确认无误后在 IDEA 里右键 pom.xml选 Maven - Reload Project再执行 clean package。如果配置正确target 目录下会出现一个 war 包。把 war 包复制到 Tomcat 的 webapps 目录双击 startup.bat 启动看到日志输出 Server startup in xxx ms 就说明启动成功。访问路径是 http://localhost:8080/war包名/ war 包名过长可以直接改名但要注意改完后 URL 跟着变。5.2 高频问题一数据库连接失败和时区异常现象Tomcat 能启动但登录后任意一个查询接口都报 500后台日志出现 Communications link failure 或 The server time zone value。原因MySQL 8.0 换了新版 JDBC 驱动同时服务器时区跟 JDBC 默认时区不一致导致连接建立失败。这是新手在环境配置阶段遇到最多的报错跟代码本身没关系。解决先把 jdbc.properties 里的 driver 改成 com.mysql.cj.jdbc.Driver再确认 url 末尾带了 serverTimezoneAsia/Shanghai 和 useSSLfalse。然后打开 Navicat用同一个账号密码手动连一次数据库能连上说明 URL 参数有问题连不上说明密码或账号配置错误。这一段排查顺序很重要不要一上来就去翻代码先确认数据库连接链路是不是通的。5.3 高频问题二端口占用和 Tomcat 启动不了现象双击 startup.bat 后窗口闪一下就关或者后台报 Address already in use: JVM_Bind。原因本机已经有一个服务占了 8080 端口常见的是之前启动过的 Tomcat 没关干净或者装了其他软件占用。解决打开 Tomcat 的 conf/server.xml找到 Connector port8080把它改成 8081 或 8090保存后重启。改完端口后前端接口请求路径里如果写死了 8080要同步改否则页面能打开但所有接口都连不上。排查端口占用时Windows 可以用 netstat -ano | findstr 8080 看到占用进程的 PID再决定是杀进程还是换端口。5.4 高频问题三页面能开但 CSS 和 JS 全部 404现象页面 HTML 能渲染但浏览器 F12 看网络请求static 下的 CSS、JS 全部 404页面裸得没法看。原因web.xml 里 DispatcherServlet 的 url-pattern 映射成 /把所有请求都截进 SpringMVC静态文件找不到对应的 Controller就返回 404 了。这个问题最玄学因为它不报异常只是样式全丢光看后台日志什么都发现不了。解决在 spring-mvc.xml 里加一段静态资源放行配置mvc:default-servlet-handler/ mvc:resources mapping/static/** location/static//加上以后SpringMVC 遇到 /static 开头的请求就直接返回文件不再进业务方法。我排查这类问题时会先看 web.xml 里 DispatcherServlet 的映射范围再去看 spring-mvc.xml 的资源配置这两处配合起来很容易踩坑。5.5 高频问题四Maven 依赖下载慢或加载失败现象Maven Reload 时卡在下载插件或者报 Could not resolve dependenciespom 文件里各种红色波浪线。原因默认中央仓库在国内访问不稳定公司网络或校园网进一步降低了连接速度。解决在 Maven 的 conf/settings.xml 里配置国内镜像仓库配置完后在 IDEA 里重新导入。这个方法对提升项目构建速度立竿见影。如果换了镜像还是报错把本地仓库里下载了一半的文件删掉再重新下载一次避免半截文件影响依赖解析。5.6 完整跑通验证路径启动完成后访问 http://localhost:8080/项目名/ 先看登录页能不能正常渲染。用初始管理员账号登录重点验证五件事第一新建工单提交后列表页出现 WO 开头的编号状态是待派单第二管理员能对待派单执行派单操作工单状态变成已派单第三处理人账号登录后能看到指派给自己的工单修改状态并填写处理说明第四列表页的状态筛选和关键字搜索都能返回正确结果第五退出登录后直接访问内部页面会被拦回登录页。五条走完核心链路就没问题了。如果某一步失败按上面四个高频问题的排查思路回去看配置不要直接改代码。6. 二次开发思路把硬编码状态流转改成配置驱动拿到这套系统后想加需求最常见的改动是增加状态或调整流转规则。比如新增一个已驳回状态或者加一个转办操作。直接用原来的 if-else 改也能跑但当状态多了以后代码会越来越难读每次改动都要把所有分支看一遍。我建议先把硬编码的状态流转改成配置驱动。设计一张 flow_config 表字段是 from_status、to_status、action、role_typeService 每次操作前查一下这条流转是否允许再执行更新// 伪代码根据当前状态和动作查目标状态 FlowConfig cfg flowConfigMapper.selectByAction( order.getStatus(), action); if (cfg null) { throw new RuntimeException(当前状态不允许该操作); } order.setStatus(cfg.getToStatus()); workOrderLogMapper.insertLog(order.getId(), operatorId, action);改造后新增一个状态只需要往表里插记录Java 代码不用动。配合 role_type 字段不同角色可执行的操作也由配置控制比散落的 if-else 清晰得多。另一个常用改造是加批量导出工单按钮。不需要动表结构复用列表查询的查询条件后端加一个导出方法把当前筛选结果用 POI 写成本地文件返回前端收到文件流直接用浏览器下载。这类小改动独立在一个 Controller 方法里不影响原有链路。做这类二次开发时我的习惯是先在草稿纸上把每个状态能执行的动作列成矩阵再改代码。从那以后我每次动工单流转都强制走一遍状态表先行的流程哪怕只是加一个按钮。这个习惯帮我挡住了好几次状态错乱的情况。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑