班主任工作台 · 技术文档

面向维护 / 二次开发的完整说明。日常使用请看 《快速上手》 。


1. 概览

项目说明
类型纯前端单页工具,无后端、无接口、无账号体系
技术栈原生 JS(ES5 风格 + 少量 ES6)、原生 CSS、零框架零构建
访问https://www.sakaay.com/class-workbench.html
数据持久化浏览器 localStorage(主数据)+ sessionStorage(路由)
外部依赖SheetJS(Excel 读写),CDN 优先、本地兜底

设计原则

  1. 配置驱动:所有模块、子表、字段都在 class-workbench-config.js 里声明,改结构不用改逻辑。
  2. 单一数据源:花名册(roster)是唯一的学生来源,其余模块通过"学生选择器"引用,禁止手输姓名。
  3. 数据隔离:多班级按 classData 分桶存储,班级之间完全独立。
  4. 防丢数据:自动快照 + 手动备份;备份整体导出 state,新增顶层字段自动纳入,不会漏备。

2. 文件结构

全部位于 docs/public/(该目录下的文件会原样部署到站点根)。

文件体积职责
class-workbench.html3 KB页面骨架:侧边栏 / 顶栏 / 内容区容器、SheetJS 异步加载策略
class-workbench.css40 KB全部样式(CSS 变量集中在 :root)
class-workbench-config.js21 KB配置中心:标签分组、模块树、子表、字段定义
class-workbench.js89 KB主逻辑:数据层、多班级、路由、通用 CRUD、筛选、备份、版本、隐私遮罩
class-workbench-views.js240 KB特殊视图:仪表盘、课程表、成绩分析、宿舍、值日、待办等
seat-map/seat-map.html112 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_v1localStorage主数据(全部班级、表格、成绩、课表、待办、快照)
wb_currentsessionStorage当前路由
wb_privacylocalStorage隐私模式开关('1' 开启),刷新保持
wb_seatmap_rosterlocalStorage工作台 → 座位表 的花名册同步通道
seat_v3_classeslocalStorage座位表班级索引({ list:[…], cur })
seat_v3_c_{id}localStorage座位表单个班级数据
seat_v2_datalocalStorage座位表旧版数据(兼容)

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: '…' }
属性说明
typetext / select / date / textarea / tags
optionsselect 的可选项数组
required必填(表单校验)
default新增时的默认值
full表单里独占一行
placeholder / hint输入提示 / 字段说明
picker'student' 强制用学生选择器;'none' 禁用
listfalse 表示不在列表里单独成列(如通讯录学号,与姓名同格显示)

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)。
  • 恢复兼容三种格式:
    1. 新版 { state: {...} }
    2. 旧版顶层平铺 { tables, grades, … }
    3. 座位表工具导出的 { source: 'seat-map', index }(只还原座位表,不动工作台数据)
  • 恢复后会为配置中新增、备份里缺失的表补空数组,避免渲染报错。

6.6 座位表数据回写(applySeatMap)

三步,缺一不可:

  1. 清空所有 seat_v3_c_* 旧 key(防脏 key 累积)
  2. 只按索引里的 id 回写(丢弃备份中的脏 key)
  3. 写回索引并修正 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' : '') + '" …>'

双向数据流

  1. 下行(花名册):打开座位表页时,把花名册写入 localStorage['wb_seatmap_roster'],iframe 启动自动读取。「↻ 刷新花名册」会带时间戳重载 iframe。
  2. 上行(排座结果):seat-map 通过 postMessage 发回 { type:'wb-seatmap-result', seating, columnGroups },父页面 onSeatMapMessage() 展开成 state.tables.seating 的行,并按坐标推算 zone / row / seatNo。

同源要求:两者必须同源部署,localStorage 与 postMessage 才能互通。


9. Excel 导入导出

SheetJS 采用异步加载 + 三级降级,不阻塞首屏:

  1. CDN:https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js
  2. 本地兜底:vendor/sheetjs.min.js
  3. 都失败 → 降级为 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=1pm(v)

座位表因为是独立应用,通过 iframe URL 传参;切换隐私时会重建 iframe(带新参数)从而生效。

10.2 判定规则

isPrivateField(f) 按字段名 + 标签双通道判定:

  • 字段名:name studentNo phone address wechat parentName guardianPhone emergencyPhone roommate building roomNo bedNo host handler allergy chronic medication
  • 标签正则:姓名 / 学号 / 联系电话 / 家庭住址 / 微信号 / 家长(姓名|电话) / 紧急联系人 / 室友 / 床位号 / 房间号 / 楼栋 / 主讲人 / 处理人 / 过敏源 / 慢性病 / 常用药物
  • 科目另外在视图层单独打码(课程表格子与图例、任课表、教师个人课表、课时统计、成绩表头与各科分析行)

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 的项 → 自动获得列表、增删改、筛选、导入导出、备份支持。

加一个特殊视图

  1. config.js 的一级 modules 加一项,给个自定义 type
  2. views.js 实现 renderXxx() / bindXxx()
  3. class-workbench.js 的 render() 加 else if (route.view === 'xxx') 分支
  4. itemClickHandler() 加对应的 navigate('xxx')
  5. renderSidebar() 加分支让它在侧边栏出现
  6. 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 依赖外部 CDNCDN 不可达时依赖本地 vendor 兜底
微信支付 支付宝