📋 本篇内容
精选5个前端真实工作场景,每个场景从需求 → 问题 → 方案 → 代码 → 效果完整走一遍。不是孤立的知识点,而是你在工作中真正会遇到的问题。
| 场景 | 核心问题 | 关键技术 |
| 📝 复杂动态表单 | 30个字段联动、动态校验 | Vue3 watch、computed rules、自定义 validator |
| 🔐 权限系统 | 不同角色看不同菜单/按钮 | 路由守卫、addRoute、自定义指令 v-permission |
| 📜 万条数据长列表 | 1万条告警数据卡死页面 | 虚拟滚动、IntersectionObserver |
| 📑 多标签页缓存 | 切换页面后表单数据丢失 | keep-alive、include 动态管理 |
| 📱 移动端适配调试 | PC正常、手机一团糟 | 响应式修复、DevTools 模拟、真机调试 |
场景一
📝 复杂动态表单 — 设备报修工单
场景
小李接到一个设备报修工单表单的需求,30个字段,字段之间有联动关系:选了不同设备类型,要显示不同字段;紧急程度选"高",要多填两个字段;选了报修人要自动填充……听起来简单,做起来全是坑。
1.1 需求分析
Figma — 设备报修工单表单设计稿
设备报修工单
⬆ 选择不同设备类型后,显示对应字段
⚠ 高紧急时,显示"影响范围"和"已采取措施"
↗ 选择报修人后自动填充:部门、联系方式
📋 联动需求清单
| # | 联动项 | 触发条件 | 效果 |
| 1 | 设备类型下拉 | 选择不同类型 | 显示对应类型专属字段 |
| 2 | 紧急程度 | 选择"高" | 显示"影响范围"+"已采取措施"字段 |
| 3 | 报修人选择 | 选择人员后 | 自动填充部门、联系方式 |
| 4 | 照片上传 | 上传后 | 最多9张,预览+删除 |
| 5 | 提交前 | 有修改未保存 | 路由跳转时提醒 |
| 6 | 提交后 | 点击提交 | 二次确认弹窗 |
1.2 搭组件结构
思路
30个字段不能全塞一个组件,要拆分。页面组件负责布局,表单组件负责字段逻辑,照片上传独立组件。
VS Code — src/views/repair/
1// RepairFormPage.vue — 页面容器
2<template>
3 <div class="repair-page">
4 <PageHeader title="设备报修工单" />
5 <RepairForm
6 :initial-data="formData"
7 @submit="handleSubmit"
8 @cancel="handleCancel"
9 />
10 </div>
11</template>
12
13// RepairForm.vue — 核心表单逻辑
14// PhotoUploader.vue — 照片上传组件
核心组件 RepairForm.vue 的关键代码:
RepairForm.vue
1<template>
2 <el-form
3 ref="formRef"
4 :model="formData"
5 :rules="dynamicRules" ← 动态校验规则
6 label-width="100px"
7 >
8 <el-form-item label="设备类型" prop="deviceType">
9 <el-select v-model="formData.deviceType">
10 <el-option v-for="t in deviceTypes" :key="t.value" />
11 </el-select>
12 </el-form-item>
13
14 <!-- 动态字段:根据设备类型显示 -->
15 <template v-if="formData.deviceType === 'temp_sensor'">
16 <el-form-item label="温度范围" prop="tempRange">
17 <el-input v-model="formData.tempRange" />
18 </el-form-item>
19 <el-form-item label="安装位置" prop="installLocation">
20 <el-input v-model="formData.installLocation" />
21 </el-form-item>
22 </template>
23
24 <!-- 高紧急额外字段 -->
25 <template v-if="formData.urgency === 'high'">
26 <el-form-item label="影响范围" prop="impactScope">
<el-form-item label="已采取措施" prop="measuresTaken">
28 </template>
29
30 <PhotoUploader v-model="formData.photos" :max="9" />
31 </el-form>
32</template>
关键的 watch 和 computed 逻辑:
// watch:设备类型变化时,清除已填的专属字段
watch(() => formData.deviceType, (newVal, oldVal) => {
if (newVal !== oldVal && oldVal) {
// 清空上一个类型的专属字段
formData.tempRange = ''
formData.installLocation = ''
formData.pressureLevel = ''
formRef.value?.clearValidate()
}
})
// computed:动态校验规则
const dynamicRules = computed(() => {
const base = {
deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
reporter: [{ required: true, message: '请选择报修人', trigger: 'change' }],
phone: [
{ required: true, message: '请输入联系方式', trigger: 'blur' },
{ validator: phoneValidator, trigger: 'blur' } ← 自定义校验器
],
description: [{ required: true, message: '请描述故障', trigger: 'blur' }]
}
// 高紧急时追加必填
if (formData.urgency === 'high') {
base.impactScope = [{ required: true, message: '请填写影响范围', trigger: 'blur' }]
base.measuresTaken = [{ required: true, message: '请填写已采取措施', trigger: 'blur' }]
}
return base
})
// 自定义手机号校验器
const phoneValidator = (rule, value, callback) => {
const phoneReg = /^1[3-9]\d{9}$/
if (!phoneReg.test(value)) {
callback(new Error('请输入正确的手机号'))
} else {
callback()
}
}
1.3 联动效果演示
BEFORE — 未选择设备类型
localhost:5173/repair/create
AFTER — 选择了"温度传感器"
localhost:5173/repair/create
设备报修工单
✓ 显示温度传感器专属字段
⚠ 高紧急 — 需额外填写
1.4 表单校验
用户点击"提交工单",但必填字段为空时:
浏览器中表单字段显示红色错误提示:
localhost:5173/repair/create
自定义 validator 比 pattern 更灵活——可以写异步校验(比如检查设备编号是否已存在),也可以根据其他字段值做条件校验。
1.5 二次确认弹窗
场景
用户填完所有字段,点击"提交工单"。因为工单提交后无法修改,需要二次确认防止误操作。
localhost:5173/repair/create
⚠️ 确认提交
确认提交报修工单?
提交后无法修改。
// 提交逻辑
const handleSubmit = async () => {
const valid = await formRef.value?.validate()
if (!valid) return
// 二次确认
try {
await ElMessageBox.confirm(
'确认提交报修工单?提交后无法修改。',
'确认提交',
{ confirmButtonText: '确认提交', cancelButtonText: '取消', type: 'warning' }
)
// 用户点确认后,真正提交
await repairApi.create(formData)
ElMessage.success('工单提交成功')
router.push('/repair/list')
} catch {
// 用户取消,什么都不做
}
}
💡 未保存提醒也要做:如果用户填了表单但没提交就点别的页面,需要在路由守卫 beforeRouteLeave 里判断 formDirty,弹窗提醒"有未保存的修改,确定离开?"。
场景二
🔐 权限系统 — 不同角色看不同内容
场景
系统有三种角色:管理员、运维人员、普通用户。不同角色看到不同菜单、不同按钮。不只是页面级别的控制,按钮也要控制。小李从路由守卫到自定义指令,一套组合拳搞定。
2.1 需求确认
—— 10:15 ——
产
产品经理
权限需求确认一下:管理员看所有菜单,运维只看设备管理+告警,普通用户只看数据展示
李
小李
按钮也要控制吗?比如删除按钮只有管理员能看到?
产
产品经理
对,按钮也要。运维不能删除,只能新增和编辑。普通用户连编辑都不能,只能看。
李
小李
管理员:所有菜单 + 所有按钮
运维人员:设备管理+告警 + 新增/编辑按钮(无删除)
普通用户:数据展示 + 仅查看(无操作按钮)
2.2 路由守卫 — 动态路由
核心思路
登录后根据角色动态添加路由。不是在前端写死所有路由再隐藏,而是根据角色只注册该角色能访问的路由。
🔐 路由权限流程图 — beforeEach 守卫
router.beforeEach() 每次路由跳转前执行拦截 → 未登录重定向 /login → 已登录则动态注册路由并放行
router/guards.js
index.js
guards.js
routes.js
1// router/guards.js — 路由守卫
2router.beforeEach(async (to, from, next) => {
3 const userStore = useUserStore()
4
5 // 未登录 → 跳转登录页
6 if (to.meta.requiresAuth && !userStore.token) {
7 next({ name: 'Login' })
8 return
9 }
10
11 // 动态路由:根据角色添加路由
12 if (!userStore.routesLoaded) {
13 const routes = await userStore.generateRoutes()
14 routes.forEach(route => router.addRoute(route))
15 userStore.routesLoaded = true
16 next({ ...to, replace: true })
17 } else {
18 next()
19 }
20})
addRoute 是 Vue Router 4 的新 API,替代了旧的 addRoutes。登录后第一次访问页面时,根据角色从后端拿到路由配置,动态注册。这样用户刷新页面也不会丢路由。
2.3 按钮权限指令 — v-permission
思路
路由守卫管页面级别,按钮级别用自定义指令。没有权限的按钮直接从 DOM 移除,不只是隐藏,更安全。
directives/permission.js
1// directives/permission.js
2import { useUserStore } from '@/stores/user'
3
4export const vPermission = {
5 mounted(el, binding) {
6 const userStore = useUserStore()
7 const required = binding.value
8 if (!userStore.permissions.includes(required)) {
9 el.parentNode?.removeChild(el) ← 直接移除 DOM
10 }
11 }
12}
13
14// 注册
15app.directive('permission', vPermission)
使用方式:
<el-button v-permission="'device:add'">新增</el-button>
<el-button v-permission="'device:edit'">编辑</el-button>
<el-button v-permission="'device:delete'" type="danger">删除</el-button>
<el-button v-permission="'device:export'">导出</el-button>
2.4 不同角色看到的不同界面
管理员视图 — 所有按钮可见
app.example.com/device/list
设备列表
👤 管理员
+ 新增
✎ 编辑
✕ 删除
↓ 导出
| 设备编号 | 设备名称 | 状态 | 操作 |
| DEV-001 | 温度传感器A | 在线 | 编辑 删除 |
| DEV-002 | 压力传感器B | 故障 | 编辑 删除 |
运维人员视图 — 无删除按钮
app.example.com/device/list
设备列表
🔧 运维人员
+ 新增
✎ 编辑
✕ 删除
↓ 导出
❌ 无 device:delete 权限 → 删除按钮已移除
| 设备编号 | 设备名称 | 状态 | 操作 |
| DEV-001 | 温度传感器A | 在线 | 编辑 |
| DEV-002 | 压力传感器B | 故障 | 编辑 |
2.5 动态菜单 — 不同角色不同侧边栏
👤 普通用户菜单
📊 数据展示 ✓
❌ 设备管理
❌ 告警中心
❌ 报修工单
只能看数据,无操作权限
diff --git a/stores/user.js b/stores/user.js
// generateRoutes — 根据角色过滤路由
async generateRoutes() {
const role = this.role
+ const serverRoutes = await api.getMenuRoutes(role)
+ // 后端返回该角色可访问的路由配置
+ const accessedRoutes = filterAsyncRoutes(serverRoutes)
- const allRoutes = asyncRoutes // 旧写法:前端写死所有路由
- const accessedRoutes = filterByRole(allRoutes, role)
this.routes = accessedRoutes
return accessedRoutes
}
场景四
📑 多标签页缓存 — Keep-Alive
场景
用户在报修工单页面填了一半表单,切换到设备列表查看信息,再切回工单页——表单数据全没了!QA提了Bug,小李知道这是因为组件被销毁了,用 keep-alive 缓存即可。
4.1 问题重现
app.example.com/repair/create
设备报修工单
← 用户填了一半表单,现在点击"设备列表"标签...
⬇ 切换标签后 ⬇
app.example.com/device/list
设备列表
| 编号 | 名称 | 状态 |
| DEV-001 | 温度传感器A | 在线 |
| DEV-002 | 压力传感器B | 故障 |
⬇ 再切回报修工单 ⬇
app.example.com/repair/create
设备报修工单
😱
表单数据全部丢失!
设备类型、故障描述...全没了
王
王姐(QA)
表单数据丢失!切换页面后回来全清空了 🔴 P1 Bug
李
小李
这是因为组件被销毁了,用 keep-alive 缓存就好。组件不销毁,数据就还在。
4.2 实现 Keep-Alive 缓存
Layout.vue
1<template>
2 <div class="app-layout">
3 <Sidebar />
4 <div class="main-area">
5 <!-- 标签页栏 -->
6 <TabBar :tabs="visitedViews" />
7
8 <!-- 核心:keep-alive 包裹 router-view -->
9 <router-view v-slot="{ Component }">
10 <keep-alive :include="cachedViews">
11 <component :is="Component" />
12 </keep-alive>
13 </router-view>
14 </div>
15 </div>
16</template>
:include 是关键——不是所有页面都需要缓存。列表页需要缓存(保留搜索条件和滚动位置),详情页和结果页不需要。通过动态管理 cachedViews 数组来控制哪些组件被缓存。
4.3 缓存管理 — 打开标签页时添加,关闭时移除
// stores/tagsView.js
export const useTagsViewStore = defineStore('tagsView', () => {
const cachedViews = ref([]) // 缓存的组件名列表
const visitedViews = ref([]) // 标签页列表
// 添加缓存(打开标签页时调用)
function addCachedView(view) {
if (cachedViews.value.includes(view.name)) return
if view.meta?.noCache) return // 不需要缓存的页面跳过
cachedViews.value.push(view.name)
}
// 删除缓存(关闭标签页时调用)
function delCachedView(view) {
const idx = cachedViews.value.indexOf(view.name)
if (idx > -1) cachedViews.value.splice(idx, 1)
}
return { cachedViews, visitedViews, addCachedView, delCachedView }
})
缓存策略
| 页面类型 | 是否缓存 | 原因 |
| 列表页(设备、告警、工单) | 缓存 ✓ | 保留搜索条件、分页、滚动位置 |
| 表单页(新建工单) | 缓存 ✓ | 防止填一半丢失 |
| 详情页(设备详情) | 不缓存 ✕ | 每次进入要最新数据 |
| 结果页(提交成功) | 不缓存 ✕ | 不需要回看 |
4.4 效果 — 切换后数据保留 ✓
app.example.com/repair/create
✅ Keep-Alive 缓存生效 — 组件未被销毁,数据保留
设备报修工单
✓ 所有之前填写的数据都保留了!
💡 注意:被 keep-alive 缓存的组件不会触发 unmounted,取而代之的是 deactivated。重新激活时触发 activated。如果需要在切换回来时刷新数据,在 activated 钩子里做。
// 缓存组件的生命周期
onMounted(() => {
// 只在首次挂载时执行
fetchDetail()
})
onActivated(() => {
// 每次从缓存激活时执行
refreshIfNeeded() // 比如详情页需要刷新
})
onDeactivated(() => {
// 每次离开(但不是销毁)时执行
// 适合暂停定时器等
})
场景五
📱 移动端适配调试 — PC正常手机一团糟
场景
小李在 PC 上把页面做得漂漂亮亮,QA 在 iPhone 上一看——字挤在一起、按钮点不到、图片溢出、弹窗超出屏幕……经典问题。他打开 Chrome DevTools 设备模拟,逐个修复。
5.1 问题现象
PC 端 — 一切正常
app.example.com/device/detail/1
设备详情 — 温度传感器A
运行参数
当前温度: 75.2°C | 正常范围: -20°C ~ 80°C | 运行时长: 1,234h
编辑设备
删除设备
查看告警
iPhone 端 — 一团糟
9:415G 🔋
设备详情 — 温度传感器A
当前温度: 75.2°C | 正常范围: -20°C~80°C | 运行时长:1,234h ← 文字溢出!
编辑
删除
告警
⬆ 按钮太小,手指点不到!
王
王姐(QA)
iPhone 上页面好乱啊,字都挤到一起了,按钮也点不到,弹窗一半在屏幕外面 😫
李
小李
收到,我用 Chrome DevTools 设备模拟先看看,然后逐个修。
5.2 Chrome DevTools 设备模拟
快捷键
Cmd + Shift + M 切换设备模式,Cmd + Shift + C 检查元素。选中元素后,在 Styles 面板可以看到当前生效的媒体查询。
5.3 逐个修复
1
文字溢出 → word-break + text-overflow
运行参数那行文字太长,在窄屏上溢出容器。
.params-row
- white-space: nowrap;
+ word-break: break-all;
+ overflow: hidden;
+ text-overflow: ellipsis;
2
按钮太小 → min-width: 44px(Apple HIG 规范)
Apple 人机交互指南规定,触摸目标最小 44×44pt。之前按钮太小,手指点不到。
@media (max-width: 768px)
.btn-action
+ min-width: 44px;
+ min-height: 44px;
- padding: 4px 6px;
+ padding: 8px 16px;
3
图片溢出 → max-width: 100% + object-fit
图片比容器宽,溢出到右侧。加上 max-width: 100% 和 object-fit: cover。
.device-image
+ max-width: 100%;
+ height: auto;
+ object-fit: cover;
+ border-radius: 8px;
4
弹窗超出屏幕 → 移动端全屏弹窗
PC 端弹窗是居中浮层,但在手机上会超出屏幕底部。移动端改为全屏弹窗。
@media (max-width: 768px)
.modal-box
- width: 500px;
- margin: auto;
+ width: 100vw;
+ height: 100vh;
+ border-radius: 0;
+ max-height: 100vh;
+ overflow-y: auto;
5.4 真机调试 — Safari Web Inspector
提示
模拟器不完全等同于真机。有些问题只在真机上出现(比如 Safari 特有的 flex bug、iOS 橡皮筋滚动等)。真机调试很重要。
Step 1: iPhone 开启 Web Inspector
设置 → Safari → 高级 → 网页检查器 → 打开
Step 2: iPhone 用 Safari 打开页面
确保 iPhone 和 Mac 在同一 Wi-Fi 下,访问开发服务器地址(如 http://192.168.1.100:5173)
Step 3: Mac Safari 连接 iPhone
Mac Safari → 菜单栏「开发」→ 你的 iPhone 名字 → 选择页面
Step 4: Mac 上看到 iPhone 的 DevTools
Safari DevTools 在 Mac 上打开,实时显示 iPhone 页面的 DOM、Console、Network。操作 iPhone 页面,Mac 上实时更新。
Safari — Web Inspector — 小李的 iPhone
检查器
📄 Elements
📋 Console
🌐 Network
⚡ Timelines
📱 已连接
小李的 iPhone 15
iOS 19.4
<div class="device-detail">
<div class="params-row" style="...">
当前温度: 75.2°C | ...
</div>
<button class="btn-action">
⬆ 在 Mac 上选中元素,iPhone 上实时高亮
⬆ 在 Mac 上修改 CSS,iPhone 上实时更新
5.5 最终效果 — 移动端完美适配 ✓
修复前
9:415G 🔋
设备详情
当前温度: 75.2°C | 正常范围: -20°C~80°C | 运行时长:1,234h
↑ 文字溢出
编辑
删除
↑ 按钮太小
弹窗内容... 超出屏幕!
修复后 ✓
9:415G 🔋
设备详情
当前温度: 75.2°C
正常范围: -20°C~80°C
运行时长: 1,234h
✓ 文字自动换行
编辑
删除
✓ 按钮满足 44px 最小触摸区域
设备图片
✓ 图片自适应容器
✓ 弹窗全屏适配
✅ 移动端适配检查清单
| 检查项 | 修复方法 | 状态 |
| 文字溢出 | word-break + text-overflow | ✓ |
| 按钮太小 | min-width/height: 44px | ✓ |
| 图片溢出 | max-width: 100% + object-fit | ✓ |
| 弹窗超出 | 移动端全屏弹窗 | ✓ |
| 真机验证 | Safari Web Inspector | ✓ |
📋 五个场景总结
| # | 场景 | 核心问题 | 解决方案 | 关键代码/API |
| 1 |
📝 复杂动态表单 |
30字段联动、动态校验 |
watch + computed rules + 自定义 validator |
el-form :rules ElMessageBox.confirm |
| 2 |
🔐 权限系统 |
角色 → 菜单 + 按钮 |
路由守卫动态路由 + v-permission 指令 |
router.addRoute() app.directive() |
| 3 |
📜 万条数据长列表 |
1万DOM节点卡死页面 |
虚拟滚动 + IntersectionObserver |
translateY IntersectionObserver |
| 4 |
📑 多标签页缓存 |
切换页面数据丢失 |
keep-alive :include 动态管理 |
keep-alive onActivated |
| 5 |
📱 移动端适配调试 |
PC正常、手机一团糟 |
媒体查询 + DevTools模拟 + 真机调试 |
@media Safari Web Inspector |
💡 核心原则:所有场景的本质都是「状态管理」。表单联动是字段状态联动,权限是角色状态映射 UI,虚拟滚动是可视状态裁剪,缓存是组件生命周期状态管理,响应式是视口状态适配。理解状态,就理解了一切。