571 Commits

Author SHA1 Message Date
xfy
0e8b5fd7b7 feat(ui): scan and mount code-runner component client-side in reader
采用 Dioxus vdom 友好的片段拆分(避免篡改 dangerous_inner_html 产物导致 hydration 冲突):
- post_content.rs: split_content_fragments 把 content_html 拆成 Html/Runnable 片段,
  Runnable 作为 <CodeRunner> 组件穿插渲染;HTML 实体解码还原源码;6 个单测
- markdown.rs: 可运行 pre 增加 data-source(转义后的原始源码), 供阅读器无损提取
- sanitizer.rs: 放行 pre 的 data-source 属性

(任务 7 的 data-source 增量一并在此提交)
2026-07-03 18:35:57 +08:00
xfy
6b875e6ba5 feat(ui): create CodeRunner Dioxus UI component with async polling
- runner.rs: CodeRunner 组件(源码展示 + Run 按钮 + 阶段/输出/错误区)
  spawn 提交 StartExec 后用 sleep_ms(WASM 友好) 轮询 GetExecResult,MAX_POLLS 兜底
  mut 信号加 cfg_attr 放行 server 的 unused_mut(AGENTS.md 约定)
  提前克隆 source/language 给 move 闭包,避免 rsx! 二次借用 (E0382)
- execute.rs: 修正双目标可见性——server fn 模块不能 cfg-gate
  共享类型 use 双目标可见,server-only 辅助 use 单独 gate(对齐 posts 模块约定)
- mod.rs: execute 不再 gate,languages/progress 仍 gate
2026-07-03 18:28:52 +08:00
xfy
576803302d refactor(editor): support dynamic languages python/javascript in codemirror
- editor.ts: 新增 languageCompartment + buildLanguageExtension(lang, schema)
  python → lang-python, node/javascript → lang-javascript, sql/缺省 → lang-sql
  新增 setLanguage(lang) 热切换; setSchema 保留当前语言不被强制切回 sql
- package.json: +@codemirror/lang-python +lang-javascript
- codemirror_bridge.rs: EditorInstance 新增 set_language wasm-bindgen extern

(产物 public/codemirror/ 被 gitignore,本地已重建)
2026-07-03 18:22:21 +08:00
xfy
a77a2e3502 feat(markdown): support rendering and sanitizing runnable pre code-blocks
- markdown.rs: CodeBlock 处理识别 `runnable` 标记 + 受支持语言,在 <pre> 上
  挂 data-runnable/data-lang/data-overrides;overrides JSON 经 escape_html 转义
  防属性注入;非可运行围栏的 language-xxx 改取纯语言 token(首个空白前)
- sanitizer.rs: clean_html 放行 <pre> 的 data-runnable/data-lang/data-overrides
- 新增 4 个 markdown 测试 + 2 个 sanitizer 测试覆盖转义/白名单/字段顺序
2026-07-03 18:19:26 +08:00
xfy
9fe128d44d feat(api): implement StartExec and GetExecResult server functions
- rate_limit.rs: 新增 CODE_EXEC 双层限流(per-second burst + 每日总额)
  governor 0.8 无 per_day,用 with_period(24h)+allow_burst 模拟
- execute.rs: StartExec(IP 提取/语言白名单/源码大小/入队/并发信号量/clamp/容器执行)
  + GetExecResult(轮询读取 DashMap);系统异常脱敏,日志记详情
- .env.example: RATE_LIMIT_CODE_EXEC_PER_SEC 改为整数(governor 不支持小数)
2026-07-03 18:15:59 +08:00
xfy
3f3bbf284c feat(api): add language registry and markdown fence info parser 2026-07-03 18:11:43 +08:00
xfy
9baa9da62f feat(api): implement in-memory task registry with gc 2026-07-03 18:10:20 +08:00
xfy
60fd6378b6 feat(api): define shared serialized structs for code runner
Co-authored-by: plan docs/superpowers/plans/2026-07-03-code-runner.md Task 3
2026-07-03 18:09:27 +08:00
xfy
5fdc88a48c fix(runner): resolve task 2 container runner review findings 2026-07-03 18:02:56 +08:00
xfy
d8d30df80c feat(infra): implement Docker execution layer using bollard client 2026-07-03 17:59:52 +08:00
xfy
b0ebba3475 fix(runner_config): remove server gate on runner_config, safeguard clamp logic, and parse allow_network case-insensitively 2026-07-03 17:52:50 +08:00
xfy
60725053e4 feat(infra): implement runner config parsing and resource limits clamping 2026-07-03 17:50:42 +08:00
xfy
2fba43dc3f chore(infra): add bollard dependency and modular structure 2026-07-03 17:49:08 +08:00
xfy
46860f7c41 fix(ui): apply mount animation inside each page component to avoid skeleton interception 2026-07-03 17:24:38 +08:00
xfy
d361bca220 feat(ui): add smooth mount animation for route transitions 2026-07-03 17:21:04 +08:00
xfy
197c08d258 style(ui): fix inconsistent padding on post card 2026-07-03 17:13:02 +08:00
xfy
e18fe18cc3 fix(admin/layout): move scrollbar to outer card edge for all admin pages 2026-07-03 16:53:10 +08:00
xfy
52b4f91622 fix(admin/write): restore scrollability by using overflow-y-auto and min-h-full 2026-07-03 16:50:53 +08:00
xfy
acfddb3e17 style(admin/write): overhaul write page to modern minimalist layout 2026-07-03 16:44:39 +08:00
xfy
d3a50728ed style(admin): overhaul layout to modern minimalist sidebar design 2026-07-03 15:50:59 +08:00
xfy
38ba6692d3 style(admin): redesign admin UI with soft rounded corners and Catppuccin theme 2026-07-03 15:46:18 +08:00
xfy
9f69a132fa refactor(admin): redesign write page for industrial aesthetic 2026-07-03 15:37:41 +08:00
xfy
cad0e3d83b fix(admin): restrict write page width to match other pages 2026-07-03 15:35:06 +08:00
xfy
d1041a84dd feat(admin): complete redesign of backend ui with industrial minimalist aesthetic 2026-07-03 15:32:02 +08:00
xfy
45f4e9cde0 refactor(ui): 去除 Rust 中硬编码颜色,统一语义色阶
跟随 Catppuccin 配色迁移,清理 src/ 中绕过 paper-* 语义 token 的
硬编码 dark:[#hex] 任意值,改用 Tailwind 默认灰阶(gray-700/800),
它们随主题切换自然适配。

- post.rs 状态徽章:dark:bg-[#333]/dark:text-[#9b9c9d] → gray-800/gray-400/500
  (同步更新测试断言)
- comments/* 与 skeletons/* 的 dark:[#2a2a2a]/[#2e2e33]/[#333]/[#5a5a62]
  → 对应 gray-600/700/800
- trash.rs 状态灯发光圈 rgba 由旧鼠尾草绿(92,122,94) 改 Latte green(64,160,43)
- ui.rs BTN_SECONDARY 注释更新为新 Teal 强调色值

注:src/pages/admin/posts.rs 的 clippy redundant_closure 报错为既有问题,
与本次配色迁移无关,不在范围内。
2026-07-03 14:25:40 +08:00
xfy
cb77d28a68 fix(theme): 跟随系统切换改为瞬切,放弃 VT 动画
诊断确认:跟随系统与手动点击的 VT 流程在 JS/CSS 层完全等价(animationName、
--tt-r、bg 翻转、clip-path 全部正确),但视觉上跟随系统场景下圆形展开动画
不显示,仅瞬切。系统偏好变化是后台事件,VT 在此上下文下动画不可靠。

改用设置语义的瞬切:JS 侧新增 applyResolvedTheme(isDark) 入口(非翻转语义,
直接 add/remove dark class),Rust effect 改调 __applyResolvedTheme 传实际
明暗值,移除圆心计算逻辑。职责清晰:
- 手动点击 → __startThemeTransition → VT 圆形展开(用户手势,动画可靠)
- 跟随系统 → __applyResolvedTheme → 瞬切(后台同步语义)
2026-07-03 13:42:17 +08:00
xfy
eb3087edf4 fix(theme): 跟随系统切换用主题按钮位置作为 VT 圆心
诊断发现跟随系统与手动点击的 JS 路径完全一致(__startViewTransition 全程
正常,无降级、无 reject),「看起来像淡入淡出」实为圆形展开动画在屏幕中心
圆心下的必然视觉效果:从中心向四周等距扩散,各方向同步推进,近似整体覆盖,
与手动点击(按钮在右上角,圆弧明显横扫)观感差异大。

改用 .theme-toggle 按钮的 getBoundingClientRect() 中心作为 VT 圆心,与手动
点击的动画起点一致;按钮不在 DOM 时回退到视口中心。同步更新注释说明。
2026-07-03 13:26:40 +08:00
xfy
188fc464e2 fix(theme): System 模式下系统偏好变化时同步 dark class
原实现存在缺口:<html> 的 dark class 运行时写入点只有「首屏预加载脚本」与
「手动点击 ThemeToggle」两处,System 模式下系统颜色偏好自动变化时无人同步——
尽管 system_dark signal 与 resolved memo 都会更新,下游(CodeMirror 等)也
能跟随,但页面配色纹丝不动,与文件头注释承诺的「实时同步 .dark class」不符。

新增 use_effect 兜底:仅 System 模式下 resolved 实际翻转时,以视口中心为圆心
复用 __startThemeTransition 触发 VT 圆形展开动画。Light/Dark 显式模式由
ThemeToggle::onclick 全权负责,effect 不介入,避免重复触发;首次挂载通过
prev_resolved 信号记录基线,自然跳过(DOM 已由 ThemePreload 设好)。
2026-07-03 11:35:10 +08:00
xfy
c79c390482 fix(ui): ADMIN_TABLE_CLASS 去掉 overflow-hidden,修复 tooltip 被裁
overflow-hidden 用于圆角裁剪表格,但会硬裁掉行内按钮的 tooltip
(无论朝上朝下)。去掉后:表格行 hover 背景 (bg-paper-entry) 与
容器背景同色,圆角处无视觉差异,tooltip 可正常溢出表格边界。
全局生效,所有 admin 表格的 tooltip 不再被裁。
2026-07-03 11:15:48 +08:00
xfy
64ad40ca61 fix(admin/posts): Tooltip 包裹后操作按钮恢复垂直居中
- Tooltip 外层 div 由块级改为 inline-flex,作为行内伸缩盒参与
  flex 行对齐,不再撑高或破坏基线
- 操作列容器补 items-center,显式锁定所有子项垂直居中
2026-07-03 11:08:04 +08:00
xfy
73b9cad550 refactor(ui): 抽取 Tooltip 组件,行内重建按钮补 tooltip
新增可复用 Tooltip 组件(group-hover 显现,支持 top/bottom 朝向),
消除顶部重建按钮手写的内联 tooltip div 重复,并为表格行内重建
按钮补上 tooltip(朝上,避免被表格容器的 overflow-hidden 裁掉)。
2026-07-03 11:03:56 +08:00
xfy
b6fe0ae082 style(admin/posts): 删除按钮 loading 用 spinner 覆盖文字
- 删除改为非乐观:点击后保留行并显示 loading,服务端成功才移除行、
  失败保留行并弹提示(顺带修掉原先「删除失败行却没了」的隐患)
- deleting 信号由 Option<i32> 改为 HashSet<i32>,支持并发删除
- 删除按钮 loading 样式与重建一致:文案恒为「删除」,spinner
  居中覆盖、文字 opacity-40 衬底
2026-07-03 10:51:16 +08:00
xfy
1530d3a2f4 style(admin/posts): 顶部重建按钮 loading 用 spinner 覆盖文字
工具条「重建内容」「重建全部」按钮 loading 时由「重建中...」文案
改为 spinner 居中覆盖、文字 opacity-40 衬底,与行内重建按钮的
loading 样式统一。
2026-07-03 10:46:57 +08:00
xfy
db87d3c886 fix(admin/posts): 重建支持并发 loading,spinner 覆盖文字
- rebuilding 信号由 Option<i32> 改为 HashSet<i32>,连续点击多篇
  文章的重建按钮可同时处于 loading 态(单值会被后点的覆盖)
- loading 样式调整:按钮文案恒为「重建」,spinner 绝对定位覆盖在
  文字上方居中,文字 opacity 调至 40% 衬底,不再切换文案
2026-07-03 10:40:11 +08:00
xfy
a5c00caa80 style(admin/posts): 操作列加宽横排,重建按钮 loading 用 spinner 图标
- 操作列宽 w-24 → w-44,三个按钮一行排开不换行
- 重建按钮 loading 态由「重建中...」文案改为 spinner 图标 + 「重建中」
- spinner 复用 public/icons/90-ring-with-gradient.svg,作为 SPINNER_SVG
  常量收入 ui.rs(修正原 SVG 损坏的孤立 ':' 选择器,重命名 id/类名
  避免多处内联时冲突),用 currentColor 继承按钮文字色
2026-07-03 10:26:14 +08:00
xfy
e55f2e12bd refactor(admin/posts): 重建按钮文案改「重建」并去掉完成弹窗
按钮态文案与原有一致(「重建中...」);on_rebuild 改为静默执行,
仅按行切换 rebuilding 按钮态,完成后不再弹浏览器 alert。
2026-07-03 10:16:13 +08:00
xfy
0fe0387ea3 feat(admin/posts): 文章列表操作列新增「重建内容」按钮
新增 rebuild_post_content_html(post_id) server function,从 DB 读取
content_md(事务内 FOR UPDATE 锁行)重新渲染 content_html/toc_html
并更新 word_count/reading_time,按影响范围精准失效列表、slug 单篇
与搜索缓存。

PostRow 操作列在「编辑」「删除」之间插入「重建内容」按钮(鼠尾草绿
文字样式),按行禁用并切换「重建中...」文案,成功/失败弹出浏览器
提示,与删除按钮的反馈模式一致。

为避免与单篇 rebuilding 信号冲突,将原批量重建工具条信号 renaming
为 batch_rebuilding。
2026-07-03 10:13:15 +08:00
xfy
edee09596c test(api/markdown): 解耦 wrap_images_with_blur 并脱离文件系统依赖
把 wrap_images_with_blur 拆成两部分:
- wrap_images_with_blur_with(html, dims_fn):纯函数核心,接受 dimensions
  查询闭包,不碰文件系统
- wrap_images_with_blur(html):薄包装,注入真实 get_image_dimensions

测试改用纯函数核心,注入确定性 dimensions(如 Some((800,600)) 或 None),
彻底脱离 uploads/ 真实文件依赖。此前的 aspect-ratio 测试硬编码了
/uploads/2026/06/18/...webp 真实路径——文件不存在则静默走缺省分支,
断言虽能过但实际验证内容被削弱。

新增覆盖(共 8 个 wrap 相关测试):
- --ar 缺省/有值两种路径
- rel_path 去查询的验证(用 RefCell 捕获 Fn 闭包入参)
- alt 保留(双层)/空 alt 不生成属性
- 斜杠分隔的 --ar 格式 + sanitizer 管线不破坏斜杠
2026-07-02 17:39:15 +08:00
xfy
ab05c01f76 test(api/database): 备份恢复补单测 + 修复 backup_path 路径穿越漏洞
backup.rs 是恢复流程的安全关键路径,此前无任何单测。本次:

提取纯函数(便于单测,无行为变更):
- is_valid_backup_filename:文件名白名单校验,替换三处内联 Regex
- parse_backup_mode:从 -- mode: 行提取模式值
- has_valid_signature:首行签名头校验

新增 15 个单测,覆盖:
- 文件名白名单(正常/路径穿越/特殊字符三类边界)
- backup_path 路径穿越纵深防御
- 签名校验(精确/前导空白/非系统文件/空内容)
- 模式解析(pg_dump/sql-fallback/缺失/空值/多次)

修复一个真实的路径穿越漏洞(测试驱动发现):
backup_path("/etc/passwd") 之前返回 /etc/passwd 而非 backups/——
因为 [BACKUP_DIR, filename].collect::<PathBuf>() 在 filename 为绝对
路径时会丢弃 BACKUP_DIR 前缀(PathBuf 语义),随后的 components 检查
在错位的路径上运行而漏判。改为先校验 filename 自身的 components
(只允许 Normal 段),从根上杜绝穿越。

注:第一道白名单 is_valid_backup_filename 已拒绝含 / 的文件名,
故该漏洞实际不可达;但纵深防御必须健壮,本修复使其名副其实。
2026-07-02 17:37:07 +08:00
xfy
533cabb0b5 test(api/database): SQL 控制台护栏补表驱动单测 + 加固
sql_console.rs 是 SQL 控制台的安全关键路径,此前无任何单测。本次:

测试(新增 24 个,覆盖护栏 1+2 全分支):
- check_guards: DROP SCHEMA/CREATE DATABASE 绝禁、DROP/TRUNCATE/ALTER
  需确认、UPDATE/DELETE 无 WHERE 绝禁(且 confirm 不能救)、多语句短路
- is_absolutely_forbidden: 大小写不敏感、误拦检查、报错名
- statement_type_name / is_read_only: AST 变体映射
- 两条安全不变量测试(见下)

加固(测试驱动发现的两个真实缺口):
1. 字符串预检 contains() 改为 token 序列匹配 is_absolutely_forbidden()——
   防多空格/制表符/换行绕过 DROP/CREATE DATABASE。
2. check_guards 补 Statement::CreateDatabase 绝禁分支——此前 sqlparser
   能解析 CREATE DATABASE(可绕过字符串预检进 AST),但 AST 层无对应分支,
   字符串预检是其唯一防线。现在双重拦截。

并锁定两条安全不变量,防止 sqlparser 升级时悄悄回退:
- DROP DATABASE 不可解析 → 字符串预检是唯一防线
- CREATE DATABASE 可解析 → 双重防线都生效
2026-07-02 17:33:19 +08:00
xfy
65b15509a7 chore(wasm): 清理 wasm32 target 下残留的 clippy lint
这些 lint 在 server build(默认 --all-features)下被 cfg 剥离故未暴露,
仅 wasm32 web-only target 触发。逐项清理:

- footer.rs: 两处 redundant closure(web_sys::window 直接传函数引用、
  sync_visible 直接传闭包)+ let _ = unit 绑定(scroll_to_*)。
- hooks/event_listener.rs: 复杂类型 Rc<RefCell<Option<(Closure,T)>>>
  抽 type ListenerState<T> 别名。
- system.rs: 两处单模式 match { Ok=>.., _=>{} } 改 if let。
- write.rs: split(|c| matches!(c,...)) 改 split([char; N]) 数组模式。
- codemirror_bridge.rs: let _ = instance.destroy() 删 unit 绑定,
  与 tiptap_bridge.rs 同处写法对齐。

wasm32 与 server 两 target clippy -D warnings 均通过。
2026-07-02 17:15:27 +08:00
xfy
fee5b7ba94 refactor(wasm): 消除 DOM 互操作中的 js_sys::eval 字符串求值
对照 Dioxus "Breaking Out" 文档审查后,将 3 处 js_sys::eval 改为
类型化调用,与 tiptap_bridge/codemirror_bridge 风格统一:

- utils/time.rs sleep_ms:eval("new Promise(r=>setTimeout(r,n))")
  改为 js_sys::Promise + web_sys::Window::set_timeout_*,复用项目内
  system.rs::wasm_sleep 同款写法。
- theme.rs 主题切换:format!-into-eval 拼 __startThemeTransition(x,y)
  改为 Reflect::get 取函数 + Function::call2,消除字符串注入面
  (原坐标虽为 f64 无实际风险,但属不必要 eval)。
- post_content.rs:__initPostContent / __initLightbox 两处 eval 字面量
  改为 invoke_optional_global 辅助函数(Reflect::get + Function::apply),
  消除旧版 "eval 字符串粘贴" 模式;__lightboxSelectors 直接复用 JsValue
  传入,省去一次回读。
2026-07-02 17:15:18 +08:00
xfy
ba5b146dfa fix(theme): 消除 detect_initial_theme 的 unreachable_code 警告
wasm 分支末尾的 return Theme::System 在 wasm target 下让尾部兜底
Theme::System 不可达(unreachable_code 警告)。把尾部兜底用
#[cfg(not(target_arch = "wasm32"))] 限定,使其只在 server 分支
(cookie 未命中)或边角构建时编译,wasm target 下函数以 wasm
分支的 return 自然结束,两端都零警告。
2026-07-02 16:49:31 +08:00
xfy
2b6b2ca9e4 fix(hooks): 修复 wasm32 target 编译失败
之前验证只跑了 server target(cargo build --all-features),漏掉 wasm32
前端 target,导致三类错误未被捕获:

1. event_listener.rs wasm 分支缺 use dioxus::prelude::*:
   use_hook/use_effect/use_drop 来自 prelude,之前重写时误删。补回。

2. event_listener.rs use_effect 闭包 move 冲突(E0507/E0310):
   use_effect 回调是 FnMut,但 acquire 是 FnOnce、handler 要 move 进事件
   闭包。用 Option::take 模式包裹,首次运行消费,避免重复 move。同时给
   A 加上 'static 约束。

3. query.rs items/total/footer.rs sync_visible 缺 mut:
   wasm 分支的 .set() 调用需要 mut 绑定。之前为消 server target 的
   unused_mut 警告误删,改用 cfg_attr 在非 wasm 时压制警告。

教训:fullstack 项目必须同时验证 wasm32 与 server 两个 target,单一
target 通过不代表整体可用。
2026-07-02 16:43:15 +08:00
xfy
291189fb77 fix(sanitizer): 折叠嵌套 if 修复 clippy::collapsible_if
865871f 引入的 checkbox type 白名单逻辑用了嵌套 if,触发 rust 1.96
的 clippy::collapsible_if,导致 make clippy 直接失败。按 clippy 建议
合并为单个 if 链,行为不变。
2026-07-02 16:36:05 +08:00
xfy
59359b18ce refactor(hooks): 新增 use_paginated 并迁移 posts/trash 分页加载
按 Dioxus 0.7 custom hooks 范式,把后台分页列表页反复出现的
use_effect + spawn + (loading/items/total/error) 三件套收口到
use_paginated 组合式 hook。统一非 wasm cfg 处理(SSR 不发请求、
loading 立即置 false),调用方只关心 fetch 闭包与页码依赖。

迁移范围(按降级预案,只做最规整的两处):
- pages/admin/posts.rs:顺手补 error signal(原先吞掉 Err),向
  trash 看齐,消除一致性债。
- pages/admin/trash.rs:直接替换,error 已有。

保留手写的形态(强行套 hook 反而更复杂):
- comments.rs:依赖 (page, status) 双 signal,status 是额外维度,
  套不进 (page, per_page) 模型;改 hook 会为单一调用点变复杂。
- system.rs 三个 tab:load_once + use_future 轮询形态,与 hook 的
  单次加载模型不契合。
- dashboard.rs:多源聚合(3 个并发 spawn 分别 set 不同 signal)。

list_posts / list_deleted_posts 是 server function,两端都生成
(wasm 端 client stub、server 端真实实现),故去掉原 cfg import。
2026-07-02 16:30:47 +08:00
xfy
759bbab7ee refactor(utils): comment_storage 从 hooks/ 迁至 utils/
该文件全是普通函数(save_author / load_pending_comments 等),没有任何
use_ 开头的 Hook,放在 hooks/ 下名不副实。内部只剩结构体与 localStorage
读写(时间/HTML 工具已在上一 commit 拆出),迁到 utils/ 后文件名与内容
自洽,hooks/ 目录回归只放真正的渲染期 Hook。

机械替换 4 个组件文件的 use 路径(hooks::comment_storage → utils::comment_storage)。
2026-07-02 16:18:52 +08:00
xfy
dcdd72a8c2 refactor(utils): 拆分 comment_storage 的时间/HTML 工具函数
消除 server 端对 hooks 模块的跨层引用:

- escape_html(+ 两份重复实现:comment_storage 用 &#39;、helpers 用 &#x27;)
  统一到 utils/html.rs,单引号采用 HTML5 标准 &#x27;。
- now_millis / relative_label_from_millis / format_relative_time_iso 及其
  单元测试迁到 utils/time.rs,与既有 sleep_ms 同构(wasm32/非 wasm 双版本)。

调用方迁移:
- api/comments/helpers.rs:删掉本地 escape_html,format_relative_time 改引
  utils::time;其 escape_html 测试随之删除(utils/html.rs 已覆盖)。
- api/comments/create.rs:两处 helpers::escape_html 改 utils::html。
- api/markdown.rs:escape_heading_text 改 utils::html。
- components/comments/pending_item.rs:format_relative_time_iso 改从
  utils::time 引入;render_pending_content 留在 comment_storage(语义紧贴
  待审核评论)。
- hooks/comment_storage.rs:is_expired 的 now_millis 改引 utils::time。

comment_storage.rs 现在只剩结构体与 localStorage 读写,文件名即将自洽。
2026-07-02 16:15:23 +08:00
xfy
f00c2bf963 refactor(hooks): 新增 use_event_listener 通用事件监听 hook
把 theme.rs 与 footer.rs 两处手写的 "use_hook 持有 Closure+target、
use_effect 注册 add_event_listener、use_drop 移除" 样板收口到一个
通用 hook,按 Dioxus 0.7 custom hooks 范式封装。

hook 设计:
- target 通过 acquire 闭包在 use_effect 首次运行时获取(此时 DOM 一定
  可用),调用方无需再自己包 use_effect。
- 泛型 AsRef<EventTarget> 统一 Window / Element / MediaQueryList。
- 非 wasm32 整体编译为 noop,调用方用 #[cfg] 隔离 web_sys 调用即可。

footer 的 "注册后首次同步" 业务逻辑保留在 hook 之外(语义不属于
事件监听)。theme 的 handler 改为重新 match_media 读取 matches,
功能等价(低频事件,开销可忽略)。
2026-07-02 16:02:51 +08:00
xfy
62bd281684 fix(admin/posts): 重建结果消息改为独立行,修复悬浮溢出表格的错位
RebuildCacheBar 的「已重建 X 篇文章」结果消息原用 absolute top-full mt-2
悬挂在按钮行下方,但 mt-2(8px) + text-sm(20px) = 28px 大于外层 space-y-6
的 24px 间距,消息底边向下溢出 4px 侵入表格容器上沿;横向 left-0 又使消息
落在右上角按钮组正下方、覆盖状态/日期/操作列,语义位置错位。

将 rebuilding / rebuild_result 状态上提到 PostsPage:
- 结果消息在 header 与表格之间独立成行,进入文档流,吃 space-y-6 正常间距,
  既不撑高 header 触发 items-center 重排(曾用 absolute 规避的旧问题),
  也不脱离流溢进表格(absolute 引入的新问题)。
- RebuildCacheBar 改为接收父级 Signal props,只渲染按钮行本身。
2026-07-02 14:19:06 +08:00