前端开发场景集 — 5个真实工作场景

5 Real-World Frontend Scenarios · Observable Problem → Solution Walkthrough

动态表单 权限系统 虚拟滚动 Keep-Alive 移动端调试 Vue 3 Element Plus Chrome DevTools

📋 本篇内容

精选5个前端真实工作场景,每个场景从需求 → 问题 → 方案 → 代码 → 效果完整走一遍。不是孤立的知识点,而是你在工作中真正会遇到的问题。

场景核心问题关键技术
📝 复杂动态表单30个字段联动、动态校验Vue3 watch、computed rules、自定义 validator
🔐 权限系统不同角色看不同菜单/按钮路由守卫、addRoute、自定义指令 v-permission
📜 万条数据长列表1万条告警数据卡死页面虚拟滚动、IntersectionObserver
📑 多标签页缓存切换页面后表单数据丢失keep-alive、include 动态管理
📱 移动端适配调试PC正常、手机一团糟响应式修复、DevTools 模拟、真机调试
场景一

📝 复杂动态表单 — 设备报修工单

场景 小李接到一个设备报修工单表单的需求,30个字段,字段之间有联动关系:选了不同设备类型,要显示不同字段;紧急程度选"高",要多填两个字段;选了报修人要自动填充……听起来简单,做起来全是坑。

1.1 需求分析

Figma — 设备报修工单表单设计稿
设备报修工单
设备类型 *
请选择设备类型 ▾
⬆ 选择不同设备类型后,显示对应字段
紧急程度 *
⚠ 高紧急时,显示"影响范围"和"已采取措施"
报修人 *
请选择报修人 ▾
↗ 选择报修人后自动填充:部门、联系方式
现场照片
📷
📷
+添加照片
最多9张,支持预览和删除
故障描述 *
请描述故障现象...
取消
提交工单

📋 联动需求清单

#联动项触发条件效果
1设备类型下拉选择不同类型显示对应类型专属字段
2紧急程度选择"高"显示"影响范围"+"已采取措施"字段
3报修人选择选择人员后自动填充部门、联系方式
4照片上传上传后最多9张,预览+删除
5提交前有修改未保存路由跳转时提醒
6提交后点击提交二次确认弹窗

1.2 搭组件结构

思路 30个字段不能全塞一个组件,要拆分。页面组件负责布局,表单组件负责字段逻辑,照片上传独立组件。
VS Code — src/views/repair/
📁 src/
EXPLORER
📂 src
📂 views
📂 repair
📄 RepairFormPage.vue
📂 components
📄 RepairForm.vue
📄 PhotoUploader.vue
📂 api
📄 repair.js
📂 utils
📄 validators.js
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
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>

关键的 watchcomputed 逻辑:

// 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
设备报修工单
设备类型 *
温度传感器 ▾
✓ 显示温度传感器专属字段
温度范围 *
-20°C ~ 80°C
安装位置 *
3号厂房B区
紧急程度 *
⚠ 高紧急 — 需额外填写
影响范围 *
整条产线停工

1.4 表单校验

用户点击"提交工单",但必填字段为空时:

Elements
Console
Console
Sources
Network
❌ el-form validation failed:
deviceType: "请选择设备类型"
reporter: "请选择报修人"
phone: "请输入联系方式"
description: "请描述故障"
tempRange: "请输入温度范围"
impactScope: "请填写影响范围"
ℹ validation run time: 12ms

浏览器中表单字段显示红色错误提示:

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 守卫
🌐
用户访问路由
已登录?
否 ↓
🚫
跳转登录页
是 ↓
👤
获取角色
📋
生成菜单
addRoute
渲染页面
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
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 }
场景三

📜 万条数据长列表 — 虚拟滚动

场景 告警记录页面有1万条数据,产品经理说"全部加载,用户要搜索和滚动浏览"。小李心想,1万条直接渲染?浏览器直接卡死啊。他决定用虚拟滚动来解决。
📜 虚拟滚动算法原理
📦 总高度容器 (phantom)
第1条
第2条
...
✅ 可视窗口
只渲染这几条
第9999条
第10000条
总高度 = 10000 × 40px
📐 核心算法:
scrollTop = 滚动偏移量
startIndex = scrollTop ÷ itemHeight
endIndex = startIndex + visibleCount
offsetY = startIndex × itemHeight
→ translateY(offsetY)
✅ 只渲染可视区域:~20个DOM节点
❌ 而非全部:10000个DOM节点
滚动时实时计算 translateY 偏移

3.1 问题复现 — 页面卡死

app.example.com/alarm/records
告警记录 共 10,000 条

⚠️ 页面未响应

页面可能正忙或已无响应。您可以等待其响应,或者关闭该页面。

等待 关闭页面

DevTools Performance 面板显示的长任务:

Performance
Memory
Rendering
Main Thread
1,847ms ⚠️
Task: render 10,000 list items — Long Task! (>500ms)
Layout
Reflow: calculating positions for 10,000 DOM nodes
Summary:
Scripting: 1,247ms | Rendering: 412ms | Painting: 188ms

3.2 原因分析

一次性渲染1万个DOM节点,浏览器扛不住。每个节点要创建元素、计算样式、布局、绘制,1万次循环下来主线程就卡死了。需要虚拟滚动——只渲染可视区域的元素。

📊 渲染成本对比

方式DOM节点数首次渲染滚动帧率内存占用
全量渲染10,0001,847ms8 FPS~120MB
虚拟滚动~2032ms60 FPS~15MB
全量渲染:10,000 DOM 节点
虚拟滚动:~20 DOM 节点(仅可视区域)

3.3 实现虚拟滚动

VirtualList.vue
VirtualList.vue
1<template>
2 <div
3 class="virtual-list"
4 ref="container"
5 @scroll="onScroll"
6 :style="{ height: containerHeight + 'px', overflow: 'auto' }"
7 >
8 <!-- 幽灵层:撑开总高度,让滚动条正确 -->
9 <div
10 class="virtual-list-phantom"
11 :style="{ height: totalHeight + 'px' }"
12 ></div>
13 <!-- 可视区域:只渲染可见的项 -->
14 <div
15 class="virtual-list-content"
16 :style="{ transform: `translateY(${offsetY}px)` }"
17 >
18 <div
19 v-for="item in visibleItems"
20 :key="item.id"
21 class="virtual-list-item"
22 :style="{ height: itemHeight + 'px' }"
23 >
24 <slot :item="item" />
25 </div>
26 </div>
27 </div>
28</template>

核心 JS 逻辑:

const containerHeight = 600 // 容器可见高度 const itemHeight = 40 // 每项高度 const visibleCount = Math.ceil(containerHeight / itemHeight) + 2 // 多渲染2个缓冲 // 滚动时计算可视范围 const onScroll = () => { const scrollTop = container.value.scrollTop startIndex = Math.floor(scrollTop / itemHeight) endIndex = startIndex + visibleCount offsetY = startIndex * itemHeight // 内容偏移 visibleItems = allItems.slice(startIndex, endIndex) }

3.4 效果对比

BEFORE — 全量渲染 10,000 节点
app.example.com/alarm/records
⚠️ 页面卡顿 — DOM 节点数: 10,000 | FPS: 8
ALM-0001 | 温度过高 | 🔴 紧急 | 2026-03-18 10:23
ALM-0002 | 压力异常 | 🟠 重大 | 2026-03-18 10:22
ALM-0003 | 设备离线 | 🟡 次要 | 2026-03-18 10:21
... 9,997 more items ...
ALM-9999 | 通信超时 | 🔵 提示 | 2026-01-02 08:01
ALM-10000 | 信号弱 | 🔵 提示 | 2026-01-01 12:00
内存: 120MB | DOM Nodes: 10,000
AFTER — 虚拟滚动 ~20 节点
app.example.com/alarm/records
✅ 流畅滚动 — DOM 节点数: ~20 | FPS: 60
ALM-0001 | 温度过高 紧急
ALM-0002 | 压力异常 重大
ALM-0003 | 设备离线 次要
ALM-0004 | 通信超时 提示
ALM-0005 | 信号弱 提示
可视区域边界 ↑
📜 滚动条完整,但 DOM 中只有这些节点
其余通过 translateY 偏移模拟
内存: 15MB | DOM Nodes: ~20

3.5 无限滚动加载(IntersectionObserver)

场景 不是一次性拿1万条,而是分批加载。用户滚动到底部时,自动加载下一页。用 IntersectionObserver 监听底部哨兵元素。
AlarmList.vue — 无限滚动
AlarmList.vue
1// 监听底部哨兵元素
2const sentinel = ref(null)
3const loading = ref(false)
4
5const observer = new IntersectionObserver((entries) => {
6 if (entries[0].isIntersecting && !loading.value) {
7 loadMore() // 哨兵进入视口 → 加载更多
8 }
9})
10
11onMounted(() => {
12 observer.observe(sentinel.value)
13})
14
15// 加载更多数据
16const loadMore = async () => {
17 loading.value = true
18 const newData = await alarmApi.getList({ page: page++, size: 50 })
19 list.value.push(...newData)
20 loading.value = false
21}
app.example.com/alarm/records
告警记录 已加载 150 / 10,000
ALM-0001 | 温度过高 紧急
ALM-0002 | 压力异常 重大
ALM-0003 | 设备离线 次要
ALM-0004 | 通信超时 提示
ALM-0005 | 信号弱 提示
ALM-0006 | 振动异常 重大
加载中...
⬆ 哨兵元素触发 IntersectionObserver
场景四

📑 多标签页缓存 — Keep-Alive

场景 用户在报修工单页面填了一半表单,切换到设备列表查看信息,再切回工单页——表单数据全没了!QA提了Bug,小李知道这是因为组件被销毁了,用 keep-alive 缓存即可。

4.1 问题重现

app.example.com/repair/create
首页
设备列表
报修工单
设备报修工单
设备类型
温度传感器 ▾
故障描述
设备温度持续偏高,已超过80°C阈值...
← 用户填了一半表单,现在点击"设备列表"标签...
⬇ 切换标签后 ⬇
app.example.com/device/list
首页
设备列表
报修工单
设备列表
编号名称状态
DEV-001温度传感器A在线
DEV-002压力传感器B故障
⬇ 再切回报修工单 ⬇
app.example.com/repair/create
首页
设备列表
报修工单
设备报修工单
😱
表单数据全部丢失!
设备类型、故障描述...全没了
设备类型
请选择 ▾
故障描述
请描述故障...
💬 Bug 反馈
王姐(QA)
表单数据丢失!切换页面后回来全清空了 🔴 P1 Bug
小李
这是因为组件被销毁了,用 keep-alive 缓存就好。组件不销毁,数据就还在。

4.2 实现 Keep-Alive 缓存

Layout.vue
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 缓存生效 — 组件未被销毁,数据保留
设备报修工单
设备类型
温度传感器 ▾
紧急程度
高 ✓
故障描述
设备温度持续偏高,已超过80°C阈值...
影响范围
整条产线停工
✓ 所有之前填写的数据都保留了!
💡 注意: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
设备编号
DEV-001
设备类型
温度传感器
安装位置
3号厂房B区
运行参数
当前温度: 75.2°C | 正常范围: -20°C ~ 80°C | 运行时长: 1,234h
编辑设备 删除设备 查看告警
iPhone 端 — 一团糟
9:415G 🔋
设备详情 — 温度传感器A
当前温度: 75.2°C | 正常范围: -20°C~80°C | 运行时长:1,234h ← 文字溢出!
编辑 删除 告警
⬆ 按钮太小,手指点不到!
← 图片溢出!
弹窗内容
⬆ 弹窗超出屏幕!
💬 Bug 反馈
王姐(QA)
iPhone 上页面好乱啊,字都挤到一起了,按钮也点不到,弹窗一半在屏幕外面 😫
小李
收到,我用 Chrome DevTools 设备模拟先看看,然后逐个修。

5.2 Chrome DevTools 设备模拟

Elements
Console
Device Mode
📱 393 × 852 ● Responsive DPR: 3x
9:415G 🔋
设备详情
设备编号
DEV-001
当前温度: 75.2°C | 正常范围: -20°C~80°C... ← overflow!
← DevTools 模拟 iPhone 14 Pro 视口
Media Query: max-width: 393px ● Active
快捷键 Cmd + Shift + M 切换设备模式,Cmd + Shift + C 检查元素。选中元素后,在 Styles 面板可以看到当前生效的媒体查询。

5.3 逐个修复

1

文字溢出 → word-break + text-overflow

运行参数那行文字太长,在窄屏上溢出容器。

Elements
div class="params-row"
  <!-- Before -->
  style="white-space: nowrap"问题!不换行

  <!-- After -->
  style=word-break: break-all; overflow: hidden; text-overflow: ellipsis;
.params-row - white-space: nowrap; + word-break: break-all; + overflow: hidden; + text-overflow: ellipsis;
2

按钮太小 → min-width: 44px(Apple HIG 规范)

Apple 人机交互指南规定,触摸目标最小 44×44pt。之前按钮太小,手指点不到。

Elements
button class="btn-action"
  <!-- Before -->
  padding: 4px 6px太小!触摸区域不足

  <!-- After -->
  min-width: 44px;
  min-height: 44px;
  padding: 8px 16px;
@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,虚拟滚动是可视状态裁剪,缓存是组件生命周期状态管理,响应式是视口状态适配。理解状态,就理解了一切。