yggdrasil/src/components/sql_result_table.rs
xfy a37b0ee811 docs: 修复 cargo doc 的两处 broken intra-doc links
- runner.rs: 把含函数调用表达式的 rustdoc 链接 `[crate::codemirror_bridge::get_module().create]`
  改为普通代码片段;get_module 是 WASM 专属符号,server target rustdoc 解析不到
- sql_result_table.rs: 模块级 doc 里裸用 `[SqlResult]` 但该类型来自 server-gated
  模块不在作用域,改为纯代码片段并补全路径
2026-07-07 18:29:54 +08:00

168 lines
7.5 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.

//! SQL 查询结果表格组件。
//!
//! 消费后端 `SqlResult``crate::api::database::sql_console::SqlResult`
//! 按单元格的 `serde_json::Value` variant 渲染:
//! NULL 显示为斜体灰色字面量,布尔显示为彩色小徽章,数字右对齐 + 等宽数字,
//! 文本截断显示。长文本可通过点击行在行下方展开跨列详情区查看完整内容。
//! 表头 sticky列宽有界`max-width`),避免长文本撑爆横向布局。
use dioxus::prelude::*;
use crate::api::database::sql_console::SqlResult;
use crate::components::ui::ADMIN_TABLE_CLASS;
/// 一行展开详情区容器的最大高度(超出纵向滚动,避免大 jsonb 撑爆页面)。
const EXPAND_MAX_HEIGHT_CLASS: &str = "max-h-80";
/// 文本单元格的列宽上限Tailwind 任意值,约束长文本不无限拉伸)。
const TEXT_CELL_MAX_WIDTH_CLASS: &str = "max-w-[24rem]";
/// 渲染数据行内的单个单元格(截断态)。
///
/// 按 `serde_json::Value` variant 分发样式NULL 斜体灰、布尔彩色徽章、
/// 数字右对齐等宽、文本截断省略。类型信息直接来自后端 `col_to_json` 的编码,
/// 前端无需依赖 PG 类型名。
fn render_cell(value: &serde_json::Value) -> Element {
use serde_json::Value;
match value {
Value::Null => rsx! {
span { class: "italic text-[var(--color-paper-tertiary)]", "NULL" }
},
Value::Bool(true) => rsx! {
span {
class: "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono",
style: "background-color: var(--color-paper-accent-soft); color: var(--color-paper-accent);",
"true"
}
},
Value::Bool(false) => rsx! {
span {
class: "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono",
style: "background-color: rgb(254 243 199); color: rgb(180 83 9);",
"false"
}
},
Value::Number(n) => rsx! {
span { class: "block text-right tabular-nums text-[var(--color-paper-primary)]",
"{n}"
}
},
Value::String(s) => rsx! {
span {
class: "block font-mono text-xs text-[var(--color-paper-secondary)] truncate {TEXT_CELL_MAX_WIDTH_CLASS}",
"{s}"
}
},
// Array / Object理论不会出现col_to_json 不产生复合类型,防御性兜底)
other => rsx! {
span { class: "block font-mono text-xs text-[var(--color-paper-secondary)] truncate {TEXT_CELL_MAX_WIDTH_CLASS}",
"{other}"
}
},
}
}
/// 渲染展开详情区中的「列名: 完整值」单行。
///
/// 与 `render_cell` 不同:值不做截断,长文本换行由外层展开区容器
/// `whitespace-pre-wrap break-all` + `max-h-80 overflow-y-auto`)承载。
fn render_expanded_value(col: &str, value: &serde_json::Value) -> Element {
use serde_json::Value;
let display = match value {
Value::Null => "NULL".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => s.clone(),
other => other.to_string(),
};
rsx! {
div { class: "flex gap-2 py-0.5",
span { class: "shrink-0 font-mono text-xs text-[var(--color-paper-tertiary)] min-w-[6rem]",
"{col}"
}
span { class: "font-mono text-xs text-[var(--color-paper-secondary)]",
"{display}"
}
}
}
}
/// SQL 查询结果表格。
#[derive(Props, Clone, PartialEq)]
pub struct SqlResultTableProps {
pub result: SqlResult,
}
/// 渲染 SQL 查询结果表格。
///
/// 一次只允许展开一行(`expanded_row` 信号记录行索引)。
/// `mut` 信号仅在 WASM 端被 `.set()`server 构建下 `.set()` 调用在 cfg 门控块内被
/// strip故加 `cfg_attr` 抑制 server 目标的 `unused_mut` 警告。
#[component]
#[allow(non_snake_case)]
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
pub fn SqlResultTable(props: SqlResultTableProps) -> Element {
let mut expanded_row: Signal<Option<usize>> = use_signal(|| None);
let cols_len = props.result.columns.len();
// colspan = 列数 + 行头箭头列
let expand_colspan = cols_len + 1;
rsx! {
div { class: "{ADMIN_TABLE_CLASS}",
div { class: "overflow-auto max-h-[70vh]",
table { class: "w-full text-sm border-collapse",
thead {
tr { class: "border-b border-[var(--color-paper-border)] sticky top-0 bg-[var(--color-paper-entry)] z-10",
th { class: "w-8 px-2 py-2", "" }
for col in props.result.columns.iter() {
th {
class: "px-4 py-2 text-left font-medium whitespace-nowrap text-[var(--color-paper-secondary)]",
"{col}"
}
}
}
}
tbody {
for (row_idx, row) in props.result.rows.iter().enumerate() {
// 数据行:点击切换展开
tr {
class: "border-b border-[var(--color-paper-border)] last:border-0 hover:bg-[var(--color-paper-accent-soft)] transition-colors cursor-pointer",
onclick: move |_| {
let cur = expanded_row();
if cur == Some(row_idx) {
expanded_row.set(None);
} else {
expanded_row.set(Some(row_idx));
}
},
td { class: "px-2 py-2 text-center text-[var(--color-paper-tertiary)] text-xs select-none",
if expanded_row() == Some(row_idx) { "" } else { "" }
}
for cell in row.iter() {
td {
class: "px-4 py-2 align-top",
{render_cell(cell)}
}
}
}
// 展开详情行(跨列)
if expanded_row() == Some(row_idx) {
tr {
td { colspan: "{expand_colspan}",
class: "px-4 py-3 bg-[var(--color-paper-theme)]",
div {
class: "{EXPAND_MAX_HEIGHT_CLASS} overflow-y-auto space-y-0.5 whitespace-pre-wrap break-all",
for (ci, col) in props.result.columns.iter().enumerate() {
{render_expanded_value(col, row.get(ci).unwrap_or(&serde_json::Value::Null))}
}
}
}
}
}
}
}
}
}
}
}
}