KEC 课程管理平台 · 前端体验优化方案(性能 / 视觉 / 交互)
- 日期:2026-09-14
- 代码版本:v1.23.1(分支
main,HEAD2d045c6) - 审查范围:
client/src(Vue 3.5 script-setup + Element Plus 2.14 + Pinia 3 + Vite 6),79 个.vue文件、约 42k 行 - 审查方式:实机截图(登录页 / Dashboard / 班级列表 / 课程查询 / 培养方案矩阵,dev 环境 5173+3002)+ 两轮并行全量代码审计(渲染性能、视觉一致性,结论均带 file:line)+ 设计令牌层逐行阅读
- 约束:不引入新 UI 库;不改业务逻辑与接口调用结构;仅 UI/交互层调整;兼容 Chrome/Edge/Firefox 90+;优先复用 Element Plus Design Token
执行摘要
核心结论:问题不是"没有设计规范",而是规范管住了颜色/圆角/间距,却没管"海拔、密度、空态、数据表现"这四个真正决定观感的维度。
styles/theme.css 的令牌体系(63 色、8pt 间距、三档圆角、四层阴影、五档字阶、动效曲线、WCAG 对比度注记、z-index 层级)在同类内部系统里属于上乘;硬编码颜色漂移仅 10 处(5 个不同值),品牌色单一来源 --brand-primary: #1c82f5,无散落的 #409EFF。弹窗宽度、表单 label-width、PageHeader 骨架、EmptyState/ListErrorState 覆盖率(15/16 页)均已收敛。
但实机截图暴露四类观感缺陷:全站单一海拔无焦点、空态为大白框灰字、表格为低密度电子表格审美、门面页(课程矩阵)关键信息被截断。性能侧存在 1 个真卡顿源(CourseQuery 无分页 + 每格多次 .find())与 4 处全量 option 渲染。
方案分三批落地,批 1 为零业务风险的纯 CSS/模板改造,可一次到位改善全站观感。
一、诊断:为什么"功能完整却显得平庸"
| # | 现象 | 实机证据 |
|---|---|---|
| 1 | 全站只有一个海拔:所有内容都是"白卡 + 1px 边 + shadow-sm"浮在 --bg-page 上,一屏之内没有视觉焦点 | Dashboard 首屏 6 个指标等宽等大排成一排(竖分隔线分列),5 张卡片视觉权重完全相同 |
| 2 | 空态 = 大白框 + 一行 13px 灰字:占大面积却零信息零引导 | Dashboard 5 张卡 4 张空态,约占首屏 70% 面积;带插画的 EmptyState 只用在列表页,看板用裸文本 |
| 3 | 表格是"2015 年电子表格"审美:斑马纹 + 约 52px 行高 + 14px 等权重正文 + 整列 - + 操作列每行 6 个按钮 | 班级页"专业"列全为 -;方案页每行 上移/下移/编辑明细/编辑信息/派生新版本/删除 六个控件 |
| 4 | 门面页信息被截断:课程矩阵每个格子都是"语文:基础模...",最关键的模块名读不到;下方约 200px 死白区 + 原生灰色横向滚动条悬在卡片中部 | 培养方案详情页首屏 |
| 5 | 登录页右卡大面积留白中仅两个灰输入框 + 一个按钮,品牌插画孤悬左下,卡片内部无节奏 | 登录页 |
结论:令牌层已完善,漂移率低;缺陷集中在令牌未覆盖的四个维度——海拔层级、信息密度、空态设计、数据可视化表现。
二、设计方向:Quiet chrome, loud data
不换库、不重刷皮肤(16 个页面已投产,重刷是纯风险)。外壳继续安静——浅侧栏、白卡、azure 主色保留——把全部视觉预算投给数据本身。五个签名动作:
2.1 海拔三层制(新增令牌,一处改全局生效)
| 层 | 用途 | 规格 |
|---|---|---|
| L0 | 页面底 | --bg-page |
| L1 | 容器卡 | 去 1px 边框、--shadow-xxs、--radius-md |
| L2 | 焦点卡 | --brand-primary-soft 底,或 --shadow-md + 1px --brand-primary-lighter 边 |
规则写入规范:一屏最多一个 L2。Dashboard 核心指标、矩阵页摘要栏是天然 L2 候选。
2.2 数字即主角
- Dashboard 指标条从"6 等分竖线分列"改为 1 核心 + 5 次要 非对称布局。
components/StatCard.vue已有core变体(soft 底 + 左色条 + 30px Archivo + grid-areas 重排),当前 Dashboard 未启用。 - 次要卡去掉竖分隔线,改 gap 排布。
- 所有表格数字列(课时/人数/版本/代码)切
--font-family-numbers+font-variant-numeric: tabular-nums+ 右对齐。这是数据产品最便宜的"专业感"。
2.3 表格去斑马纹、提密度
- 关闭
stripe,改 1px--border-light行分隔线;保留现有 soft hover(global.css:55-58)。 - 行高 52 → 44(信息密度约 +18%);表头维持 12px/600/
--bg-subtle浅底(global.css:61-65)。
2.4 空态全面升级
- 统一走
components/EmptyState.vue(插画 + 一句引导 + 可选 CTA),替换看板/矩阵页的裸文本。 - Dashboard 空卡给跳转 CTA(如"暂无排课数据 → 去排课")。
- 矩阵空课程组折叠为单行提示,不再保留约 200px 白区。
2.5 筛选区容器化
- 筛选控件包
--bg-subtle圆角容器(或加顶/底分隔线),与表格区明确分界。 - 状态类筛选(在读/合班/培养方案)与数据类筛选(名称/学院/专业)分组摆放,消除"8 个同款灰框漂浮"观感(班级页现状)。
三、渲染性能(按收益排序)
P0 — 真卡顿源
views/query/CourseQuery.vue:95— 无分页 el-table + 每格多次线性查找:data="data"为getCourseQuery()全量响应(api/query.js:31,接口不收分页参数);文件内无el-pagination、无max-height。课程量上百即整表 DOM 全渲染,且每行带type="expand"嵌套表。- 加重因素:每个学期格调用
getSemesterHours4–5 次(:157,166,167,200,202,每次内部.find())、getTextbooks3 次(:169,171,200)→ 每渲染 O(行×学期×find)。 - 修复:加客户端分页(slice +
el-pagination,与兄弟查询页一致);预计算Map<planId, Map<sem, {hours, textbooks}>>computed。模式现成:components/CourseMatrixTable.vue:263的textbookGrid。
views/query/TeacherHoursHistory.vue:51— 1000 个 el-option 全渲染getTeachers({ page_size: 1000 })后在filterableselect 内渲染至多 1000 个 option(:21)。- 修复:
el-select-v2(虚拟化,一行替换)或:remote-method服务端过滤。
全量课程/教材 option 列表同病(4 处)
views/plan/PlanDetail.vue:323(取全量)+:81(渲染全部课程 option)components/CourseEditPopover.vue:52(全部教材)views/teaching/components/TeacherFilterBar.vue:32、views/teaching/components/HourSettingsCard.vue:21(全部课程)- 修复:统一换
el-select-v2(:options数组传入)。
P1
views/teaching/TeachingStatistics.vue:309,310,314— 每行 3 次嵌套循环combinedUnitsOf(row)在#default插槽内每行调 3 次,函数体为 details×classes 嵌套循环(:506-514)。pageSize=100 时每次渲染 300 次嵌套遍历。- 修复:在
filteredTeacherscomputed(:460)内一次性算好挂到行对象;或 WeakMap 记忆化(模式见TeacherSelectDialog.vue:294-305)。
components/CourseMatrixTable.vue:58-63— 每格 4 次判定调用isEditableCell(course, s)×4 +cellClass×1 / 格;课程×8 学期约 300–500 格/渲染。- 修复:预计算每格 flags 网格(同
textbookGrid模式)。
全站 el-table 无
max-height/ 虚拟滚动;13 处分页允许 100 条/页el-table-v2出现 0 次。100 条/页 × 10–15 列(含 tag/tooltip/按钮)在 el-table 下 DOM 压力大。- 位置:CollegeList:91、CourseList:125、MajorList:91、TrainingLevelList:95、PlanList:176、TeacherList:186、TeachingStatistics:338、TeacherLoadQuery:364/523、UnifiedSemesterQuery:262、UnifiedTextbookQuery:193、ArrangeClassTable:175、ClassTable:156。
- 修复:
page-sizes上限收 50;>100 行场景(统计/查询页)评估迁移el-table-v2。
P2 — 顺手修
- 模板内联箭头函数(每次渲染重建):
:index="(i) => (currentPage - 1) * pageSize + i + 1"— CollegeList.vue:24、CourseList.vue:51、MajorList.vue:24、TrainingLevelList:28、PlanList.vue:37。→ 提为 script 内具名函数。 components/Layout.vue:216-220— 3 个 computed 内getComputedStyle(document.documentElement);Layout 永不卸载,且对 CSS 变量变更不响应。→ 读一次缓存或改 class 绑定。views/teaching/TeacherList.vue:316— 全站唯一deep: truewatch(对象小、reload 已 debounce 300ms,影响有限)。→ 改对 8 个字段的 getter watch。views/textbook/TextbookList.vue:319-321—globalIndex(row)为findIndex,每行调 3 次(:69,133,142)。→ 用插槽$index。components/AlertCard.vue:39,51—v-for内内联数组字面量:class="['alert-item', group.itemClass]"。→ 提为计算属性/字符串拼接。
已核实无需处理
- v-for key:约 110 处全部带
:key;仅 2 处 index key 且位于静态数组(Dashboard.vue:84 骨架v-for="i in 5";CompareTeachersDialog.vue:58 固定 2 元素)。 - 路由:25 个路由组件全懒加载;仅 Layout.vue 立即加载(外壳,正确)。
- 构建:Element Plus 按需引入(unplugin-vue-components + ElementPlusResolver,vite.config.js:15-21);manualChunks 拆分 element-icons/element-table/vue-vendor/axios;gzip 预压缩;main.js 仅注册 42 个图标。无包体问题。
- store/生命周期:无重同步计算;
stores/auth.js:25,36的 JSON.parse 为小 payload;AuditLog 详情解析仅在弹窗打开时;导入为服务端解析 Excel。
四、视觉一致性收敛(量化基线)
4.1 间距:37% 绕过令牌
var(--space-N)369 处 vs 硬编码 px 214 处。px 频次:6×47、2×32、10×31、8×30、4×24、12×16、3×14、14×14、20×10、16×6、1×5、18×4、32×3、24×3。- off-grid 值:2/3/6/10/14/18/22/34px。
- 同值双写:
gap: var(--space-2)×40 vsgap: 8px×11 vsgap: 6px×17 vsgap: 10px×9;混合写法padding: var(--space-4) 20px×3、padding: 10px 14px×5。 - theme.css:114-116 自立 off-scale 令牌:
--space-card: 20px、--space-card-insight: 18px、--space-card-lg: 28px。 - 动作:一次性替换收敛到
--space-*;三个 off-scale 令牌改为 24/16/32 或显式保留但冻结新增。
4.2 字号:约 45 处硬编码与令牌值完全重复
- 令牌使用:caption(12)×69、body-sm(13)×61、body(14)×46、micro(11)×20、h3(16)×12、display(24)×10、h2(20)×6、subtitle(15)×3、display-lg(32)×1。
- 硬编码重复值:16px×8、14px×7、13px×4、12px×4、20px×4;off-scale:10px×5、22px×4、18px×3、26px×2、19px×2、72/36/30px×1。
- 字重:令牌 ×132 vs 硬编码 600×5、800×2(800 不在令牌阶内)。
- 动作:纯替换,零风险。
4.3 圆角 / 阴影:低-中漂移
- 圆角:
--radius-sm×38、--radius-md×26、--radius-lg×2、50%×14、999px×7;游离值 9px×2、3px×2、12px×2、4px×1、22px×1。 - 阴影:
--shadow-sm×6、--shadow-md×4、--el-box-shadow-light×4(两套令牌体系混用)、--shadow-xs×2、glow×4、手写×4(0 8px 22px var(--brand-primary-shadow)×2 等)。 - 动作:阴影统一收敛到
--shadow-*单体系。
4.4 共享组件采用率(最大杠杆)
16 个列表/查询页骨架普查(PageHeader 16/16 全覆盖):
| 页面 | FilterBar | EmptyState | ListErrorState | useCrudList |
|---|---|---|---|---|
| class/ClassList | ClassFilterBar(自封装) | 经 ClassTable | Y | N(自有 useClassList) |
| teaching/TeacherList | TeacherFilterBar(自封装) | Y | Y | N |
| query/CourseQuery | Y | Y | Y | N |
| query/TeacherLoadQuery | Y | Y | Y | N |
| query/UnifiedSemesterQuery | Y | Y(顺序在 FilterBar 之前,偏离) | Y | N |
| query/TeacherHoursHistory | Y | N | N | N |
| textbook/TextbookList | N(手搓 .page-toolbar) | Y | Y | N |
| plan/PlanList | N | Y | Y | N |
| course/CourseList | N | Y | Y | N |
| college/CollegeList | N | Y | Y | Y |
| major/MajorList | N | Y | Y | Y |
| trainingLevel/TrainingLevelList | N | Y | Y | Y |
| system/UserManagement | N | Y | Y | N |
| system/AuditLog | N(手搓 select) | Y | Y | N |
| query/UnifiedTextbookQuery | N | Y | Y | N |
| query/PlanQuery | N | Y | Y | N |
- FilterBar 采用 4/16、useCrudList 采用 3/16,6 页手搓
.page-toolbar。 - 动作:迁移手搓页到 FilterBar/useCrudList,使"新增页面自动合规"。
4.5 小离群(逐项修)
- label-width:规范 100px×16 处;离群 PlanNewVersionDialog.vue:10(130)、CompareTeachersDialog.vue:20(90)、TeacherDetail.vue:59(110)。
- TeacherSelectDialog.vue:7 宽度
isMobile ? '95%' : '80%',未走--dialog-width-*令牌。 - AuditLog.vue:5 的
#extra用type="danger",其余页#extra全 primary。 - 导入/导出按钮 3 种摆放习语:
.page-toolbar内(TextbookList:45-49、CourseList:32-36)/ FilterBar 抽屉 footer(ClassFilterBar:116-120、UnifiedSemesterQuery:110-114)/ 独立 BatchBar。→ 统一进.page-toolbar .action-buttons。 - loading 习语混用:
v-loading为主,el-skeleton5 处(Dashboard、TeachingStatistics、TeacherLoadQuery:169、TeacherDetail、TeacherHoursArchive)。→ 规范二选一:表格v-loading、看板卡el-skeleton。 - 内联 style 集中:TeacherFormDialog.vue ×9、MatrixLegend.vue ×6。→ 提为 class。
#fff×5 同规则复制粘贴(ClassTable.vue:305、TeachingStatistics.vue:648、ArrangeClassTable.vue:262、TeacherLoadQuery.vue:648、TeacherHoursArchive.vue:537)。→ 收敛为单一令牌。
五、交互易用性
- 筛选联动:已有 debounce +
v-loading,基础合格。补:导出/导入按钮绑定:loading;表格刷新时分页区 skeleton,消除约 300ms"点了没反应"空窗。 - 三态补齐:
views/query/TeacherHoursHistory.vue为全站唯一 loading/空/错三态全裸页面,优先补齐;PlanQuery 用"—"文本占位(:54-63)替代 v-loading,统一。 - 操作列收敛:方案页每行 6 按钮 → 保留"编辑明细"主链接 +
el-dropdown收纳 上移/下移/派生新版本/删除。行内交互噪音减半。 - 点击/悬停反馈:已有
:active scale(0.97)+ 主色 hover glow(global.css:44-50)、操作列 hover 浮现(global.css:801-813),保留;补表格行操作图标按钮focus-visible环。 - 矩阵页阅读顺序:legend 从"学期周数控制行下方"上移到矩阵正上方右侧(图例应紧贴其解释对象);原生横向滚动条换细轨样式。
- 空态 CTA:见 2.4。
六、落地排期(三批,均可独立发布)
| 批次 | 内容 | 工期 | 风险 |
|---|---|---|---|
| 1 | 设计方向五动作:海拔三层令牌、去斑马纹/行高 44、数字字体+右对齐、空态统一+CTA、筛选区容器化 | 1–2 天 | 零业务风险,纯 CSS/模板 |
| 2 | 性能 P0+P1:CourseQuery 分页+预计算、4 处 select-v2、TeachingStatistics/Matrix 预计算、page-sizes 收 50 | 2–3 天 | 需回归查询/统计页 |
| 3 | 一致性收敛:间距/字号/阴影替换、FilterBar+useCrudList 迁移、操作列收敛、TeacherHoursHistory 三态、小离群逐项 | 2 天 | 逐页核对 |
批 1 为观感杠杆点:不动一行业务代码,第一节四类截图缺陷全部命中。
附:审查方法说明
- 实机截图环境:dev(client 5173 / server 3002),登录态由使用者提供;未改动任何数据。
- 性能结论基于代码路径分析(调用次数 × 数据规模估算),未做 profiler 采样;P0 项的数据规模假设(课程/教师数百至千级)需以生产数据量复核优先级。
- 一致性数字为全量 grep 计数(79 个 .vue 文件),可复算。