跳转到内容

KEC 课程管理平台 · 前端体验优化方案(性能 / 视觉 / 交互) ​

  • 日期:2026-09-14
  • 代码版本:v1.23.1(分支 main,HEAD 2d045c6)
  • 审查范围: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 — 真卡顿源 ​

  1. views/query/CourseQuery.vue:95 — 无分页 el-table + 每格多次线性查找

    • :data="data" 为 getCourseQuery() 全量响应(api/query.js:31,接口不收分页参数);文件内无 el-pagination、无 max-height。课程量上百即整表 DOM 全渲染,且每行带 type="expand" 嵌套表。
    • 加重因素:每个学期格调用 getSemesterHours 4–5 次(:157,166,167,200,202,每次内部 .find())、getTextbooks 3 次(:169,171,200)→ 每渲染 O(行×学期×find)。
    • 修复:加客户端分页(slice + el-pagination,与兄弟查询页一致);预计算 Map<planId, Map<sem, {hours, textbooks}>> computed。模式现成:components/CourseMatrixTable.vue:263 的 textbookGrid。
  2. views/query/TeacherHoursHistory.vue:51 — 1000 个 el-option 全渲染

    • getTeachers({ page_size: 1000 }) 后在 filterable select 内渲染至多 1000 个 option(:21)。
    • 修复:el-select-v2(虚拟化,一行替换)或 :remote-method 服务端过滤。
  3. 全量课程/教材 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 ​

  1. views/teaching/TeachingStatistics.vue:309,310,314 — 每行 3 次嵌套循环

    • combinedUnitsOf(row) 在 #default 插槽内每行调 3 次,函数体为 details×classes 嵌套循环(:506-514)。pageSize=100 时每次渲染 300 次嵌套遍历。
    • 修复:在 filteredTeachers computed(:460)内一次性算好挂到行对象;或 WeakMap 记忆化(模式见 TeacherSelectDialog.vue:294-305)。
  2. components/CourseMatrixTable.vue:58-63 — 每格 4 次判定调用

    • isEditableCell(course, s) ×4 + cellClass ×1 / 格;课程×8 学期约 300–500 格/渲染。
    • 修复:预计算每格 flags 网格(同 textbookGrid 模式)。
  3. 全站 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 — 顺手修 ​

  1. 模板内联箭头函数(每次渲染重建)::index="(i) => (currentPage - 1) * pageSize + i + 1" — CollegeList.vue:24、CourseList.vue:51、MajorList.vue:24、TrainingLevelList:28、PlanList.vue:37。→ 提为 script 内具名函数。
  2. components/Layout.vue:216-220 — 3 个 computed 内 getComputedStyle(document.documentElement);Layout 永不卸载,且对 CSS 变量变更不响应。→ 读一次缓存或改 class 绑定。
  3. views/teaching/TeacherList.vue:316 — 全站唯一 deep: true watch(对象小、reload 已 debounce 300ms,影响有限)。→ 改对 8 个字段的 getter watch。
  4. views/textbook/TextbookList.vue:319-321 — globalIndex(row) 为 findIndex,每行调 3 次(:69,133,142)。→ 用插槽 $index。
  5. 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 vs gap: 8px ×11 vs gap: 6px ×17 vs gap: 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 全覆盖):

页面FilterBarEmptyStateListErrorStateuseCrudList
class/ClassListClassFilterBar(自封装)经 ClassTableYN(自有 useClassList)
teaching/TeacherListTeacherFilterBar(自封装)YYN
query/CourseQueryYYYN
query/TeacherLoadQueryYYYN
query/UnifiedSemesterQueryYY(顺序在 FilterBar 之前,偏离)YN
query/TeacherHoursHistoryYNNN
textbook/TextbookListN(手搓 .page-toolbar)YYN
plan/PlanListNYYN
course/CourseListNYYN
college/CollegeListNYYY
major/MajorListNYYY
trainingLevel/TrainingLevelListNYYY
system/UserManagementNYYN
system/AuditLogN(手搓 select)YYN
query/UnifiedTextbookQueryNYYN
query/PlanQueryNYYN
  • 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-skeleton 5 处(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)。→ 收敛为单一令牌。

五、交互易用性 ​

  1. 筛选联动:已有 debounce + v-loading,基础合格。补:导出/导入按钮绑定 :loading;表格刷新时分页区 skeleton,消除约 300ms"点了没反应"空窗。
  2. 三态补齐:views/query/TeacherHoursHistory.vue 为全站唯一 loading/空/错三态全裸页面,优先补齐;PlanQuery 用"—"文本占位(:54-63)替代 v-loading,统一。
  3. 操作列收敛:方案页每行 6 按钮 → 保留"编辑明细"主链接 + el-dropdown 收纳 上移/下移/派生新版本/删除。行内交互噪音减半。
  4. 点击/悬停反馈:已有 :active scale(0.97) + 主色 hover glow(global.css:44-50)、操作列 hover 浮现(global.css:801-813),保留;补表格行操作图标按钮 focus-visible 环。
  5. 矩阵页阅读顺序:legend 从"学期周数控制行下方"上移到矩阵正上方右侧(图例应紧贴其解释对象);原生横向滚动条换细轨样式。
  6. 空态 CTA:见 2.4。

六、落地排期(三批,均可独立发布) ​

批次内容工期风险
1设计方向五动作:海拔三层令牌、去斑马纹/行高 44、数字字体+右对齐、空态统一+CTA、筛选区容器化1–2 天零业务风险,纯 CSS/模板
2性能 P0+P1:CourseQuery 分页+预计算、4 处 select-v2、TeachingStatistics/Matrix 预计算、page-sizes 收 502–3 天需回归查询/统计页
3一致性收敛:间距/字号/阴影替换、FilterBar+useCrudList 迁移、操作列收敛、TeacherHoursHistory 三态、小离群逐项2 天逐页核对

批 1 为观感杠杆点:不动一行业务代码,第一节四类截图缺陷全部命中。


附:审查方法说明 ​

  • 实机截图环境:dev(client 5173 / server 3002),登录态由使用者提供;未改动任何数据。
  • 性能结论基于代码路径分析(调用次数 × 数据规模估算),未做 profiler 采样;P0 项的数据规模假设(课程/教师数百至千级)需以生产数据量复核优先级。
  • 一致性数字为全量 grep 计数(79 个 .vue 文件),可复算。