xfy e6f7704367 feat(admin): 全项目原生 <select> 统一替换为 FormSelect
将剩余 4 处原生下拉(write 编辑器状态胶囊、db_status/server_status
自动刷新、export 导出格式)全部迁移到主题化的 FormSelect:

- FormSelect 新增 trigger_class 可选 prop(缺省全宽表单款),并导出
  FORM_SELECT_COMPACT_CLASS 供工具栏内联小下拉复用;write.rs 状态选择器
  以胶囊 trigger_class 复刻原外观(面板贴底栏自动上翻,替代原
  「隐形 select 覆盖 span」hack)
- 面板宽度改为 max(触发器, 最长选项)(w-max min-w-full + 100vw 上限),
  紧凑触发器(如「手动」)下长选项(如「500ms」「SQL (INSERT)」)
  不再被截断
- db_status/server_status 的字符串 match 反查收编为类型安全选项表
  (Option<u32> 直进直出)
- 因 system/write 页面 SSR 真实引用 FormSelect,移除 FORM_SELECT_ID /
  wrap_index 上不再需要的 #[allow(dead_code)]

src 下已无任何原生 <select> 元素。浏览器实测:紧凑面板 93.8px 宽于
触发器 73.8px、胶囊面板上翻贴顶 6px 间隙、选中 check 与高亮行正确。
2026-07-28 14:38:12 +08:00

145 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 数据导出 tab。
use dioxus::prelude::*;
use crate::components::forms::{FORM_SELECT_COMPACT_CLASS, FormSelect};
use crate::components::ui::BTN_PRIMARY_SM;
/// 数据导出 tab按表/按查询导出 SQL/CSV走 Axum 流式下载。
#[allow(non_snake_case)]
pub(super) fn ExportTab() -> Element {
use crate::components::ui::ADMIN_CARD_CLASS;
// 导出模式:"table" / "query"
let mut mode = use_signal(|| "table".to_string());
let mut table_name = use_signal(String::new);
let mut query = use_signal(String::new);
let mut format = use_signal(|| "csv".to_string());
let mut include_columns = use_signal(|| true);
// 触发下载:构造 GET /api/database/export?... URL 并打开
let do_export = move || {
#[cfg(target_arch = "wasm32")]
{
let source = if mode().as_str() == "table" {
format!("table:{}", table_name.read().trim())
} else {
format!("query:{}", query.read())
};
let url = format!(
"/api/database/export?source={}&format={}&include_columns={}",
urlencode(&source),
format(),
include_columns(),
);
if let Some(window) = web_sys::window() {
let _ = window.open_with_url(&url);
}
}
};
rsx! {
div { class: "space-y-4",
div { class: "{ADMIN_CARD_CLASS} p-4 space-y-4",
// 模式选择
div { class: "flex items-center gap-4",
label { class: "flex items-center gap-2 text-sm text-paper-primary",
input {
r#type: "radio",
name: "export-mode",
checked: mode() == "table",
onchange: move |_| mode.set("table".to_string()),
}
"按表导出"
}
label { class: "flex items-center gap-2 text-sm text-paper-primary",
input {
r#type: "radio",
name: "export-mode",
checked: mode() == "query",
onchange: move |_| mode.set("query".to_string()),
}
"按查询导出"
}
}
// 表名输入
if mode().as_str() == "table" {
div {
label { class: "block text-sm text-paper-secondary mb-1", "表名" }
input {
r#type: "text",
class: "w-full px-3 py-2 text-sm border border-paper-border rounded bg-paper-theme text-paper-primary font-mono",
placeholder: "如 posts",
value: "{table_name}",
oninput: move |e| table_name.set(e.value()),
}
p { class: "text-xs text-paper-secondary mt-1",
"仅支持 public schema 下的用户表,表名需为合法标识符"
}
}
} else {
// 查询输入
div {
label { class: "block text-sm text-paper-secondary mb-1",
"SELECT 查询(只读)"
}
textarea {
class: "w-full px-3 py-2 text-sm border border-paper-border rounded bg-paper-theme text-paper-primary font-mono",
rows: "4",
placeholder: "SELECT id, title FROM posts WHERE published = true",
value: "{query}",
oninput: move |e| query.set(e.value()),
}
}
}
// 格式 + 选项
div { class: "flex flex-wrap items-center gap-4",
div { class: "flex items-center gap-2",
span { class: "text-sm text-paper-secondary", "格式" }
FormSelect {
trigger_class: Some(FORM_SELECT_COMPACT_CLASS),
value: format(),
options: vec![
("csv".to_string(), "CSV"),
("sql".to_string(), "SQL (INSERT)"),
],
onchange: move |v| format.set(v),
}
}
label { class: "flex items-center gap-1 text-sm text-paper-secondary",
input {
r#type: "checkbox",
class: "mr-1",
checked: include_columns(),
onchange: move |e| include_columns.set(e.checked()),
}
"包含列名CSV 表头 / INSERT 列清单)"
}
}
button { class: "{BTN_PRIMARY_SM}", onclick: move |_| do_export(), "导出并下载" }
}
p { class: "text-xs text-paper-secondary",
"导出走流式响应大表不会占满内存。SQL 格式仅含 INSERT 语句(不含 DDL/schema"
}
}
}
}
/// 简易 URL 编码(避免引入新依赖;仅编码导出参数里的特殊字符)。
/// 仅在 WASM 前端的导出按钮里用。
#[cfg(target_arch = "wasm32")]
fn urlencode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char);
}
b' ' => out.push('+'),
_ => out.push_str(&format!("%{:02X}", b)),
}
}
out
}