一块旧安卓屏背后的全栈小工程
三月份去实习帮云衢中学做了一套学生奖惩公示系统,跑在教学楼走廊的展示屏上。
普通的网页都好办,但因为那块屏是一台 Android 4.4 的老设备,导致很多新的前端方式都运用不上。
仓库:HG-dev17/yunqu · 线上:yunqu.cookiesky.cn
一、需求
现实中要解决的事很简单:
- 学校要公开表彰与批评,鼓励和警告学生的平时行为;
- 数据由德育处、年级处老师录,老师是不碰代码的,只能做后端;
- 换学期要能一键切换,历史记录要能查;
- 手机、旧平板、走廊那台老盒子,都得能打开。
二、技术选型
第一版就定了:展示端是纯静态页,数据放 CSV。
理由很实际:
- 学校没有服务器,也没有人运维;
- 静态托管免费、不会挂;
- CSV 用 Excel 和 WPS 都能打开,老师看得懂;
- 改一条记录 = 改一行文本,出问题能直接看出来。
数据目录长这样:
1 | data/ |
semesters.csv 可以定义新的学期:
1 | key,name,start_date,end_date,file |
页面按今天的日期落在哪个区间,自动判断“当前学期”,再加载对应文件。这样换学期只需要改一行日期。
三、Android 4.4 的解决办法
第一次上机测试就翻了:白屏、滚不动、按钮错位。
原因是——那台设备的内核老到没有 fetch,没有 Promise,没有 async/await,连 Array.prototype.forEach 都要靠 polyfill 补。
于是整个 js/script.js 被改写成 ES5 + XHR 回调:
1 | if (!Array.prototype.forEach) { |
1 | function fetchText(url, cb) { |
这带来两个连锁问题:
- 回调地狱:学期索引 → 记录文件 → 公告 → 渲染,四层嵌套。解决办法是把每层拆成独立函数 + 一个
cb参数,用命名函数而不是匿名嵌套; - 没有
AbortController:滚动/切学期时没法取消旧请求,只能在回调里校验“这是不是当前学期”,过期结果直接丢弃。
还有一个隐蔽的坑:BOM。Excel 另存的 CSV 是带 BOM 的 UTF-8,第一个字段会变成 \ufeffkey,对象取值永远 undefined。后来统一用:
1 | function stripBOM(s) { |
服务器导出时也统一写 encoding='utf-8-sig'。
四、展示端
大屏场景和网页场景完全不同:没有鼠标、没有触摸、也没人管它。
所以做了这些:
- 奖惩榜自动滚动:整块列表匀速上移,到底部无缝回顶;
- 公告栏自动轮播:多条公告按顺序播,可含图片和视频,视频播完自动下一条;
- 暂停/重置:给维护老师留两个按钮,不被学生乱点;
- 左右两栏:左奖惩、右公告,把 16:9 的屏用满(1.0.4.29 版本从上下布局改的);
- 倒计时:直接读
countdown.csv,中考倒计时按”天”刷新。
自动滚动的实现没有用 CSS 动画(旧内核的 transform 动画会抖),而是 JS 定时器 + translateY,速度可调(scrollSpeed = 30)。手动滚动模式后来又单独修了一轮,解决“滚动留白和抽动”——根因是间隙高度算错,回顶时跳了一格。
五、公告栏
公告最初只是一段文字,后来老师的需求一条条加上来:
| 版本 | 加了什么 |
|---|---|
| 1.0.2.5 | 公告栏 + 公告管理界面 |
| 1.0.5.13 | 支持图片、视频;内容允许为空;图片点击放大 |
| 1.0.6.8 | 自动滚动、视频自动播放、按年级筛选 |
announcement.csv 就多了几列:
1 | text,grade,image,video,time |
text 允许为空,只展示图片或视频——这一条看起来小,但它把”公告”从通知变成了展示板,老师可以直接贴流动红旗的照片。
六、管理端与本地服务
展示端是静态的,但录入需要一个能改数据的界面。
admin.html 直接读写同一批 CSV(配合本地起服务),all_semesters.html 做跨学期汇总。
真正做“在线可编辑”时,才加了 server.py:
1 | Flask + SQLite |
因为没有服务器,无法修改静态的网页,设计上选择了:数据库只做“编辑期的暂存”,最终导出CSV进行替换。
老师改完点导出 → 写回 data/*.csv → 提交仓库 → 静态站自动更新。
还加了一个”从 CSV 导入数据库”的逻辑,并且判断”该学期是否已有数据,有就跳过”,避免每次重启把老师的编辑覆盖掉。
七、版本日志
我把每次改动都记在 README 里,从 1.0.0.0 到 1.0.6.8,一共三十多次提交。回头看,真正花时间的地方几乎都不是功能:
- 兼容旧内核(3 次紧急修复);
- 时间格式(
2026/3/18和2026-03-18混用导致筛选全空); - 缓存(旧浏览器必须手动刷新才更新,最后给资源加版本参数解决);
- 删除数据时弹三次确认框(事件重复绑定)。
