yggdrasil/pages/admin/system/
export.rs1use 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#[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 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}