Skip to main content

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}