班主任工作台 · 技术文档
面向维护 / 二次开发的完整说明。日常使用请看 《快速上手》 。
1. 概览
| 项目 | 说明 |
|---|---|
| 类型 | 纯前端单页工具,无后端、无接口、无账号体系 |
| 技术栈 | 原生 JS(ES5 风格 + 少量 ES6)、原生 CSS、零框架零构建 |
| 访问 | https://www.sakaay.com/class-workbench.html |
| 数据持久化 | 浏览器 localStorage(主数据)+ sessionStorage(路由) |
| 外部依赖 | SheetJS(Excel 读写),CDN 优先、本地兜底 |
设计原则
- 配置驱动:所有模块、子表、字段都在
class-workbench-config.js里声明,改结构不用改逻辑。 - 单一数据源:花名册(
roster)是唯一的学生来源,其余模块通过"学生选择器"引用,禁止手输姓名。 - 数据隔离:多班级按
classData分桶存储,班级之间完全独立。 - 防丢数据:自动快照 + 手动备份;备份整体导出
state,新增顶层字段自动纳入,不会漏备。
2. 文件结构
全部位于 docs/public/(该目录下的文件会原样部署到站点根)。
| 文件 | 体积 | 职责 |
|---|---|---|
class-workbench.html | 3 KB | 页面骨架:侧边栏 / 顶栏 / 内容区容器、SheetJS 异步加载策略 |
class-workbench.css | 40 KB | 全部样式(CSS 变量集中在 :root) |
class-workbench-config.js | 21 KB | 配置中心:标签分组、模块树、子表、字段定义 |
class-workbench.js | 89 KB | 主逻辑:数据层、多班级、路由、通用 CRUD、筛选、备份、版本、隐私遮罩 |
class-workbench-views.js | 240 KB | 特殊视图:仪表盘、课程表、成绩分析、宿舍、值日、待办等 |
seat-map/seat-map.html | 112 KB | 座位表工具(独立应用,通过 iframe 嵌入) |
vendor/sheetjs.min.js | — | SheetJS 本地兜底(CDN 不可达时使用) |
加载顺序(强依赖,不可调换)
1<script src="class-workbench-config.js"></script> <!-- 提供 window.WB_CONFIG -->
2<script src="class-workbench.js"></script> <!-- 主逻辑,挂载 window.WB -->
3<script src="class-workbench-views.js"></script> <!-- 特殊视图,依赖 window.WB -->
views.js 首行即 var WB = window.WB;,一旦顺序错了整个页面白屏。
主逻辑通过 window.WB 对外暴露,供 views.js 调用:escapeHtml、uid、today、showToast、openModal、closeModal、state、navigate、getTable、saveState、openForm、privacyOn、maskText。
3. 运行机制与数据流
3.1 启动流程
1init()
2 ├─ migrateRosterTags() 花名册旧字段(position/talent)合并进 tags
3 ├─ bindGlobal() 顶栏按钮:版本 / 备份 / 恢复 / 隐私
4 ├─ updatePrivacyBtn() 同步隐私按钮状态
5 └─ render() 按当前路由渲染
3.2 渲染流程
1render()
2 ├─ updateCrumb() 面包屑
3 ├─ renderSidebarActive() 侧边栏(含条数徽标、折叠状态)
4 └─ 按 route.view 分发
5 ├─ dashboard → WB_VIEWS.renderDashboard() + bindDashboard()
6 ├─ schedule → WB_VIEWS.renderSchedule() + bind…
7 ├─ dailySchedule → WB_VIEWS.renderDailySchedule()
8 ├─ grades → WB_VIEWS.renderGrades()
9 ├─ todo → WB_VIEWS.renderTodo()
10 ├─ table → renderTablePage(route.table) + bindTablePage() ← 通用 CRUD
11 └─ 其它 → "未找到页面"
重绘约定:多数视图用 rebindRoot()(克隆替换 #content)清掉旧的根级监听,避免事件重复累积。局部刷新(如筛选、勾选完成)只重写 tbody 或局部容器,不整页重绘。
3.3 路由
路由对象存 sessionStorage['wb_current'],形如:
1{ view: 'table', module: 'students', table: 'roster' }
2{ view: 'grades', param: 'exam:期末考试' }
navigate(view, opts) 写路由并触发 render()。刷新页面会停留在上一个模块。
4. 存储设计
4.1 使用的 key
| key | 存储 | 用途 |
|---|---|---|
wb_v1 | localStorage | 主数据(全部班级、表格、成绩、课表、待办、快照) |
wb_current | sessionStorage | 当前路由 |
wb_privacy | localStorage | 隐私模式开关('1' 开启),刷新保持 |
wb_seatmap_roster | localStorage | 工作台 → 座位表 的花名册同步通道 |
seat_v3_classes | localStorage | 座位表班级索引({ list:[…], cur }) |
seat_v3_c_{id} | localStorage | 座位表单个班级数据 |
seat_v2_data | localStorage | 座位表旧版数据(兼容) |
4.2 主数据结构
1state = {
2 tables: { roster: [], contacts: [], mental: [], … }, // 所有业务子表
3 grades: { exams: [], scores: {} }, // 考试批次 + 分数
4 todo: { items: [], archiveLog: [] },
5 schedule: { days, periods, grid, subjects, teachers, mode },
6 teacherSched: { days, periods, subjects, teachers, cur }, // 教师个人课表(独立)
7 versions: [], // 版本快照,上限 60
8 collapsedGroups: {}, // 侧边栏折叠状态
9 quickWords: [], // 仪表盘百度快捷词
10 classes: { list: [{ id, name }], cur }, // 班级列表
11 classData: { [classId]: { tables, grades, schedule, todo, versions, collapsedGroups } }
12}
4.3 多班级
- 顶层是"当前班级"的工作副本;
classData[classId]是各班的持久化桶。 - 切换 / 新建 / 删除前必须调
saveCurClass(),把顶层副本写回桶,否则丢失改动。 loadClass(id):保存当前 → 切cur→ 从桶恢复 → 为配置中新增但桶里缺失的表补空数组 → 重置路由到仪表盘。- 旧版(单班级)数据首次启动由
migrateClasses()自动收进「默认班级」桶。
5. 配置中心:class-workbench-config.js
5.1 模块树
1window.WB_CONFIG = {
2 version: 1,
3 tagGroups: [ … ],
4 modules: [ … ]
5}
modules 一级项的 type 决定渲染方式:
| type | 含义 | 渲染去向 |
|---|---|---|
dashboard | 首页仪表盘 | WB_VIEWS.renderDashboard |
schedule | 课程表 | WB_VIEWS.renderSchedule(内含三模式) |
dailySchedule | 作息时间表 | WB_VIEWS.renderDailySchedule |
grades | 成绩分析 | WB_VIEWS.renderGrades |
todo | 待办备忘录 | WB_VIEWS.renderTodo |
group | 分组容器 | 渲染子项,走通用 CRUD 表格 |
group 下的 subs 每项只要有 fields,就是一个标准数据表;没有 fields 的走特殊视图(如 dorm 床位画布、duty 周视图)。
5.2 字段定义
1{ name: 'phone', label: '联系电话', type: 'text', required: true, placeholder: '…' }
| 属性 | 说明 |
|---|---|
type | text / select / date / textarea / tags |
options | select 的可选项数组 |
required | 必填(表单校验) |
default | 新增时的默认值 |
full | 表单里独占一行 |
placeholder / hint | 输入提示 / 字段说明 |
picker | 'student' 强制用学生选择器;'none' 禁用 |
list | false 表示不在列表里单独成列(如通讯录学号,与姓名同格显示) |
tags 类型会渲染分组标签编辑器,并按 tagGroups 取色。
5.3 标签系统
tagGroups 定义分组与配色(班内职务 / 才艺特长 / 个人性格 / 健康信息 / 关注标记 / 自定义)。
数据里 tags 是扁平数组,渲染时反查归属分组取色;不在任何预置组的标签归入「自定义」。
5.4 增删改模块
- 加子表:在对应
group的subs里追加{ id, icon, label, fields: [...] },启动即自动建空数组、进侧边栏、拥有完整 CRUD / 筛选 / 导入导出 / 备份。 - 加字段:往
fields里加一项,历史数据该字段为空,渲染显示—,无需迁移脚本。 - 加一级模块:追加
modules项;自定义type需要同时在render()和itemClickHandler()里加分支,并在views.js里实现render*/bind*。
6. 主逻辑:class-workbench.js
6.1 数据层
| 函数 | 说明 |
|---|---|
loadState() / persist() | 读 / 写 wb_v1,写入失败会 toast 提示(容量满或被禁用) |
saveState() | persist() + autoSnapshot()(日常保存走它) |
getTable(id) | 取表(不存在则建空数组) |
findTableDef(id) / findModule(id) | 反查配置定义 |
createVersion()内部只persist()不调saveState(),避免递归。
6.2 通用 CRUD(表格页)
renderTablePage(tableId)生成卡片:工具栏 → 筛选栏 → 批量操作条 → 表格。- 单元格渲染统一走
cellHtml(tableId, field, row),首次渲染和筛选后重绘共用它,保证表现一致(隐私打码也只在这一个出口做)。 - 姓名列走
renderNameCell():通讯录显示「学号 · 姓名」并给重名学生加×N角标。 - 筛选(
applyFilter):关键词全字段模糊 + 第一个select字段作分类 + 第一个date字段作日期范围 +tags字段作标签云(需同时拥有所选标签)。筛选状态存在filterState[tableId],模块内记忆。 - 选中态
selectedRows[tableId]是索引Set;批量删除按索引从大到小splice,避免索引漂移;删除后清空选中。
6.3 学生选择器
isStudentPickerField() 判定:picker:'student' 显式声明,或字段名为 name 且标签含"学生/姓名"(花名册自身除外)。
选择器数据源只来自花名册,只读不允许手输;可以在选择器里「+ 新建学生」,直接写入花名册。
6.4 版本快照
| 函数 | 说明 |
|---|---|
createVersion(name, auto) | 深拷贝 tables / grades / todo / schedule 存一份 |
autoSnapshot() | 距上一个自动快照超过 10 分钟才生成 |
restoreVersion(id) | 覆盖当前班级数据(二次确认),并 saveCurClass() 同步回桶 |
上限 60 条,超出丢弃最旧的。快照不含座位表。
6.5 备份 / 恢复
导出结构:
1{ version, backupTime, state, seatMap: { index, classes, legacy } }
- 备份:整体导出
state(新增顶层业务字段自动纳入,不会漏备)+ 座位表独立存储(按索引收集,并兜底扫描seat_v3_c_前缀的 key,跳过历史脏 key)。 - 恢复兼容三种格式:
- 新版
{ state: {...} } - 旧版顶层平铺
{ tables, grades, … } - 座位表工具导出的
{ source: 'seat-map', index }(只还原座位表,不动工作台数据)
- 新版
- 恢复后会为配置中新增、备份里缺失的表补空数组,避免渲染报错。
6.6 座位表数据回写(applySeatMap)
三步,缺一不可:
- 清空所有
seat_v3_c_*旧 key(防脏 key 累积) - 只按索引里的 id 回写(丢弃备份中的脏 key)
- 写回索引并修正
cur指向有数据的班级
少了第 3 步,若
cur指向无数据的班级,座位表打开就是整页空白。
7. 特殊视图:class-workbench-views.js
| 视图 | 要点 |
|---|---|
| 仪表盘 | 统计卡 + 本周待办 + 本周日程 + 待沟通家长 + 重点学生 + 材料倒计时;标题可点击跳对应模块;内置百度搜索(快捷词可增删并持久化) |
| 课程表 | 三种模式:班级课表(grid)/ 任课表(teacher)/ 教师个人课表(personal)。点击格子录科目与任课教师,支持自定义周次、编辑时段、课时统计、清空、导入导出 |
| 作息时间表 | 自定义午别 / 节次 / 各年级时间,支持导入导出与批量修改 |
| 成绩分析 | 先建考试批次再录分;支持名次、跨批次趋势、各班各科对比、达标/分数线分析 |
| 住宿信息 | 默认床位画布(可视化分配),可切回表格模式批量导入 |
| 值日排班 | 周视图;卫生评价自动生成积分(优 +2 / 良 +1 / 中 0 / 差 −1),可手动微调 |
| 待办备忘录 | 完成后可「📦 归档」到任意模块,字段按待办内容自动预填,归档去向可追溯 |
课程表的两套数据(重要)
- 班级课表:
state.schedule - 教师个人课表:
state.teacherSched(独立于schedule,互不同步)
两套各自维护 days / periods / subjects / teachers,改动互不影响,这是刻意设计。
8. 座位表集成
座位表是独立应用,通过 iframe 嵌入,不是工作台的一部分:
1'<iframe src="seat-map/seat-map.html?wb=1' + (privacyOn() ? '&privacy=1' : '') + '" …>'
双向数据流
- 下行(花名册):打开座位表页时,把花名册写入
localStorage['wb_seatmap_roster'],iframe 启动自动读取。「↻ 刷新花名册」会带时间戳重载 iframe。 - 上行(排座结果):seat-map 通过
postMessage发回{ type:'wb-seatmap-result', seating, columnGroups },父页面onSeatMapMessage()展开成state.tables.seating的行,并按坐标推算zone / row / seatNo。
同源要求:两者必须同源部署,localStorage 与 postMessage 才能互通。
9. Excel 导入导出
SheetJS 采用异步加载 + 三级降级,不阻塞首屏:
- CDN:
https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js - 本地兜底:
vendor/sheetjs.min.js - 都失败 → 降级为 CSV,并在点击导入时给出准确提示
加载状态通过 window.WB_XLSX_STATE 暴露:loading / ready / failed。
页面不等库就绪就渲染,点「批量添加」时按三态提示。
本地兜底文件是占位文件(不会定义
XLSX),所以代码用typeof XLSX !== 'undefined' && XLSX.read判断,能正确识别"回退也失败"。上线前记得把真实的sheetjs.min.js放进vendor/。
10. 隐私模式(一键打码)
10.1 架构
开关状态存 localStorage['wb_privacy'],三个文件各自实现打码,互相同步:
| 层 | 判据 | 打码函数 |
|---|---|---|
| 工作台主逻辑 | privacyOn() | maskText(v) + isPrivateField(f);通用表格统一走 cellHtml() |
| 工作台特殊视图 | WB.privacyOn() | HM(v)(内部调 WB.privacyOn) |
| 座位表(iframe) | URL 参数 privacy=1 | pm(v) |
座位表因为是独立应用,通过 iframe URL 传参;切换隐私时会重建 iframe(带新参数)从而生效。
10.2 判定规则
isPrivateField(f) 按字段名 + 标签双通道判定:
- 字段名:
namestudentNophoneaddresswechatparentNameguardianPhoneemergencyPhoneroommatebuildingroomNobedNohosthandlerallergychronicmedication - 标签正则:姓名 / 学号 / 联系电话 / 家庭住址 / 微信号 / 家长(姓名|电话) / 紧急联系人 / 室友 / 床位号 / 房间号 / 楼栋 / 主讲人 / 处理人 / 过敏源 / 慢性病 / 常用药物
- 科目另外在视图层单独打码(课程表格子与图例、任课表、教师个人课表、课时统计、成绩表头与各科分析行)
10.3 刻意不打码的部分
| 位置 | 原因 |
|---|---|
data-name / data-tt 等属性、<option> 的 value | 打码后点击查详情就找不到人了 |
| 表单输入框、学生选择器 | 编辑时自己也看不见,没法改 |
| 头像圆圈里的姓氏首字 | 单字符,打码会撑破样式 |
| toast 提示、confirm 确认框 | 一闪而过的交互反馈 |
10.4 扩展
新增一个隐私字段:
- 通用表格字段 → 把字段名加进
isPrivateField()的名单(或让标签命中正则) - 特殊视图里的硬编码输出 → 把
H(x)换成HM(x) - 座位表 → 把
s.n/stu.n包一层pm()
这是渲染层遮罩,不改动数据。备份文件与导出的 Excel 里仍是真实信息。
11. 二次开发指引
加一个子表(最常见)
编辑 config.js,在目标 group.subs 追加带 fields 的项 → 自动获得列表、增删改、筛选、导入导出、备份支持。
加一个特殊视图
config.js的一级modules加一项,给个自定义typeviews.js实现renderXxx()/bindXxx()class-workbench.js的render()加else if (route.view === 'xxx')分支itemClickHandler()加对应的navigate('xxx')renderSidebar()加分支让它在侧边栏出现updateCrumb()加面包屑文案
调试要点
- 数据都在
localStorage['wb_v1'],可直接JSON.parse(localStorage.getItem('wb_v1'))查看 - 清空数据:
localStorage.removeItem('wb_v1')后刷新 - 隐私开关:
localStorage.setItem('wb_privacy','1')/ 删掉该 key - 控制台有
[班主任工作台]前缀的日志(SheetJS 加载状态等)
发布注意
docs/public/下的文件原样部署,改完直接发版即可,无需构建- 记得确认
vendor/sheetjs.min.js是真实库文件(占位文件会让 Excel 功能降级为 CSV) - 入口说明页在
docs/35.工具/班主任管理工具.md
12. 已知边界
| 边界 | 说明 |
|---|---|
| 单机、数据不共享 | 无后端,换设备只能靠「备份 / 恢复」手工同步 |
| 清缓存 = 丢数据 | 必须教育用户定期备份 |
| localStorage 容量 | 约 5 MB,写入失败会 toast 提示;数据量大时注意 |
| 快照不含座位表 | 恢复快照 ≠ 恢复座位表,座位表只能靠完整备份保存 |
| 座位表需同源 | 依赖 localStorage + postMessage 通信 |
| 无并发控制 | 两个标签页同时编辑会互相覆盖 |
| Excel 依赖外部 CDN | CDN 不可达时依赖本地 vendor 兜底 |
