ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Node.js+Vue+ElementUI的实验室设备管理系统开发实践

基于Node.js+Vue+ElementUI的实验室设备管理系统开发实践 做实验室设备管理这类的内部系统我的经验是别把架子铺得太大。早年接过一个改造需求学校里各个实验室的设备还在靠Excel台账维护借用靠登记本报修靠微信群喊管理员每天光核对设备状态就要花一两个小时。当时的想法很直接做一个能查设备、能走借用流程、能提交报修的小系统把线下那套流程搬到线上。技术选型上没有犹豫太久后端用Node.js前端用VueUI组件库用ElementUI三周左右出了第一版。这个组合做内部管理系统非常顺手尤其是你的核心场景全是“表单表格状态流转”的时候Vue加ElementUI几乎是把轮子直接递到你手里。这篇文章就按当时的实施路径把这个系统的设计思路、落地过程和踩过的坑完整梳理一遍。1. 项目整体设计与技术选型1.1 系统到底要解决什么问题实验室设备管理的业务线看着简单真正拆开之后其实有四五条完整的流程贯穿其中。第一是设备台账每台设备要有档案编号、名称、规格、存放位置、责任人、购置日期、当前状态这些字段一个都不能少。第二是借用流转实验室之间的设备借用往往有跨天甚至跨周的需求需要预约、审批、借出确认、归还登记几个环节。第三是维修报修设备出现故障后要能快速上报管理员接单后要能指派维修人员维修过程要有进度状态修完要登记结果。第四是人员权限学生、授课老师、实验室管理员这三类角色能看的页面和能做的操作完全不一样。如果还是用Excel加微信群的方式推进最大的问题不是记录本身而是状态不透明。设备在谁手里、什么时候该还、哪台设备已经报修三天没人处理这些信息分散在各自的文档和聊天记录里谁也说不清。系统要解决的核心就是“状态集中化”让每一台设备当前处于什么状态、每一次借用走到哪个环节、每一个报修单卡在谁那里打开系统就能看清楚。明白了这个目标数据库设计和接口设计就都有了明确方向。1.2 技术栈选型的真实考量选Node.js而不是Spring Boot核心原因是这个项目的复杂度不需要Spring全家桶。设备借用和报修管理系统说白了就是增删改查加状态流转涉及高并发、分布式事务的概率基本为零。Node.js的Express框架写REST接口非常轻JavaScript语法对前后端团队来说心智负担小JSON数据在前后端之间流转不需要做任何序列化转换这套链路处理业务系统相当顺滑。另外当时接这个项目还有一个客观条件团队里没有专职Java后端但前端工程师都会JavaScript用Node.js让前端同事也能参与到后端开发里人力分配灵活很多。Vue加ElementUI这个组合在内部管理系统的场景里几乎是标准答案。Vue的核心是数据驱动视图表格里的数据变了页面自动更新不需要手动操作DOM。ElementUI则把后台管理系统里高频使用的组件全部封装好了表格、表单、弹窗、分页、日期选择器、步骤条、时间线拿过来就能用样式统一还不用自己调CSS。选型的时候不是没考虑过React加Ant Design但团队当时对Vue更熟而且ElementUI的中文文档和社区案例更丰富遇到问题搜一下基本都有答案。到了这种体量的项目选哪个框架其实差距不大选最顺手的最重要关键是团队成员能在最短时间内把功能铺开。2. 环境准备与工程初始化2.1 Node.js 安装与环境变量配置当时好几台电脑在装环境阶段就卡住了所以这块单独拿出来说。Node.js的安装本身很简单去官网下载LTS版本的安装包Windows系统一路Next就行。但有几个细节值得注意第一版本选择。当时用的是Node.js 16.x的LTS版本不建议装最新的奇数版本即使是官方支持LTS也意味着更稳定很多npm包在老版本上验证过少踩兼容性的坑。第二安装路径。默认会装到C:\Program Files\nodejs安装包会自动把路径写进系统环境变量PATH里所以装完之后理论上在任意终端窗口执行node -v都能看到版本号。这里有一个容易出问题的点如果你之前已经打开过终端窗口装完Node.js之后再窗口里执行命令会报“node不是内部或外部命令”因为环境变量的变更不会自动刷新到已经打开的终端重新开一个新的命令行窗口就好了。第三npm的镜像配置。npm默认指向官方源国内网络环境下拉取依赖速度比较慢。可以在用户目录下找到.npmrc文件添加一行配置把registry改成国内镜像源。这个是纯技术加速手段属于常规操作改完之后执行npm config get registry确认生效。node -v npm -v npm config get registry做完这三步Node.js环境才算真正可用。一个容易忽略的细节是npm的全局安装目录执行npm install -g xxx时包会被安装到Node.js安装目录下的nodejs目录里如果你在安装Node.js时改了安装位置这个全局目录也要手动确认一下。2.2 Vue 工程创建与 npm 依赖安装项目初始化用的是Vue CLI也就是npm install -g vue/cli安装脚手架工具然后执行vue create lab-equipment-manager创建项目。创建过程中CLI会问你选默认配置还是手动配置建议选手动把Router、Vuex一并选上。当时用的是Vue 2ElementUI 2.x版本对应Vue 2如果你的项目是新起炉灶且团队没有历史包袱也可以用Vue 3加Element Plus但本文系统当时是基于Vue 2实现的。项目创建完成之后安装运行依赖npm install element-ui axiosaxios是HTTP库用来调后端接口element-ui提供全部UI组件。这里有一个实操上的建议ElementUI组件库引入可以按需引入也可以全量引入。系统开发初期建议全量引入省去按需配置的麻烦等系统稳定之后再考虑体积优化因为内部系统对首屏加载速度没有太苛刻的要求全量引入更省心。// main.js import Vue from vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import App from ./App.vue Vue.use(ElementUI) new Vue({ render: h h(App) }).$mount(#app)前端项目跑起来之后浏览器里按F12打开开发者工具找一下Vue Devtools插件。Chrome商店搜“Vue Devtools”安装这个插件在调试Vue组件状态时非常有用尤其是排查data数据为什么没更新、computed属性算出来的值是什么之类的问题。2.3 目录结构与代码分层前后端分离项目的目录组织是一个容易被忽视但影响后续维护的工作。当时的前端目录结构大致是这样的src/ api/ # 接口请求封装 assets/ # 静态资源 components/ # 公共组件 router/ # 路由配置 store/ # Vuex状态管理 views/ # 页面组件 device/ # 设备管理 rental/ # 租赁管理 repair/ # 报修管理 login/ # 登录 layout/ # 布局框架后端的目录结构则是server/ app.js # Express入口 routes/ # 路由定义 controllers/ # 业务控制器 services/ # 服务层 models/ # 数据模型 utils/ # 工具函数 config/ # 配置文件前后端分两个目录放彼此依赖通过接口约定维护。接口规划提前用文档列出来字段名、数据类型、错误码都约定好避免开发过程中前后端各改各的。3. 数据库设计与后端接口实现3.1 核心数据表设计这个系统的数据关系不算复杂核心是四张表用户表、设备表、租赁记录表、报修记录表。设备表和租赁记录表之间是一对多关系一台设备可以对应多条租赁记录设备和报修记录也是一对多关系。当时用的MySQL建表时每个字段都加了注释这一步坚持下来后期收益很大。用户表的字段设计如下字段类型说明idint主键自增usernamevarchar(50)登录名唯一passwordvarchar(255)加密后的密码real_namevarchar(50)真实姓名roletinyint角色1学生 2教师 3管理员created_atdatetime创建时间设备表的字段稍微多一点字段类型说明idint主键自增device_novarchar(50)设备编号唯一namevarchar(100)设备名称modelvarchar(100)规格型号locationvarchar(100)存放位置statustinyint状态1空闲 2借出 3维修中 4报废managervarchar(50)责任人created_atdatetime创建时间租赁记录表和报修记录表主要存流程信息比如租赁记录表有设备ID、借用人ID、预计归还时间、实际归还时间、审批状态报修记录表有设备ID、报修人ID、故障描述、处理状态、维修人员、处理结果。两个表都会冗余一份设备名称字段查询列表时不用再关联设备表虽然违反了数据库第三范式的约束但在实际业务查询里能少一次join性能更好内部系统量级也不存在一致性问题这个冗余是可以接受的。3.2 设备接口与分页查询实现后端框架用Express数据库驱动用mysql2连接方式用连接池。写接口的时候遵循一个简单分层routes只做路由映射controller处理请求参数和响应service写业务逻辑。以设备列表接口为例// routes/device.js const express require(express) const router express.Router() const deviceController require(../controllers/device) router.get(/list, deviceController.getDeviceList) router.post(/add, deviceController.addDevice) router.put(/update, deviceController.updateDevice) router.delete(/delete/:id, deviceController.deleteDevice) module.exports router// controllers/device.js const deviceService require(../services/device) exports.getDeviceList async (req, res) { try { const { page 1, pageSize 10, keyword , status } req.query const result await deviceService.getList({ page: parseInt(page), pageSize: parseInt(pageSize), keyword, status }) res.json({ code: 200, data: result }) } catch (e) { res.json({ code: 500, message: e.message }) } }// services/device.js const db require(../models/db) exports.getList async ({ page, pageSize, keyword, status }) { const params [] let where WHERE 11 if (keyword) { where AND (name LIKE ? OR device_no LIKE ?) params.push(%${keyword}%, %${keyword}%) } if (status) { where AND status ? params.push(status) } const offset (page - 1) * pageSize const [rows] await db.query( SELECT * FROM devices ${where} ORDER BY id DESC LIMIT ?, ?, [...params, offset, pageSize] ) const [totalRows] await db.query(SELECT COUNT(*) AS total FROM devices ${where}, params) return { list: rows, total: totalRows[0].total, page, pageSize } }分页查询里有一个细节LIMIT子句中的offset和pageSize不能直接使用LIMIT ${offset}, ${pageSize}字符串拼接虽然数字型参数不会有SQL注入风险但用参数占位符是统一习惯防止其他字段拼接时遗漏。COUNT查询必须跟LIST查询使用相同的WHERE条件否则总条数不对前端分页组件显示的页数就会出错。还一个实用设计是返回结构统一{ code: 200, data: ..., message: success }code为非200时前端统一弹出message提示这样前端axios响应拦截器只需要判断code不用每个接口单独处理错误。3.3 登录鉴权与权限控制登录鉴权用的JWT方案。用户提交账号密码后端比对bcrypt加密后的密码比对通过后用jsonwebtoken签发一个token有效期设置为24小时。前端拿到token存localStorageaxios请求拦截器在每次请求头里带上Authorization: Bearer token。后端写一个简单的中间件在处理请求前校验token解析出用户ID和角色挂到req对象上供业务逻辑使用。const jwt require(jsonwebtoken) exports.auth (req, res, next) { const authHeader req.headers.authorization || const token authHeader.split( )[1] if (!token) { return res.json({ code: 401, message: 未登录 }) } try { const decoded jwt.verify(token, process.env.JWT_SECRET) req.user decoded next() } catch (e) { return res.json({ code: 401, message: 登录已过期 }) } }权限控制用中间件包装。比如审批租赁单只有管理员角色能用就在路由里加一个校验角色的中间件const { auth, checkRole } require(../middleware/auth) router.put(/approve, auth, checkRole(3), rentalController.approveRental)checkRole实现很简单读到req.user.role和期望值比对不一致就返回403。这个方案虽然简单但足够覆盖内部系统的权限需求。要注意的点是JWT_SECRET不能写在代码里用环境变量配置提交代码时不入库。4. 前端页面设计与 ElementUI 核心组件实战4.1 整体布局与路由设计前端页面骨架是典型的后台布局左侧菜单栏、右侧内容区。ElementUI的el-container加el-aside加el-main组合一下就能拼出来菜单数据从路由表里读。侧边菜单项和路由是一一对应的配置一次路由就能同时生成菜单。// router/index.js const routes [ { path: /login, component: () import(/views/login/Login.vue) }, { path: /, component: () import(/views/layout/Layout.vue), redirect: /device/list, children: [ { path: device/list, component: () import(/views/device/DeviceList.vue), meta: { title: 设备管理 } }, { path: rental/list, component: () import(/views/rental/RentalList.vue), meta: { title: 租赁管理 } }, { path: repair/list, component: () import(/views/repair/RepairList.vue), meta: { title: 报修管理 } } ] } ]Vue Router有一个常用功能是路由守卫。系统需要在用户未登录的情况下禁止访问业务页面所以在全局守卫里判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })动态路由参数在这个系统里也有一个使用场景查看设备详情时跳转/device/detail/:id路由里用:id做占位符。这块儿建议维护一份统一的路由表命名规范菜单级别的路由用meta.title组件内部根据路由参数拉取对应详情可以少写不少重复的弹窗代码。4.2 设备列表页表格、搜索、分页联动设备列表页是这个系统最核心也最典型的一个页面顶部是搜索区域中间是表格底部是分页组件。搜索条件包括关键字匹配设备名称或编号、设备状态下拉选择点击查询按钮重新加载第一页数据。template div classdevice-list el-form :inlinetrue :modelsearchForm el-form-item label关键字 el-input v-modelsearchForm.keyword placeholder设备名称/编号 clearable keyup.enter.nativehandleSearch / /el-form-item el-form-item label状态 el-select v-modelsearchForm.status placeholder全部状态 clearable el-option label空闲 value1 / el-option label借出 value2 / el-option label维修中 value3 / /el-select /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button /el-form-item /el-form el-table :datatableData border stripe el-table-column propdevice_no label设备编号 width150 / el-table-column propname label设备名称 min-width200 / el-table-column proplocation label存放位置 width180 / el-table-column propstatus label状态 width100 template slot-scopescope el-tag :typestatusType[scope.row.status]{{ statusLabel[scope.row.status] }}/el-tag /template /el-table-column el-table-column label操作 width200 template slot-scopescope el-button typetext clickhandleRent(scope.row)借用/el-button el-button typetext clickhandleRepair(scope.row)报修/el-button el-button typetext clickhandleDetail(scope.row)详情/el-button /template /el-table-column /el-table el-pagination background layouttotal, sizes, prev, pager, next :totaltotal :current-pagesearchForm.page :page-sizesearchForm.pageSize :page-sizes[10, 20, 50] current-changehandlePageChange size-changehandleSizeChange / /div /template这里有一个前端开发的细节非常值得提醒el-pagination的current-page和page-size是双向绑定的但如果你在接口返回后直接修改这两个值组件内部状态和外部数据可能不同步轻微的抖动会导致重复请求。建议在请求数据之前把页码参数维护在一份独立对象里接口返回后再同步给组件绑定的属性期间组件的变化只触发请求函数不直接修改查询条件。搜索和分页联动时搜索条件改变后要把page重置为1否则你搜索出来的结果是从第N页开始的。4.3 租赁流程与状态展示租赁流程是整个系统里状态流转最复杂的模块。用户在前端选择一台空闲状态的设备填写预计使用日期和备注提交租赁申请。管理员的审批页面看到待审批列表数据里有一列“审批状态”待审批、已通过、已驳回。通过之后这个租赁单会变成“借出中”状态用户归还设备后管理员在系统中登记租赁单变成“已完成”同时设备表里的状态从“借出”改回“空闲”。设备状态和租赁单状态的关系用代码维护提交申请时锁定设备状态为“借出中”不能立刻锁应该在管理员审批通过后锁否则用户提交了申请被驳回设备状态也得跟着回滚麻烦。当时在实现时踩过这个坑后来调整方案提交申请时不改设备状态审批通过时同时更新设备状态为“借出”并写入租赁记录审批驳回时只改租赁单状态。这样设备状态的变化始终跟着审批结果走逻辑清晰。状态展示用ElementUI的el-timeline组件效果很好。热词里提到的“时间线自定义timestamp插槽”在这里就用上了默认时间戳格式是2023-05-10 14:00:00但业务上希望显示“提交申请”、“审批通过”、“已借出”、“已归还”这种语义化的节点标签可以利用插槽自定义el-timeline el-timeline-item v-for(item, index) in rentalEvents :keyindex :timestampitem.time template #timestamp span classcustom-time{{ item.actionLabel }}/span span classcustom-time-detail{{ item.time }}/span /template {{ item.content }} /el-timeline-item /el-timeline这样时间线既能展示流程节点又能把操作人和备注信息塞进去。4.4 表单校验、下拉多选与全选表单集中在新增设备、新增租赁单、新增报修单这几个页面。ElementUI的el-form支持rules校验规则比手写if判断要省很多事。以新增设备表单为例el-form refform :modelform :rulesrules label-width100px el-form-item label设备编号 propdeviceNo el-input v-modelform.deviceNo / /el-form-item el-form-item label设备名称 propname el-input v-modelform.name / /el-form-item /el-formrules: { deviceNo: [{ required: true, message: 请输入设备编号, trigger: blur }], name: [{ required: true, message: 请输入设备名称, trigger: blur }] }校验规则要绑定到prop才能生效表单项的prop和form对象里的字段名必须完全一致这个细节写代码的时候容易手滑。日期范围选择器要注意value-format属性不设置的话提交的是Date对象接口层还要做格式化设置成value-formatyyyy-MM-dd HH:mm:ss能省不少事。下拉多选在设备批量借用场景中非常常见比如管理员要给一个实验室一次性批好几台设备。el-select加上multiple属性就支持多选但“全选”的交互ElementUI没有直接提供需要自己处理。一般的做法是在选项列表最前面加一个值为all的选项change事件里判断如果选了all就手动把全部设备ID塞进绑定值里取消all就清空。handleSelectChange(value) { if (value.includes(all)) { if (this.selectAllFlag) { // 取消全选 this.form.deviceIds [] this.selectAllFlag false } else { this.form.deviceIds this.allDeviceIds.map(item item.id) this.selectAllFlag true } } }注意这个逻辑里有一个常见的bugel-select的change事件只在值变化时触发当你取消了其他选项但还保留着all时select会默认all为选中状态导致数据混乱。稳妥做法是在值变化后检查数组里是否含有all重新计算selectAllFlag再决定是否要重新注入全量ID。4.5 让 el-dialog 可拖拽、可调整宽高ElementUI 2.x的el-dialog默认不支持拖拽和调整大小但实际使用中管理员经常要把弹窗挪开对照着看下面的表格数据。这个需求如果不用第三方库就用原生事件去实现。简单做法是通过自定义指令给dialog的头部绑定mousedown事件在body上绑定mousemove和mouseup动态修改弹窗元素的left和top。width属性可以动态绑定然后给外层加一个resize手柄。Vue.directive(dialog-drag, { bind(el, binding, vnode) { const dialogHeader el.querySelector(.el-dialog__header) const dialog el.querySelector(.el-dialog) dialogHeader.style.cursor move dialogHeader.addEventListener(mousedown, (e) { const rect dialog.getBoundingClientRect() const offsetX e.clientX - rect.left const offsetY e.clientY - rect.top const moveHandler (event) { dialog.style.left event.clientX - offsetX px dialog.style.top event.clientY - offsetY px } const upHandler () { document.removeEventListener(mousemove, moveHandler) document.removeEventListener(mouseup, upHandler) } document.addEventListener(mousemove, moveHandler) document.addEventListener(mouseup, upHandler) }) } })这个方案胜在轻量没有引入额外依赖。实际使用中要注意dialog默认有margin: 0 auto居中样式拖拽后需要设置left为具体数值同时把margin重置为0否则位置会跳动。如果你觉得手写这些事件太麻烦也可以直接引入vue-draggable-resizable组件库它把拖拽和缩放都封装好了配置项也不复杂。但作为一个以最小依赖为原则的团队我倾向于用指令解决。5. 常见问题与排查技巧实录5.1 npm.ps1 脚本禁止运行的解决方案这条问题在搜索结果里出现频率极高我相信每个Windows上用Node.js的人都被它坑过。报错信息大概是npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。原因是Windows PowerShell的默认执行策略是Restricted禁止运行任何脚本文件而npm在PowerShell里是通过npm.ps1脚本执行的。解决办法有两种第一种管理员身份打开PowerShell修改执行策略Set-ExecutionPolicy RemoteSignedRemoteSigned表示本地脚本可以运行从网络下载的脚本需要签名。对普通用户来说这个策略是最合适的既不会放开所有脚本又能让npm正常运行。修改后执行npm -v验证。第二种如果不想修改PowerShell策略可以直接改用cmd命令行窗口运行npm命令cmd没有这个限制。虽然这个方案绕开了问题但长期来看还是建议修改执行策略因为很多开发工具都会用到PowerShell执行脚本。细节提示执行策略修改后需要关闭当前PowerShell窗口重新打开才会生效而且Set-ExecutionPolicy命令如果在普通权限下执行会报错一定要右键“以管理员身份运行”。另外有些电脑已经开启了Git Bash那里执行npm不会遇到这个问题。5.2 npm install 的常见报错与处理npm install在Windows环境下最容易报错的是node-sass安装失败。这类报错通常是网络问题导致的二进制包下载失败。如果你遇到安装node-sass卡住或报错比较好的做法是用国内的镜像源安装或者在项目根目录添加.npmrc文件指定sass_binary_site为可用的国内镜像地址也可以把node-sass替换成dart-sass方式很多。npm install还会遇到的一个问题是不同版本依赖的锁文件冲突。如果项目里有package-lock.json团队成员之间要尽量保持一致不要混用npm和yarn否则锁文件会被反复修改安装出来的依赖树不一致。另外提一句卸载Node.js的情况Windows卸载Node.js不能直接删除安装目录要从“控制面板—程序和功能”里正常卸载否则会把npm全局包的路径搞乱残留的配置会让你再次安装时遇到奇怪的报错。5.3 数据表格与表单的典型问题el-table的selection-change和row-click冲突算是一个高频问题。表格设置了多选列后点击行也会触发选中变化如果你在row-click里写了跳转详情逻辑就会出现“想选中一行却打开了详情页”的尴尬。解决方案是仔细区分两个事件的触发场景多选模式下尽量不在row-click里放跳转逻辑或者用selection-change配合手动操作状态。另一个高频场景是表格里的状态字段显示。后端返回status是数字1、2、3前端直接显示数字肯定不行用计算属性或者方法做映射。我习惯在每个页面组件里维护一个statusMap对象computed: { statusLabel() { return { 1: 空闲, 2: 借出, 3: 维修中, 4: 报废 } } }template里用{{ statusLabel[row.status] }}渲染加一个statusType映射el-tag的类型。这套做法在多个页面复用建议抽到公共的utils文件里。日期回显问题也算一个数据库里的DATETIME格式在MySQL返回给Node.js时可能会被转成YYYY-MM-DDTHH:mm:ss.sssZ这种ISO字符串前端显示不符合习惯。处理方式是后端在SQL查询时用DATE_FORMAT格式化或者前端封装一个formatDate函数统一处理。我的做法是在后端就格式化好前端不做任何转换保持vue数据和展示一致。5.4 常见问题速查表问题现象直接原因解决操作npm命令报禁止脚本运行PowerShell执行策略受限管理员执行Set-ExecutionPolicy RemoteSignednode不是内部或外部命令环境变量未生效或未配置重开终端验证检查PATH是否包含Node安装目录vue命令找不到CLI未全局安装或目录未配置重新npm install -g vue/cli检查全局npm目录element-ui组件没样式未引入CSS文件main.js引入element-ui/lib/theme-chalk/index.css表格选中行页面刷新未处理selection用selection-change手动存选中行数据接口401循环跳登录token过期但拦截器无限重试响应拦截器里对401做特殊跳转避免重复请求el-dialog拖拽位置跳动居中样式未重置拖拽后设置位置时重置margin5.5 ElementUI 版本与兼容性经验写到最后不得不提醒一个东西ElementUI 2.x和Vue 3不兼容如果你收到一个旧项目要先把项目用的Vue版本看清楚Vue 3项目只能用Element Plus。当时接这个项目时团队已经确定用Vue 2所以选的ElementUI现在的新项目我都会考虑用Vue 3加Element Plus。原因倒不是功能差异而是生态和维护状态Vue 3已经成为默认选择Element Plus也迭代到了稳定期。不过如果你要维护的是老系统ElementUI 2.x其实完全够用内部管理系统不太需要追赶新特性。我整个项目开发下来最深的体会是所有功能都建在“状态流转”这条主线上设备状态、租赁状态、报修状态三个状态机把系统串了起来前端组件只是让状态的展示和操作更直观。所以拿到类似的需求先花时间把状态定义和流转规则理清楚再考虑页面和接口开发效率会高很多。另外一个经验是内部系统会长期处于“加字段、加状态、加按钮”的维护期做设计时多一点规范少一点临时起意后面就能少返几次工。
返回列表