Compare commits
11 Commits
369b4838a3
...
62fe87961f
| Author | SHA1 | Date | |
|---|---|---|---|
| 62fe87961f | |||
| 431e6e1db4 | |||
| 48a328f553 | |||
| 962bef2276 | |||
| 45811df96e | |||
| 408b29dd9d | |||
| 8855cdd49d | |||
| 656e35c79f | |||
| 0b53e968c1 | |||
| fbd706b198 | |||
| d1b5b8ee4e |
8
Makefile
8
Makefile
@ -1,4 +1,4 @@
|
||||
.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental build-core build-core-incremental highlight-css test doc doc-open start
|
||||
.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental build-core build-core-incremental highlight-css test doc doc-open start clippy fix
|
||||
|
||||
build:
|
||||
@$(MAKE) build-editor
|
||||
@ -75,6 +75,12 @@ test:
|
||||
@cd libs/lightbox && pnpm test
|
||||
@cd libs/yggdrasil-core && pnpm test
|
||||
|
||||
clippy:
|
||||
@cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
fix:
|
||||
@cargo fix --allow-dirty
|
||||
|
||||
# 只编译当前 crate 的文档(--no-deps 跳过依赖,--document-private-items
|
||||
# 让纯 binary crate 的内部模块/私有项也进文档,否则页面基本是空的)。
|
||||
# RUSTDOCFLAGS 把 rustdoc 的 --default-theme=ayu 透传过去——cargo doc 本身
|
||||
|
||||
@ -707,3 +707,4 @@
|
||||
}
|
||||
|
||||
/* ========== Blur-up 渐进图片加载(已移入 @layer components) ========== */
|
||||
@source "./src/**/*.{rs,html}";
|
||||
|
||||
BIN
public/images/xiantiaoxiaogou_03.webp
Normal file
BIN
public/images/xiantiaoxiaogou_03.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 452 KiB |
@ -147,12 +147,19 @@ pub fn CommentForm(post_id: i32, parent_id: Option<i64>, parent_indent: Option<i
|
||||
class: "block text-sm font-medium text-paper-secondary mb-1",
|
||||
"内容 *"
|
||||
}
|
||||
textarea {
|
||||
id: "comment-content-{id_suffix}",
|
||||
class: "{INPUT_CLASS} min-h-[100px] resize-y",
|
||||
value: "{content_md}",
|
||||
disabled: submitting(),
|
||||
oninput: move |e| content_md.set(e.value()),
|
||||
div { class: "relative bg-paper-entry rounded-lg",
|
||||
textarea {
|
||||
id: "comment-content-{id_suffix}",
|
||||
class: "{INPUT_CLASS} !bg-transparent relative z-10 peer block min-h-[100px] resize-y",
|
||||
value: "{content_md}",
|
||||
disabled: submitting(),
|
||||
oninput: move |e| content_md.set(e.value()),
|
||||
}
|
||||
img {
|
||||
src: "/images/xiantiaoxiaogou_03.webp",
|
||||
alt: "",
|
||||
class: "absolute bottom-1.5 right-1.5 w-24 pointer-events-none opacity-60 peer-focus:opacity-10 transition-opacity duration-300 z-0",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -77,7 +77,7 @@ pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element {
|
||||
}
|
||||
|
||||
div {
|
||||
class: "prose prose-sm dark:prose-invert max-w-none text-paper-secondary",
|
||||
class: "prose prose-sm dark:prose-invert max-w-none text-paper-secondary md-content",
|
||||
dangerous_inner_html: comment.content_html.as_deref().unwrap_or(""),
|
||||
}
|
||||
|
||||
|
||||
@ -207,6 +207,7 @@ pub fn FilterTabs(
|
||||
let mut indicator_style = use_signal(|| "left: 0px; width: 0px; opacity: 0;".to_string());
|
||||
let id_prefix = use_hook(|| TAB_GROUP_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst));
|
||||
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_variables))]
|
||||
let update_indicator = move |active: String| {
|
||||
spawn(async move {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@ -254,11 +255,7 @@ pub fn FilterTabs(
|
||||
button {
|
||||
id: "tab-{id_prefix}-{value}",
|
||||
key: "{value}",
|
||||
class: if active_value == *value {
|
||||
"cursor-pointer px-4 py-2 text-sm font-medium text-paper-primary transition-colors"
|
||||
} else {
|
||||
"cursor-pointer px-4 py-2 text-sm font-medium text-paper-secondary hover:text-paper-primary transition-colors"
|
||||
},
|
||||
class: if active_value == *value { "cursor-pointer px-4 py-2 text-sm font-medium text-paper-primary transition-colors" } else { "cursor-pointer px-4 py-2 text-sm font-medium text-paper-secondary hover:text-paper-primary transition-colors" },
|
||||
onclick: {
|
||||
let v = value.to_string();
|
||||
move |_| {
|
||||
|
||||
@ -336,25 +336,25 @@ mod tests {
|
||||
fn relative_label_hours() {
|
||||
let (label, _) = relative_label_from_millis(60 * 60_000, ISO);
|
||||
assert_eq!(label, "1 小时前");
|
||||
let (label, _) = relative_label_from_millis(3 * 3600_000, ISO);
|
||||
let (label, _) = relative_label_from_millis(3 * 3_600_000, ISO);
|
||||
assert_eq!(label, "3 小时前");
|
||||
let (label, _) = relative_label_from_millis(23 * 3600_000, ISO);
|
||||
let (label, _) = relative_label_from_millis(23 * 3_600_000, ISO);
|
||||
assert_eq!(label, "23 小时前");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_label_days() {
|
||||
let (label, _) = relative_label_from_millis(24 * 3600_000, ISO);
|
||||
let (label, _) = relative_label_from_millis(24 * 3_600_000, ISO);
|
||||
assert_eq!(label, "1 天前");
|
||||
let (label, _) = relative_label_from_millis(7 * 24 * 3600_000, ISO);
|
||||
let (label, _) = relative_label_from_millis(7 * 24 * 3_600_000, ISO);
|
||||
assert_eq!(label, "7 天前");
|
||||
let (label, _) = relative_label_from_millis(29 * 24 * 3600_000, ISO);
|
||||
let (label, _) = relative_label_from_millis(29 * 24 * 3_600_000, ISO);
|
||||
assert_eq!(label, "29 天前");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_label_falls_back_to_date_over_30_days() {
|
||||
let (label, absolute) = relative_label_from_millis(60 * 24 * 3600_000, ISO);
|
||||
let (label, absolute) = relative_label_from_millis(60 * 24 * 3_600_000, ISO);
|
||||
assert_eq!(label, "2026-06-22");
|
||||
assert_eq!(absolute, "2026-06-22");
|
||||
}
|
||||
|
||||
22
src/main.rs
22
src/main.rs
@ -325,16 +325,20 @@ fn main() {
|
||||
// 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖。
|
||||
// 注意:src/ssr_cache.rs 中的世代号是未来就绪基础设施,当前并不会使
|
||||
// Dioxus 0.7 的 SSR 缓存实际失效(Dioxus 未暴露相应 API)。在 API 可用
|
||||
// 之前,SSR_CACHE_SECS 仍是唯一有效的兜底 TTL。
|
||||
// 之前,SSR_CACHE_SECS 仍是唯一有效的兜底 TTL——它就是内容写入后
|
||||
// SSR 页面可见滞后的上界。
|
||||
let ssr_cache_secs = std::env::var("SSR_CACHE_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(3600);
|
||||
tracing::info!(
|
||||
ssr_cache_secs,
|
||||
"增量渲染缓存生效(写入后内容可见滞后的上界);\
|
||||
调小可缩短滞后,代价是 SSR 重渲染更频繁"
|
||||
);
|
||||
let config = ServeConfig::builder().incremental(
|
||||
dioxus::server::IncrementalRendererConfig::default().invalidate_after(
|
||||
std::time::Duration::from_secs(
|
||||
std::env::var("SSR_CACHE_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(3600),
|
||||
),
|
||||
),
|
||||
dioxus::server::IncrementalRendererConfig::default()
|
||||
.invalidate_after(std::time::Duration::from_secs(ssr_cache_secs)),
|
||||
);
|
||||
|
||||
// SSR 世代号中间件:把当前全局世代号注入请求扩展,并对 GET 请求的
|
||||
|
||||
@ -17,26 +17,7 @@ pub fn About() -> Element {
|
||||
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight", "关于" }
|
||||
}
|
||||
article { class: "prose dark:prose-invert max-w-none text-paper-content leading-relaxed",
|
||||
p { "Yggdrasil 是一个以文字为主的简约博客系统。" }
|
||||
p {
|
||||
"它使用 Rust + Dioxus 构建,采用 PostgreSQL 作为数据库,支持 Markdown 写作、标签管理和暗色模式。"
|
||||
}
|
||||
h2 { class: "text-xl font-bold text-paper-primary mt-8 mb-4", "技术栈" }
|
||||
ul { class: "list-disc pl-5 space-y-1",
|
||||
li { "Rust + Dioxus 0.7 (全栈 Web 框架)" }
|
||||
li { "PostgreSQL + tokio-postgres (数据库)" }
|
||||
li { "Tailwind CSS (样式)" }
|
||||
li { "Tiptap Editor (富文本编辑器)" }
|
||||
li { "pulldown-cmark (Markdown 渲染)" }
|
||||
}
|
||||
h2 { class: "text-xl font-bold text-paper-primary mt-8 mb-4", "特性" }
|
||||
ul { class: "list-disc pl-5 space-y-1",
|
||||
li { "Markdown 写作与实时预览" }
|
||||
li { "文章标签与归档" }
|
||||
li { "暗色/亮色主题切换" }
|
||||
li { "响应式设计" }
|
||||
li { "文章搜索" }
|
||||
}
|
||||
p { "世界……遗忘我……" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ pub fn PostsPage(page: i32) -> Element {
|
||||
action: EmptyStateAction {
|
||||
label: "写文章".to_string(),
|
||||
to: Route::Write {},
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
div { class: "{ADMIN_TABLE_CLASS}",
|
||||
|
||||
@ -106,7 +106,6 @@ pub fn TrashPage(page: i32) -> Element {
|
||||
// 自动清理配置卡片(抽取为子组件 AutoPurgeSettings,见文件末尾)。
|
||||
AutoPurgeSettings { settings }
|
||||
|
||||
|
||||
// 批量操作栏(选中时显示)
|
||||
if !selected_ids().is_empty() {
|
||||
div { class: "flex items-center gap-3 p-3 bg-paper-theme rounded-lg",
|
||||
@ -133,9 +132,9 @@ pub fn TrashPage(page: i32) -> Element {
|
||||
if web_sys::window()
|
||||
.and_then(|w| {
|
||||
w.confirm_with_message(
|
||||
"确定要彻底删除选中的文章吗?此操作不可恢复。",
|
||||
)
|
||||
.ok()
|
||||
"确定要彻底删除选中的文章吗?此操作不可恢复。",
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
@ -214,7 +213,7 @@ pub fn TrashPage(page: i32) -> Element {
|
||||
}
|
||||
}
|
||||
th { class: "px-4 py-3 font-medium", "标题" }
|
||||
th { class: "px-4 py-3 font-medium", "原状态" } // 底部:清空回收站 + 分页
|
||||
th { class: "px-4 py-3 font-medium", "原状态" }
|
||||
th { class: "px-4 py-3 font-medium w-28", "删除时间" }
|
||||
th { class: "px-4 py-3 font-medium w-24 text-center", "剩余" }
|
||||
th { class: "px-4 py-3 font-medium w-32 text-right", "操作" }
|
||||
@ -287,9 +286,9 @@ pub fn TrashPage(page: i32) -> Element {
|
||||
if web_sys::window()
|
||||
.and_then(|w| {
|
||||
w.confirm_with_message(
|
||||
"确定要清空回收站吗?所有已删除文章将被彻底移除,此操作不可恢复。",
|
||||
)
|
||||
.ok()
|
||||
"确定要清空回收站吗?所有已删除文章将被彻底移除,此操作不可恢复。",
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
@ -425,145 +424,141 @@ fn AutoPurgeSettings(settings: Signal<TrashSettings>) -> Element {
|
||||
// 设置面板(可折叠带平滑动画)
|
||||
div {
|
||||
class: "grid transition-all duration-300 ease-in-out",
|
||||
style: if settings_panel_open() {
|
||||
"grid-template-rows: 1fr; opacity: 1; pointer-events: auto;"
|
||||
} else {
|
||||
"grid-template-rows: 0fr; opacity: 0; pointer-events: none;"
|
||||
},
|
||||
style: if settings_panel_open() { "grid-template-rows: 1fr; opacity: 1; pointer-events: auto;" } else { "grid-template-rows: 0fr; opacity: 0; pointer-events: none;" },
|
||||
div { class: "overflow-hidden min-h-0",
|
||||
div { class: "border-t border-paper-border p-5 space-y-6",
|
||||
// 开关行:启用自动清理
|
||||
div { class: "flex items-center justify-between gap-4",
|
||||
div { class: "min-w-0",
|
||||
div { class: "text-sm font-medium text-paper-primary",
|
||||
"启用自动清理"
|
||||
div { class: "flex items-center justify-between gap-4",
|
||||
div { class: "min-w-0",
|
||||
div { class: "text-sm font-medium text-paper-primary",
|
||||
"启用自动清理"
|
||||
}
|
||||
div { class: "text-xs text-paper-secondary mt-1",
|
||||
"后台任务定期彻底删除超过保留期的文章"
|
||||
}
|
||||
}
|
||||
div { class: "text-xs text-paper-secondary mt-1",
|
||||
"后台任务定期彻底删除超过保留期的文章"
|
||||
// 自定义开关(toggle switch)—— 取代原生 checkbox
|
||||
button {
|
||||
role: "switch",
|
||||
aria_checked: "{settings_draft_enabled()}",
|
||||
class: if settings_draft_enabled() { "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-accent cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" } else { "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-tertiary cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" },
|
||||
onclick: move |_| {
|
||||
settings_draft_enabled.set(!settings_draft_enabled());
|
||||
just_saved.set(false);
|
||||
},
|
||||
// 滑块圆点
|
||||
span { class: if settings_draft_enabled() { "absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm dark:shadow-black/30 transition-transform duration-200 translate-x-5" } else { "absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm dark:shadow-black/30 transition-transform duration-200" } }
|
||||
}
|
||||
}
|
||||
// 自定义开关(toggle switch)—— 取代原生 checkbox
|
||||
button {
|
||||
role: "switch",
|
||||
aria_checked: "{settings_draft_enabled()}",
|
||||
class: if settings_draft_enabled() { "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-accent cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" } else { "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-tertiary cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" },
|
||||
onclick: move |_| {
|
||||
settings_draft_enabled.set(!settings_draft_enabled());
|
||||
just_saved.set(false);
|
||||
},
|
||||
// 滑块圆点
|
||||
span { class: if settings_draft_enabled() { "absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm dark:shadow-black/30 transition-transform duration-200 translate-x-5" } else { "absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm dark:shadow-black/30 transition-transform duration-200" } }
|
||||
}
|
||||
}
|
||||
|
||||
// 保留天数行
|
||||
div { class: "space-y-3",
|
||||
div { class: "min-w-0",
|
||||
div { class: "text-sm font-medium text-paper-primary",
|
||||
"保留天数"
|
||||
}
|
||||
div { class: "text-xs text-paper-secondary mt-1",
|
||||
"文章删除后保留的时长,到期后自动彻底清除(1–365)"
|
||||
}
|
||||
}
|
||||
// 数字输入 + 步进按钮 + 单位后缀
|
||||
div { class: "flex items-center gap-3",
|
||||
div { class: "flex items-center rounded-lg border border-paper-border bg-paper-entry overflow-hidden",
|
||||
// 减号
|
||||
button {
|
||||
class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
|
||||
r#type: "button",
|
||||
aria_label: "减少保留天数",
|
||||
onclick: move |_| {
|
||||
let cur: i32 = settings_draft_days().trim().parse().unwrap_or(30);
|
||||
let next = cur.saturating_sub(1).max(1);
|
||||
settings_draft_days.set(next.to_string());
|
||||
just_saved.set(false);
|
||||
},
|
||||
"−"
|
||||
// 保留天数行
|
||||
div { class: "space-y-3",
|
||||
div { class: "min-w-0",
|
||||
div { class: "text-sm font-medium text-paper-primary",
|
||||
"保留天数"
|
||||
}
|
||||
// 数字输入(无边框,衔接步进按钮)
|
||||
input {
|
||||
r#type: "number",
|
||||
min: "1",
|
||||
max: "365",
|
||||
class: "w-14 h-9 px-1 text-center text-sm tabular-nums text-paper-primary bg-transparent border-0 focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
|
||||
value: "{settings_draft_days()}",
|
||||
oninput: move |e| {
|
||||
settings_draft_days.set(e.value());
|
||||
just_saved.set(false);
|
||||
},
|
||||
}
|
||||
// 加号
|
||||
button {
|
||||
class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
|
||||
r#type: "button",
|
||||
aria_label: "增加保留天数",
|
||||
onclick: move |_| {
|
||||
let cur: i32 = settings_draft_days().trim().parse().unwrap_or(30);
|
||||
let next = cur.saturating_add(1).min(365);
|
||||
settings_draft_days.set(next.to_string());
|
||||
just_saved.set(false);
|
||||
},
|
||||
"+"
|
||||
div { class: "text-xs text-paper-secondary mt-1",
|
||||
"文章删除后保留的时长,到期后自动彻底清除(1–365)"
|
||||
}
|
||||
}
|
||||
span { class: "text-xs text-paper-secondary", "天" }
|
||||
}
|
||||
}
|
||||
|
||||
// 底部操作行:未保存提示 + 保存按钮
|
||||
div { class: "flex items-center justify-between gap-4 pt-1",
|
||||
// 草稿状态提示
|
||||
if just_saved() {
|
||||
span { class: "inline-flex items-center gap-1.5 text-xs text-paper-accent",
|
||||
svg {
|
||||
class: "w-3.5 h-3.5",
|
||||
view_box: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_width: "2.5",
|
||||
path {
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
d: "M5 13l4 4L19 7",
|
||||
// 数字输入 + 步进按钮 + 单位后缀
|
||||
div { class: "flex items-center gap-3",
|
||||
div { class: "flex items-center rounded-lg border border-paper-border bg-paper-entry overflow-hidden",
|
||||
// 减号
|
||||
button {
|
||||
class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
|
||||
r#type: "button",
|
||||
aria_label: "减少保留天数",
|
||||
onclick: move |_| {
|
||||
let cur: i32 = settings_draft_days().trim().parse().unwrap_or(30);
|
||||
let next = cur.saturating_sub(1).max(1);
|
||||
settings_draft_days.set(next.to_string());
|
||||
just_saved.set(false);
|
||||
},
|
||||
"−"
|
||||
}
|
||||
// 数字输入(无边框,衔接步进按钮)
|
||||
input {
|
||||
r#type: "number",
|
||||
min: "1",
|
||||
max: "365",
|
||||
class: "w-14 h-9 px-1 text-center text-sm tabular-nums text-paper-primary bg-transparent border-0 focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
|
||||
value: "{settings_draft_days()}",
|
||||
oninput: move |e| {
|
||||
settings_draft_days.set(e.value());
|
||||
just_saved.set(false);
|
||||
},
|
||||
}
|
||||
// 加号
|
||||
button {
|
||||
class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
|
||||
r#type: "button",
|
||||
aria_label: "增加保留天数",
|
||||
onclick: move |_| {
|
||||
let cur: i32 = settings_draft_days().trim().parse().unwrap_or(30);
|
||||
let next = cur.saturating_add(1).min(365);
|
||||
settings_draft_days.set(next.to_string());
|
||||
just_saved.set(false);
|
||||
},
|
||||
"+"
|
||||
}
|
||||
}
|
||||
"已保存"
|
||||
}
|
||||
} else if dirty() {
|
||||
span { class: "text-xs text-paper-secondary", "有未保存的更改" }
|
||||
} else {
|
||||
span { class: "text-xs text-transparent select-none",
|
||||
"·"
|
||||
span { class: "text-xs text-paper-secondary", "天" }
|
||||
}
|
||||
}
|
||||
// 保存按钮:启用主题色,禁用/保存中态灰化
|
||||
button {
|
||||
class: if saving_settings() { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else if just_saved() { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-paper-theme bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" },
|
||||
disabled: saving_settings() || just_saved() || !dirty(),
|
||||
onclick: move |_| {
|
||||
let days: i32 = settings_draft_days().parse().unwrap_or(30);
|
||||
let enabled = settings_draft_enabled();
|
||||
saving_settings.set(true);
|
||||
spawn(async move {
|
||||
if let Ok(s) = update_trash_settings(enabled, days).await {
|
||||
settings.set(s);
|
||||
just_saved.set(true);
|
||||
|
||||
// 底部操作行:未保存提示 + 保存按钮
|
||||
div { class: "flex items-center justify-between gap-4 pt-1",
|
||||
// 草稿状态提示
|
||||
if just_saved() {
|
||||
span { class: "inline-flex items-center gap-1.5 text-xs text-paper-accent",
|
||||
svg {
|
||||
class: "w-3.5 h-3.5",
|
||||
view_box: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_width: "2.5",
|
||||
path {
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
d: "M5 13l4 4L19 7",
|
||||
}
|
||||
}
|
||||
saving_settings.set(false);
|
||||
});
|
||||
},
|
||||
if saving_settings() {
|
||||
"保存中…"
|
||||
"已保存"
|
||||
}
|
||||
} else if dirty() {
|
||||
span { class: "text-xs text-paper-secondary", "有未保存的更改" }
|
||||
} else {
|
||||
"保存设置"
|
||||
span { class: "text-xs text-transparent select-none",
|
||||
"·"
|
||||
}
|
||||
}
|
||||
// 保存按钮:启用主题色,禁用/保存中态灰化
|
||||
button {
|
||||
class: if saving_settings() { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else if just_saved() { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-paper-theme bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" },
|
||||
disabled: saving_settings() || just_saved() || !dirty(),
|
||||
onclick: move |_| {
|
||||
let days: i32 = settings_draft_days().parse().unwrap_or(30);
|
||||
let enabled = settings_draft_enabled();
|
||||
saving_settings.set(true);
|
||||
spawn(async move {
|
||||
if let Ok(s) = update_trash_settings(enabled, days).await {
|
||||
settings.set(s);
|
||||
just_saved.set(true);
|
||||
}
|
||||
saving_settings.set(false);
|
||||
});
|
||||
},
|
||||
if saving_settings() {
|
||||
"保存中…"
|
||||
} else {
|
||||
"保存设置"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -665,8 +660,6 @@ fn TrashRow(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_auto_purge_settings_has_transition_class() {
|
||||
let full_code = include_str!("trash.rs");
|
||||
|
||||
@ -458,8 +458,13 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
||||
// h-[60vh]:随窗口高度自适应;min-h-[400px]:窗口过矮时仍可用。
|
||||
div { class: "h-[60vh] min-h-[400px] flex flex-col my-4",
|
||||
div {
|
||||
class: "flex-1 min-h-0 w-full border border-[var(--color-paper-border)] rounded-xl overflow-hidden bg-[var(--color-paper-entry)] shadow-[0_2px_8px_rgba(0,0,0,0.04)] dark:shadow-none",
|
||||
class: "relative group flex-1 min-h-0 w-full border border-[var(--color-paper-border)] rounded-xl overflow-hidden bg-[var(--color-paper-entry)] shadow-[0_2px_8px_rgba(0,0,0,0.04)] dark:shadow-none",
|
||||
id: "tiptap-editor",
|
||||
img {
|
||||
src: "/images/xiantiaoxiaogou_03.webp",
|
||||
alt: "",
|
||||
class: "absolute bottom-1.5 right-1.5 opacity-60 group-focus-within:opacity-10 transition-opacity duration-300 pointer-events-none z-0",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -656,9 +661,7 @@ fn CoverUploader(cover_image: Signal<String>, cover_uploading: Signal<bool>) ->
|
||||
// —— 上传中:骨架占位 + 文案 ——
|
||||
if cover_uploading() {
|
||||
div { class: "absolute inset-0 flex flex-col items-center justify-center gap-3 bg-[var(--color-paper-tertiary)]/30 animate-pulse",
|
||||
span { class: "text-sm text-[var(--color-paper-secondary)]",
|
||||
"上传中..."
|
||||
}
|
||||
span { class: "text-sm text-[var(--color-paper-secondary)]", "上传中..." }
|
||||
}
|
||||
}
|
||||
|
||||
@ -712,9 +715,7 @@ fn CoverUploader(cover_image: Signal<String>, cover_uploading: Signal<bool>) ->
|
||||
}
|
||||
// 底部渐变遮罩 + "更换封面"提示(hover 出现)。
|
||||
div { class: "absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover/cover:opacity-100 transition-opacity flex items-end justify-center pb-2 pointer-events-none",
|
||||
span { class: "text-xs text-white/90",
|
||||
"点击下方区域可重新上传"
|
||||
}
|
||||
span { class: "text-xs text-white/90", "点击下方区域可重新上传" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@ fn TagsContent() -> Element {
|
||||
rsx! {
|
||||
EmptyState {
|
||||
title: "暂无标签",
|
||||
description: "当前还没有任何标签。"
|
||||
description: "当前还没有任何标签。",
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -76,7 +76,9 @@ fn TagsContent() -> Element {
|
||||
tag: tag.name.clone(),
|
||||
},
|
||||
"{tag.name}"
|
||||
sup { class: "ml-1 text-sm text-paper-secondary", "{tag.post_count}" }
|
||||
sup { class: "ml-1 text-sm text-paper-secondary",
|
||||
"{tag.post_count}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,7 +132,7 @@ fn TagDetailContent(tag: String) -> Element {
|
||||
rsx! {
|
||||
EmptyState {
|
||||
title: "暂无文章",
|
||||
description: "该标签下还没有发布任何文章。"
|
||||
description: "该标签下还没有发布任何文章。",
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -41,10 +41,21 @@ static GLOBAL_GENERATION: LazyLock<AtomicU64> = LazyLock::new(AtomicU64::default
|
||||
pub struct SsrGeneration(pub u64);
|
||||
|
||||
/// 原子递增并返回新的全局世代号。
|
||||
///
|
||||
/// 任何文章写入操作都会调用本函数,标记 SSR 渲染结果已过期。当前 Dioxus 0.7
|
||||
/// 不读取此世代号(详见模块文档),实际失效仍依赖 `SSR_CACHE_SECS` 兜底 TTL。
|
||||
/// 此处打一条 info 日志,既证明写入路径已执行,也提醒部署者此处存在滞后——
|
||||
/// 避免"我改了文章怎么没变"被误判成 DB/发布链路问题。
|
||||
pub fn bump_global_generation() -> u64 {
|
||||
GLOBAL_GENERATION
|
||||
let new = GLOBAL_GENERATION
|
||||
.fetch_add(1, Ordering::SeqCst)
|
||||
.wrapping_add(1)
|
||||
.wrapping_add(1);
|
||||
tracing::info!(
|
||||
new_generation = new,
|
||||
"SSR 世代号已递增(写入路径触发)。注意:Dioxus 0.7 不读取此值,\
|
||||
SSR 缓存需等待 SSR_CACHE_SECS 过期后才反映新内容"
|
||||
);
|
||||
new
|
||||
}
|
||||
|
||||
/// 返回当前全局世代号。
|
||||
|
||||
17
src/theme.rs
17
src/theme.rs
@ -181,15 +181,14 @@ pub fn ThemeToggle() -> Element {
|
||||
let x = coords.x;
|
||||
let y = coords.y;
|
||||
// JS 从 DOM 现状推导目标主题(不传 isDark),避免与 Signal 状态不同步。
|
||||
let _ = js_sys::eval(&format!(
|
||||
"if (window.__startThemeTransition) \
|
||||
window.__startThemeTransition({x}, {y});",
|
||||
x = x,
|
||||
y = y,
|
||||
));
|
||||
// theme.set 推迟到动画结束:其触发的 Dioxus 微任务重渲染会打断 VT。
|
||||
// gen 确保连续点击只有最新回调 set。JS 已自治切换 dark class,
|
||||
// 动画完成后再更新 Dioxus 状态,避免组件重渲染可能带来的干扰
|
||||
let _ = js_sys::eval(
|
||||
&format!(
|
||||
"if (window.__startThemeTransition) \
|
||||
window.__startThemeTransition({x}, {y});", // theme.set 推迟到动画结束:其触发的 Dioxus 微任务重渲染会打断 VT。
|
||||
x = x,
|
||||
y = y,
|
||||
),
|
||||
);
|
||||
let gen = click_gen() + 1;
|
||||
click_gen.set(gen);
|
||||
let mut theme_clone = theme;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user