yggdrasil/pages/admin/note_editor/history.rs
1use super::{save::restore_revision, state::EditorState};
2use crate::api::notes::note_history;
3use crate::models::note::NoteRevision;
4use crate::utils::error::failure_message;
5use crate::utils::time::{hm, md, to_local};
6use leptos::prelude::*;
7use leptos::task::spawn_local;
8
9/// 历史版本:每次保存成新版本后重新读取。恢复会把那一版写成新的工作稿。
10///
11/// 位于路由的 `<Suspense>` 内且会重新读取,因此不用 `Resource`:Effect + 世代号写镜像信号,
12/// 刷新期间保留上一份列表。错误已转成中文提示文本。
13#[component]
14pub(super) fn HistoryPanel(
15 state: EditorState,
16 #[prop(into)] locked: Signal<bool>,
17) -> impl IntoView {
18 // 只有换了笔记或版本号才重新读取;发布、收录不产生新版本。
19 let key = Memo::new(move |_| state.saved.with(|n| n.as_ref().map(|n| (n.id, n.version))));
20 let current = Memo::new(move |_| key.get().map(|(_, version)| version));
21 let revisions = RwSignal::new(None::<Result<Option<Vec<NoteRevision>>, String>>);
22 let generation = StoredValue::new(0_u64);
23 Effect::new(move |_| {
24 let key = key.get();
25 let Some(token) = generation.try_update_value(|g| {
26 *g += 1;
27 *g
28 }) else {
29 return;
30 };
31 let Some((id, _)) = key else {
32 revisions.set(Some(Ok(None)));
33 return;
34 };
35 spawn_local(async move {
36 let result = note_history(id)
37 .await
38 .map(Some)
39 .map_err(|e| failure_message("历史版本加载失败", &e));
40 if generation.try_get_value() == Some(token) {
41 let _ = revisions.try_set(Some(result));
42 }
43 });
44 });
45
46 view! {
47 <section class="note-side-section" aria-labelledby="note-history-h">
48 <div class="note-side-head">
49 <h2 id="note-history-h">"历史版本"</h2>
50 <span>"恢复会生成新草稿"</span>
51 </div>
52 {move || match revisions.get() {
53 Some(Ok(None)) => {
54 view! { <p class="note-side-hint">"保存后开始记录版本。"</p> }.into_any()
55 }
56 Some(Err(message)) => {
57 view! {
58 <p class="note-side-hint" role="alert">
59 {message}
60 </p>
61 }
62 .into_any()
63 }
64 None => {
65 view! {
66 <p class="note-side-hint" role="status">
67 "正在读取历史版本…"
68 </p>
69 }
70 .into_any()
71 }
72 Some(Ok(Some(items))) => {
73 view! {
74 <ol class="note-history">
75 {items
76 .into_iter()
77 .map(|revision| {
78 let version = revision.version;
79 let t = to_local(revision.created_at);
80 let when = format!("{} {}", md(&t, '-'), hm(&t));
81 let title = (!revision.title.is_empty())
82 .then(|| format!(" · {}", revision.title));
83 view! {
84 <li>
85 <span class="note-history-v">{format!("v{version}")}</span>
86 <span class="note-history-when">{when} {title}</span>
87 {move || {
88 if current.get() == Some(version) {
89 view! { <span class="note-history-current">"当前"</span> }
90 .into_any()
91 } else {
92 view! {
93 <button
94 type="button"
95 aria-label=format!("恢复 v{version}")
96 disabled=move || locked.get()
97 on:click=move |_| {
98 spawn_local(async move {
99 restore_revision(state, version).await;
100 });
101 }
102 >
103 "恢复"
104 </button>
105 }
106 .into_any()
107 }
108 }}
109 </li>
110 }
111 })
112 .collect_view()}
113 </ol>
114 }
115 .into_any()
116 }
117 }}
118 </section>
119 }
120}