很多做毕设或者课程项目的同学,一上来就喜欢问“用什么技术栈好”。如果只是做个展示用的Demo,那随便什么框架都能跑起来;但如果是想做一个能应对真实场景、后续还能继续扩展的管理系统,Node.js作为后端接口层,配合Vue 2 + ElementUI做管理端,几乎是我见过性价比最高的组合之一。这篇就把高校洗衣店管理系统从零搭到能用的完整思路、核心代码、以及我当时踩过的坑,一次性说清楚。
1. 为什么高校洗衣店管理系统选了这套组合:技术选型的真实动机
先说结论:这套系统本质是一个典型的“管理后台+业务流程引擎”,它需要处理订单流转、用户管理、洗衣设备状态跟踪等结构化数据,选Node.js+Vue+ElementUI不是因为它“网红”,而是因为它恰好匹配这个项目的真实需求。
1.1 从业务场景反推技术需求
高校洗衣店和普通商业洗衣店有个很大区别:用户群体集中在校园,订单量有非常明显的潮汐特征。比如中午、傍晚是取送衣物的高峰,期末季前后会有一波被褥清洗小高峰。这意味着系统的核心压力不在地图定位、不在实时音视频,而在订单状态的快速流转和后台数据的管理效率上。
基于这个判断,系统实际需要的核心能力是:
- 学生端/店员端都要能快速提交和查询洗衣订单。
- 管理员需要看到不同状态(待取件、清洗中、待付款、已完成等)的订单列表。
- 需要有按时间段、按订单号、按用户信息的检索能力。
- 页面不需要花哨,但表单、列表、状态标签这些基础交互必须稳定。
Vue+ElementUI在“后台管理类页面”的开发效率上是经过大量项目验证的,ElementUI的表格、表单、弹窗、步骤条组件几乎是直接对着需求长出来的,能省掉大量写CSS和交互逻辑的时间。
1.2 Node.js在中间层扮演的角色
可能有人会问:为什么不用Java Spring Boot?不是不行,但在这个系统里,Node.js有个天然优势——前后端语言统一,数据格式处理链路短。前端拿到的是JSON,Node后端从MongoDB或MySQL查出来的也直接是JSON,中间不需要做重型对象映射。对学生项目或小团队来说,维护成本明显更低。
我个人的建议是,如果系统规模预期就是几千用户、日订单几百笔这个量级,Node.js的异步I/O模型完全扛得住,而且开发迭代速度快很多。更重要的是,通过Node.js可以把“权限校验”“参数过滤”“订单状态机”这类业务规则集中管理,而不是散落在每个接口里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境与工程初始化:把坑挡在写代码之前
这个部分看着基础,但我见过太多人在环境上卡了两三天。尤其是Windows环境下,Node.js和npm的权限问题、版本匹配问题,几乎每个做Vue项目的人都会遇到一次。
2.1 Node.js环境配置中的高频问题
先说一个热搜词里反复出现的报错:
code复制npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本
这个报错的原因很简单:Windows PowerShell默认执行策略是Restricted,不允许运行未签名的.ps1脚本。解决方法有两种:
- 以管理员身份打开PowerShell,执行:
bash复制
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned - 或者在项目里直接用cmd命令行,绕开PowerShell。
我更推荐第一种,因为后续还有npm run serve等操作,只改当前用户的执行策略,安全风险也可控。
2.2 工程结构划分
我用的是前后端分离结构,前端工程和后端服务独立部署,开发时通过代理解决跨域。
code复制laundry-system/
├── frontend/ # Vue 2 + ElementUI 前端工程
│ ├── src/
│ │ ├── api/ # 接口请求封装
│ │ ├── router/ # 前端路由
│ │ ├── views/ # 页面组件
│ │ │ ├── order/
│ │ │ ├── user/
│ │ │ └── device/
│ │ ├── components/
│ │ └── App.vue
│ └── package.json
├── backend/ # Node.js 后端服务
│ ├── routes/ # 接口路由
│ ├── models/ # 数据模型
│ ├── middleware/ # 认证与权限中间件
│ └── app.js
└── README.md
前端用Vue CLI创建,后端用Express框架。选Express不是因为老,而是因为它足够轻、中间件生态成熟,适合这个体量的系统。如果你喜欢更工程化的结构,也可以换成NestJS,但学习成本会高一些。
2.3 让前后端Dev服务器协同工作
在frontend/vue.config.js中配置代理:
javascript复制module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
};
这样前端代码里请求/api/orders时,开发服务器会自动转发到Node服务的3000端口,不需要在业务代码里写完整的跨域地址,避免了一堆CORS的麻烦事。
3. 后端接口与数据模型设计:订单状态机是最核心的部分
高校洗衣店系统的后端,表面上是一堆增删改查接口,但真正的灵魂是订单状态变化的设计。洗衣订单不是简单的“提交后就不管了”,它至少要经历这些状态:
code复制待取件 -> 已收衣 -> 清洗中 -> 洗涤完成 -> 待付款 -> 已完成
中间还可能出现“用户取消”或“异常挂起”状态。如果状态流转不做约束,前端想改成什么状态就调什么接口,后期数据一定乱成粥。
3.1 订单模型设计
以MongoDB为例,订单集合的核心字段如下:
javascript复制{
orderNo: 'XD20240101001',
userId: ObjectId('...'),
items: [
{ name: '羽绒服', count: 1, price: 25 },
{ name: '卫衣', count: 2, price: 15 }
],
totalAmount: 55,
status: 'pending_pickup', // 状态值统一用英文枚举
pickupCode: '6842',
createdAt: ISODate('...'),
updatedAt: ISODate('...')
}
这里特别说明一下pickupCode这个字段。高校洗衣店取件时,确认凭证是个高频操作,与其让店员手动搜索订单号,不如在下单时生成一个4位数取件码,用户凭码取件,店员在后台输入取件码即可快速定位订单,体验好很多。
3.2 状态机中间件
为了不让状态修改的代码散落在各个路由处理函数中,我写了一个通用的状态流转校验中间件:
javascript复制const allowedTransitions = {
'pending_pickup': ['picked_up', 'cancelled'],
'picked_up': ['washing', 'cancelled'],
'washing': ['washed'],
'washed': ['pending_payment'],
'pending_payment': ['completed']
};
function checkTransition(from, to) {
return allowedTransitions[from] && allowedTransitions[from].includes(to);
}
每个修改状态的路由都必须调用checkTransition做校验,否则返回400。这个逻辑看起来不起眼,但它保证了订单数据的可追溯性,也避免了“已完成订单还能被改成清洗中”这种逻辑灾难。
3.3 Token认证中间件
洗衣店管理后台有学生用户、店员、管理员等不同角色,接口必须有权限区分。我用JWT做登录态管理,中间件里解析Token,并挂载到req.user上:
javascript复制const jwt = require('jsonwebtoken');
module.exports = function authRequired(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ message: '未登录' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ message: '登录已过期' });
}
};
在需要区分管理员权限的接口上,再加一层requireAdmin中间件即可。这里要提醒一句,不要把JWT_SECRET写死在代码里,放到.env环境变量中,防止工程上传到公开仓库时密钥泄露。
4. 前端核心页面与ElementUI组件的实战细节
前端部分是这套系统里工作量最大、也是热搜词里问题最集中的区域。我把最有代表性的几个页面拆开说,并附上避坑经验。
4.1 订单列表页:分页组件没那么简单
ElementUI的el-pagination组件很常用,但很多人第一次接入时会把“当前页数据”和“后端分页数据”搞混。
核心逻辑是:前端只负责把页码和每页条数传给后端,由后端返回当前页的数据列表和总条数。前端不能自以为是地对已经拿到的全量数据做切片,除非你的数据量只有几十条。
我在src/views/order/OrderList.vue的写法是:
vue复制<el-pagination
background
layout="total, prev, pager, next, sizes"
:total="total"
:current-page="query.page"
:page-sizes="[10, 20, 50]"
:page-size="query.pageSize"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
javascript复制methods: {
async fetchOrders() {
const res = await getOrdersApi(this.query);
this.orders = res.data.list;
this.total = res.data.total;
},
handlePageChange(page) {
this.query.page = page;
this.fetchOrders();
}
}
这里的query对象包含page、pageSize、status、keyword等参数,每次请求都复用同一个请求函数,逻辑很统一。
4.2 让el-dialog支持拖拽和缩放
热搜词里有一条“怎么让elementui el-dialog可拖拽、可改变宽高”,这个需求在后台管理系统里非常常见。尤其门店收银人员可能一边看着订单信息一边操作,固定大小的弹窗很影响效率。
ElementUI的el-dialog默认不支持拖拽,但可以用一小组自定义指令实现。下面是一个简单的拖拽指令:
javascript复制// directives/draggable.js
Vue.directive('dialog-drag', {
bind(el, binding, vnode) {
const dialog = el.querySelector('.el-dialog__header');
const target = el.querySelector('.el-dialog');
dialog.style.cursor = 'move';
dialog.addEventListener('mousedown', (e) => {
const startX = e.clientX;
const startY = e.clientY;
const offsetLeft = target.offsetLeft;
const offsetTop = target.offsetTop;
const mousemove = (ev) => {
const moveX = ev.clientX - startX;
const moveY = ev.clientY - startY;
target.style.left = offsetLeft + moveX + 'px';
target.style.top = offsetTop + moveY + 'px';
target.style.margin = '0';
};
const mouseup = () => {
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
};
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', mouseup);
});
}
});
弹窗缩放稍微复杂一点,需要在右下角加一个拖拽手柄,动态修改弹窗宽高。如果你的需求不复杂,可以优先做拖拽,缩放功能在某些场景下容易和表格滚动条产生操作冲突。
4.3 下拉多选与全选:ElementUI的一个隐藏坑
“elementui下拉多选全选”这个问题,通常出现在给订单批量分配洗衣机/批量修改负责人的场景中。
el-select开启multiple后,默认只能一个个选,没有全选按钮。曲线解决方案是在下拉列表顶部加一个“全选”选项,值为一个特殊标记:
vue复制<el-select v-model="selectedDevices" multiple filterable>
<el-option
label="全部设备"
value="__ALL__"
/>
<el-option
v-for="device in deviceOptions"
:key="device.id"
:label="device.name"
:value="device.id"
/>
</el-select>
然后监听change事件:
javascript复制handleDeviceChange(value) {
if (value.includes('__ALL__')) {
this.selectedDevices = this.deviceOptions.map(d => d.id);
}
}
这个方案能覆盖绝大多数“全选”需求。不过在数据量很大的时候(比如几千台设备),不建议用这种方式,而是应该在后端做批量指派,用逗号分隔的ID列表传参。
4.4 el-table里的状态标签和时间格式化
订单状态建议用el-tag显示,不同状态映射不同type:
vue复制<el-table-column label="订单状态" width="120">
<template slot-scope="{ row }">
<el-tag :type="statusMap[row.status].type">
{{ statusMap[row.status].label }}
</el-tag>
</template>
</el-table-column>
javascript复制const statusMap = {
'pending_pickup': { label: '待取件', type: 'warning' },
'washing': { label: '清洗中', type: 'primary' },
'washed': { label: '洗涤完成', type: 'success' },
'pending_payment': { label: '待付款', type: 'danger' },
'completed': { label: '已完成', type: 'info' }
};
这样改动一处,全页面的状态文案和颜色都跟着变,很符合“维护成本最低”的原则。
4.5 时间线插槽自定义timestamp
如果你的系统有一个“订单进度追踪”页面,用ElementUI的时间线el-timeline展示很直观。默认的timestamp是纯文本,如果想在时间旁边加上图标或更多操作,需要用插槽自定义:
vue复制<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:timestamp="activity.time"
placement="top"
>
<el-card>
<p>{{ activity.content }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
插槽自定义可以做更多事情,比如在#timestamp插槽里同时展示操作人姓名,让时间线信息更丰富。
5. 踩坑实录:Node.js与Vue项目中最容易绊倒人的五个问题
这部分我特意放到最后,因为前面那些页面效果可以慢慢调,但下面这几个问题如果是上线前才暴露,会非常被动。
5.1 Vue 2与Node版本的生命周期问题
如果你现在才刚开始下载Node.js,大概率装的是Node 18甚至更高版本。这时候用Vue CLI 4创建Vue 2项目,可能会遇到OpenSSL相关的报错:
code复制error:0308010C:digital envelope routines::unsupported
原因是Node 17以后OpenSSL默认算法变更,而旧版Webpack还依赖旧的MD4哈希策略。解决办法有两个:
- 在package.json的scripts里设置
set NODE_OPTIONS=--openssl-legacy-provider - 或者用nvm切换到Node 16版本
我的建议是:如果做Vue 2项目,直接使用Node 16 LTS版本,省心很多。Vue 2生态和Node 16的配合是经过大量生产环境验证的。
5.2 前端路由的两种模式
vue-router默认是hash模式,URL里会带#号。如果部署上线后想用history模式(URL更干净),必须在后端服务里做try-files重写,否则刷新页面就会变成404。
Express里可以加一段:
javascript复制const path = require('path');
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
});
但要注意,这段代码必须放在所有API路由定义之后,否则会拦截掉接口请求。这是一个经典的“看起来简单但顺序错了就全线崩溃”的坑。
5.3 表单校验与后端校验必须同时做
ElementUI的el-form加上rules可以做前端校验,比如“手机号格式不正确”“必填项不能为空”,但这只是用户体验层面的拦截。接口层必须做二次校验,否则用Postman甚至直接在浏览器控制台发fetch请求就能绕过页面规则,往数据库写入脏数据。
在Express中,我习惯在关键路由里手动校验:
javascript复制const { orderNo, items } = req.body;
if (!orderNo || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ message: '订单参数不完整' });
}
为了节省时间可以引入Joi或express-validator做Schema校验,但千万不要认为前端校验过了就万事大吉。
5.4 上传图片与静态资源路径
洗衣店订单有时需要上传衣物瑕疵照片,这就涉及文件上传。Express接multer时,最常踩的坑是:图片能传上去,但前端图片无法显示。
关键是静态资源托管:
javascript复制app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
这样在前端访问http://localhost:3000/uploads/xxx.jpg就能显示,而前端页面代码里不要拼本机绝对路径,应该使用相对路径或从接口返回的完整路径。
5.5 部署时的跨域与代理配置
开发环境有vue.config.js的proxy,生产环境如果前端静态文件由Nginx托管,则需要在Nginx配置反向代理,把/api转发到Node服务。这样上线后的域名是同一个,不存在跨域问题,Cookie也能正常工作。
Nginx关键配置看起来像:
nginx复制location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
不要把proxy_pass后的地址忘了写http://,也不要在这类配置改动后不重启Nginx,这两点很基础但出错频率极高。
6. 项目扩展:从洗衣店管理系统往“校园生活服务平台”方向延伸
如果你的目标不只是交一份毕设,而是想把这个系统作为后续持续迭代的作品集项目,有几个扩展方向值得考虑。
6.1 引入支付模拟与优惠券系统
高校洗衣店真正的商业闭环是“线上提交、线下洗护、在线支付”。可以接入微信/支付宝的模拟支付流程,再增加优惠券模块。优惠券的核销逻辑其实非常练手——涉及到发放条件、有效期、使用门槛、核销状态,是一套完整的状态机设计。
6.2 设备状态看板与远程监控
扩展一个“洗衣机/烘干机状态看板”页面,显示每台设备是空闲、运行中、离线还是故障。这个功能的背后是设备心跳上报接口,Node.js可以通过Socket.IO建立长连接接收设备状态。前端则可以用ElementUI的Progress组件或Tag组件展示状态,整个页面会非常有“大屏感”。
6.3 Vue 3与Element Plus迁移
当前系统如果用的是Vue 2 + ElementUI,迟早要面对Vue 3的迁移。好消息是,这个项目的业务逻辑和组件划分如果做得够清晰,迁移时主要工作量在模板组件名称替换和Composition API重构上,核心数据结构、接口设计几乎不用动。这从侧面说明了业务层与视图层分离的重要性。
我个人习惯是,在开发新模块时,即使还在Vue 2工程里,也会刻意把业务逻辑抽取到独立的JavaScript模块中,而不是全部写在组件的methods里。这样后期无论是升级Vue 3还是把逻辑迁移到后端,成本都会低很多。
6.4 数据统计与可视化的价值
后台管理到后期一定缺不了一个数据统计页:今日订单量、本周营业额、高峰时间段、高频洗衣品类。ElementUI本身不提供图表,但可以配合ECharts使用。做折线图和柱状图的本质,是把后端的聚合查询结果,比如按天分组的订单量数组,映射到ECharts的series中,难度不大,但视觉产出非常直观。
7. 这套系统在实际使用中的表现与经验总结
最后说一点实际使用的体会。我搭过的类似洗衣店管理系统,在实际运行中最被高频使用的页面不是复杂的数据看板,而是订单列表和取件确认页。所以开发时资源分配也要有侧重,订单列表页的查询性能、操作流畅度,比首页有没有酷炫的图表重要得多。
所谓查询性能,核心其实在数据库索引设计。后端对orderNo、status、createdAt、userId这些字段建立索引,数据量到十万级时查询速度依然能保持在百毫秒内。页面加载慢,很多时候不是前端组件的问题,而是后端少建了一个索引。
项目走到上线后要不断收集使用者的反馈。比如店员可能在取件时要求快速定位用户的所有未完成订单,这时候就需要增加“按手机尾号搜索”的功能。这些需求在开发初期很容易被忽略,但它们才是最鲜活的业务真实需求。
如果你正在做类似的系统,我的建议是先老老实实把订单状态机理清楚,再开始写页面,不然后面很可能会反复改数据结构,越改越乱。技术选型永远是为了服务和业务逻辑,这才是这个洗衣店管理系统真正值得投入时间的地方。
