单选框和复选框组

This commit is contained in:
tommy
2025-11-06 09:54:40 +08:00
parent f551a41a5b
commit c1ae0eb401
5 changed files with 325 additions and 39 deletions

View File

@@ -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}"
}
}
}
}
}
}
}
}
}
}

View File

@@ -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}"
}
}
}
}
}
}
}
}
}
}

View File

@@ -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);