Skip to main content

yggdrasil/pages/admin/system/
export.rs

1//! 导出 tab:按表或按查询流式导出 CSV / SQL。
2
3use leptos::prelude::*;
4
5use crate::components::forms::{FormInput, FormTextarea, ToggleSwitch};
6use crate::components::ui::{
7    button_class, ButtonSize, ButtonVariant, Icon, IconName, SegmentedControl, SegmentedItem,
8    SegmentedKind, SegmentedSize,
9};
10
11/// 数据导出 tab:按表或按查询导出 CSV / SQL,走 Axum 流式下载。
12#[component]
13pub(super) fn ExportTab() -> impl IntoView {
14    let mode = RwSignal::new("table".to_string());
15    let table_name = RwSignal::new(String::new());
16    let query = RwSignal::new(String::new());
17    let file_format = RwSignal::new("csv".to_string());
18    let include_columns = RwSignal::new(true);
19    let is_table_mode = move || mode.with(|m| m == "table");
20    let ready = move || {
21        if is_table_mode() {
22            !table_name.with(|t| t.trim().is_empty())
23        } else {
24            !query.with(|q| q.trim().is_empty())
25        }
26    };
27    // 触发下载:构造 GET /api/database/export?... 并打开。
28    let do_export = move |_| {
29        #[cfg(target_arch = "wasm32")]
30        {
31            let source = if is_table_mode() {
32                format!("table:{}", table_name.get_untracked().trim())
33            } else {
34                format!("query:{}", query.get_untracked())
35            };
36            let url = format!(
37                "/api/database/export?source={}&format={}&include_columns={}",
38                super::urlencode(&source),
39                file_format.get_untracked(),
40                include_columns.get_untracked(),
41            );
42            if let Some(window) = web_sys::window() {
43                let _ = window.open_with_url(&url);
44            }
45        }
46    };
47
48    view! {
49        <section class="admin-panel sys-export">
50            <div class="settings-rows">
51                <div class="settings-row">
52                    <span class="settings-label">"方式"</span>
53                    <div class="settings-control">
54                        <SegmentedControl
55                            items=vec![
56                                SegmentedItem::new("table".to_string(), "按表"),
57                                SegmentedItem::new("query".to_string(), "按查询"),
58                            ]
59                            value=mode
60                            on_change=move |v: String| mode.set(v)
61                            aria_label="导出方式"
62                            kind=SegmentedKind::Radio
63                            size=SegmentedSize::Sm
64                        />
65                    </div>
66                </div>
67                {move || {
68                    if is_table_mode() {
69                        view! {
70                            <div class="settings-row">
71                                <label class="settings-label" for="export-table">
72                                    "表名"
73                                </label>
74                                <div class="settings-control">
75                                    <FormInput
76                                        id="export-table"
77                                        class="field settings-mono"
78                                        placeholder="如 posts"
79                                        value=table_name
80                                        oninput=move |v: String| table_name.set(v)
81                                    />
82                                    <p class="settings-hint">"只支持 public schema 下的用户表。"</p>
83                                </div>
84                            </div>
85                        }
86                            .into_any()
87                    } else {
88                        view! {
89                            <div class="settings-row">
90                                <label class="settings-label" for="export-query">
91                                    "查询"
92                                </label>
93                                <div class="settings-control">
94                                    <FormTextarea
95                                        id="export-query"
96                                        class="field field-textarea settings-mono"
97                                        rows=4u32
98                                        placeholder="SELECT id, title FROM posts WHERE status = 'published'"
99                                        value=query
100                                        oninput=move |v: String| query.set(v)
101                                    />
102                                    <p class="settings-hint">"只接受只读的 SELECT。"</p>
103                                </div>
104                            </div>
105                        }
106                            .into_any()
107                    }
108                }}
109                <div class="settings-row">
110                    <span class="settings-label">"格式"</span>
111                    <div class="settings-control">
112                        <SegmentedControl
113                            items=vec![
114                                SegmentedItem::new("csv".to_string(), "CSV"),
115                                SegmentedItem::new("sql".to_string(), "SQL"),
116                            ]
117                            value=file_format
118                            on_change=move |v: String| file_format.set(v)
119                            aria_label="导出格式"
120                            kind=SegmentedKind::Radio
121                            size=SegmentedSize::Sm
122                        />
123                        <p class="settings-hint">"SQL 格式只含 INSERT 语句,不含表结构。"</p>
124                    </div>
125                </div>
126                <div class="settings-row settings-switch-row">
127                    <span class="settings-label">"包含列名"</span>
128                    <p class="settings-hint">"CSV 写入表头,SQL 的 INSERT 写出列清单。"</p>
129                    <ToggleSwitch
130                        checked=include_columns
131                        aria_label="包含列名"
132                        ontoggle=move |_| include_columns.update(|v| *v = !*v)
133                    />
134                </div>
135            </div>
136            <div class="sys-export-foot">
137                <p>"流式导出,大表不会占满内存。"</p>
138                <button
139                    class=button_class(ButtonVariant::Primary, ButtonSize::Md)
140                    type="button"
141                    disabled=move || !ready()
142                    on:click=do_export
143                >
144                    <Icon name=IconName::Download size=15/>
145                    "导出并下载"
146                </button>
147            </div>
148        </section>
149    }
150}