267 Commits

Author SHA1 Message Date
xfy
e6f7704367 feat(admin): 全项目原生 <select> 统一替换为 FormSelect
将剩余 4 处原生下拉(write 编辑器状态胶囊、db_status/server_status
自动刷新、export 导出格式)全部迁移到主题化的 FormSelect:

- FormSelect 新增 trigger_class 可选 prop(缺省全宽表单款),并导出
  FORM_SELECT_COMPACT_CLASS 供工具栏内联小下拉复用;write.rs 状态选择器
  以胶囊 trigger_class 复刻原外观(面板贴底栏自动上翻,替代原
  「隐形 select 覆盖 span」hack)
- 面板宽度改为 max(触发器, 最长选项)(w-max min-w-full + 100vw 上限),
  紧凑触发器(如「手动」)下长选项(如「500ms」「SQL (INSERT)」)
  不再被截断
- db_status/server_status 的字符串 match 反查收编为类型安全选项表
  (Option<u32> 直进直出)
- 因 system/write 页面 SSR 真实引用 FormSelect,移除 FORM_SELECT_ID /
  wrap_index 上不再需要的 #[allow(dead_code)]

src 下已无任何原生 <select> 元素。浏览器实测:紧凑面板 93.8px 宽于
触发器 73.8px、胶囊面板上翻贴顶 6px 间隙、选中 check 与高亮行正确。
2026-07-28 14:38:12 +08:00
xfy
8b4a2b674e feat(admin): 封装 FormSelect 组件并修复 MCP 页下拉箭头错位
/admin/mcp 的两个 <select> 直接套用文本框的 INPUT_CLASS,原生下拉箭头
由浏览器绘制,深色主题下位置/颜色不协调(垂直不居中、无法跟随主题色)。

- components/forms.rs 新增泛型 FormSelect:appearance-none 隐藏原生箭头,
  右侧叠 pointer-events-none 的 chevron SVG(top-1/2 -translate-y-1/2
  精确居中);option value 内部用序号占位,onchange 直接回传选中项的 T,
  调用方免去「字符串反查枚举」样板
- mcp.rs 两个下拉改用以简形式传入 SCOPE_OPTIONS / LIFETIME_OPTIONS

浏览器实测:chevron 垂直居中偏差 0.00px,右间距 16px(与左内边距对称)。
2026-07-28 13:52:28 +08:00
xfy
9eac025b3e feat(mcp): 完整工具面 + token 管理 + 管理页(T2-T5 并行实现 + 合并)
T2 token 管理(src/api/mcp_tokens.rs):create/list/reveal/revoke + get_mcp_client_configs
服务端函数,均经 get_current_admin_user 鉴权;TokenLifetime 枚举(1/7/30/90天/永不过期)。
src/mcp/config.rs 生成 4 种客户端配置(Claude Code/Cursor/Cline/通用 + CLI)。
src/pages/admin/mcp.rs 令牌管理页:列表 + 新建表单 + 一次性明文展示/重查 + 撤销 +
配置片段卡片。路由 /admin/mcp,导航项加入 admin_layout。

T3 read 工具 + Resources(tools/read.rs, resources.rs):search_posts/get_post/list_tags
(read 作用域,仅已发布);published-post Resources(post://{slug},游标分页)。
search_published 提取为共享函数。

T4 write 工具(tools/{posts,comments,tags,media}.rs):create/update/publish/trash/delete_post、
评论审核、标签 CRUD、媒体 base64 上传(WebP 转码 + 去重)。复用既有 helper 的 SQL 与
缓存失效(moka + ssr_cache)。write 作用域,可读草稿。

T5 admin 工具(tools/{settings,runner}.rs):get/update_settings、run_code(沙箱执行)。
admin 作用域。

合并(server.rs):用 rmcp tool_router 的「命名路由 + 组合」模式,7 个工具组在
YggMcpServer 上 impl,combined_router 用 + 合并成单一 ServerHandler。各工具组鉴权
独立,经 Extension<Parts> 读 McpPrincipal + scope.grants 校验。

新增依赖 base64 0.22(媒体上传解码)。验证:server/web 双目标编译通过、clippy
--all-features -D warnings 干净、659 单测 + 1 集成全绿(+20 新测试)。
2026-07-28 11:53:32 +08:00
xfy
f6e1dff600 perf(admin): 放慢仪表盘动画节奏
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
- 卡片入场 400→600ms(内联 animation-duration 覆盖),阶梯延迟 0/60/120/180→0/120/240/360ms
- 近期文章行 row-enter 300→450ms,stagger 步进 50→80ms
- CountUp 滚动时长 500→900ms
2026-07-27 14:56:00 +08:00
xfy
61634882c5 feat(admin): 仪表盘添加分块进场动画与数字滚动
- StatCard 三张卡 animate-page-enter 阶梯入场(0/60/120ms),待审评论卡数据就绪后补挂入场类(180ms),近期文章行 animate-row-enter 逐行 stagger(50ms 步进)——动画挂在数据就绪的真实分支上,避免骨架屏截断
- 新增 CountUp 组件:数值 easeOutQuint 滚动(500ms),use_effect 内驱动保持渲染纯净;命中 prefers-reduced-motion 直接显示终值
- input.css 新增 row-enter 关键帧;reduced-motion 媒体查询补全 animate-page-enter/popover-enter/row-enter 覆盖(此前仅 page-enter 生效)
- BTN_SECONDARY 补 active:scale-[0.98] 按压反馈,与 BTN_PRIMARY 对齐
2026-07-27 14:39:10 +08:00
xfy
775c58a9e6 feat(admin): 素材页支持多选与批量删除
卡片勾选框:仅未引用素材可选(与单删保护语义一致),hover 显现、
有选择时常显,选中卡片加主题色 ring;stop_propagation 防触发灯箱。
批量条:已选计数 / 全选本页(可切换取消本页) / 清除 / 删除所选
(两步确认),选择跨翻页与筛选保留。

服务端新增 BatchDeleteAssets:一次查询取引用存在性,被引用的跳过,
其余删文件(NotFound 容忍)+ 批量删 DB 行 + 清派生缓存,返回
删除/跳过/释放/失败统计。
2026-07-27 11:40:02 +08:00
xfy
248060d920 feat(admin): 素材页搜索框添加 300ms 防抖
query 仍作输入框受控绑定,debounced_query 才是请求参数:停顿
300ms 无新输入才提交,击键重启 effect 使旧延时任务经 peek 比对
后静默过期。页码重置移入防抖提交点——放在 oninput 里会让 page
变化用旧关键词抢发一次请求。
2026-07-27 11:29:28 +08:00
xfy
0086f0f3ea feat(admin): 素材卡片「复制」更名「复制路径」
按钮复制的是 /uploads/ 相对路径而非完整 URL,名称如实反映行为;
title 同步为「复制图片相对路径」。
2026-07-27 11:23:57 +08:00
xfy
d610fd5837 feat(admin): 素材页「孤儿」更名为「未引用」
与「引用中」对仗更自然。覆盖统计行、筛选 tab、清理按钮、
清理结果横幅、卡片徽章及 purge 服务端消息;代码标识符
(orphan_count / purge_orphan_assets 等)保持不变。
2026-07-27 11:16:59 +08:00
xfy
a890257009 feat(admin): Pagination 组件支持页码跳转
当前页码渲染为可编辑输入框(仅回调式翻页且总页数 > 1 时):
聚焦编辑、回车跳转(自动夹取到合法范围,非法输入还原)、失焦回显。
posts / assets / trash 三个 signal 翻页页已接入 on_jump;
路由式翻页(comments、前台)不传 on_jump,页码保持纯文本。
2026-07-27 11:13:26 +08:00
xfy
e33e89c258 fix(assets): 工具栏右侧按钮沉入 FilterTabs 的 mb-6 区域紧贴横幅
工具栏 items-end 对齐的是 flex 项的 margin box:FilterTabs 带 mb-6 使容器
底端低于下划线 24px,右侧按钮组无底 margin 被一并拉到底端,距横幅仅 4px。
按钮组同加 mb-6(移除补偿用的 pb-1)后,按钮底边与 tabs 下划线齐平,
下划线→横幅、按钮→横幅、横幅→网格均为 24px。
2026-07-27 10:01:56 +08:00
xfy
b4bea7db0d fix(assets): 操作横幅上下间距不对称(40px/8px → 24px/24px)
FilterTabs 自带 mb-6 在 flex 工具栏内撑出 24px,再叠横幅 mt-4 形成 40px
空洞,而横幅下方仅网格 mt-2 的 8px。横幅改 mt-4 为 mb-6:上方回归 tabs
标准间距 24px,下方与网格 mt-2 margin 塌陷同为 24px,与 comments/system
页节奏一致。
2026-07-27 09:57:15 +08:00
xfy
55e4712f28 fix(assets): 删除返回「素材不存在」时刷新网格自愈过期数据
行已被其他会话/重建删除时,本页网格仍显示幽灵卡片(success:false 不触发
reload)。refs 为空的业务拒绝即「行已不在 DB」,此时同样 bump reload,
让网格收敛到服务端真实状态;引用中拒绝(refs 非空)不误刷新。
2026-07-27 09:46:46 +08:00
xfy
b98059edcc fix(assets): 引用徽标加 z-10 修复被 blur-img 展示层遮盖
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
灯箱改造(051ca94)把缩略图换成 .blur-img 双层结构后,.blur-img-full
自带 z-index:1,而徽标 span 是 z-index:auto,绘制序被图片压过,
导致「孤儿 / 被 N 篇引用」徽标在 DOM 中存在但不可见。

浏览器实测:修复前 elementsFromPoint 命中 blur-img-full,
修复后命中徽标本身,琥珀色孤儿药丸恢复显示。
2026-07-24 18:31:45 +08:00
xfy
051ca947e3 feat(assets): 素材网格接入灯箱预览
- 缩略图改为与前台正文图一致的 .blur-img 双层结构(?w=20 占位 +
  ?thumb=300x300 展示层 data-src),点击由全局注入的 lightbox.js 接管;
  图集模式(当前页内 ←/→ 切换),灯箱加载原图(data-src 去 query)。
- 数据异步到达/筛选/翻页/刷新后由 use_effect 调 __initLightbox 重新绑定
  (双保险契约同 PostContent:__lightboxSelectors + 直接调用兜底)。
- lightbox.ts 补 data-lb-bound 幂等守卫:Dioxus keyed diff 复用节点时
  重复初始化会叠加 click 监听(点一次开两次);附回归测试。
- invoke_optional_global 提取为 src/utils/js.rs 共享,post_content 复用。

浏览器实测:47 张绑定齐全,原图打开/图集切换/Esc 关闭正常,
筛选重建与重建索引重取后重绑幂等(单次点击仅 append 一次 overlay)。
2026-07-24 17:39:34 +08:00
xfy
45d70e1dc6 refactor(assets): 清理 lint,对齐项目惯例
- api/assets/mod.rs 按 posts/comments 惯例加
  #![allow(clippy::unused_unit, deprecated, unused_imports)]
  (#[server] 宏固有噪音)
- 页面交互导入按 target_arch = wasm32 门控;组件级放行非 WASM 未用绑定
- 合并 check_upload_dimensions 进 upload_dimensions(单函数返回尺寸,
  消除死代码),测试同步更名
2026-07-24 16:44:47 +08:00
xfy
5612f16425 feat(assets): 封面「从素材库选择」联动
- AssetPickerModal:素材网格(最新 60 + 搜索)单击回填封面 URL,
  内嵌「上传新图」(复用 upload_image_file,成功即选中,
  失败留在 modal 内提示),上传期间置位 cover_uploading 拦截保存
- CoverUploader 空态加「素材库」入口(与「输入链接」并列)
2026-07-24 16:32:43 +08:00
xfy
0c47e87d30 feat(assets): 全量重建索引
- rebuild_assets_index:扫 uploads/(跳过点目录)→ upsert assets
  (xmax=0 判别新增;技术字段 IS DISTINCT FROM 才更新,保留 alt,
  幂等重跑 updated=0)→ 删文件已消失的行 → 全表扫 posts 重建 refs
- 磁盘扫描与 header 尺寸读取移入 spawn_blocking
- extract_asset_paths 提升 pub(crate) 供重建复用同一提取逻辑
- 前端「重建索引」按钮(进行中禁点,结果进横幅)
2026-07-24 16:27:02 +08:00
xfy
799455da43 feat(assets): 删除保护、孤儿清理与 alt 编辑
- delete_asset:引用中业务拒绝(Ok(success:false) + 引用文章列表),
  孤儿硬删除(文件 + DB 行级联 refs + 派生缓存)
- purge_orphan_assets:一键清理无引用且超过 7 天保护窗的素材,
  容忍单项文件删除失败,返回数量/释放字节/失败数
- update_asset_alt:管理性 alt 编辑
- invalidate_asset_caches:IMAGE_CACHE 前缀批量失效 + 尺寸缓存失效;
  磁盘派生缓存(哈希命名不可枚举)由既有清理任务回收
- 前端:卡片操作区三态(复制/alt/删除 ↔ alt 编辑 ↔ 确认删除),
  顶栏「清理孤儿」两步确认(带数量与总大小),操作结果横幅
2026-07-24 16:23:00 +08:00
xfy
99b96e7a0c feat(assets): 素材管理只读列表页
- list_assets:引用状态筛选 + filename/alt 搜索 + 时间/大小排序 +
  分页(60/页),一次响应带齐 tabs 计数与可清理孤儿统计
- /admin/assets 网格页:?thumb=300x300 缩略图卡片 + 引用徽标,
  单路由客户端分页(对齐管理文章模式),导航加「素材」入口
2026-07-24 16:15:19 +08:00
xfy
5a10b07bff fix(router): 修复错误冒泡触发的 404 页「返回首页」卡死
PostDetail 对不存在的 slug 抛出 ServerFnError(404) 后,ErrorLayout 的
ErrorBoundary 捕获错误并在 fallback 渲染 NotFound。此时 ErrorBoundary 持有错误,
根据 Dioxus 0.7.9 的渲染逻辑(有错则永远渲染 fallback,不渲染 children/Outlet),
点击 Link 切换路由虽更新了 URL,但页面仍停留在 fallback,表现为「URL 变了页面不变」。

根因:ErrorBoundary 捕获错误后不会自动恢复,必须显式调用 clear_errors() 才能
重新渲染 children。原实现用 Link 导航无法干预点击时机清除错误。

修复:NotFound 与 ErrorLayout 的通用错误 fallback 中,「返回首页」改用 button,
onclick 内先 clear_errors() 清除错误边界,再 navigator().push 导航。
- NotFound 通过 try_consume_context::<ErrorContext>() 获取可能存在的错误边界:
  路由匹配命中的 404 无错误(clear_errors 是 no-op),错误冒泡的 404 才需清除。
- ErrorLayout 的 handle_error 闭包参数本身就是 ErrorContext,clone 后在按钮内使用。
2026-07-24 10:34:18 +08:00
xfy
db74a96de1 style: 格式化 Rust 与 TS 代码 2026-07-23 13:34:27 +08:00
xfy
68e290a9fe feat(write): 新建文章默认发布状态改为直接发布
将 /admin/write 新建模式的默认发布状态从「存为草稿」改为「直接发布」。
编辑模式仍回填文章实际状态,不受影响。
2026-07-23 10:10:51 +08:00
xfy
52e5ee2736 feat(admin): 代码试运行沙箱加 bun 语言按钮
SUPPORTED_LANGS 追加 'bun'(仅 canonical key,不展示别名以免按钮拥挤),
default_source 加 bun 分支——示例用 TypeScript 类型注解(:string / :number)
体现 bun 跑 TS 的语言特性。

aliases(js/ts/rs 等)经 normalize_lang 归一即命中此处某项,按钮无需重复。
2026-07-21 15:24:41 +08:00
xfy
78a8d188d1 fix(admin): 重建结果消息改绝对定位,避免撑高容器顶起按钮
RebuildCacheBar 原用 flex-col 把「已重建 x 篇文章」消息排在按钮下方,
参与文档流。其祖先 header 为 md:items-end(底边固定),消息出现时
新增高度全部转化为按钮上移,「重建内容/重建全部」被顶起、「发布文章」
相对下沉,三者错位。

改为外层 relative + 消息 absolute top-full 脱离文档流:出现/消失都
不影响按钮行高度,布局稳定。双 target 编译通过。
2026-07-16 14:36:33 +08:00
xfy
d1c41e8fc5 fix(wasm): hooks 模块移出 server gate,双端可见
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
mod hooks 原被 cfg(feature = "server") 包裹,WASM 前端无法引用 hooks 中的
代码。移出后 server 与 WASM 两端均能访问 hooks 模块。

同步修正 backup.rs 的 import 排序(cargo fmt 自动调整)
2026-07-16 11:37:13 +08:00
xfy
c6cb401522 refactor(admin): 拆分 system.rs 为 system/ 目录(按 tab 分文件)
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
pages/admin/system.rs 单文件 1792 行装 5 个 tab(数据库状态/服务器状态/SQL控制台/导出/备份恢复),按已有 api/database/ 的对称模式拆成 system/ 目录。

关键前提(已验证):5 个 tab 状态完全独立、互不共享,切换时父组件用 key 强制卸载,零跨文件状态耦合。

拆分结构(与 api/database/mod.rs 全路径风格对称):
- system/mod.rs: SystemTab enum + System 入口 + format_bytes(pub(super),3 tab 共用) + tests
- system/db_status.rs: DbStatusTab
- system/server_status.rs: ServerStatusTab + format_uptime(仅它用)
- system/sql_console.rs: SqlConsoleTab(codemirror_bridge 依赖)
- system/export.rs: ExportTab + urlencode(wasm32)
- system/backup.rs: BackupTab + BackupRow + urlencode_dl(自包含,不再跨文件依赖 export.rs)

路由侧零改动: admin/mod.rs 的 pub mod system + pub use system::System 不变(Rust 自动认 system/mod.rs)。双目标编译 + clippy 严格 + dx check + SystemTab 测试全部通过。
2026-07-15 18:28:39 +08:00
xfy
27ed605223 fix(comments): 修复 section.rs / dashboard.rs 括号不匹配导致编译失败
两处同一类错误:match arm 内 `if {} else {}` 表达式后多了一个 `}`,
导致 arm 被提前关闭,触发 "unexpected closing delimiter" 编译错误。

- section.rs:125 — `if !has_any {…}` 块后多余的 `}`,删除使 if-else 合一
- dashboard.rs:119 — 同类问题,另需补 `;` 终止 `let` 语句
2026-07-15 18:12:00 +08:00
xfy
886eeca915 style: 全项目 rsx! 格式规范化(cargo fmt)
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
2026-07-15 17:49:03 +08:00
xfy
65a7b1226f style: 全项目格式规范化(cargo fmt)+ Docker daemon 断连容错
- 全项目 .rs 统一 cargo fmt 排版(换行/缩进/参数分行/导入排序)
- infra/docker: DOCKER_CLIENT 连接失败不 panic,降级返回 None
  (博客不依赖 Docker,panic=abort 下会导致整个进程崩溃)
- infra/mod: 去除多余空行
- database/mod: 模块声明按字母序重新排序
2026-07-15 17:22:06 +08:00
xfy
ffa0a72e51 feat(admin/posts): tab 栏加平滑滑动指示器动画
PostsTabs 此前用 border-b-2 边框瞬切表示选中态,缺少 tab 切换的滑动动画。
移植 FilterTabs(system/comments 页)的滑动指示器机制:相对定位容器 + 绝对
定位滑块(贴底边盖住外层 border-b),WASM 端测量目标 button 的
offsetLeft/offsetWidth 动态定位,transition-all duration-300 驱动滑块从一个
tab 平滑滑到另一个。

选中态从 border-b-2 边框改为文字颜色区分(text-paper-primary),滑块负责
视觉指示。回收站数量角标保留不变。

与 FilterTabs 视觉一致,但 PostsTabs 因需支持角标故未直接复用 FilterTabs
(后者 items 是纯文本元组)。本地自建 POSTS_TAB_GROUP_ID 计数器给每处实例
唯一 DOM id 前缀。
2026-07-15 10:47:31 +08:00
xfy
25299d266f refactor(admin): 合并 /admin/posts 与 /admin/posts/trash 为单路由 + tab 切换
将「全部文章」与「回收站」从两个独立路由(及各自的分页变体 PostsPage /
PostsTrashPage)合并为单一 /admin/posts 路由,用顶部 tab 在二者间切换。
tab 状态与翻页均改为客户端 signal 驱动(不走路由、不深链),与 system.rs 的
tab 模式一致:admin 内部页,刷新回到「全部文章」第 1 页。

路由瘦身:删除 PostsTrash / PostsTrashPage / PostsPage 三个变体,Router 不再
需要「静态段 trash 优先于 :page」的排序技巧。

主要改动:
- posts.rs: Posts 改为 tab 容器(PostsTab 枚举 + active_tab signal + key 化
  match 渲染),header 文案随 tab 切换;PostsPage 主体下沉为 AllPostsList,
  翻页改 current_page signal + use_paginated 回调式 Pagination。
- posts_trash.rs: PostsTrashPage → PostsTrashPanel(无参,signal 翻页),
  删除入口组件与 PostsTabs import(header/tab 由容器统一提供)。
- PostsTabs: 从路由驱动(use_route)改为 signal 驱动(on_change 回调)。
- 修复 tab 水平对齐 bug:两个 tab 统一 inline-flex items-center 同盒模型 +
  外层容器加 items-center,根除原先「全部文章」(inline 文本) 与「回收站」
  (inline-flex 带角标) 盒模型不一致导致的垂直错位。
- admin_layout.rs: is_posts_route 简化为只匹配 Route::Posts{}。
- RebuildCacheBar 改为完全自持 state(rebuilding + result),结果消息行随组件
  内联渲染,与父组件零耦合。
2026-07-15 10:21:50 +08:00
xfy
9e57247a2d refactor: 统一 WASM sleep 到 utils::time::sleep_ms
删除 system.rs 的私有 wasm_sleep(与 utils::time::sleep_ms 逐字相同),
6 处调用改用 crate::utils::time::sleep_ms。
ui.rs 的内联 Promise+setTimeout(50ms) 同样替换为 sleep_ms(50)。

消除 3 份 sleep 实现中的 2 份冗余副本,utils::time::sleep_ms 成为全项目
唯一的 sleep 入口(已有 native tokio + WASM 双实现)。
2026-07-14 14:55:34 +08:00
xfy
0e4109358f docs(system): 修复 BackupRowProps 文档里 Popover 的 broken intra-doc link
Some checks failed
CI / check (push) Failing after 11m58s
CI / build (push) Has been skipped
[`Popover`] 简写形式解析失败——Popover 的 use 只在 fn BackupRow 函数体里,
对 BackupRowProps struct 的 doc comment 不可见。补全为全路径
[`Popover`](crate::components::ui::Popover),保留可点击跳转。
2026-07-14 13:39:34 +08:00
xfy
ee9d2ecd6e style(ui): 组件圆角类对齐三档梯度(32/16/8)
配合 input.css 的 token 化,把散落在 RSX 组件里的孤悬圆角档位
统一到内容档 16px (rounded-2xl):

消除 24px (rounded-3xl, 4处无规范依据的中间档):
- ADMIN_CARD_CLASS / ADMIN_TABLE_CLASS (ui.rs)
- write.rs 编辑器容器 / write_skeleton.rs 编辑器骨架

消除 12px (rounded-xl, 10处):
- INPUT_CLASS (forms.rs) 与 write 页输入框统一到 16px
- write 页 3 处输入框 + 4 处错误提示框
- admin_layout 退出按钮
- system/admin_skeleton/posts_trash 卡片容器
- write_skeleton 骨架条(模拟输入框形状,跟随真实圆角)

改造后全站只剩 4 种圆角语义: 32px容器 / 16px内容 / 8px微元素 /
胶囊按钮,孤悬档位彻底清除。
2026-07-13 16:21:10 +08:00
xfy
5888f1fc41 feat(system): 备份恢复删除/恢复改用 Popover 确认框替代浏览器 confirm
新增通用 Popover 组件(src/components/ui.rs,与 Tooltip 对称),定位用
position:fixed + MouseEvent::client_coordinates() 视口坐标锚定,逃出表格
overflow-hidden;透明遮罩 z-40 兜底点击外部关闭,组件内 use_effect 注册
全局 keydown 监听 Esc 关闭(use_drop 清理),150ms 淡入缩放动画
(input.css 新增 popover-enter keyframes)。

BackupRow 删除/恢复按钮 onclick 读点击坐标打开对应确认 popover,确认
按钮回调父组件。顺带简化恢复链路:原生 confirm 是阻塞式才需要
pending_restore signal + 确认 use_future 的间接机制,popover 非阻塞后
on_restore 直接 busy + restore_backup + active_task_id,移除 pending_restore
signal 及其 use_future(进度轮询 use_future 不变,仍服务创建备份+恢复)。
2026-07-13 15:08:24 +08:00
xfy
c1530956b0 style(system): 数据导出 tab 卡片宽度与其它 tab 对齐
ExportTab 根容器带了 max-w-2xl 把卡片限宽在 672px,而 DbStatusTab /
ServerStatusTab / SqlConsoleTab / BackupTab 都不限宽填满 max-w-7xl
内容区。去掉 max-w-2xl 让数据导出卡片与其它 tab 宽度一致。内部表单
输入用 w-full,加宽后自然拉伸,无空白。
2026-07-13 14:23:14 +08:00
xfy
2e40836480 fix(system): 备份/恢复任务轮询永不启动导致按钮卡死在 loading
点击「创建备份」后接口返回 task id,但按钮一直转圈、列表不刷新。
根因是 AGENTS.md 踩坑记录里的「Reactive hooks 不追踪普通 props/snapshot」:

进度轮询 use_future 在挂载时把 active_task_id 快照进 _task_id_for_poll
(彼时为 None),use_future 只运行一次即 return;用户点击后 create_backup
返回 task id 并 active_task_id.set(Some(id)),但 future 已结束、永不重跑
→ 轮询不启动 → busy 永远 true。恢复确认 use_future 有同样的快照陷阱
(_pending_for_confirm),导致点「恢复」连确认框都不弹。

改用与 DbStatusTab 自动刷新一致的长生命周期 loop 模式:use_future 只跑
一次,内部 loop 每轮迭代读 active_task_id() / pending_restore() 信号,
空闲时 200ms yield 呼吸。这样信号更新在下一轮迭代即被感知,无需依赖
ReactiveContext 对挂载期快照的订阅。
2026-07-13 13:56:22 +08:00
xfy
8cf34769e5 feat(not_found): commit HTTP 404 status code during SSR 2026-07-09 17:33:19 +08:00
xfy
1b38162ca4 feat(post_detail): propagate NotFound and other load errors to ErrorBoundary 2026-07-09 17:31:49 +08:00
xfy
b5a64f8dcf fix(post): 上下篇切换后可运行代码块消失
承接前两轮修复:路由重跑(79978aa)、正文更新(f86cb48)后,翻页时文章正文
正确切换了,但可运行代码块(CodeRunner)消失——容器 div 还在,CodeMirror
编辑器没挂载。

根因:上下篇切换(/post/rust → /post/go)时 PostContent 组件被复用(仅重渲染,
非重新挂载)。其内部 CodeRunner 用片段索引作 key(runner-{i}),而两篇文章的
代码块索引恰好相同(都是 1/3/5),keyed diff 按相同 key 复用 CodeRunner 实例。
复用的实例保留 use_hook/use_effect 状态:挂载 use_effect 的「防重复 init」守卫
(editor_handle.is_some())阻止 CodeMirror 挂载到新的(已替换的)DOM 容器,
而旧 CodeMirror 还绑定在被销毁的 rust DOM 上。同时 PostContent 自身的 use_effect
(__initPostContent 复制按钮 / 灯箱初始化)也不重跑,新文章交互脚本不初始化。

曾尝试直接给 PostContent 加 key=post.slug,无效:Dioxus 的 key diff
(diff_keyed_children)只在「兄弟节点列表」里生效,对单个非列表元素的 key
变化走 diff_non_keyed 路径、按位置复用,不触发 remount(经 use_hook 探针确认:
翻页后 CodeRunner 函数体执行了但 use_hook 从未重新执行)。

修复:把 PostContent 包进单元素 keyed 列表(for + iter::once + key=slug),
使其进入 keyed diff 路径——slug 变化时旧实例移除、新实例创建,连带 CodeRunner
重新挂载、交互脚本重新初始化。

验证(headless chromium,rust↔go 各含3个代码块):
- 修复前:翻页后容器在、CodeMirror 挂载数=0(红灯)
- 修复后:CodeMirror 挂载数=3,骨架屏清零;prev/next 双向 + SSR 首屏均正常
2026-07-09 17:12:48 +08:00
xfy
1c56fd8ec1 fix(home,tags): 同变体路由分页/切标签后列表不更新
延续上一个 commit(fix(post))发现的同类 bug,统一修复 home 和 tags:

- home: /page/1 → /page/2(同 Route::HomePage 变体)后文章列表不更新
- tags: /tags/a → /tags/b(同 Route::TagDetail 变体)后标签下文章不更新

根因完全一致:use_server_future 闭包内读取的 current_page / tag 是普通 prop
(i32 / String),move 进闭包后成为冻结快照,不建立反应式订阅,同变体导航
复用组件实例时 future 不重跑。

注意 / → /page/2(Route::Home → Route::HomePage,跨变体)不受影响,因为跨
变体会新挂载组件、future 首次运行;只有同变体间的 prop 变化才触发 bug。

修复:闭包内通过 router().current::<Route>() 读取当前 page/tag 建立订阅。
Home(/ 路由,Route::Home 变体无 page 字段)调用 HomePosts 时走兜底分支用
传入的 current_page。

验证(headless chromium,本地设 APP_BASE_URL 绕过 CSRF 403):
- home 修复前:/page/1 点下一页,URL 变 /page/2,首篇仍是 page1 内容(复现)
- home 修复后:首篇随分页变为 page2 内容
- tags 因数据库无多文章标签,无法端到端验证;代码模式与 home 一致且编译通过
2026-07-09 16:22:08 +08:00
xfy
79978aa4a4 fix(post): 修复上/下一篇导航后文章内容不更新
点击文章底部的上一篇/下一篇后,URL 正确变化为 /post/<new-slug>,但页面
内容仍停留在旧文章,只有刷新浏览器才生效。

根因:use_server_future(内部即 use_resource)依赖 ReactiveContext 追踪
闭包内读取的 signal 来决定重跑时机。但 PostDetail 的 slug 是路由宏注入的
普通 String prop,move 进闭包后成为冻结快照,读取它不建立任何反应式订阅。
同一 Route::PostDetail 变体间的导航(/post/a -> /post/b)会复用组件实例、
仅更新 props,而 use_server_future 的 task 只在首次 hook 时创建一次,因此
永远不会重跑。

此前 commit 225bb24 把 slug_signal 镜像 + render 期 set 改成直接读 prop,
消除了反模式,但也丢掉了唯一能触发重跑的订阅源(slug_signal 的读取),
反而引入了这个 bug。

修复:在闭包内通过 dioxus::router::router().current::<Route>() 读取当前
slug。current() 内部调用 subscribe_to_current_context(),在 use_server_future
的 ReactiveContext 中注册订阅,路由变化即触发重跑。render body 保持纯净。

验证(headless chromium,本地需设 APP_BASE_URL 绕过 CSRF 403):
- 修复前:点 Next 后 URL 变 /post/1783580058,标题仍为旧文章(复现 bug)
- 修复后:URL 变,标题随之变为新文章
2026-07-09 15:34:25 +08:00
xfy
6a3ac038da refactor(write): 摘要字段移至右侧栏
摘要从左栏(标题下方)移到右栏侧边栏,作为独立分节位于标签与封面图之间,
与链接/标签/封面统一为带边框输入框的分节式布局。骨架屏同步更新。
2026-07-09 14:45:15 +08:00
xfy
6e12b33769 fix(write): 彻底修复骨架屏高度不撑满
骨架屏在两条渲染路径都被高度链断裂卡住,内容只占页面 40-50%,
底部大片空白。

根因:高度链在两处父容器断裂,导致骨架屏无法引用到确定高度:
1. 未登录态(admin_layout):骨架屏被包在 div.p-10.animate-pulse,
   该 div 是普通块级元素,无 flex-1 不撑满 main、非 flex 容器使
   骨架屏 flex-1 无效、无 h-full。SSR 初始态走这条。
2. 登录态(write.rs 覆盖层):absolute inset-0 虽撑满,但非 flex
   容器,骨架屏 flex-1 无意义;改 h-full 又遇 height:100% 在
   非显式高度父容器下解析不可靠的经典坑。

修复:统一让两条路径的父容器都成为 flex flex-col,骨架屏根用
flex-1(比 height:100% 可靠,不依赖父显式 height):
- admin_layout 包裹层: p-10.animate-pulse → flex-1.min-h-0.
  flex.flex-col.animate-pulse(去掉对 write 不合适的 p-10,
  非 write 页面 padding 由 main_class 自带)
- write.rs 覆盖层: absolute inset-0 → absolute inset-0 flex flex-col
- write_skeleton 根: h-full → flex-1
2026-07-09 14:29:34 +08:00
xfy
aba2aa2105 style(write): 移除右侧栏背景,回归透明
侧边栏不再用 paper-entry 整栏底色,改为透明(与左栏共用
paper-theme 底),靠 border-l 和节间分隔线划分区域。
同步:输入框改回 paper-entry 底 + 实色边框,封面图拖拽区
也改回 paper-entry,保持与透明侧栏的层次对比。
2026-07-09 14:08:00 +08:00
xfy
abc130eae7 refactor(write): 重新设计右侧栏为分节式轻量布局
原「卡片套卡片」结构(slug+标签包在一个重卡片里、封面图独立卡片)
层级冗余、视觉笨重。改为分节式(section)布局:

- 侧边栏整体用 paper-entry 底色,与左栏 paper-theme 形成层次区分
- 链接/标签/封面图三节用细分隔线 border-b 分隔,而非独立卡片
- 标题改用 uppercase 小标签 + tracking-wide,更专业紧凑
- 输入框改用 paper-theme 底 + 透明边框,聚焦时显主色边框
- 封面图拖拽区底色改 paper-theme,与侧边栏背景对比清晰

移除不再使用的 META_LABEL_CLASS 常量。
2026-07-09 14:03:07 +08:00
xfy
9b39e972e7 style(admin): 收紧后台所有页面左右边距 px-10 → px-6
px-10(40px) 偏宽,收紧到 px-6(24px) 更紧凑。
- admin_layout 的 main_class: 影响 dashboard/comments/posts/
  posts_trash/runner/system 等所有非 write 页面
- write 页面左栏内容区 + 底栏,保持与其它页面一致对齐
2026-07-09 13:52:29 +08:00
xfy
012c21532d refactor(write): 移除「撰写新文章/内容编辑器」页头条
页头条占用了顶部垂直空间且与左侧栏的标题输入框语义重复,
直接移除,让写作区从顶部开始,编辑器获得更多高度。
2026-07-09 13:50:46 +08:00
xfy
7b678481c6 refactor(write): 写文章页改为左右两栏布局,编辑器自适应高度
原单列布局把编辑器挤到折叠线以下、又被元信息卡片压缩(h-[60vh]),
显得太矮。改为左右两栏:

- 左栏(主写作区): 标题 + 摘要 + 编辑器,编辑器改为 flex-1 撑满
  剩余高度(内部 .tiptap-editor/.ProseMirror 已是 height:100%)。
- 右栏(侧边栏, w-80): Slug + 标签 + 封面卡片纵向堆叠,独立滚动。
- 页头条提到两栏之上,底栏(取消/状态/发布)不变,仍横跨两列贴底。

#tiptap-editor 容器 id 保持不变,tiptap_bridge 按 id 挂载不受影响。
所有信号/server function/状态逻辑零改动,纯结构样式调整。
2026-07-09 13:40:37 +08:00