本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/draft_saver/说明.md。
本地草稿保护插件(draft_saver)
FlintHub 发帖/回帖/博客表单的本地草稿自动保存与恢复。纯前端实现:草稿只存用户浏览器 localStorage,零后端、零数据库、零核心改动。
功能
| 场景 | 行为 |
|---|---|
| 输入停顿 800ms | 自动写入 localStorage(防抖) |
| 关闭页面/刷新 | 用内存副本快速落盘,不丢最新输入 |
| 重新进入表单页 | 弹「恢复/忽略」提示条,恢复自动回填(含富文本) |
| 提交成功(页面跳转) | 自动清除草稿键,下次不再弹 |
| 提交校验失败(不跳转) | 草稿保留,下次仍可恢复 |
| 草稿超过 7 天 | 弹「删除/保留」选择,不静默删数据 |
| localStorage 已满 | 自动清理过期草稿后重试,仍满则提示 |
| 隐私模式/浏览器受限 | 提示草稿无法持久保存 |
接入范围(4 类表单页,自动识别,无模板改动)
| 页面 | 路由 | 表单 |
|---|---|---|
| 新建主题帖 | /post/new | 页面唯一主表单 |
| 发表回复 | /thread/{id} | #replyForm(内嵌详情页) |
| 新建博客 | /blog/new | 页面唯一主表单 |
| 编辑博客 | /blog/{id}/edit | 页面唯一主表单 |
数据隔离
草稿键:flinthub_draft_{userId}_{formId}_{entityId}_{pathname}
- 用户隔离:键含 userId,且负载内
user_id双校验,切换账号不串; - 实体隔离:编辑博客 B 不会覆盖博客 A 的草稿(
blog_{id}); - 路径去参:
?category_id=等查询参数不参与键,同页草稿共享。
目录结构
plugins/draft_saver/
├── plugin.json # 插件清单(hooks: layout_head_end)
├── Plugin.php # 主类(纯前端,无建表)
├── hook/
│ └── layout_head_end.php # 按模板名注入 CSS/JS + 用户 ID
├── assets/
│ ├── draft.js # 核心逻辑(defer 加载)
│ └── draft.css # 提示条样式(--mn-* 变量)
└── 说明.md安装 / 卸载
- 安装:目录放入
plugins/后,手动更新plugins/plugins_cache.json追加draft_saver条目(含hooks.layout_head_end),或从后台插件页激活(自动重建缓存)。钩子生效前提:缓存中存在条目且activated: true。 - 停用:
plugin.json改activated: false+ 刷新插件缓存 → 4 个表单页立即恢复原样(零残留)。 - 卸载:删除插件目录与缓存条目即可;浏览器 localStorage 中遗留的
flinthub_draft_*键属用户侧数据,不代为删除(可在 DevTools → Application → Local Storage 手动清理)。
测试方法
Chrome DevTools → Application → Local Storage 查看键值;输入内容 → 刷新/关闭重进页面 → 确认恢复弹窗出现、恢复/忽略正常、提交成功后键被清除。
技术说明(关键决策)
- 富文本读写:编辑器(editor.js)会把原 textarea 隐藏并移除 name,读
el.value恒为空;本插件读window.ACTIVE_EDITORS[field].editor.innerHTML,恢复时写回编辑器并触发 input 刷新字数。 - 提交清理:本站表单为普通 POST 整页提交(无 AJAX 回调),采用「submit 打标 + pagehide 清键」:只有真正跳转(提交成功)才清,校验失败页面不跳转则草稿保留。
- 加载顺序:draft.js 用
defer,保证 editor.js(head 同步脚本)先执行并完成编辑器初始化。 - 安全性:恢复的富文本过轻量净化(拦截 script/iframe/事件属性/javascript:、data: URL)。