mirror of
https://github.com/mztlive/dx-admin-template.git
synced 2025-12-22 21:59:59 +00:00
单选框和复选框组
This commit is contained in:
@@ -2,6 +2,9 @@ You are an expert [0.7 Dioxus](https://dioxuslabs.com/learn/0.7) assistant. Diox
|
||||
|
||||
Provide concise code examples with detailed descriptions
|
||||
|
||||
|
||||
You can query documents through context7 mcp.
|
||||
|
||||
# Dioxus Dependency
|
||||
|
||||
You can add Dioxus to your `Cargo.toml` like this:
|
||||
|
||||
@@ -432,6 +432,89 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ui-radio-chip-group,
|
||||
.ui-checkbox-chip-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ui-choice-group-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-choice-group-options {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ui-radio-chip,
|
||||
.ui-checkbox-chip {
|
||||
appearance: none;
|
||||
background-color: hsl(var(--background));
|
||||
border: 1px dashed hsl(var(--border));
|
||||
border-radius: 0.65rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
justify-content: center;
|
||||
min-height: 2.2rem;
|
||||
min-width: 3.5rem;
|
||||
padding: 0.45rem 1.1rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.ui-radio-chip:hover:not([data-disabled="true"]),
|
||||
.ui-checkbox-chip:hover:not([data-disabled="true"]) {
|
||||
border-color: hsl(var(--foreground) / 0.6);
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.ui-radio-chip[data-state="selected"],
|
||||
.ui-checkbox-chip[data-state="selected"] {
|
||||
border: 1px solid hsl(var(--foreground));
|
||||
color: hsl(var(--foreground));
|
||||
background-color: hsl(var(--background));
|
||||
box-shadow: 0 1px 2px hsl(var(--foreground) / 0.2);
|
||||
}
|
||||
|
||||
.ui-radio-chip[data-disabled="true"],
|
||||
.ui-checkbox-chip[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.ui-radio-chip:focus-visible,
|
||||
.ui-checkbox-chip:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 2px hsl(var(--background)),
|
||||
0 0 0 4px hsl(var(--ring) / 0.45);
|
||||
}
|
||||
|
||||
.ui-chip-label {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ui-chip-description {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.ui-switch {
|
||||
appearance: none;
|
||||
background-color: hsl(var(--muted));
|
||||
|
||||
@@ -56,3 +56,112 @@ pub fn Checkbox(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct CheckboxChipOption {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub description: Option<String>,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl CheckboxChipOption {
|
||||
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
value: value.into(),
|
||||
description: None,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self) -> Self {
|
||||
self.disabled = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CheckboxChipGroup(
|
||||
mut values: Signal<Vec<String>>,
|
||||
#[props(into)] options: Vec<CheckboxChipOption>,
|
||||
#[props(into, default)] label: Option<String>,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(optional)] on_values_change: Option<EventHandler<Vec<String>>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-checkbox-chip-group", class);
|
||||
let current_values = values();
|
||||
let group_disabled = disabled;
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "group",
|
||||
"aria-disabled": group_disabled,
|
||||
if let Some(label_text) = label.clone() {
|
||||
span {
|
||||
class: "ui-choice-group-label",
|
||||
"{label_text}"
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-choice-group-options",
|
||||
for option in options.iter().cloned() {
|
||||
{
|
||||
let option_label = option.label.clone();
|
||||
let option_value = option.value.clone();
|
||||
let option_description = option.description.clone();
|
||||
let is_disabled = group_disabled || option.disabled;
|
||||
let is_selected = current_values.iter().any(|item| item == &option_value);
|
||||
let mut values_signal = values.clone();
|
||||
let handler = on_values_change.clone();
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
class: "ui-checkbox-chip",
|
||||
"data-state": if is_selected { "selected" } else { "idle" },
|
||||
"data-disabled": is_disabled,
|
||||
role: "checkbox",
|
||||
"aria-checked": if is_selected { "true" } else { "false" },
|
||||
"aria-disabled": if is_disabled { "true" } else { "false" },
|
||||
r#type: "button",
|
||||
disabled: is_disabled,
|
||||
onclick: move |_| {
|
||||
if is_disabled {
|
||||
return;
|
||||
}
|
||||
values_signal.with_mut(|items| {
|
||||
if let Some(index) = items.iter().position(|item| item == &option_value) {
|
||||
items.remove(index);
|
||||
} else {
|
||||
items.push(option_value.clone());
|
||||
}
|
||||
});
|
||||
if let Some(callback) = handler.clone() {
|
||||
callback.call(values_signal());
|
||||
}
|
||||
},
|
||||
span {
|
||||
class: "ui-chip-label",
|
||||
"{option_label}"
|
||||
}
|
||||
if let Some(description) = option_description.clone() {
|
||||
span {
|
||||
class: "ui-chip-description",
|
||||
"{description}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,3 +103,115 @@ pub fn RadioGroupItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct RadioChipOption {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub description: Option<String>,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl RadioChipOption {
|
||||
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
value: value.into(),
|
||||
description: None,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self) -> Self {
|
||||
self.disabled = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RadioChipGroup(
|
||||
mut value: Signal<Option<String>>,
|
||||
#[props(into)] options: Vec<RadioChipOption>,
|
||||
#[props(into, default)] label: Option<String>,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(optional)] on_value_change: Option<EventHandler<String>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-radio-chip-group", class);
|
||||
let current_value = value();
|
||||
let group_disabled = disabled;
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "radiogroup",
|
||||
"aria-disabled": group_disabled,
|
||||
if let Some(label_text) = label.clone() {
|
||||
span {
|
||||
class: "ui-choice-group-label",
|
||||
"{label_text}"
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-choice-group-options",
|
||||
for option in options.iter().cloned() {
|
||||
{
|
||||
let option_label = option.label.clone();
|
||||
let option_value = option.value.clone();
|
||||
let option_description = option.description.clone();
|
||||
let is_disabled = group_disabled || option.disabled;
|
||||
let is_selected = current_value
|
||||
.as_ref()
|
||||
.map(|selected| selected == &option_value)
|
||||
.unwrap_or(false);
|
||||
let mut value_signal = value.clone();
|
||||
let handler = on_value_change.clone();
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
class: "ui-radio-chip",
|
||||
"data-state": if is_selected { "selected" } else { "idle" },
|
||||
"data-disabled": is_disabled,
|
||||
role: "radio",
|
||||
"aria-checked": if is_selected { "true" } else { "false" },
|
||||
"aria-disabled": if is_disabled { "true" } else { "false" },
|
||||
r#type: "button",
|
||||
disabled: is_disabled,
|
||||
onclick: move |_| {
|
||||
if is_disabled {
|
||||
return;
|
||||
}
|
||||
let already_selected = value_signal()
|
||||
.as_ref()
|
||||
.map(|selected| selected == &option_value)
|
||||
.unwrap_or(false);
|
||||
if !already_selected {
|
||||
value_signal.set(Some(option_value.clone()));
|
||||
if let Some(callback) = handler.clone() {
|
||||
callback.call(option_value.clone());
|
||||
}
|
||||
}
|
||||
},
|
||||
span {
|
||||
class: "ui-chip-label",
|
||||
"{option_label}"
|
||||
}
|
||||
if let Some(description) = option_description.clone() {
|
||||
span {
|
||||
class: "ui-chip-description",
|
||||
"{description}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::components::ui::{
|
||||
Avatar, Badge, BadgeVariant, Button, ButtonSize, ButtonVariant, Card, CardContent,
|
||||
CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DateRange, DateRangePicker,
|
||||
Input, Label, Pagination, Popover, Select, SelectOption, Slider, Switch, Table, TableBody,
|
||||
TableCell, TableHead, TableHeader, TableRow, ToggleGroup, ToggleGroupItem, ToggleGroupMode,
|
||||
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,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use dioxus::prelude::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
const PAGE_SIZE: usize = 8;
|
||||
const AVAILABLE_TAGS: &[&str] = &["加急", "赠品", "VIP", "缺货", "重复下单", "需回访"];
|
||||
@@ -499,7 +499,7 @@ pub fn Orders() -> Element {
|
||||
let fulfillment_filter = use_signal(|| None::<FulfillmentStatus>);
|
||||
let channel_filter = use_signal(|| None::<SalesChannel>);
|
||||
let method_filter = use_signal(|| None::<PaymentMethod>);
|
||||
let tags_filter = use_signal(|| HashSet::<String>::new());
|
||||
let tags_filter = use_signal(|| Vec::<String>::new());
|
||||
let min_total = use_signal(|| 0.0f32);
|
||||
let flagged_only = use_signal(|| false);
|
||||
let date_range = use_signal(|| None::<DateRange>);
|
||||
@@ -546,8 +546,8 @@ pub fn Orders() -> Element {
|
||||
let fulfillment_selected = fulfillment_filter();
|
||||
let channel_selected = channel_filter();
|
||||
let method_selected = method_filter();
|
||||
let tag_filter_set = tags_filter();
|
||||
let active_tag_count = tag_filter_set.len();
|
||||
let selected_tags = tags_filter();
|
||||
let active_tag_count = selected_tags.len();
|
||||
let min_total_value = min_total();
|
||||
let flagged_only_value = flagged_only();
|
||||
let date_range_selected = date_range();
|
||||
@@ -573,6 +573,10 @@ pub fn Orders() -> Element {
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "all".to_string());
|
||||
let tag_chip_options: Vec<CheckboxChipOption> = AVAILABLE_TAGS
|
||||
.iter()
|
||||
.map(|tag| CheckboxChipOption::new(*tag, *tag))
|
||||
.collect();
|
||||
|
||||
let filtered: Vec<Order> = all_orders
|
||||
.iter()
|
||||
@@ -609,10 +613,10 @@ pub fn Orders() -> Element {
|
||||
.map(|expected| order.payment_method == expected)
|
||||
.unwrap_or(true);
|
||||
|
||||
let matches_tags = if tag_filter_set.is_empty() {
|
||||
let matches_tags = if selected_tags.is_empty() {
|
||||
true
|
||||
} else {
|
||||
tag_filter_set
|
||||
selected_tags
|
||||
.iter()
|
||||
.all(|tag| order.tags.iter().any(|candidate| candidate == tag))
|
||||
};
|
||||
@@ -889,35 +893,10 @@ pub fn Orders() -> Element {
|
||||
span { class: "ui-field-helper", {format!("最低金额:¥{}", min_total_value as i32)} }
|
||||
}
|
||||
div { class: "ui-stack orders-filter-wide", style: "gap: 0.5rem;",
|
||||
Label { "标签" }
|
||||
div { class: "orders-tag-cloud",
|
||||
for tag in AVAILABLE_TAGS.iter() {
|
||||
{
|
||||
let tag_value = tag.to_string();
|
||||
let is_checked = tag_filter_set.contains(*tag);
|
||||
rsx! {
|
||||
label { style: "display: inline-flex; align-items: center; gap: 0.4rem;",
|
||||
Checkbox {
|
||||
checked: is_checked,
|
||||
on_checked_change: {
|
||||
let mut setter = tags_filter.clone();
|
||||
let tag_value = tag_value.clone();
|
||||
move |checked: bool| {
|
||||
setter.with_mut(|tags| {
|
||||
if checked {
|
||||
tags.insert(tag_value.clone());
|
||||
} else {
|
||||
tags.remove(&tag_value);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
span { "{tag}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CheckboxChipGroup {
|
||||
label: "标签",
|
||||
values: tags_filter.clone(),
|
||||
options: tag_chip_options.clone(),
|
||||
}
|
||||
span { class: "ui-field-helper", {format!("已选标签:{}", active_tag_count)} }
|
||||
}
|
||||
@@ -961,7 +940,7 @@ pub fn Orders() -> Element {
|
||||
setter_fulfillment.set(None);
|
||||
setter_channel.set(None);
|
||||
setter_method.set(None);
|
||||
setter_tags.set(HashSet::new());
|
||||
setter_tags.set(Vec::new());
|
||||
setter_range.set(None);
|
||||
setter_min_total.set(0.0);
|
||||
setter_flagged.set(false);
|
||||
|
||||
Reference in New Issue
Block a user