diff --git a/assets/styling/shadcn.css b/assets/styling/shadcn.css index 1ae2e52..bd7ce6f 100644 --- a/assets/styling/shadcn.css +++ b/assets/styling/shadcn.css @@ -78,6 +78,12 @@ transform 0.2s ease; } +.ui-button-content { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + .ui-button[data-size="sm"] { height: 2rem; padding: 0 0.75rem; @@ -1402,6 +1408,89 @@ color: hsl(var(--muted-foreground)); } +/* Data Table - Interactive Table */ +.ui-data-table { + width: 100%; +} + +.ui-data-table-toolbar { + display: flex; + justify-content: flex-end; + align-items: center; + padding: 0.75rem 0; + gap: 0.5rem; +} + +.ui-data-table-columns { + position: relative; +} + +.ui-data-table-columns-trigger svg { + flex-shrink: 0; +} + +.ui-data-table-columns-popover { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + min-width: 200px; + padding: 0.5rem; + background-color: hsl(var(--popover)); + border: 1px solid hsl(var(--border)); + border-radius: calc(var(--radius)); + box-shadow: var(--shadow-lg); + z-index: 50; + animation: fadeIn 0.15s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-0.5rem); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.ui-data-table-columns-option { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 0.75rem; + font-size: 0.875rem; + color: hsl(var(--foreground)); + cursor: pointer; + border-radius: calc(var(--radius) - 4px); + transition: background-color 0.15s ease; + user-select: none; +} + +.ui-data-table-columns-option:hover { + background-color: hsl(var(--muted) / 0.5); +} + +.ui-data-table-columns-option span { + flex: 1; +} + +.ui-data-table-empty { + padding: 3rem 1rem; + text-align: center; + color: hsl(var(--muted-foreground)); + font-size: 0.875rem; +} + +.ui-data-table-checkbox-cell { + width: 48px; + padding: 0.75rem; +} + +.ui-table-row.is-selected { + background-color: hsl(var(--muted) / 0.5); +} + .ui-calendar { border: 1px solid hsl(var(--border)); border-radius: calc(var(--radius) - 2px); diff --git a/src/components/ui/table.rs b/src/components/ui/table.rs index 68ecd3d..9397b5f 100644 --- a/src/components/ui/table.rs +++ b/src/components/ui/table.rs @@ -4,9 +4,9 @@ use std::{ rc::Rc, }; +use super::{utils::merge_class, Button, ButtonSize, ButtonVariant}; use crate::components::ui::Checkbox; use dioxus::prelude::*; -use super::utils::merge_class; #[component] pub fn Table(#[props(into, default)] class: Option, children: Element) -> Element { let classes = merge_class("ui-table", class); @@ -249,30 +249,53 @@ pub fn InteractiveTable( rsx! { div { class: wrapper_class, + onclick: { + let mut open = columns_menu_open.clone(); + move |_| { + if open() { + open.set(false); + } + } + }, div { class: "ui-data-table-toolbar", if !toggleable_columns.is_empty() { div { class: "ui-data-table-columns", - onfocusout: { - let mut open = columns_menu_open.clone(); - move |_| open.set(false) - }, - button { + Button { + variant: ButtonVariant::Outline, + size: ButtonSize::Sm, class: "ui-data-table-columns-trigger", - "data-open": if columns_menu_open() { "true" } else { "false" }, - onclick: { + on_click: { let mut open = columns_menu_open.clone(); - move |_| { + move |evt: MouseEvent| { + evt.stop_propagation(); let next = !open(); open.set(next); } }, - "列显隐" + svg { + width: "16", + height: "16", + view_box: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + rect { x: "3", y: "3", width: "7", height: "7" } + rect { x: "14", y: "3", width: "7", height: "7" } + rect { x: "14", y: "14", width: "7", height: "7" } + rect { x: "3", y: "14", width: "7", height: "7" } + } + "列控制" } if columns_menu_open() { div { class: "ui-data-table-columns-popover", + onclick: move |evt| { + evt.stop_propagation(); + }, for column in toggleable_columns.iter() { { let column_id = column.id.clone(); diff --git a/src/views/orders.rs b/src/views/orders.rs index 8f42ca3..809f01b 100644 --- a/src/views/orders.rs +++ b/src/views/orders.rs @@ -1,9 +1,9 @@ use crate::components::ui::{ Avatar, Badge, BadgeVariant, Button, ButtonSize, ButtonVariant, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CheckboxChipGroup, CheckboxChipOption, - DateRange, DateRangePicker, Input, Label, Pagination, Popover, Select, SelectOption, Slider, - Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, ToggleGroup, - ToggleGroupItem, ToggleGroupMode, + DateRange, DateRangePicker, Input, InteractiveTable, Label, Pagination, Popover, Select, + SelectOption, Slider, Table, TableBody, TableCaption, TableCell, TableColumnConfig, + TableFooter, TableHead, TableHeader, TableRow, TableRowData, }; use chrono::NaiveDate; use dioxus::prelude::*; @@ -948,6 +948,134 @@ pub fn Orders() -> Element { span { class: "orders-metric-sub", "调整筛选条件或清除限制重新查看。" } } } else { + // 示例1:基础表格 + h3 { style: "margin-top: 1rem;", "1. 基础表格 (Table)" } + Table { + TableCaption { "订单数据表格" } + TableHeader { + TableRow { + TableHead { "订单号" } + TableHead { "客户" } + TableHead { "状态" } + TableHead { "金额" } + } + } + TableBody { + for order in paginated_orders.iter().take(3).cloned() { + TableRow { + TableCell { "{order.number}" } + TableCell { "{order.customer_name}" } + TableCell { + Badge { variant: order.status.badge(), "{order.status.label()}" } + } + TableCell { {format!("¥{:.2}", order.total)} } + } + } + } + TableFooter { + TableRow { + TableCell { "总计 (前3项)" } + TableCell { } + TableCell { } + TableCell { + { + let total: f32 = paginated_orders.iter().take(3).map(|o| o.total).sum(); + format!("¥{:.2}", total) + } + } + } + } + } + + // 示例2:InteractiveTable with 行选择 + 列可见性控制 + h3 { style: "margin-top: 2rem;", "2. 高级数据表格 (InteractiveTable) - 行选择 + 列切换" } + { + let mut selected_rows = use_signal(|| std::collections::HashSet::::new()); + let selected_count = selected_rows().len(); + + let columns = vec![ + TableColumnConfig { + id: "number".to_string(), + label: "订单号".to_string(), + toggleable: false, + visible_by_default: true, + }, + TableColumnConfig { + id: "customer".to_string(), + label: "客户".to_string(), + toggleable: true, + visible_by_default: true, + }, + TableColumnConfig { + id: "date".to_string(), + label: "日期".to_string(), + toggleable: true, + visible_by_default: true, + }, + TableColumnConfig { + id: "status".to_string(), + label: "状态".to_string(), + toggleable: true, + visible_by_default: true, + }, + TableColumnConfig { + id: "payment".to_string(), + label: "支付".to_string(), + toggleable: true, + visible_by_default: true, + }, + TableColumnConfig { + id: "channel".to_string(), + label: "渠道".to_string(), + toggleable: true, + visible_by_default: false, + }, + TableColumnConfig { + id: "total".to_string(), + label: "金额".to_string(), + toggleable: true, + visible_by_default: true, + }, + ]; + + let rows: Vec = paginated_orders + .iter() + .map(|order| { + let mut cells = std::collections::HashMap::new(); + cells.insert("number".to_string(), order.number.clone()); + cells.insert("customer".to_string(), format!("{} ({})", order.customer_name, order.customer_email)); + cells.insert("date".to_string(), order.placed_on.format("%Y-%m-%d").to_string()); + cells.insert("status".to_string(), order.status.label().to_string()); + cells.insert("payment".to_string(), order.payment_status.label().to_string()); + cells.insert("channel".to_string(), order.channel.label().to_string()); + cells.insert("total".to_string(), format!("¥{:.2}", order.total)); + TableRowData { + id: order.number.clone(), + cells, + } + }) + .collect(); + + rsx! { + div { style: "margin-bottom: 0.5rem;", + span { style: "font-size: 0.875rem; color: var(--muted-foreground);", + "已选择 {selected_count} 行" + } + } + InteractiveTable { + columns: columns, + rows: rows, + default_selected: Some(vec![]), + empty_state: Some("没有数据".to_string()), + on_selection_change: move |_selected: Vec| { + // 选中的订单回调 + } + } + } + } + + // 示例3:完整功能表格 (原始设计) + h3 { style: "margin-top: 2rem;", "3. 完整功能表格" } Table { TableHeader { TableRow {