一看到这个项目标题,做管理系统开发的朋友应该已经有画面了:一个典型的“Node.js 后端 + Vue 前端 + ElementUI 组件库”前后端分离项目,业务对象是留守儿童群体。这个选题本身很有社会价值,而且技术栈非常主流,很适合作为毕设、实训项目或者个人作品集项目来落地。今天我就以这个“留守儿童身心关爱平台”为例,把这套技术栈从环境搭建、数据库设计、后端接口开发到前端页面实现的完整链路,以及我实际开发中踩过的坑、反复调整的细节,一次性讲清楚。
1. 项目概述与需求拆解
1.1 这个平台到底要解决什么问题
留守儿童这个群体的核心痛点在于:父母不在身边,日常监护可能落在祖辈或亲戚身上,学校和社区很难系统性地掌握每个孩子的真实状态。“身心关爱”这四个字拆开来看,其实是两条业务线——身体层面的健康状况跟踪,心理层面的情绪状态、行为表现记录和干预。传统的线下纸质档案方式,数据分散、更新滞后、无法统计,更谈不上及时预警。
所以这个平台的核心价值,就是把这些散落在老师、志愿者、临时监护人手里的信息,统一收拢到一个 Web 系统里。用户端分角色登录,管理员维护儿童基础档案,志愿者和老师可以填报家访记录、心理辅导记录,平台内置心理测评问卷,孩子(或者代填人)提交后可自动计算得分并生成关注等级,所有关爱行为都留下时间线,方便回溯。整体来看,这就是一个带有“档案管理 + 业务记录 + 测评分析”三重属性的信息管理系统,并不涉及复杂的算法,但对数据规范性、权限控制、操作便利性要求很高。
1.2 技术选型的底层逻辑
为什么选 Node.js + Vue + ElementUI 这套组合,而不是 Spring Boot + Thymeleaf 或者别的?我的判断有几点。第一,这套系统属于典型的 CRUD 密集型应用,业务逻辑并不复杂,Node.js 的异步模型和 JavaScript 全栈语言统一,前后端可以共用一些工具函数和类型定义,开发效率非常高。第二,Vue 2 的生态在 ElementUI 加持下,管理后台的开发速度优势明显,表格、表单、弹窗、分页这些高频组件开箱即用,不需要从零写样式。第三,对于毕设或者小团队项目来说,这套技术栈的学习曲线相对平缓,一个人能同时 hold 住前后端。
当然它也有缺点,比如 Node.js 在处理 CPU 密集型任务时表现一般,但这套平台根本没有这类场景,所以选型是成立的。生态上有人会纠结用 Vue 2 + ElementUI 还是 Vue 3 + Element Plus,我的建议是:如果你是在校生做毕设,导师可能更熟悉 Vue 2 + ElementUI,资料最多、报错最好查;如果是新入职场的项目,可以上 Vue 3 + Element Plus。下面所有代码基于 Vue 2,但设计思路完全通用。
1.3 功能模块清单与页面规划
| 模块 | 核心功能 | 涉及角色 |
|---|---|---|
| 登录认证 | 账号密码登录、Token 校验、退出登录 | 所有用户 |
| 儿童档案管理 | 新增、编辑、删除、详情、分页列表、关键词搜索 | 管理员、志愿者 |
| 关爱记录管理 | 家访记录、心理辅导、学习帮扶等分类记录 | 志愿者、老师 |
| 心理测评问卷 | 问卷配置、在线作答、自动计分、等级评估 | 管理员、监护人 |
| 留言咨询 | 提交咨询、后台回复、状态标记 | 监护人、志愿者 |
| 数据看板 | 儿童总数、地区分布、测评预警列表、近期关爱动态 | 管理员 |
| 系统管理 | 用户管理、角色权限控制、个人信息修改 | 管理员 |
页面规划上,前端采用经典的侧边栏 + 顶栏布局,侧边栏根据角色渲染菜单,顶部放用户信息和退出按钮,主内容区用 router-view 承载路由页面。页面数量控制在 10 个左右,每个页面职责单一,不建议把功能堆成一个大杂烩页面。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与工程初始化
2.1 Node.js 安装与环境配置
这一步看着简单,实际上是整个项目里新人翻车率最高的环节。首先是版本选择,我建议装 LTS 版本,不要追新。Node.js 的奇数版本(比如 21、23)属于 Current 版本,稳定性存疑,装完以后很容易出现依赖兼容性问题。下载地址直接去官网找 Windows Installer 或者 macOS Installer,一路 Next 就行,安装路径建议保持默认,因为很多第三方工具会主动查找默认安装路径。
装完以后验证是否成功,打开终端执行:
bash复制node -v
npm -v
如果能看到版本号,说明 Node.js 本体没问题。但我在实际教学和开发中,经常遇到一种情况:node 命令能执行,npm 命令却报错,最常见的就是下面这段:
bash复制npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。
这个问题不是 npm 装坏了,而是 PowerShell 的执行策略默认禁用了脚本运行。解决办法很简单,以管理员身份打开 PowerShell,执行:
powershell复制Set-ExecutionPolicy RemoteSigned
然后输入 Y 确认,再重开一个终端窗口执行 npm -v 就好了。我自己一般也顺手把 npm 的镜像源切到国内源,省得后面安装依赖时等半天:
bash复制npm config set registry https://registry.npmmirror.com
2.2 Vue 工程创建与项目目录规划
环境装好后,推荐用 Vue CLI 来创建前端项目。虽然现在 Vite 很流行,但 Vue CLI 在 Vue 2 项目里依然是最稳的选择。
bash复制npm install -g @vue/cli
vue create care-platform-front
创建时选择 “Manually select features”,勾选 Babel、Router、Vuex,Vue 版本选 2.x。装完依赖后,再单独引入 ElementUI:
bash复制npm i element-ui -S
npm i axios -S
npm i sass-loader node-sass -D
注意 node-sass 在 Node.js 新版本下安装很容易失败,如果你用的是 Node 18 以上,建议直接换成 dart-sass,也就是 npm i sass -D,然后 webpack 配置里不需要额外改动,Vue CLI 会自动识别。这里也是一个高发坑,我后来统一用 sass 替代了 node-sass,再也没出过编译报错。
前端安装完成后,开发调试阶段可以先 mock 一下样式,但正常流程是先把后端跑起来。后端目录我单独建一个 server 目录,和前端平级,前后端完全分离。推荐用 Express 作为 Web 框架,因为生态成熟、资料多,另外再装几个核心依赖:
bash复制npm i express mysql2 sequelize jsonwebtoken bcryptjs cors
2.3 前后端联调思路
项目目录结构定下来以后,前后端联调之前要把代理搞定。开发环境下前端 devServer 跑在 8080,后端接口跑在 3000,浏览器直接访问后端接口会跨域。我习惯在前端根目录创建 vue.config.js 做代理转发,避免后端写一堆 CORS 配置:
javascript复制module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
}
这样前端页面里所有请求都写成 /api/xxx,开发环境由 webpack 代理转发,生产环境由 Nginx 再转发一次,代码里不需要区分环境,省心。
3. 数据库设计与后端接口开发
3.1 核心数据表设计
数据库选 MySQL,直接用 Sequelize 作为 ORM,不用手写 SQL。表结构我拆成了六张核心表,设计上有几个关键点要说明。
第一张是用户表 users,字段包括 id、username、password、real_name、phone、role、avatar 等。role 字段我直接用字符串枚举(admin / volunteer / teacher / guardian),不搞复杂的 RBAC 表,因为这套系统的角色数量少、权限层级简单,多搞两张表反而增加维护成本。
第二张是儿童档案表 children,字段包括 id、name、gender、birthday、school、grade、guardian_name、guardian_phone、address、physical_status、psychological_status、created_at、updated_at。这里有两个字段要特别注意:guardian_phone 是临时监护人的联系方式,不是父母的,别设计反了;psychological_status 是冗余字段,存储最新的心理关注等级,方便列表页直接展示,不用每次联表查询测评记录。
第三张是关爱记录表 care_records,字段包括 id、child_id、user_id、care_type、content、contact_time、remark。care_type 用枚举:home_visit(家访)、psych_counseling(心理辅导)、study_help(学习帮扶)、material_support(物资帮扶)。记录内容要允许长文本,因为实际家访记录往往写得很详细。
第四张是问卷表 questionnaires,字段包括 id、title、description、content、status、created_at。content 字段是 JSON 字符串,存储问卷的题目配置,包括题目类型(单选/多选/评分)、选项列表、分值。这样设计的好处是问卷模板可以动态配置,不需要为每道题单独建表。
第五张是答卷表 answers,字段包括 id、questionnaire_id、child_id、user_id、content、score、level、created_at。content 存用户提交的原始答案 JSON,score 是程序计算后的总分,level 是根据总分区间生成的关注等级(普通/关注/重点)。这里推荐一个设计习惯:原始答案和计算结果同时存,一方面方便回溯,另一方面如果后面的评估规则改了,可以重新计算历史数据,不用重新收集数据。
第六张是留言表 messages,字段包括 id、user_id、content、reply_content、reply_user_id、reply_time、is_read、created_at。留言场景是监护人或者志愿者向平台管理员咨询问题,管理员在后端回复,回复后 is_read 标记为已读。
3.2 RESTful 接口设计与实现示例
接口设计遵循 RESTful 风格,统一返回格式为 { code: 200, message: 'success', data: {...} }。我用一个小工具函数统一包装:
javascript复制const response = (res, data = null, message = 'success', code = 200) => {
res.status(code).json({ code, message, data });
};
登录接口是第一个要写的接口。密码需要 hash 处理,我用的 bcryptjs:
javascript复制const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
router.post('/auth/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ where: { username } });
if (!user) return response(res, null, '账号不存在', 400);
const valid = bcrypt.compareSync(password, user.password);
if (!valid) return response(res, null, '密码错误', 400);
const token = jwt.sign({ id: user.id, role: user.role }, 'your_secret_key', { expiresIn: '24h' });
response(res, { token, user: { id: user.id, realName: user.real_name, role: user.role } });
});
儿童档案的分页查询接口,需要考虑搜索关键字和分页参数:
javascript复制router.get('/children', async (req, res) => {
const { page = 1, pageSize = 10, keyword = '' } = req.query;
const offset = (Number(page) - 1) * Number(pageSize);
const where = {};
if (keyword) {
where[Op.or] = [
{ name: { [Op.like]: `%${keyword}%` } },
{ school: { [Op.like]: `%${keyword}%` } }
];
}
const { rows, count } = await Child.findAndCountAll({
where,
offset,
limit: Number(pageSize),
order: [['created_at', 'DESC']]
});
response(res, { list: rows, total: count });
});
这里的 offset 计算逻辑是骨灰级基础,但每次都要写对:页码从 1 开始,所以 (page - 1) * pageSize,一旦写成 page * pageSize 或忘记减一,列表第一页就会漏数据。
3.3 鉴权中间件与权限控制
留守儿童数据涉及未成年人隐私,权限控制必须做到位。我封装了一个简单的 JWT 鉴权中间件:
javascript复制const auth = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return response(res, null, '未登录', 401);
try {
const decoded = jwt.verify(token, 'your_secret_key');
req.user = decoded;
next();
} catch (e) {
response(res, null, 'Token 失效', 401);
}
};
再包一层角色校验:
javascript复制const checkRole = (roles) => (req, res, next) => {
if (!roles.includes(req.user.role)) {
return response(res, null, '无权限访问', 403);
}
next();
};
实际使用时,只需要在路由上叠加中间件:router.post('/children', auth, checkRole(['admin', 'volunteer']), handler)。删除操作我限制只有 admin 能执行,新增和编辑允许 admin 和 volunteer,查询所有登录用户都可访问,留言回复限制 admin。这套控制看起来简单,但已经覆盖了业务需求。
4. 前端核心模块与 ElementUI 实战
4.1 路由设计与页面骨架
Vue Router 配置是整个前端的骨架。我采用侧边栏菜单和路由配置联动的方式,每一条菜单对应一个路由,路由的 meta.title 就是侧边栏显示的文字。同时利用路由懒加载,按需加载页面组件,减少首屏体积:
javascript复制const router = new VueRouter({
routes: [
{ path: '/login', component: () => import('@/views/Login.vue') },
{
path: '/',
component: () => import('@/layout/Layout.vue'),
redirect: '/dashboard',
children: [
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '数据看板' } },
{ path: 'children', name: 'ChildList', component: () => import('@/views/Child/ChildList.vue'), meta: { title: '儿童档案' } },
{ path: 'records', name: 'RecordList', component: () => import('@/views/Record/RecordList.vue'), meta: { title: '关爱记录' } },
{ path: 'survey', name: 'Survey', component: () => import('@/views/Survey/Survey.vue'), meta: { title: '心理测评' } },
{ path: 'messages', name: 'Message', component: () => import('@/views/Message/Message.vue'), meta: { title: '留言咨询' } }
]
}
]
});
路由守卫控制登录状态,没有 token 一律踢回登录页:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.path !== '/login' && !token) {
next('/login');
} else {
next();
}
});
这个守卫很简单,但很实用。实际项目里还可以再扩展:根据用户角色动态过滤菜单,比如 guardian 角色不需要看到系统管理菜单,但这个在 Vue 2 里需要用到路由过滤函数,复杂度略高,我建议放在菜单渲染层处理,而不是路由层。
4.2 首页数据看板实现
数据看板是平台所有角色的第一落眼点,也是评审时最容易加分的页面。我用三块内容填充看板:顶部四个统计卡片、中部儿童年龄分布柱状图、底部重点关注儿童表格和近期关爱动态时间线。
统计卡片直接请求后端聚合接口,后端一次性返回:
javascript复制router.get('/dashboard/stats', auth, async (req, res) => {
const childTotal = await Child.count();
const recordTotal = await CareRecord.count();
const focusTotal = await Child.count({ where: { psychological_status: 'focus' } });
const todayRecordTotal = await CareRecord.count({
where: { created_at: { [Op.gte]: new Date(new Date().setHours(0, 0, 0, 0)) } }
});
response(res, { childTotal, recordTotal, focusTotal, todayRecordTotal });
});
4.2 儿童年龄分布我用的 ECharts,折线图和柱状图交替使用。注意 ECharts 在 Vue 2 里不要直接操作 DOM,用 ref 拿到容器,在 mounted 里初始化实例,组件销毁时记得调用 chart.dispose(),否则多页面切换时会内存泄漏。
4.3 表单与校验:儿童档案信息录入
儿童档案表单是使用频率最高的表单,字段多、校验要求高。我的经验是表单校验规则写在 data 里统一管理,规则字段和 el-form-item 的 prop 一一对应:
vue复制<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="儿童姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="form.gender">
<el-radio label="1">男</el-radio>
<el-radio label="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker v-model="form.birthday" type="date" value-format="yyyy-MM-dd" />
</el-form-item>
</el-form>
日期选择器这里特别注意:如果没设置 value-format,组件返回的是一个 Date 对象,转 JSON 的时候会变成 ISO 字符串,MySQL 的 DATE 字段存进去会报错或者数据异常。我统一加了 value-format="yyyy-MM-dd",让接口直接接收字符串,省掉一层转换。
4.4 数据列表与分页组件的封装
el-table 搭配 el-pagination 是后台最常见的组合。网上很多教程是直接在页面里写一遍分页逻辑,但这样每个列表页都复制一大坨代码。我封装了一个公共的 Pagination.vue 组件,把分页的所有状态管理收拢到组件内部:
vue复制<template>
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:current-page.sync="page"
:page-size.sync="pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</template>
数据请求统一抽成 mixin,列表页只需要配置请求接口和搜索参数:
javascript复制export default {
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: { page: 1, pageSize: 10, keyword: '' }
};
},
methods: {
async fetchList() {
this.loading = true;
try {
const { data } = await api.getList(this.queryParams);
this.list = data.list;
this.total = data.total;
} finally {
this.loading = false;
}
},
handlePageChange(page) {
this.queryParams.page = page;
this.fetchList();
},
handleSizeChange(size) {
this.queryParams.pageSize = size;
this.queryParams.page = 1;
this.fetchList();
}
}
};
这里有个经验:切换 pageSize 时,页码必须重置回 1。因为当前页可能是第 8 页,一页显示 50 条的话总共可能只有 2 页,不重置就会跑到空白页。这个问题我在实际开发中被测试同事提过 bug,后续就记住了。
4.5 下拉多选与全选的实现
ElementUI 的 el-select 多选模式下,自带多选标签和可清除功能,但“全选”需要自己实现。我通常用一个逻辑:当前选中的值数组长度等于所有选项长度时,显示为全选状态,点击全选则清空所有选项:
vue复制<el-select v-model="selectedTypes" multiple placeholder="请选择关爱类型">
<el-checkbox v-model="checkAll" @change="handleCheckAll">全选</el-checkbox>
<el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
注意事项:el-select 的弹层里放 el-checkbox 是可以的,但是点击全选时要防止事件冒泡导致下拉面板关闭。解决方法是在 el-checkbox 外层加一个 @click.stop.native。这个细节在刚开始没注意,每次点击全选弹层瞬间就关了,体验很差。全选逻辑本身很简单:
javascript复制handleCheckAll(val) {
this.selectedTypes = val ? this.typeOptions.map(item => item.value) : [];
}
另外,如果选项来自后端异步数据,全选判断的时候要注意列表是否加载完整,不能只比对当前页的数据。
4.6 el-dialog 弹窗拖拽与自由调整宽高
ElementUI 的 el-dialog 默认不支持拖拽,但实际给人演示项目时,对话框能拖拽会显著提升体验。我用自定义指令实现拖拽功能,核心是监听 mousemove 和 mouseup:
javascript复制Vue.directive('dialog-drag', {
bind(el) {
const dialogHeader = el.querySelector('.el-dialog__header');
const dialog = el.querySelector('.el-dialog');
dialogHeader.style.cursor = 'move';
dialogHeader.onmousedown = (e) => {
const rect = dialog.getBoundingClientRect();
const startX = e.clientX;
const startY = e.clientY;
const originX = rect.left;
const originY = rect.top;
document.onmousemove = (ev) => {
dialog.style.left = originX + ev.clientX - startX + 'px';
dialog.style.top = originY + ev.clientY - startY + 'px';
dialog.style.margin = '0';
};
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
};
};
}
});
使用的时候在 el-dialog 标签上加一行 v-dialog-drag 即可。这里有个细节:拖拽之后 dialog 的 margin 会变成 0,因为 ElementUI 默认样式里 .el-dialog 是 margin: 15vh auto 50px,如果不重置,拖拽位置会在下次鼠标按下时跳回原位。另外,el-dialog 的 append-to-body 属性建议设置为 true,否则拖拽的 transform 定位在某些布局下会失效。
4.7 心理测评问卷的交互与自动计分
心理测评是本项目的业务亮点。前端展示问卷题目,用户作答后提交,后端根据题目配置计算总分。前端问卷页面我直接用 v-for 循环题目,单选用 el-radio-group,多选用 el-checkbox-group,评分题用 el-rate。
问卷提交前的校验:每道题都必须作答。可以用一个计算属性判断未答题目数量:
javascript复制computed: {
unansweredCount() {
return this.questions.filter(q => !this.answers[q.id]).length;
}
}
这个 computed 的好处是,页面可以实时显示“还有 3 道题未完成”,提交时直接拦截并提示具体题目编号。不要等提交后让后端去校验,那样用户还得回页面慢慢找,体验差。
提交答案时,计算得分的逻辑要放在后端做。因为分数规则如果写在多个前端页面里,后续改规则就要动前端代码重新打包,而且前端算分可以被人为篡改。后端拿到 answers 数组后,遍历对比题目配置:
javascript复制let score = 0;
questions.forEach(q => {
const answer = answers[q.id];
if (q.type === 'single') {
const option = q.options.find(o => o.value === answer);
score += option.score;
} else if (q.type === 'multiple') {
answer.forEach(val => {
const option = q.options.find(o => o.value === val);
score += option.score;
});
}
});
计算完 score 后,按区间映射 level。比如 0-10 分是 normal,11-20 是 attention,21 以上是 focus,同时回写 children 表的心理状态字段,这样首页的关注列表直接读 children 表就可以。
5. 部署上线与数据安全
5.1 前端构建与后端部署
项目开发完成后要部署上线。前端执行打包命令:
bash复制npm run build
生成 dist 目录,里面是纯静态文件。后端在服务器上用 PM2 守护进程运行比较稳妥:
bash复制npm i -g pm2
pm2 start app.js --name care-platform-server
Nginx 配置里,静态文件指向 dist 目录,接口请求反向代理到 Node.js 的 3000 端口:
nginx复制server {
listen 80;
server_name your-domain.com;
root /var/www/care-platform/dist;
index index.html;
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html;
}
}
这里 try_files 是必要的,否则 Vue Router 在 history 模式下刷新子路由页面会 404。如果不改服务端,也可以直接用 hash 模式规避这个问题,但 URL 会带 #,不美观。
5.2 留守儿童数据的隐私保护
留守儿童属于未成年人,系统的数据隐私保护是很重要的非功能需求。第一层,接口层面所有涉及儿童详细信息的接口都要走鉴权中间件,不允许未登录访问;第二层,数据库密码字段加密存储,不能明文;第三层,前端展示做数据脱敏,比如列表页监护人电话显示前三位和后四位,详情页点开才展示完整号码;第四层,后端日志不要打印完整的儿童身份证号、家庭住址等敏感字段。这些处理在技术实现上成本很低,但在评审和实际使用中会给系统加分不少。
5.3 性能优化与体验细节
数据量上来以后,列表页的查询要加索引,这个在 Sequelize 迁移脚本里直接处理。首页看板的统计数据可以后端做缓存,比如每个统计项存 Redis,设置 5 分钟过期,避免每次刷新页面都重新 count 全表。前端首屏优化方面,ElementUI 组件库按需引入,不要 import ElementUI from 'element-ui' 整个引入,改成按组件引入,打包体积能小 40% 以上。
按需引入的做法是装 babel-plugin-component,然后在 babel.config.js 里配置:
javascript复制module.exports = {
plugins: [
['component', { libraryName: 'element-ui', styleLibraryName: 'theme-chalk' }]
]
};
之后在 main.js 里手动 import 用到的组件,比如 Button、Table、Form、Dialog、Pagination 等。我见过不少项目图省事直接全量引入,开发时确实省事,但打包出来的 vendor.js 有几 MB,打开首页白屏时间明显变长。
6. 常见问题与排查技巧实录
6.1 npm 命令报错:无法加载文件 npm.ps1
这个在前面已经说过了,再补充一个变体。如果 Set-ExecutionPolicy RemoteSigned 执行后仍然报错,检查一下是否用管理员权限开的 PowerShell。另外,如果你在 VS Code 或者 Cursor 的终端里执行,终端可能继承了非管理员权限,需要单独以管理员身份打开外部终端重试。还有一种情况是全局 npm 路径没加入 PATH,导致 npm 命令找不到,这时候要检查环境变量里的 Path 是否包含 C:\Program Files\nodejs\。
6.2 el-dialog 拖拽后恢复位置失效
自定义指令的拖拽用了 document.onmousemove,问题是如果用户鼠标移出 dialog 区域再松开,mouseup 事件没有触发,下次拖动时位置计算就会错乱。我在实际开发中加了一个兜底:鼠标移出 document 也清空事件:
javascript复制document.addEventListener('mouseup', clearDrag);
window.addEventListener('blur', clearDrag);
事件监听需要在指令的 unbind 钩子里移除,避免组件销毁后残留监听器。
6.3 分页组件切换后数据错乱
排查这类问题,先看网络请求的参数,再看响应数据。常见原因有三:一是 pageSize 切换后未重置 page;二是搜索关键字变化后未重置 page;三是快速点击分页时,前一个请求比后一个请求晚返回,导致列表显示旧数据。第三个问题相对隐蔽,解决方法是给请求加一个递增序号,响应回来时只处理最新一次的请求:
javascript复制let requestSeq = 0;
async function fetchList(params) {
const seq = ++requestSeq;
const res = await api.getList(params);
if (seq !== requestSeq) return;
this.list = res.data.list;
}
6.4 Node 版本导致依赖安装失败
node-sass 是重灾区,为了解决它我还写过一篇详细教程。现在直接建议:新项目一律用 sass 替代。如果你确实要装 node-sass,注意当前 Node 版本和 node-sass 版本对应关系,Node 18 对应 node-sass 8.0,Node 20 对应 node-sass 9.0,装之前先用 nvm list 查一下当前版本。另外,webpack 报错 Module build failed: Error: Node Sass does not yet support your current environment 的时候,先别急着卸载重装,大概率是二进制文件下载失败,执行 npm rebuild node-sass 或者手动设置镜像源。
6.5 前端请求跨域问题
开发时通过 vue.config.js 代理一般不会有问题,部署后如果 Nginx 配置不对,接口会报 502。常见原因是 proxy_pass 目标的 port 写错,或者 Node 服务没有监听在对应端口。排查时先 curl 一下本机接口:curl http://127.0.0.1:3000/api/xxx,能通再查 Nginx,别一上来就怀疑代码。生产环境还有一种情况是 HTTPS 页面请求 HTTP 接口,浏览器会拦截,需要保证页面协议和接口协议一致。
6.6 Vue DevTools 调试技巧
开发调试阶段,Vue DevTools 是必不可少的工具。有两点容易踩坑:检测不到 Vue 实例,先确认是在开发环境运行页面,生产环境会因为代码压缩而禁用 devtools;组件状态查看了半天找不到某个变量,可能是没在 data 里声明而是直接挂在 this 上,这种情况不是响应式的,也不该出现在 devtools 的 data 面板里,排查的时候要留意。
最后再分享一个我自己折腾这个项目时总结的经验:留守儿童身心关爱平台这类业务系统,功能上不追求花哨,但一定要把每一个常规操作打磨到位。比如档案列表加载时要有 loading 状态,提交表单要防止重复点击,删除操作要有二次确认弹窗,这些细节加起来就是项目质量的底气。技术栈方面,Node.js + Vue + ElementUI 的组合在中小型管理系统领域非常成熟,把这一套流程完整走一遍,相当于打通了 Web 全栈开发的基本功闭环,后面的项目不管用什么框架,都能很快迁移上手。
