用 Playwright 对移动端逐页截图分析后,发现了一系列严重的可用性问题——表格截断、筛选栏占满首屏、暗黑模式下导航栏刺眼白色。一天时间,13 个文件,+2400 行代码,完成了 4 个页面的移动端重构和邮箱自助修改功能。本文记录整个分析和修复过程。
一、Playwright 驱动的问题发现
这次优化的起点不是用户反馈,而是自动化截图分析。我们编写了一套 Playwright 脚本,在 iPhone 14 (390px) 和 375px 两种视口下对所有前台页面截图,然后逐帧对比分析问题。
核心发现:
| 页面 | 问题 | 严重度 |
|---|---|---|
| 资格统计 | 10 列表格只能看到 3 列,核心数据全部丢失 | 致命 |
| 管理员评论 | el-table 严重截断,评论内容/评分不可见 | 致命 |
| 前台评论 | 筛选栏占首屏 60%,内容不可见 | 高 |
| 排行榜 | 筛选栏占首屏 70% | 高 |
| 底部导航栏 | 暗黑模式下仍为白色背景 | 中 |
| 移动端 header | 没有暗黑模式切换入口 | 中 |
这个分析方法本身就是一个值得分享的经验:不要靠感觉判断移动端适配质量,用真实设备视口的截图说话。
二、可折叠筛选栏:一行摘要 + 展开面板
2.1 问题:筛选栏吞掉了整个首屏
移动端纵向排列 4-6 个筛选字段 + 按钮,高度轻松超过 400px。在 844px 高度的 iPhone 14 上,减去 header (44px) 和 tabbar (62px),可用高度只有 738px——筛选栏直接占了一半以上。
2.2 方案:双模式切换
我们实现了「收起态」和「展开态」两种模式:
<!-- 收起态:一行摘要 -->
<div v-if="!filterExpanded" class="filter-summary" @click="filterExpanded = true">
<div class="filter-tags">
<el-tag v-if="queryParams.competitionId" size="small" closable
@close.stop="clearSingleFilter('competitionId')">
</el-tag>
<!-- 其他激活的筛选条件 Tag... -->
<span v-if="!hasActiveFilters" class="no-filter">全部评论</span>
</div>
<el-button text type="primary" size="small">
<el-icon><Filter /></el-icon>筛选
</el-button>
</div>
<!-- 展开态:完整表单 -->
<FilterBar v-if="filterExpanded" class="filter-container-mobile">
<!-- 筛选字段 + 重置/完成按钮 -->
</FilterBar>
关键设计决策:
- Tag 可单独关闭:点击 Tag 上的 × 可直接清除某个筛选条件,无需展开面板
- 桌面端不受影响:通过
.desktop-filter/.mobile-filter的display: none在 768px 断点切换 - 小说类型用 Chip 按钮组:从
el-select改为el-check-tag,减少一次点击
效果:首屏从只能看到筛选栏 → 直接看到 3 张评论卡片 + FAB 按钮。
三、移动端卡片视图:替代不可用的表格
3.1 资格统计页:从零到完整
修复前,10 列表格在移动端只能看到 ID、用户名和操作按钮——笔名、届次、等效评论、实际评论、总字数、资格状态、更新时间全部丢失。
我们参照前台评论页已有的 desktop-table / mobile-card-list 双视图模式,为资格统计页设计了卡片布局:
<div class="stat-card">
<div class="card-header">
<div class="card-user">
<span class="user-name"></span>
<span class="user-id"></span>
</div>
<el-tag :type="row.isQualified ? 'success' : 'danger'" size="small">
</el-tag>
</div>
<div class="card-body">
<!-- 2×2 网格:届次、等效评论、实际评论、总字数 -->
</div>
<div class="card-footer">
<span class="update-time"></span>
<el-button link type="primary" size="small" @click="handleRecalculateUser(row)">重算</el-button>
</div>
</div>
3.2 管理员评论页:同样的方案
管理员评论页的 el-table 也严重截断,用相同思路添加了卡片视图,每张卡片展示标题+类型、笔名/届次/评分、评论预览、编辑/删除操作。
四、排行榜资格详情:从 80% 抽屉到全屏重构
4.1 问题
点击排行榜中的评论者名字,会打开一个 80% 宽度的右侧抽屉显示资格详情。这个抽屉在移动端有多个致命问题:
- 5 列统计卡片网格崩塌
- 7 列明细表格完全截断
- 没有关闭按钮
- 左侧露出排行榜内容,视觉干扰
4.2 方案:条件渲染两套抽屉
<!-- 桌面端:保持原有右侧抽屉 -->
<el-drawer v-if="!isMobile" v-model="showDetail" size="80%">
<!-- 桌面版内容不变 -->
</el-drawer>
<!-- 移动端:全屏底部抽屉 -->
<el-drawer v-if="isMobile" v-model="showDetail" direction="btt" size="100%">
<div class="mobile-detail-header">
<el-button text @click="showDetail = false">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<span>资格详情</span>
</div>
<div class="mobile-detail-body">
<!-- 状态卡片:用户名 + 届次 + 资格Tag -->
<!-- 统计 2列网格:等效/字数/总评/深评 -->
<!-- 综合检查:列表 + 通过/未过 Tag -->
<!-- 明细:卡片列表替代 7 列表格 -->
</div>
</el-drawer>
关键改动:
- 统计从 5 列 → 2 列网格
- 明细表格 → 每条一张卡片(标题+类型/字数+权重+深评+累计)
- 综合检查从嵌套括号文字 → 结构化列表 + Tag
- 标题从「爱丽丝-第5届-未参赛」纯文字 → 用户名大字 + 届次小字 + 状态 Tag
五、暗黑模式补全
5.1 底部导航栏
暗黑模式下 tabbar 仍为 rgba(255, 255, 255, 0.96)——在深色页面上非常刺眼。
由于 FrontLayout 使用了 <style scoped>,html.dark 选择器不生效。解决方案是新增一个非 scoped 的 <style> 块:
<style>
html.dark .front-layout .mobile-tabbar {
background: rgba(30, 30, 46, 0.96);
border-top-color: rgba(140, 150, 200, 0.15);
}
</style>
5.2 移动端暗黑切换入口
移动端 header 只有标题和用户头像,没有 ThemeSwitch。直接在 .mobile-actions 中添加已有的 <ThemeSwitch /> 组件即可。
六、邮箱自助修改功能
6.1 需求
原来修改邮箱需要联系管理员。我们实现了验证码自助修改流程。
6.2 后端:两个新 API
// POST /api/users/profile/email/send-code
func SendEmailChangeCode(c *gin.Context) {
// 1. 校验新邮箱 ≠ 当前邮箱
// 2. 校验新邮箱未被其他用户注册(唯一性)
// 3. 复用 EmailService.SendVerificationCode(newEmail, "change_email")
}
// PUT /api/users/profile/email
func ConfirmEmailChange(c *gin.Context) {
// 1. EmailService.VerifyCode 验证验证码
// 2. 再次校验唯一性(防并发)
// 3. 更新邮箱
}
复用了现有的 EmailService,无需新建任何服务。唯一性校验做了两次——发送时一次(用户体验),确认时再一次(安全防并发)。
6.3 前端交互
邮箱区域从 disabled input 改为纯文本 + 「修改」链接,点击后展开验证码流程:
admin@laping.test 修改 ← 默认态
↓ 点击修改
[请输入新邮箱 ] [发送验证码] ← 编辑态
[请输入验证码 ] ← 发送后出现
[取消] [确认修改]
60 秒倒计时防止频繁发送,cancelEmailEdit() 可随时退出。
七、个人信息页 UI 优化
7.1 表单 label 上下布局
移动端 640px 以下,表单 label 从左侧 100px 固定宽度改为位于输入框上方,输入框全宽。用 :deep() 覆盖 Element Plus 默认布局。
7.2 密码卡片可折叠
修改密码三个输入框默认展示占据大量空间。改为移动端默认收起,点击标题展开:
<div class="clickable-header" @click="passwordExpanded = !passwordExpanded">
<span>修改密码</span>
<el-icon :class="{ 'is-rotated': passwordExpanded }"><ArrowDown /></el-icon>
</div>
<div v-show="passwordExpanded || !isMobileView">
<!-- 密码表单 -->
</div>
桌面端通过 || !isMobileView 始终展示,箭头图标用 CSS 隐藏。
八、样式一致性统一
跨 4 个页面的可折叠筛选栏最初存在不一致:
- label 最小宽度:50px / 60px / 70px / 80px
- label 文字:「届次」vs「比赛届数」,「类型」vs「小说类型」
- 完成按钮:有的带 Search 图标,有的不带
统一标准后:
min-width: 70px- 同名字段用相同文字(比赛届数、小说类型、笔名、小说标题)
- 完成按钮统一
type="primary"蓝色,不带图标 - 小说类型统一使用
el-check-tagChip 按钮组
总结
- Playwright 截图分析法:不要靠感觉判断移动端适配,用真实视口截图发现问题比手动测试更高效
- 双视图模式:
desktop-table+mobile-card-list是表格页面移动端适配的通用方案 - 可折叠筛选:移动端首屏寸土寸金,筛选栏必须可以收起
- scoped 与 html.dark:暗黑模式样式需要放在非 scoped
<style>块中 - 条件渲染两套布局:
v-if="isMobile"比 CSS 媒体查询更灵活,适合结构差异大的场景 - 复用现有服务:邮箱修改功能复用了已有的 EmailService,零新依赖
- 统一设计规范:跨页面组件样式必须从第一天就定标准,否则越做越散