mirror of
https://github.com/mztlive/dx-admin-template.git
synced 2025-12-22 21:59:59 +00:00
优化日历组件
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1874,6 +1874,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"dioxus 0.7.0",
|
||||
"dioxus-motion",
|
||||
"js-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -11,6 +11,9 @@ dioxus = { version = "0.7.0", features = ["router", "fullstack"] }
|
||||
dioxus-motion = "0.3.1"
|
||||
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
# The feature that are only required for the web = ["dioxus/web"] build target should be optional and only enabled in the web = ["dioxus/web"] feature
|
||||
|
||||
@@ -1421,6 +1421,20 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ui-date-range-nav-group {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.ui-date-range-nav {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-date-range-title {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
@@ -1458,6 +1472,27 @@
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.ui-date-range-shortcuts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ui-date-range-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.ui-date-range-shortcut,
|
||||
.ui-date-range-clear,
|
||||
.ui-date-range-confirm {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-date-range-calendars {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use super::button::{Button, ButtonSize, ButtonVariant};
|
||||
use chrono::{Datelike, Duration, NaiveDate};
|
||||
use dioxus::prelude::*;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::time::SystemTime;
|
||||
use crate::components::ui::PopoverHandle;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
@@ -49,6 +53,53 @@ fn add_months(date: NaiveDate, offset: i32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(year, month as u32, 1).unwrap_or(date)
|
||||
}
|
||||
|
||||
fn last_day_of_month(date: NaiveDate) -> NaiveDate {
|
||||
let first = first_day_of_month(date);
|
||||
let next_month = add_months(first, 1);
|
||||
next_month - Duration::days(1)
|
||||
}
|
||||
|
||||
fn quick_range_week(today: NaiveDate) -> DateRange {
|
||||
let start = today - Duration::days(today.weekday().num_days_from_monday() as i64);
|
||||
let end = start + Duration::days(6);
|
||||
DateRange::new(start, end)
|
||||
}
|
||||
|
||||
fn quick_range_month(today: NaiveDate) -> DateRange {
|
||||
let start = first_day_of_month(today);
|
||||
let end = last_day_of_month(today);
|
||||
DateRange::new(start, end)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn today_date() -> NaiveDate {
|
||||
use js_sys::Date;
|
||||
|
||||
let date = Date::new_0();
|
||||
let year = date.get_full_year() as i32;
|
||||
let month = (date.get_month() + 1) as u32;
|
||||
let day = date.get_date() as u32;
|
||||
NaiveDate::from_ymd_opt(year, month, day)
|
||||
.unwrap_or_else(|| NaiveDate::from_ymd_opt(1970, 1, 1).expect("unix epoch"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn today_date() -> NaiveDate {
|
||||
let now = SystemTime::now();
|
||||
let datetime: chrono::DateTime<chrono::Utc> = now.into();
|
||||
datetime.date_naive()
|
||||
}
|
||||
|
||||
fn describe_range(range: DateRange) -> String {
|
||||
if range.start == range.end {
|
||||
range.start.format("%b %d %Y").to_string()
|
||||
} else {
|
||||
let start_label = range.start.format("%b %d");
|
||||
let end_label = range.end.format("%b %d %Y");
|
||||
format!("{start_label} → {end_label}")
|
||||
}
|
||||
}
|
||||
|
||||
fn days_for_month(month_start: NaiveDate) -> Vec<NaiveDate> {
|
||||
let start_offset = month_start.weekday().num_days_from_monday() as i64;
|
||||
let mut cursor = month_start - Duration::days(start_offset);
|
||||
@@ -77,81 +128,176 @@ pub fn DateRangePicker(
|
||||
#[props(optional)] initial_month: Option<NaiveDate>,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
) -> Element {
|
||||
let today = today_date();
|
||||
|
||||
let initial_month = value()
|
||||
.map(|range| range.start)
|
||||
.or(initial_month)
|
||||
.unwrap_or_else(|| NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"));
|
||||
let month = use_signal(move || first_day_of_month(initial_month));
|
||||
.map(first_day_of_month)
|
||||
.unwrap_or_else(|| first_day_of_month(today));
|
||||
let month = use_signal(move || initial_month);
|
||||
let hover_date = use_signal(|| None::<NaiveDate>);
|
||||
let draft_range = use_signal({
|
||||
let initial_value = value();
|
||||
move || initial_value
|
||||
});
|
||||
let popover_handle = try_use_context::<PopoverHandle>();
|
||||
let classes = merge_class("ui-date-range", class);
|
||||
let on_change_handler = on_change.clone();
|
||||
|
||||
let active_range = value();
|
||||
{
|
||||
let mut draft_signal = draft_range.clone();
|
||||
let value_signal = value.clone();
|
||||
use_effect(move || {
|
||||
draft_signal.set(value_signal());
|
||||
});
|
||||
}
|
||||
|
||||
let confirmed_range = value();
|
||||
let pending_range = draft_range();
|
||||
let base_month = month();
|
||||
|
||||
let month_label = |date: NaiveDate| date.format("%B %Y").to_string();
|
||||
|
||||
let preview_primary_text = match (pending_range, confirmed_range) {
|
||||
(Some(pending), Some(confirmed)) if pending == confirmed => {
|
||||
format!("当前: {}", describe_range(pending))
|
||||
}
|
||||
(Some(pending), _) => format!("待确认: {}", describe_range(pending)),
|
||||
(None, Some(_)) => "待确认: 清除日期".to_string(),
|
||||
(None, None) => "选择一个开始日期以创建范围。".to_string(),
|
||||
};
|
||||
let preview_secondary_text = match (pending_range, confirmed_range) {
|
||||
(Some(pending), Some(confirmed)) if pending != confirmed => {
|
||||
Some(format!("当前: {}", describe_range(confirmed)))
|
||||
}
|
||||
(None, Some(confirmed)) => Some(format!("当前: {}", describe_range(confirmed))),
|
||||
_ => None,
|
||||
};
|
||||
let confirm_disabled = match (pending_range, confirmed_range) {
|
||||
(None, None) => true,
|
||||
(Some(pending), Some(confirmed)) if pending == confirmed => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
div {
|
||||
class: "ui-date-range-toolbar",
|
||||
button {
|
||||
class: "ui-date-range-nav",
|
||||
r#type: "button",
|
||||
"aria-label": "Previous month",
|
||||
onclick: {
|
||||
let mut month_signal = month.clone();
|
||||
move |_| {
|
||||
let next = add_months(month_signal(), -1);
|
||||
month_signal.set(next);
|
||||
}
|
||||
},
|
||||
"‹"
|
||||
div {
|
||||
class: "ui-date-range-nav-group",
|
||||
Button {
|
||||
class: "ui-date-range-nav",
|
||||
variant: ButtonVariant::Ghost,
|
||||
on_click: {
|
||||
let mut month_signal = month.clone();
|
||||
move |_| {
|
||||
let next = add_months(month_signal(), -12);
|
||||
month_signal.set(next);
|
||||
}
|
||||
},
|
||||
"«"
|
||||
}
|
||||
Button {
|
||||
class: "ui-date-range-nav",
|
||||
variant: ButtonVariant::Ghost,
|
||||
on_click: {
|
||||
let mut month_signal = month.clone();
|
||||
move |_| {
|
||||
let next = add_months(month_signal(), -1);
|
||||
month_signal.set(next);
|
||||
}
|
||||
},
|
||||
"‹"
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-date-range-labels",
|
||||
span { class: "ui-date-range-title", "{month_label(month())}" }
|
||||
span { class: "ui-date-range-title", "{month_label(add_months(month(), 1))}" }
|
||||
span { class: "ui-date-range-title", "{month_label(base_month)}" }
|
||||
span { class: "ui-date-range-title", "{month_label(add_months(base_month, 1))}" }
|
||||
}
|
||||
button {
|
||||
class: "ui-date-range-nav",
|
||||
r#type: "button",
|
||||
"aria-label": "Next month",
|
||||
onclick: {
|
||||
div {
|
||||
class: "ui-date-range-nav-group",
|
||||
Button {
|
||||
class: "ui-date-range-nav",
|
||||
variant: ButtonVariant::Ghost,
|
||||
on_click: {
|
||||
let mut month_signal = month.clone();
|
||||
move |_| {
|
||||
let next = add_months(month_signal(), 1);
|
||||
month_signal.set(next);
|
||||
}
|
||||
},
|
||||
"›"
|
||||
}
|
||||
Button {
|
||||
class: "ui-date-range-nav",
|
||||
variant: ButtonVariant::Ghost,
|
||||
on_click: {
|
||||
let mut month_signal = month.clone();
|
||||
move |_| {
|
||||
let next = add_months(month_signal(), 12);
|
||||
month_signal.set(next);
|
||||
}
|
||||
},
|
||||
"»"
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-date-range-shortcuts",
|
||||
Button {
|
||||
class: "ui-date-range-shortcut",
|
||||
size: ButtonSize::Sm,
|
||||
variant: ButtonVariant::Secondary,
|
||||
on_click: {
|
||||
let mut draft_signal = draft_range.clone();
|
||||
let mut month_signal = month.clone();
|
||||
let mut hover_signal = hover_date.clone();
|
||||
move |_| {
|
||||
let next = add_months(month_signal(), 1);
|
||||
month_signal.set(next);
|
||||
let selected = quick_range_week(today);
|
||||
draft_signal.set(Some(selected));
|
||||
month_signal.set(first_day_of_month(selected.start));
|
||||
hover_signal.set(None);
|
||||
}
|
||||
},
|
||||
"›"
|
||||
"本周"
|
||||
}
|
||||
Button {
|
||||
class: "ui-date-range-shortcut",
|
||||
size: ButtonSize::Sm,
|
||||
variant: ButtonVariant::Secondary,
|
||||
on_click: {
|
||||
let mut draft_signal = draft_range.clone();
|
||||
let mut month_signal = month.clone();
|
||||
let mut hover_signal = hover_date.clone();
|
||||
move |_| {
|
||||
let selected = quick_range_month(today);
|
||||
draft_signal.set(Some(selected));
|
||||
month_signal.set(first_day_of_month(selected.start));
|
||||
hover_signal.set(None);
|
||||
}
|
||||
},
|
||||
"本月"
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-date-range-preview",
|
||||
match active_range {
|
||||
Some(range) if range.start != range.end => {
|
||||
let start_label = range.start.format("%b %d").to_string();
|
||||
let end_label = range.end.format("%b %d %Y").to_string();
|
||||
rsx! { span { "{start_label} → {end_label}" } }
|
||||
}
|
||||
Some(range) => {
|
||||
let label = range.start.format("%b %d %Y").to_string();
|
||||
rsx! { span { "Selected {label}" } }
|
||||
}
|
||||
None => rsx! { span { "Pick a start date to begin the range." } },
|
||||
span { class: "ui-date-range-preview-primary", "{preview_primary_text}" }
|
||||
for text in preview_secondary_text.iter() {
|
||||
span { class: "ui-date-range-preview-secondary", "{text}" }
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-date-range-calendars",
|
||||
for offset in 0..2 {
|
||||
{
|
||||
let calendar_month = add_months(month(), offset);
|
||||
let calendar_month = add_months(base_month, offset);
|
||||
let days = days_for_month(calendar_month);
|
||||
let active_month = calendar_month.month();
|
||||
let range_signal = value.clone();
|
||||
let range_signal = draft_range.clone();
|
||||
let hover_signal = hover_date.clone();
|
||||
let on_change_handler = on_change_handler.clone();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-date-range-calendar",
|
||||
@@ -165,18 +311,9 @@ pub fn DateRangePicker(
|
||||
{
|
||||
let preview = range_preview(range_signal(), hover_signal());
|
||||
let in_current_month = day.month() == active_month;
|
||||
let mut range_signal = range_signal.clone();
|
||||
let mut hover_signal = hover_signal.clone();
|
||||
let is_selected_start = preview
|
||||
.map(|range| range.start == day)
|
||||
.unwrap_or(false);
|
||||
let is_selected_end = preview
|
||||
.map(|range| range.end == day)
|
||||
.unwrap_or(false);
|
||||
let is_in_range = preview
|
||||
.map(|range| range.contains(&day))
|
||||
.unwrap_or(false);
|
||||
|
||||
let is_selected_start = preview.map(|range| range.start == day).unwrap_or(false);
|
||||
let is_selected_end = preview.map(|range| range.end == day).unwrap_or(false);
|
||||
let is_in_range = preview.map(|range| range.contains(&day)).unwrap_or(false);
|
||||
rsx! {
|
||||
button {
|
||||
class: "ui-calendar-day",
|
||||
@@ -186,51 +323,43 @@ pub fn DateRangePicker(
|
||||
"data-outside": if in_current_month { "false" } else { "true" },
|
||||
onclick: {
|
||||
let day_value = day;
|
||||
let on_change_handler = on_change_handler.clone();
|
||||
let mut draft_signal = range_signal.clone();
|
||||
let mut hover_signal = hover_signal.clone();
|
||||
move |_| {
|
||||
let current_range = range_signal();
|
||||
match current_range {
|
||||
let current_range = draft_signal();
|
||||
let next_range = match current_range {
|
||||
Some(current) if current.start != current.end => {
|
||||
let new_range = DateRange::new(day_value, day_value);
|
||||
range_signal.set(Some(new_range));
|
||||
if let Some(handler) = on_change_handler.clone() {
|
||||
handler.call(Some(new_range));
|
||||
}
|
||||
Some(DateRange::new(day_value, day_value))
|
||||
}
|
||||
Some(current) => {
|
||||
if day_value == current.start {
|
||||
let new_range = DateRange::new(day_value, day_value);
|
||||
range_signal.set(Some(new_range));
|
||||
if let Some(handler) = on_change_handler.clone() {
|
||||
handler.call(Some(new_range));
|
||||
}
|
||||
Some(DateRange::new(day_value, day_value))
|
||||
} else {
|
||||
let new_range = DateRange::new(current.start, day_value);
|
||||
range_signal.set(Some(new_range));
|
||||
if let Some(handler) = on_change_handler.clone() {
|
||||
handler.call(Some(new_range));
|
||||
}
|
||||
Some(DateRange::new(current.start, day_value))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let new_range = DateRange::new(day_value, day_value);
|
||||
range_signal.set(Some(new_range));
|
||||
if let Some(handler) = on_change_handler.clone() {
|
||||
handler.call(Some(new_range));
|
||||
}
|
||||
None => Some(DateRange::new(day_value, day_value)),
|
||||
};
|
||||
draft_signal.set(next_range);
|
||||
hover_signal.set(None);
|
||||
}
|
||||
},
|
||||
onmouseenter: {
|
||||
let day_value = day;
|
||||
let draft_signal = range_signal.clone();
|
||||
let mut hover_signal = hover_signal.clone();
|
||||
move |_| {
|
||||
if let Some(range) = draft_signal() {
|
||||
if range.start == range.end {
|
||||
hover_signal.set(Some(day_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onmouseenter: move |_| {
|
||||
let range = range_signal();
|
||||
if let Some(range) = range {
|
||||
if range.start == range.end {
|
||||
hover_signal.set(Some(day));
|
||||
}
|
||||
}
|
||||
onmouseleave: {
|
||||
let mut hover_signal = hover_signal.clone();
|
||||
move |_| hover_signal.set(None)
|
||||
},
|
||||
onmouseleave: move |_| hover_signal.set(None),
|
||||
"{day.day()}"
|
||||
}
|
||||
}
|
||||
@@ -242,6 +371,50 @@ pub fn DateRangePicker(
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-date-range-controls",
|
||||
Button {
|
||||
class: "ui-date-range-clear",
|
||||
variant: ButtonVariant::Ghost,
|
||||
size: ButtonSize::Sm,
|
||||
on_click: {
|
||||
let mut draft_signal = draft_range.clone();
|
||||
let mut hover_signal = hover_date.clone();
|
||||
move |_| {
|
||||
draft_signal.set(None);
|
||||
hover_signal.set(None);
|
||||
}
|
||||
},
|
||||
"清除"
|
||||
}
|
||||
Button {
|
||||
class: "ui-date-range-confirm",
|
||||
size: ButtonSize::Sm,
|
||||
disabled: confirm_disabled,
|
||||
on_click: {
|
||||
let mut value_signal = value.clone();
|
||||
let mut hover_signal = hover_date.clone();
|
||||
let draft_signal = draft_range.clone();
|
||||
let on_change = on_change_handler.clone();
|
||||
let popover_handle = popover_handle.clone();
|
||||
move |_| {
|
||||
let selection = draft_signal();
|
||||
if selection.is_none() && value_signal().is_none() {
|
||||
return;
|
||||
}
|
||||
value_signal.set(selection);
|
||||
if let Some(handler) = on_change.clone() {
|
||||
handler.call(selection);
|
||||
}
|
||||
if let Some(mut handle) = popover_handle {
|
||||
handle.state.set(false);
|
||||
}
|
||||
hover_signal.set(None);
|
||||
}
|
||||
},
|
||||
"确定"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PopoverHandle {
|
||||
pub state: Signal<bool>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Popover(
|
||||
trigger: Element,
|
||||
@@ -7,6 +12,7 @@ pub fn Popover(
|
||||
#[props(into, default = "bottom".to_string())] placement: String,
|
||||
) -> Element {
|
||||
let mut open = use_signal(|| false);
|
||||
use_context_provider(|| PopoverHandle { state: open.clone() });
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
|
||||
@@ -862,17 +862,6 @@ pub fn Orders() -> Element {
|
||||
move |range: Option<DateRange>| setter.set(range)
|
||||
},
|
||||
}
|
||||
div { class: "orders-date-actions",
|
||||
Button {
|
||||
variant: ButtonVariant::Ghost,
|
||||
size: ButtonSize::Sm,
|
||||
on_click: {
|
||||
let mut setter = date_range.clone();
|
||||
move |_| setter.set(None)
|
||||
},
|
||||
"清除日期"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user