773 Commits

Author SHA1 Message Date
xfy
79af7d345e fix(admin): 修复重建内容后标题行纵向错位
点击「重建内容 / 重建全部」后,结果消息撑高了 RebuildCacheBar,外层
header 的 items-center 据此重新居中,把「文章管理」标题与「+ 写文章」按钮
整体下移,而重建按钮停在容器顶部,造成三者不再同处一行。

根因:c024de1 抽取 RebuildCacheBar 时把结果消息纳入子组件的 flex-col 文档流
(重构前消息是页面级兄弟节点,不参与 header 居中计算)。

修复:根容器改 relative,结果消息用 absolute 悬挂于按钮行下方、脱离文档流,
组件盒高恒等于按钮行高度,无论消息是否出现都不影响 header 的垂直居中。

验证:dx check + cargo clippy 全绿。
2026-06-29 11:49:28 +08:00
xfy
a1aef5bda9 fix(admin): add cursor-pointer to comment status filter buttons 2026-06-29 11:41:52 +08:00
xfy
dbe95f1173 feat(admin): use global EmptyState component in comments management page 2026-06-29 11:39:32 +08:00
xfy
72d21c6670 feat(admin): use global EmptyState component in posts management page 2026-06-29 11:38:50 +08:00
xfy
46e9a29f1e fix(theme): 消除 ThemeToggle 在 wasm/server 构建的 unused_mut 警告
Some checks failed
CI / check (push) Failing after 6m25s
CI / build (push) Has been skipped
dx build 的 wasm target 报两条 unused_mut:

- theme (160行): wasm 端的 theme.set 被非 wasm cfg 分支剥离,写操作转移
  到副本 theme_clone,theme 本身在 wasm 下只被读取/Copy。加
  #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]——仅 wasm 端压住,
  server 端仍需 mut(theme.set(next) 实际存在)。
- gen_clone (191行): 闭包内只读 gen_clone(),从未 set,两端都不需 mut,
  直接删除。

顺带把原来无条件挂在 click_gen 上的 #[allow(unused_mut)] 改成
#[cfg_attr(not(target_arch = "wasm32"), ...)]——click_gen 只在 wasm 分支被
set(189行),server 端该分支被剥离。改为按 cfg 方向标注,与 theme 对称,
语义更精确(不再无差别压住 wasm 端的检查)。

匹配项目既定模式(post_cover.rs:20 的 cfg_attr allow)。零行为变更,
dx check / cargo check(双 target) / clippy / theme 测试均通过。
2026-06-29 11:31:57 +08:00
xfy
b923851284 docs(main): 注明 CompressionLayer 默认跳过 image/*
Some checks failed
CI / check (push) Failing after 5m37s
CI / build (push) Has been skipped
避免后续误以为压缩层会对 WebP/PNG/JPEG 等已是压缩格式的图片再次
压缩而浪费 CPU。tower-http 的 DefaultPredicate 开箱即跳过 image/*
(例外 image/svg+xml)、gRPC、SSE 及 <32 字节响应,无需额外配置。

纯文档性注释,无行为变更。
2026-06-29 11:16:30 +08:00
xfy
74f8c212f6 feat(csrf): warn at startup when APP_BASE_URL is unset
APP_BASE_URL 未设置时 trusted_origin 回退到请求 Host 头推导本站 origin,
反向代理后若 Host 头可被客户端影响存在 CSRF 绕过风险。.env.example 虽然
警告了,但默认值就是空——'cp .env.example .env 后忘改' 是最常见的部署失误。

加启动时一次性 WARN(与 image.rs 的启动告警同范式):
- warn_if_app_base_url_unset() 在 main.rs 启动序列调用,与 validate_database_url
  等配置告警归在一处
- 抽出纯函数 app_base_url_is_set() 承载判断逻辑,便于测试,打日志副作用与之解耦
- 5 个单测覆盖 unset/empty/whitespace/set/trim 边界

每请求路径的 trusted_origin 保持纯函数不变,不引入 dedup 状态或刷屏风险。
2026-06-29 11:02:58 +08:00
xfy
c237007111 docs: add production deployment guide
服务端的安全逻辑(会话 Cookie Secure 标志、限流按真实客户端 IP 聚合、
CSRF origin 校验)都默认前置反向代理并终结 TLS,但缺少一份成体系的部署
文档说明这一点。不知情的部署者直接把 3000 端口暴露到公网 HTTP 会同时踩中
cookie 无 Secure、限流退化/可伪造、CSRF Host 回退可绕过三个坑。

新增 docs/DEPLOYMENT.md:
- 醒目的前置警告,禁止裸暴露 3000 端口到公网 HTTP
- nginx + Caddy 最小配置示例(含 XFF 覆写、body 上限对齐应用侧 10 MiB、
  代理超时对齐应用侧 300s)
- 生产环境必设变量三件套及 TRUSTED_PROXY_COUNT 设错的两种后果
- Docker 部署示例(绑定回环地址,反代作为唯一公网入口)
- 部署后验证清单

README 加「生产部署」节链接到该文档。代码无需修改——三个环境变量的保守
默认值与 .env.example 的警告注释设计上是正确的,缺口纯粹是文档。
2026-06-29 10:55:24 +08:00
xfy
8ec4ecd310 chore: migrate JS subprojects from npm to pnpm
Some checks failed
CI / check (push) Failing after 4m41s
CI / build (push) Has been skipped
Switch tiptap-editor, lightbox, and yggdrasil-core from npm to pnpm.
Replace package-lock.json with pnpm-lock.yaml and update Makefile accordingly.
2026-06-29 10:35:04 +08:00
xfy
cb73a9b979 chore: fix cargo clippy warnings (unused import and false positive unused_mut) 2026-06-29 10:14:27 +08:00
xfy
178870427c feat(ui): refactor EmptyState to accept dynamic string props and add empty state to archives page 2026-06-29 10:11:00 +08:00
xfy
824ad5a3d3 chore(Makefile): remove build hints and start target
Build instructions have been moved to AGENTS.md.
The verbose echo messages and the standalone start target
are no longer needed.
2026-06-29 09:59:14 +08:00
xfy
7615def3d1 fix: add required [web.resource.dev] for dioxus 0.7.9
dioxus-cli 0.7.9 requires a `dev` sub-table under [web.resource].
Added [web.resource.dev] with the same style/script arrays as the
production [web.resource] section.
2026-06-29 09:56:07 +08:00
xfy
18db852f30 fix(build): correct asset path resolution for local release build and remove duplicated resources 2026-06-26 18:35:48 +08:00
xfy
225c477f8c chore(theme): remove debug prints 2026-06-26 18:27:27 +08:00
xfy
82a8c511fc fix(theme): remove dark class from AppRouter div 2026-06-26 18:24:00 +08:00
xfy
230ec60f24 fix(theme): remove transition from body to fix VT snapshots
The CSS transition on the body's background-color (0.3s ease) was
causing the browser's View Transitions to capture a stale snapshot
during the Dark -> Light change. Even though the .dark class was
removed synchronously and we tried to disable transitions via an
!important override class, the interaction between Tailwind classes
and the raw CSS transition on body still caused the root VT snapshot
to render as Dark when it should have been Light.

By removing the transition from body entirely (which is redundant
anyway since we're using View Transitions for theme changes), the
body jumps to its target color instantly, guaranteeing the NEW
snapshot accurately reflects the final Light theme, eliminating
the 'ghost circle' or 'circle outline' bug.
2026-06-26 18:21:13 +08:00
xfy
065302544d fix(theme): always expand NEW layer and explicitly set html background
When turning Dark off (Dark -> Light transition), the NEW layer is Light.
Previously we tried shrinking the OLD layer (Dark) over it, but if the
root snapshot captured a transparent html, revealing the transparent layer
produced no visual change.

Now we:
1. Always keep the NEW layer on top (z-index: 2).
2. Always animate the NEW layer expanding from 0 to max radius.
3. Explicitly set background-color on html in input.css to ensure the
   VT root snapshots are fully opaque, avoiding transparent captures that
   make expansions invisible.
4. Add background: var(--color-paper-theme) directly to ::view-transition-new
   just to be absolutely certain it's painted opaque.
2026-06-26 18:17:13 +08:00
xfy
93d3858a37 chore(deps): update package-lock.json 2026-06-26 18:15:00 +08:00
xfy
0ff24888ce refactor(theme): use pure CSS View Transitions without WAAPI or style injection
Adopt a fully CSS-driven strategy for the theme transition:
- Use CSS variables (--tt-x, --tt-y, --tt-r) injected onto documentElement
- Define static @keyframes in style.css using these variables
- Key logic: the Dark layer is ALWAYS placed on top (z-index: 2).
  - Light -> Dark: NEW layer is Dark, so NEW expands from circle(0) to cover OLD.
  - Dark -> Light: OLD layer is Dark, so OLD shrinks to circle(0) to reveal NEW.

This completely eliminates reliance on Web Animations API (which had bugs
with clip-path when animation:none is set) and eliminates dynamic <style>
injection (which caused timing gaps and style residue).
The result is perfectly symmetrical and bug-free in both directions.
2026-06-26 18:14:29 +08:00
xfy
76eedebf05 fix(theme): remove animation:none that blocks WAAPI on VT pseudo-elements
Chrome does not allow WAAPI clip-path animations on ::view-transition-new
when animation:none !important is set via CSS. This caused dark→light
transitions to have no visible animation.

Replace with opacity:1 !important to neutralize the UA fade-in/out
animations: the UA animations still 'run' but cannot change opacity
because !important author declarations override CSS animation values.
WAAPI clip-path is no longer blocked.
2026-06-26 18:11:16 +08:00
xfy
0f9c8fb8a3 fix(theme): lock VT pseudo-element opacity/blend via CSS !important
Gray expanding circle was caused by UA default animations (fade-in/out)
and mix-blend-mode: plus-lighter not being fully overridden by WAAPI
opacity animations alone.

Fix: use CSS with !important to hard-lock the VT pseudo-elements:
- animation: none !important — kills UA fade-in/out
- opacity: 1 !important — both layers stay fully opaque
- mix-blend-mode: normal !important — no color addition

WAAPI now only controls clip-path on ::view-transition-new(root),
which doesn't conflict with any CSS property (no CSS clip-path exists).
This cleanly separates concerns: CSS handles static properties,
WAAPI handles the dynamic circle expansion.
2026-06-26 18:08:28 +08:00
xfy
f0ea1a294f refactor(theme): switch VT animation from CSS injection to Web Animations API
Replace dynamic <style> injection (@keyframes + pseudo-element rules)
with document.documentElement.animate({ pseudoElement }) calls.

Previous approach accumulated <style> elements across transitions,
causing subsequent dark→light animations to be invisible. The Web
Animations API:
- Creates per-call Animation objects with no DOM residue
- Has higher composite priority than CSS animations, naturally
  overriding UA default fade-in/out without !important
- Eliminates style element lifecycle management entirely

Static CSS now only sets mix-blend-mode: normal (not animatable via
WAAPI) and the is-theme-transitioning transition suppression.
2026-06-26 18:02:26 +08:00
xfy
b542952619 fix(theme): pre-inject VT styles before startViewTransition
Previous approach injected ::view-transition-new animation in vt.ready,
creating a timing gap where the NEW layer had no clip-path and was fully
visible, making dark→light transitions invisible.

Now:
- Inject <style> with @keyframes + VT pseudo-element rules BEFORE
  calling startViewTransition, so styles are ready when pseudo-elements
  are created.
- Force synchronous getComputedStyle() in VT callback to ensure the
  NEW snapshot captures final background-color, not a mid-transition
  value.
2026-06-26 17:55:45 +08:00
xfy
ba9db2d75a fix(theme): fix dark→light transition animation invisible
The static CSS rule 'animation: none' on ::view-transition-new(root)
caused the new (light) screenshot to be fully visible before the JS-
injected clip-path @keyframes kicked in. This meant the light layer
was already covering the dark layer, making the expanding circle
invisible.

Fix: move all VT pseudo-element styles (animation, mix-blend-mode)
into the dynamically injected <style> block in vt.ready, so they
apply atomically with the clip-path keyframes.

Also:
- Add transition:none !important during VT to prevent body's
  background-color 0.3s transition from producing a stale capture.
- Restore Dioxus theme.set() that was disabled for debugging, so
  the toggle icon and localStorage persist correctly.
- Use !important on injected animation to reliably override any
  UA default VT animations.
2026-06-26 17:50:45 +08:00
xfy
c1781d7831 fix(theme): VT 期间冻结 CSS 过渡,让圆形展开可见
body 有 transition: background-color .3s,VT 在 toggle dark class
后下一帧拍 NEW 快照时背景色才走了 ~5%,新旧快照几乎一样,
圆形展开看不出颜色差。修复:回调里加 vt-freeze 类禁用所有
过渡,让 NEW 快照立即拿到新主题配色;动画结束(finished)后
移除。补 1 个测试覆盖 freeze 生命周期。
2026-06-26 14:36:41 +08:00
xfy
1e78c7f0f9 fix(doc): 修复 /doc 路由与 Dioxus 静态托管冲突的 panic
Dioxus 在 dev 用 nest_service("/doc", ServeDir) 托管 public/doc/ 目录,
手动在 static_routes 注册 /doc 会在 merge 时触发路由冲突 panic。

根因(dioxus-server serve_dir_cached):dev 模式对 public/ 下的目录用
nest_service 挂整个 ServeDir,注册了 /doc;我在 static_routes 加的
精确 /doc 路由与之同名冲突。

修复:
- main.rs: 移除手动注册的 /doc 与 /doc/ 重定向路由,交给 Dioxus 托管。
  文档深层路径 /doc/yggdrasil/* 由 ServeDir 直接服务。
- Makefile: doc 目标拷贝后额外生成 public/doc/index.html 重定向页,
  ServeDir 访问目录根时返回它,经 meta refresh + JS 跳转到
  yggdrasil/index.html,让裸路径 /doc 也能直达文档。
- build/clean 同步集成 make doc 与清理 public/doc。
2026-06-26 14:27:21 +08:00
xfy
30a861f910 docs(agents): 新增 yggdrasil-core 子工程章节
与 lightbox/tiptap-editor 并列,记录用途、构建、注入方式与
主题展开动画的 View Transitions 实现。同步更新 make test 注释
与 Build Artifacts 列表。
2026-06-26 14:09:46 +08:00
xfy
6979bd1010 feat(theme): 圆形展开主题切换动画(View Transitions API)
点击主题按钮时,新主题从按钮位置以圆形向外展开覆盖全屏。
JS 同步 toggle dark class 拍快照 + CSS clip-path circle keyframes;
Rust theme.set 事后对齐状态(use_effect 幂等)。
reduced-motion 与不支持 VT 的浏览器自动降级为瞬切。
2026-06-26 14:06:23 +08:00
xfy
a0067c73d6 chore(core): yggdrasil-core 接入构建管线与全局注入
Dioxus.toml 追加 yggdrasil-core 的 css/script;Makefile 加
build-core / build-core-incremental,接入 build/build-linux/dev/test。
2026-06-26 13:58:54 +08:00
xfy
ac1f92d816 refactor(core): 迁移 post-content 到 yggdrasil-core,删除 public/js
逐行 TypeScript 化(逻辑不变),改走 window.__initPostContent 全局入口。
post-content.js 已迁移进 libs/yggdrasil-core/,public/js/ 目录随之删除。
2026-06-26 13:57:48 +08:00
xfy
afabdf4655 chore(core): 搭建 yggdrasil-core 子工程骨架
照搬 lightbox 的 Vite IIFE 工程结构,作为核心 JS 统一归处。
此 commit 仅打通构建管线,业务逻辑后续 task 填入。
同时把 public/yggdrasil-core 加入根 .gitignore。
2026-06-26 13:51:39 +08:00
xfy
731547e6df docs(agents): 新增 Workflow 章节,约定每完成功能点即提交
Some checks failed
CI / check (push) Failing after 5m8s
CI / build (push) Has been skipped
- Agent 自主判断提交时机,验证通过后直接提交,无需等待指令
- 顺带更新 make build 注释,补充 doc 步骤与 make doc/doc-open 命令
2026-06-26 11:08:55 +08:00
xfy
25e01a109b fix(write): 移除 cover_uploading 多余的 mut 绑定
本组件只读取(on_submit 拦截)与传递给 CoverUploader,写入全在子组件
内部。Signal 是 Copy 类型,.set() 不要求 mut 绑定,编译器的 unused_mut
警告是真阳性。
2026-06-26 11:07:31 +08:00
xfy
33db6dc5aa feat(doc): 文档托管到 /doc 路径
- main.rs: static_routes 加 /doc 与 /doc/ 的 301 重定向到
  /doc/yggdrasil/index.html,绕开 Router 的 /:..segments 兜底。
  文档深层路径由 Dioxus 自动托管 public/ 提供,无需额外代码。
- .gitignore: 忽略 public/doc 构建产物(配合 make doc 拷贝)
2026-06-26 11:04:29 +08:00
xfy
c024de1b09 refactor(posts): 抽取 RebuildCacheBar 子组件,降低 PostsPage 复杂度
PostsPage 主体从约 181 行降至约 121 行。「重建内容 / 重建全部」工具条(两个按钮
+ 状态信号 rebuilding/rebuild_result + do_rebuild 异步闭包 + 结果消息)整体迁入
RebuildCacheBar 子组件,完全自洽,无任何 prop。

布局调整:原结果消息是页面级兄弟节点,现纳入子组件垂直容器(按钮行在上、消息
在下),保持「消息显示在按钮下方」的视觉语义;+ 写文章 链接仍与工具条在标题行
水平并排。

依据 dioxus-render-purity skill 的 god component 治理建议。验证:cargo build +
dx check + cargo test (406) 全绿。
2026-06-26 10:50:29 +08:00
xfy
a534ee436f refactor(trash): 抽取 AutoPurgeSettings 子组件,降低 TrashPage 复杂度
TrashPage 主体从约 484 行降至约 266 行。自动清理配置(可折叠设置面板:
开关 + 保留天数步进 + 保存 + 已保存反馈)整体迁入 AutoPurgeSettings 子组件,
含 6 个私有 signal(draft_days/enabled、panel_open、saving、just_saved)+ dirty
memo + chevron_rotate 派生。

接口:settings(已保存配置)作为双向绑定 signal prop——子组件加载/保存时写入,
父组件读取 retention_days 供 TrashRow 的「剩余天数」展示。比 CoverUploader 的
两 signal 接口更简。

附带简化:原 settings_loaded 守卫删除——子组件挂载即触发一次自身 effect 加载
配置,不再嵌入列表翻页 effect,逻辑更清晰。

依据 dioxus-render-purity skill 的 god component 治理建议。验证:cargo build +
dx check + cargo test (406) 全绿。
2026-06-26 10:31:23 +08:00
xfy
2b5cd5f64f docs(makefile): cargo doc 默认使用 ayu 主题
通过 RUSTDOCFLAGS="--default-theme=ayu" 透传给 rustdoc,生成产物
根节点写入 data-theme="ayu"。默认值语义:浏览器未记住主题时生效。
2026-06-26 10:17:12 +08:00
xfy
aa3bbdc522 docs(pages): 补充 pages 模块文档
最后一个缺模块文档的文件,至此 cargo doc 三维度全满:
371 pub 项全覆盖 / 122 文件模块文档全覆盖 / 零 rustdoc 警告
2026-06-26 10:05:40 +08:00
xfy
31bd59ba25 chore(skills): 移除 frontend-design skill 2026-06-26 09:58:47 +08:00
xfy
f342742820 docs(write): 清理 CoverUploader 重复/失实的文档注释
最终 review 指出 c2eb261 留下了一段重复且失实的 doc(仍声称 cover_uploading
对本组件私有,与已改为双向绑定 prop 的事实矛盾)。删除旧段落,保留正确描述
两个双向绑定 signal 的版本。
2026-06-26 09:57:15 +08:00
xfy
c2eb2614bb fix(write): 恢复 cover_uploading 上传中保存拦截
code review 指出上一提交(835ada0)移除 on_submit 的 cover_uploading()
拦截是真实回归:uploads_in_flight 只覆盖编辑器内图片上传,不含封面上传
路径(upload_image_file 是独立 async,不更新该计数)。用户选大图后立即
点保存会把空/旧封面落库,封面被静默丢弃。

修复:cover_uploading 改由父组件 write_editor 声明并作为 prop 传入
CoverUploader(与 cover_image 同为双向绑定 signal),恢复 on_submit 的
上传中拦截。子组件仍负责 set,父组件读,接口从 1 个 signal 扩为 2 个
(cover_image + cover_uploading),均为父组件真正需要的状态。
2026-06-26 09:52:55 +08:00
xfy
835ada094d refactor(write): 抽取 CoverUploader 子组件,降低 god component 复杂度
write_editor 从 746 行降至约 480 行。封面上传(5 个私有 signal + 上传闭包 +
封面 rsx)整体迁入 CoverUploader 子组件,仅通过 cover_image signal 双向绑定
与父组件通信。依据 dioxus-render-purity skill 的 god component 治理建议。

附带调整:
- META_LABEL_CLASS / META_INPUT_CLASS 提升到模块级常量,供 write_editor 与
  CoverUploader 共用(原为 write_editor 函数局部 const,CoverUploader 取不到)。
- 移除 on_submit 中的 cover_uploading() 上传中拦截:该 signal 已私有化到子组件,
  且上传耗时极短(前端并发上传有 uploads_in_flight 拦截兜底),收益不抵跨组件
  暴露 signal 的复杂度。

验证:cargo build + dx check + cargo test (406) 全绿。
2026-06-26 09:42:14 +08:00
xfy
015edc5b6a test(post_detail): 锁定 slug 重取不再依赖镜像 signal
防止 post_detail.rs 回退到 render 期 set signal 反模式(commit 225bb24 已修)。
用源码字符串契约断言,与 theme.rs 的 preload-script 测试同模式。
2026-06-26 09:34:17 +08:00
xfy
afebd441ae refactor(trash): dirty 派生值改用 use_memo
依据 dioxus-render-purity skill 规则二:派生值不应在 render 期内联重算。
dirty 是 settings_draft_* 与 settings 的纯派生布尔值,改用 use_memo,
依赖信号不变时跳过 trim/parse 重算。同步更新两处使用点为 dirty() 调用。
2026-06-26 09:33:13 +08:00
xfy
9e9a3faeed docs(plan): 新增 Dioxus 反模式清零实施计划
覆盖 antipatterns 文档点名的三类反模式的剩余项:
- Task 1: trash.rs 的 dirty 派生值改 use_memo(小)
- Task 2: 为已修复的 post_detail slug 反模式补回归测试(小)
- Task 3: 从 write_editor 抽取 CoverUploader 子组件(大)

god component 全量拆分(TrashPage 等)明确排除出本计划,列为后续独立计划。
Task 3 经核对真实源码(行号、API 调用、import 路径)写成「移动现有代码」式步骤。
2026-06-25 18:33:56 +08:00
xfy
96da2f27d5 docs: 补齐 tiptap_bridge 文档注释并新增 make doc 目标
- tiptap_bridge.rs: 为 wasm-bindgen 桥接层全部 19 处 pub 项补文档
  (JS 类型映射、getter/setter、EditorHandle::new、pub use 重导出)
- markdown.rs: 用反引号包裹注释内字面量 <span>,消除 rustdoc 警告
- Makefile: 新增 doc / doc-open,纯 binary crate 需 --document-private-items
  才能让内部模块进文档
2026-06-25 18:26:56 +08:00
xfy
225bb2406f docs(skill)+fix: 新增 dioxus-render-purity skill 并修复 post_detail 渲染期 set signal
依据 Dioxus 0.7 antipatterns 官方指南,做两件事:

1. 新增 skill .agents/skills/dioxus-render-purity/
   把「render 函数必须纯净」做成可复用 skill,列出三类反模式(render 期
   副作用 / 用 use_signal 存派生值 / 用 use_effect 算派生值)及自检清单,
   防止后续编辑(人或 AI)再次在组件里写 signal.set 等副作用。触发关键词
   覆盖 dioxus/component/rsx/signal/use_effect 及对 src/pages、src/components
   的编辑。

2. 修复 post_detail.rs 的真实违规(被该 skill 捕获的典型反例):
   原代码用 use_signal 镜像 slug prop,再在 render 期 if slug_signal()!=slug
   { slug_signal.set(...) } 触发 server future 重跑——这是文档明确点名的
   「don't mutate state during render」。改为直接读 prop:
   use_server_future(move || get_post_by_slug(slug.clone())),路由 slug 变化
   时 Dioxus 自动重跑 future,无需镜像信号。key 改用 post.slug(post 已在
   match 分支内可用,避免 slug 被 move 后再读)。同步更新模块文档注释。

验证:dx check 通过、cargo test 405 passed。
2026-06-25 18:24:57 +08:00
xfy
9a00972be4 fix: 修复格式化工具破坏的单行 if/else 与闭包语法
格式化工具在展开单行语句时引入了 5 处语法错误,导致 dx serve 无法编译:

- comments.rs / trash.rs:on_select 的 `if checked { insert } else { remove }`
  丢失 else 分支,变为悬空语句 + 括号不匹配。
- trash.rs(3 处):`web_sys::window().and_then(|w| {...}).unwrap_or(false) {`
  展开闭包体时漏掉闭合 `)`,吞掉了 if 块的开括号。
- write.rs:封面图 `if cv.contains('?') {...} else {...}` 丢失 else,
  多出一个 `}`,导致 src 块括号失衡。

均还原为正确的多行 if/else 结构。验证:dx check 通过、cargo test 405 passed。
2026-06-25 18:11:35 +08:00
xfy
0398cc6c66 perf(render): 列表加显式 key + release 启用 panic=abort
依据 Dioxus 0.7 optimizing 指南做两项改进:

- 为数据驱动列表渲染补全显式 key(13 处,9 个文件):
  home/archives/tags/search 文章与标签列表、post_card/post_footer
  标签、header 桌面/移动导航、admin dashboard/posts/write 列表。
  使用各类型稳定唯一字段(post.id / tag / tag.name / item.label /
  err.id / year_group.year / month_group.month_en)作为 key,
  让 Dioxus diff 正确识别元素身份。骨架屏 for _ in 0..N 静态占位
  循环与已带 key 的评论树/admin 行不动。

- [profile.release] 加 panic = "abort":WASM 去掉 unwind 元数据减小
  体积;server 端错误处理走 Result+?(见 error.rs),不依赖 panic
  unwind,无副作用。

其余文件为本机格式化(单行折叠/多行展开),一并提交。

验证:dx check 通过、cargo test 405 passed。
2026-06-25 18:07:22 +08:00