mirror of
https://github.com/mztlive/dx-admin-template.git
synced 2025-12-22 21:59:59 +00:00
订单页面
This commit is contained in:
@@ -198,6 +198,69 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.orders-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.orders-metric-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 18px 20px;
|
||||
border-radius: calc(var(--radius));
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--card));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.orders-metric-label {
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.orders-metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.orders-metric-sub {
|
||||
font-size: 0.85rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.orders-filter-grid {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.orders-filter-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.orders-tag-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.orders-empty {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
border: 1px dashed hsl(var(--border));
|
||||
border-radius: calc(var(--radius));
|
||||
color: hsl(var(--muted-foreground));
|
||||
background-color: hsl(var(--muted) / 0.25);
|
||||
}
|
||||
|
||||
.sidebar-profile-role {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.8rem;
|
||||
|
||||
@@ -907,6 +907,57 @@ fn TableSample() -> Element {
|
||||
}
|
||||
```
|
||||
|
||||
`InteractiveTable` 在基础结构上封装了行多选与列显隐控制:
|
||||
|
||||
- `columns: Vec<TableColumnConfig>` 定义列元数据,`fixed()` 列不可隐藏,`hide_by_default()` 默认隐藏。
|
||||
- `rows: Vec<TableRowData>` 通过 `from_pairs` 或 `with_cell` 构造行数据,`id` 必须唯一。
|
||||
- `on_selection_change` / `on_visibility_change` 回调分别返回当前选中的行 ID 和可见列 ID(按定义顺序)。
|
||||
- `default_selected` 设置初始选中行,`empty_state` 自定义空数据提示。
|
||||
|
||||
```rust
|
||||
use crate::components::ui::*;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
fn InteractiveTableSample() -> Element {
|
||||
let columns = vec![
|
||||
TableColumnConfig::new("id", "ID").fixed(),
|
||||
TableColumnConfig::new("project", "项目"),
|
||||
TableColumnConfig::new("status", "状态"),
|
||||
TableColumnConfig::new("updated", "更新时间"),
|
||||
];
|
||||
|
||||
let rows = vec![
|
||||
TableRowData::from_pairs(
|
||||
"1",
|
||||
[
|
||||
("project", "Alpha"),
|
||||
("status", "部署中"),
|
||||
("updated", "2024-06-12"),
|
||||
],
|
||||
),
|
||||
TableRowData::from_pairs(
|
||||
"2",
|
||||
[
|
||||
("project", "Beta"),
|
||||
("status", "通过"),
|
||||
("updated", "2024-06-10"),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
rsx! {
|
||||
InteractiveTable {
|
||||
columns,
|
||||
rows,
|
||||
default_selected: Some(vec!["1".to_string()]),
|
||||
on_selection_change: move |ids| log::info!("选中: {ids:?}"),
|
||||
on_visibility_change: move |cols| log::info!("可见列: {cols:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Calendar
|
||||
|
||||
单月日期选择器。
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
use std::{
|
||||
cmp::max,
|
||||
collections::{HashMap, HashSet},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::components::ui::Checkbox;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
@@ -104,3 +111,365 @@ pub fn TableCaption(#[props(into, default)] class: Option<String>, children: Ele
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TableColumnConfig {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub toggleable: bool,
|
||||
pub visible_by_default: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TableColumnConfig {
|
||||
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
toggleable: true,
|
||||
visible_by_default: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fixed(mut self) -> Self {
|
||||
self.toggleable = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hide_by_default(mut self) -> Self {
|
||||
self.visible_by_default = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TableRowData {
|
||||
pub id: String,
|
||||
pub cells: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TableRowData {
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
cells: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_cell(mut self, column_id: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
self.cells.insert(column_id.into(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_pairs<T, K, V>(id: impl Into<String>, cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = (K, V)>,
|
||||
K: Into<String>,
|
||||
V: Into<String>,
|
||||
{
|
||||
let mut row = Self::new(id);
|
||||
for (column, value) in cells {
|
||||
row.cells.insert(column.into(), value.into());
|
||||
}
|
||||
row
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn InteractiveTable(
|
||||
#[props(into)] columns: Vec<TableColumnConfig>,
|
||||
#[props(into)] rows: Vec<TableRowData>,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(into, default)] table_class: Option<String>,
|
||||
#[props(into, default)] default_selected: Option<Vec<String>>,
|
||||
#[props(into, default)] empty_state: Option<String>,
|
||||
#[props(optional)] on_selection_change: Option<EventHandler<Vec<String>>>,
|
||||
#[props(optional)] on_visibility_change: Option<EventHandler<Vec<String>>>,
|
||||
) -> Element {
|
||||
let wrapper_class = merge_class("ui-data-table", class);
|
||||
let inner_table_class = merge_class("ui-table", table_class);
|
||||
|
||||
let initial_selected: HashSet<String> =
|
||||
default_selected.unwrap_or_default().into_iter().collect();
|
||||
let selected_rows = use_signal({
|
||||
let initial_selected = initial_selected.clone();
|
||||
move || initial_selected.clone()
|
||||
});
|
||||
|
||||
let initial_visible: HashSet<String> = {
|
||||
let mut visible = HashSet::new();
|
||||
for column in &columns {
|
||||
if column.visible_by_default || !column.toggleable {
|
||||
visible.insert(column.id.clone());
|
||||
}
|
||||
}
|
||||
if visible.is_empty() {
|
||||
if let Some(first) = columns.first() {
|
||||
visible.insert(first.id.clone());
|
||||
}
|
||||
}
|
||||
visible
|
||||
};
|
||||
let visible_columns = use_signal({
|
||||
let initial_visible = initial_visible.clone();
|
||||
move || initial_visible.clone()
|
||||
});
|
||||
|
||||
let toggleable_columns = Rc::new(
|
||||
columns
|
||||
.iter()
|
||||
.filter(|column| column.toggleable)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
let column_order = Rc::new(
|
||||
columns
|
||||
.iter()
|
||||
.map(|column| column.id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
let row_order = Rc::new(rows.iter().map(|row| row.id.clone()).collect::<Vec<_>>());
|
||||
let min_visible_columns = max(
|
||||
columns.iter().filter(|column| !column.toggleable).count(),
|
||||
1,
|
||||
);
|
||||
|
||||
let selection_handler = on_selection_change.clone();
|
||||
let visibility_handler = on_visibility_change.clone();
|
||||
let columns_menu_open = use_signal(|| false);
|
||||
|
||||
let selected_snapshot = selected_rows();
|
||||
let visible_snapshot = visible_columns();
|
||||
|
||||
let selected_count = row_order
|
||||
.iter()
|
||||
.filter(|id| selected_snapshot.contains(*id))
|
||||
.count();
|
||||
let all_selected = !rows.is_empty() && selected_count == row_order.len();
|
||||
|
||||
let empty_message = empty_state.unwrap_or_else(|| "暂无数据".to_string());
|
||||
|
||||
let mut selection_signal_header = selected_rows.clone();
|
||||
let row_order_for_header = row_order.clone();
|
||||
let selection_handler_header = selection_handler.clone();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: wrapper_class,
|
||||
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 {
|
||||
class: "ui-data-table-columns-trigger",
|
||||
"data-open": if columns_menu_open() { "true" } else { "false" },
|
||||
onclick: {
|
||||
let mut open = columns_menu_open.clone();
|
||||
move |_| {
|
||||
let next = !open();
|
||||
open.set(next);
|
||||
}
|
||||
},
|
||||
"列显隐"
|
||||
}
|
||||
if columns_menu_open() {
|
||||
div {
|
||||
class: "ui-data-table-columns-popover",
|
||||
for column in toggleable_columns.iter() {
|
||||
{
|
||||
let column_id = column.id.clone();
|
||||
let column_label = column.label.clone();
|
||||
let mut visible_signal = visible_columns.clone();
|
||||
let handler = visibility_handler.clone();
|
||||
let column_order = column_order.clone();
|
||||
let min_visible = min_visible_columns;
|
||||
|
||||
let is_visible = visible_snapshot.contains(&column_id);
|
||||
|
||||
rsx! {
|
||||
label {
|
||||
class: "ui-data-table-columns-option",
|
||||
Checkbox {
|
||||
checked: is_visible,
|
||||
on_checked_change: move |checked| {
|
||||
let mut next = visible_signal();
|
||||
if checked {
|
||||
if next.insert(column_id.clone()) {
|
||||
visible_signal.set(next.clone());
|
||||
if let Some(handler) = handler.clone() {
|
||||
let payload = column_order
|
||||
.iter()
|
||||
.filter(|id| next.contains(*id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
handler.call(payload);
|
||||
}
|
||||
}
|
||||
} else if next.len() > min_visible && next.remove(&column_id) {
|
||||
visible_signal.set(next.clone());
|
||||
if let Some(handler) = handler.clone() {
|
||||
let payload = column_order
|
||||
.iter()
|
||||
.filter(|id| next.contains(*id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
handler.call(payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
span { "{column_label}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if selected_count > 0 {
|
||||
span {
|
||||
class: "ui-data-table-selection-indicator",
|
||||
"已选择 {selected_count} 行"
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-data-table-scroll",
|
||||
Table {
|
||||
class: Some(inner_table_class.clone()),
|
||||
TableHeader {
|
||||
TableRow {
|
||||
TableHead {
|
||||
class: Some("ui-data-table-checkbox-cell".to_string()),
|
||||
Checkbox {
|
||||
checked: all_selected,
|
||||
disabled: rows.is_empty(),
|
||||
on_checked_change: move |checked| {
|
||||
let mut next = selection_signal_header();
|
||||
let handler = selection_handler_header.clone();
|
||||
if checked {
|
||||
let mut changed = false;
|
||||
for id in row_order_for_header.iter() {
|
||||
if next.insert(id.clone()) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
selection_signal_header.set(next.clone());
|
||||
if let Some(handler) = handler.clone() {
|
||||
let payload = row_order_for_header
|
||||
.iter()
|
||||
.filter(|id| next.contains(*id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
handler.call(payload);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut changed = false;
|
||||
for id in row_order_for_header.iter() {
|
||||
if next.remove(id) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
selection_signal_header.set(next.clone());
|
||||
if let Some(handler) = handler.clone() {
|
||||
let payload = row_order_for_header
|
||||
.iter()
|
||||
.filter(|id| next.contains(*id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
handler.call(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
for column in columns.iter().cloned() {
|
||||
if visible_snapshot.contains(&column.id) {
|
||||
TableHead { "{column.label}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TableBody {
|
||||
for row in rows.iter().cloned() {
|
||||
{
|
||||
let row_id = row.id.clone();
|
||||
let is_selected = selected_snapshot.contains(&row_id);
|
||||
let mut selection_signal = selected_rows.clone();
|
||||
let handler = selection_handler.clone();
|
||||
let row_order = row_order.clone();
|
||||
|
||||
rsx! {
|
||||
TableRow {
|
||||
class: Some(if is_selected { "is-selected".to_string() } else { String::new() }),
|
||||
TableCell {
|
||||
class: Some("ui-data-table-checkbox-cell".to_string()),
|
||||
Checkbox {
|
||||
checked: is_selected,
|
||||
on_checked_change: move |checked| {
|
||||
let mut next = selection_signal();
|
||||
if checked {
|
||||
if next.insert(row_id.clone()) {
|
||||
selection_signal.set(next.clone());
|
||||
if let Some(handler) = handler.clone() {
|
||||
let payload = row_order
|
||||
.iter()
|
||||
.filter(|id| next.contains(*id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
handler.call(payload);
|
||||
}
|
||||
}
|
||||
} else if next.remove(&row_id) {
|
||||
selection_signal.set(next.clone());
|
||||
if let Some(handler) = handler.clone() {
|
||||
let payload = row_order
|
||||
.iter()
|
||||
.filter(|id| next.contains(*id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
handler.call(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for column in columns.iter().cloned() {
|
||||
if visible_snapshot.contains(&column.id) {
|
||||
{
|
||||
let value = row
|
||||
.cells
|
||||
.get(&column.id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
rsx! { TableCell { "{value}" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if rows.is_empty() {
|
||||
div {
|
||||
class: "ui-data-table-empty",
|
||||
"{empty_message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// need dioxus
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use views::{Components, Home, Navbar};
|
||||
use views::{Components, Home, Navbar, Orders};
|
||||
|
||||
/// Define a components module that contains all shared components for our app.
|
||||
mod components;
|
||||
@@ -27,6 +27,8 @@ enum Route {
|
||||
#[route("/components")]
|
||||
// The components gallery reuses the full UI showcase so new primitives stay discoverable.
|
||||
Components {},
|
||||
#[route("/orders")]
|
||||
Orders {},
|
||||
}
|
||||
|
||||
// We can import assets in dioxus with the `asset!` macro. This macro takes a path to an asset relative to the crate root.
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
|
||||
mod components;
|
||||
mod home;
|
||||
mod orders;
|
||||
|
||||
pub use components::Components;
|
||||
pub use home::Home;
|
||||
pub use orders::Orders;
|
||||
|
||||
mod navbar;
|
||||
pub use navbar::Navbar;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::{
|
||||
components::ui::{
|
||||
Avatar, Button, ButtonSize, ButtonVariant, Sidebar, SidebarContent, SidebarFooter,
|
||||
SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset,
|
||||
SidebarLayout, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail,
|
||||
SidebarSeparator,
|
||||
Button, ButtonSize, ButtonVariant, Sidebar, SidebarContent, SidebarFooter, SidebarGroup,
|
||||
SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarLayout,
|
||||
SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, SidebarSeparator,
|
||||
},
|
||||
Route,
|
||||
};
|
||||
@@ -13,6 +12,7 @@ fn page_title(route: &Route) -> &'static str {
|
||||
match route {
|
||||
Route::Home {} => "Dashboard overview",
|
||||
Route::Components {} => "Component library",
|
||||
Route::Orders {} => "Order management",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub fn Navbar() -> Element {
|
||||
let _title = page_title(¤t_route);
|
||||
let is_dashboard = matches!(current_route, Route::Home { .. });
|
||||
let is_components = matches!(current_route, Route::Components { .. });
|
||||
let is_orders = matches!(current_route, Route::Orders { .. });
|
||||
|
||||
let theme_label = {
|
||||
let is_dark = is_dark.clone();
|
||||
@@ -72,6 +73,15 @@ pub fn Navbar() -> Element {
|
||||
href: Some(Route::Home {}.to_string()),
|
||||
}
|
||||
}
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Orders",
|
||||
description: Some("Manage filters and fulfillment queues".to_string()),
|
||||
icon: Some("🧾".to_string()),
|
||||
active: is_orders,
|
||||
href: Some(Route::Orders {}.to_string()),
|
||||
}
|
||||
}
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Components",
|
||||
|
||||
1056
src/views/orders.rs
Normal file
1056
src/views/orders.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user