From 1f885b54f304696b72ea4971c59ccfc926a7b6ae Mon Sep 17 00:00:00 2001 From: lsy Date: Wed, 25 Dec 2024 23:26:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=BC=B9=E7=AA=97=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F,=E5=AE=8C=E5=96=84=E4=B8=BB=E9=A2=98=E6=9B=B4?= =?UTF-8?q?=E6=94=B9=EF=BC=8C=E5=A6=82=E6=9E=9C=E5=82=A8=E5=AD=98=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E5=92=8C=E5=BD=93=E5=89=8D=E4=B8=BB=E9=A2=98=E7=9B=B8?= =?UTF-8?q?=E5=90=8C=E5=B0=B1=E4=B8=8D=E5=82=A8=E5=AD=98=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E9=BB=98=E8=AE=A4=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/Cargo.lock | 5 + frontend/Cargo.toml | 5 +- frontend/assets/styling/global.css | 13 +- frontend/assets/styling/tailwind.css | 116 ++++++++++++----- frontend/src/common/dom.rs | 34 +++-- frontend/src/common/helps.rs | 9 ++ frontend/src/common/mod.rs | 3 +- frontend/src/components/mod.rs | 2 +- frontend/src/components/notification.rs | 166 +++++++++++++++++++----- frontend/src/components/theme_toggle.rs | 55 +++++--- frontend/src/main.rs | 50 ++++--- frontend/src/views/home.rs | 13 +- frontend/tailwind.config.js | 11 ++ 13 files changed, 351 insertions(+), 131 deletions(-) create mode 100644 frontend/src/common/helps.rs diff --git a/frontend/Cargo.lock b/frontend/Cargo.lock index 268ca8f..892f35c 100644 --- a/frontend/Cargo.lock +++ b/frontend/Cargo.lock @@ -1474,6 +1474,9 @@ version = "0.1.0" dependencies = [ "dioxus", "dioxus-free-icons", + "getrandom 0.2.15", + "js-sys", + "rand 0.8.5", "wasm-bindgen", "web-sys", ] @@ -1736,8 +1739,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 3129f2f..bfdfb4d 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -10,7 +10,10 @@ edition = "2021" dioxus = { version = "0.6.0", features = ["router"] } dioxus-free-icons = { version = "0.9", features = ["bootstrap"] } wasm-bindgen = "0.2.99" -web-sys = { version = "0.3.76", features = ["Window","Storage","MediaQueryList","Document"] } +web-sys = { version = "0.3.76", features = ["Window","Storage","MediaQueryList","Document","DomTokenList","Element","MediaQueryListEvent"] } +js-sys = "0.3.76" +rand = "0.8.5" +getrandom = { version = "0.2", features = ["js"] } [features] default = ["dioxus/web"] diff --git a/frontend/assets/styling/global.css b/frontend/assets/styling/global.css index b3be38c..81fc517 100644 --- a/frontend/assets/styling/global.css +++ b/frontend/assets/styling/global.css @@ -1,11 +1,18 @@ .light{ --accent-color:#3F51B5; -} -.light .box{ - background: + color:black; } .dark{ --accent-color:#4d648d; + color:white; +} + +html.dark{ + background-color:black; +} + +html.light{ + background-color:white; } diff --git a/frontend/assets/styling/tailwind.css b/frontend/assets/styling/tailwind.css index f500824..85a5e10 100644 --- a/frontend/assets/styling/tailwind.css +++ b/frontend/assets/styling/tailwind.css @@ -562,30 +562,76 @@ video { position: relative; } -.right-1 { - right: 0.25rem; -} - .right-8 { right: 2rem; } -.top-1 { - top: 0.25rem; -} - .top-10 { top: 2.5rem; } -.float-end { - float: inline-end; +.top-\[0\.06rem\] { + top: 0.06rem; +} + +.m-2 { + margin: 0.5rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.flex { + display: flex; +} + +.h-1 { + height: 0.25rem; } .w-\[20rem\] { width: 20rem; } +.min-w-0 { + min-width: 0px; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +@keyframes progress { + 0% { + width: 0%; + } + + 100% { + width: 100%; + } +} + +.animate-progress { + animation: 5s progress linear infinite; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + .truncate { overflow: hidden; text-overflow: ellipsis; @@ -596,18 +642,27 @@ video { border-radius: 0.25rem; } -.border-accent { - border-color: var(--accent-color); +.border-2 { + border-width: 2px; } -.bg-yellow-200 { +.\!border-none { + border-style: none !important; +} + +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity, 1)); +} + +.bg-white { --tw-bg-opacity: 1; - background-color: rgb(254 240 138 / var(--tw-bg-opacity, 1)); + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); } -.px-2 { - padding-left: 0.5rem; - padding-right: 0.5rem; +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; } .py-3 { @@ -615,29 +670,18 @@ video { padding-bottom: 0.75rem; } -.text-base { - font-size: 1rem; - line-height: 1.5rem; +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity, 1)); } -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; +.hover\:text-accent:hover { + color: var(--accent-color); } -.backdrop-blur-xl { - --tw-backdrop-blur: blur(24px); - -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -} - -.hover\:border-2:hover { - border-width: 2px; -} - -.dark\:bg-sky-500:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(14 165 233 / var(--tw-bg-opacity, 1)); +.dark\:text-gray-800:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity, 1)); } @media not all and (min-width: 640px) { diff --git a/frontend/src/common/dom.rs b/frontend/src/common/dom.rs index 3070689..b8494fa 100644 --- a/frontend/src/common/dom.rs +++ b/frontend/src/common/dom.rs @@ -1,5 +1,5 @@ use super::error::{CustomErrorInto, CustomResult}; -use web_sys::{window, Document, MediaQueryList, Storage, Window}; +use web_sys::{window, Document, Element, Storage, Window}; fn get_window() -> CustomResult { Ok(window().ok_or("浏览器window对象不存在")?) @@ -8,7 +8,18 @@ fn get_window() -> CustomResult { fn get_storage() -> CustomResult { get_window()? .local_storage()? - .ok_or("浏览器不支持localStorage".into_custom_error()) + .ok_or("获取浏览器Storge对象失败".into_custom_error()) +} +fn get_document() -> CustomResult { + get_window()? + .document() + .ok_or("获取浏览器Document对象失败".into_custom_error()) +} + +fn get_element(ele_name: &str) -> CustomResult { + get_document()? + .query_selector(ele_name)? + .ok_or(format!("获取元素{}失败", ele_name).into_custom_error()) } pub fn get_local_storage_value(key: &str) -> CustomResult { @@ -21,6 +32,10 @@ pub fn set_local_storage_value(key: &str, value: &str) -> CustomResult<()> { Ok(get_storage()?.set_item(key, value)?) } +pub fn remove_local_storage_value(key: &str) -> CustomResult<()> { + Ok(get_storage()?.remove_item(key)?) +} + pub fn get_media_theme() -> CustomResult { let media_query = get_window()? .match_media("(prefers-color-scheme: dark)")? @@ -32,13 +47,10 @@ pub fn get_media_theme() -> CustomResult { Ok("light".to_string()) } -pub fn set_element_class(ele_name: &str, class_name: &str) -> CustomResult<()> { - get_window()? - .document() - .ok_or("浏览器document对象不存在".into_custom_error())? - .query_selector(ele_name)? - .ok_or(format!("获取元素{}失败", ele_name).into_custom_error())? - .set_class_name(class_name); - - Ok(()) +pub fn add_element_class(ele_name: &str, class_name: &str) -> CustomResult<()> { + Ok(get_element(ele_name)?.class_list().add_1(class_name)?) +} + +pub fn remove_element_class(ele_name: &str, class_name: &str) -> CustomResult<()> { + Ok(get_element(ele_name)?.class_list().remove_1(class_name)?) } diff --git a/frontend/src/common/helps.rs b/frontend/src/common/helps.rs new file mode 100644 index 0000000..9e02d93 --- /dev/null +++ b/frontend/src/common/helps.rs @@ -0,0 +1,9 @@ +use rand::seq::SliceRandom; + +pub fn generate_random_string(length: usize) -> String { + let charset = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let mut rng = rand::thread_rng(); + (0..length) + .map(|_| *charset.choose(&mut rng).unwrap() as char) + .collect() +} diff --git a/frontend/src/common/mod.rs b/frontend/src/common/mod.rs index d49a1df..fba8473 100644 --- a/frontend/src/common/mod.rs +++ b/frontend/src/common/mod.rs @@ -1,2 +1,3 @@ pub mod dom; -pub mod error; \ No newline at end of file +pub mod error; +pub mod helps; diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs index fe3f360..1673bda 100644 --- a/frontend/src/components/mod.rs +++ b/frontend/src/components/mod.rs @@ -3,4 +3,4 @@ pub use navbar::Navbar; pub mod theme_toggle; pub use theme_toggle::Toggle; pub mod notification; -pub use notification::Message; +pub use notification::Toast; diff --git a/frontend/src/components/notification.rs b/frontend/src/components/notification.rs index e5d88bc..4f9e963 100644 --- a/frontend/src/components/notification.rs +++ b/frontend/src/components/notification.rs @@ -1,55 +1,155 @@ use crate::common::error::{CustomErrorInto, CustomResult}; +use crate::common::helps::generate_random_string; +use crate::Route; use dioxus::prelude::*; +use dioxus_free_icons::icons::bs_icons::{BsCheckCircle, BsInfoCircle, BsXCircle, BsXLg}; use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::bs_icons::BsXLg; #[derive(PartialEq, Clone)] -pub enum MessageType { +pub struct NotificationProvider { + pub notifications: Vec, +} + +#[derive(PartialEq, Clone)] +pub enum NotificationType { Info, - Warn, Error, Success, } #[derive(PartialEq, Props, Clone)] -pub struct MessageProps { - #[props(default="wight".to_string())] - color: String, +pub struct NotificationProps { #[props(default="".to_string())] title: String, #[props(default="".to_string())] - message: String, - #[props(default=3)] - time:u64, + content: String, + #[props(default = 5)] + time: u64, + #[props(default=NotificationType::Info)] + r#type: NotificationType, +} + +#[derive(PartialEq, Clone)] +struct NoticationColorMatching { + color: String, + icon: Element, +} + +fn get_color_matching(notification_type: &NotificationType) -> NoticationColorMatching { + match notification_type { + NotificationType::Info => NoticationColorMatching { + color: String::from("rgba(0,168,91,0.85)"), + icon: rsx! { + Icon { + icon: BsInfoCircle, + width:18, + height:18, + } + }, + }, + NotificationType::Error => NoticationColorMatching { + color: String::from("rgba(225,45,57,0.85)"), + icon: rsx! { + Icon { + icon: BsXCircle, + width:18, + height:18, + } + }, + }, + NotificationType::Success => NoticationColorMatching { + color: String::from("rgba(38,131,255,0.85)"), + icon: rsx! { + Icon { + icon: BsCheckCircle, + width:18, + height:18, + } + }, + }, + } } #[component] -pub fn Message(props: MessageProps) -> Element { +pub fn Notification() -> Element { + let notifications = use_context::>()().notifications; return rsx! { div { - class: "w-[20rem] px-2 py-3 absolute right-8 top-10 backdrop-blur-xl max-sm:w-[12rem] hover:border-2 border-accent rounded ", - style: "background-color:{props.color}", - - div { - - class:"text-lg", - div { - class:"float-end relative top-1 right-1", - Icon{ - icon:BsXLg, - } - } - div { - class:"truncate", - "我是标题水水水水水水水水水水水水水水水水水水水水水sssssssssssss水水水ssssssssssssssss" + class: "w-[20rem] absolute right-8 top-10 max-sm:w-[12rem]", + {notifications.iter().map(|item| { + let color_matching=get_color_matching(&item.r#type); + rsx!{ + div { + id:format!("notification-{}",generate_random_string(10)), + class:"rounded px-3 py-3 m-2 !border-none text-gray-300 dark:text-gray-800 ", + style:format!("background-color:{}",color_matching.color), + div { + div { + class:"flex items-center justify-between", + div { + onclick:|e|{ - } - } - div { - class:"text-base", - "我是水水水水水水水水水ssssssssssssssssssssss水水水水内容" - } - - } + }, + class:"mr-2 relative top-[0.06rem] hover:text-accent", + {color_matching.icon}, + } + div { + class:"truncate min-w-0", + {item.title.clone()} + } + div { + class:"ml-1 flex-shrink-0", + Icon{ + icon:BsXLg, + height:18, + width:18 + } + } + } + div { + class:"mt-2 h-1 bg-white animate-progress", + style: format!("animation-duration: {}s", item.time), + } + } + } + } + })} + } + Outlet:: {} }; } + +pub struct Toast(); + +impl Toast { + pub fn show(props: NotificationProps) { + let mut notification_context = use_context::>(); + let mut new_provider = notification_context().clone(); + new_provider.notifications.push(props); + notification_context.set(new_provider); + } + pub fn info(title: String, content: String) { + Self::show(NotificationProps { + title, + content, + time: 5, + r#type: NotificationType::Info, + }); + } + pub fn error(title: String, content: String) { + Self::show(NotificationProps { + title, + content, + time: 5, + r#type: NotificationType::Error, + }); + } + pub fn success(title: String, content: String) { + Self::show(NotificationProps { + title, + content, + time: 5, + r#type: NotificationType::Success, + }); + } +} diff --git a/frontend/src/components/theme_toggle.rs b/frontend/src/components/theme_toggle.rs index 3f63707..2ce0311 100644 --- a/frontend/src/components/theme_toggle.rs +++ b/frontend/src/components/theme_toggle.rs @@ -1,10 +1,14 @@ -use crate::common::dom::{set_element_class, set_local_storage_value}; +use crate::common::dom::{ + add_element_class, get_local_storage_value, get_media_theme, remove_element_class, + remove_local_storage_value, set_local_storage_value, +}; use dioxus::{logger::tracing, prelude::*}; use dioxus_free_icons::icons::bs_icons::BsMoonStars; use dioxus_free_icons::icons::bs_icons::BsSun; use dioxus_free_icons::Icon; + #[derive(Debug, Clone, PartialEq)] -pub struct IsDark(pub String); +pub struct ThemeProvider(pub String); #[derive(PartialEq, Props, Clone)] pub struct ToggleProps { @@ -18,37 +22,34 @@ pub struct ToggleProps { #[component] pub fn Toggle(props: ToggleProps) -> Element { - let mut dark_context = use_context::>(); + let mut theme_context = use_context::>(); rsx! { div { onclick: move |_| { - let theme; - if dark_context().0=="light" { - theme="dark" + let system_theme=get_media_theme().unwrap_or_else(|_|"".to_string()); + let target_theme; + + if theme_context().0=="light" { + target_theme="dark".to_string() }else { - theme="light" + target_theme="light".to_string() }; - dark_context.set(IsDark(theme.to_string())); - match set_local_storage_value("theme",theme) { - Ok(_)=>{}, - Err(_)=>{ - tracing::error!("主题储存失败"); - }, + let _=remove_element_class("html", &theme_context().0); + theme_context.set(ThemeProvider(target_theme.clone())); + let _=add_element_class("html", &target_theme); + if target_theme==system_theme { + let _=remove_local_storage_value("theme"); + }else{ + let _=set_local_storage_value("theme", &target_theme); } - match set_element_class("html",theme) { - Ok(_)=>{}, - Err(e)=>{ - tracing::error!("主题类名设置失败:{}",e); - }, - } } , { - match {dark_context().0.as_str()} { + match {theme_context().0.as_str()} { "dark"=>{ rsx!( Icon{ @@ -78,3 +79,17 @@ pub fn Toggle(props: ToggleProps) -> Element { } } + +pub fn get_theme() -> String { + let storage_theme = get_local_storage_value("theme"); + match storage_theme { + Ok(s) => s, + Err(_) => { + let device_theme = get_media_theme(); + match device_theme { + Ok(s) => s, + Err(_) => "light".to_string(), + } + } + } +} diff --git a/frontend/src/main.rs b/frontend/src/main.rs index baab299..d4cdf59 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -1,7 +1,9 @@ use dioxus::{logger::tracing, prelude::*}; +use wasm_bindgen::prelude::*; mod common; -use common::dom::{get_local_storage_value, get_media_theme, set_element_class}; -use components::theme_toggle::IsDark; +use common::dom::{add_element_class, get_media_theme, remove_element_class}; +use components::notification::{Notification, NotificationProvider}; +use components::theme_toggle::{get_theme, ThemeProvider}; use components::Navbar; use views::Home; @@ -10,12 +12,11 @@ mod components; mod views; #[derive(Debug, Clone, Routable, PartialEq)] -#[rustfmt::skip] enum Route { + #[layout(Notification)] #[layout(Navbar)] #[route("/")] Home {}, - } const FAVICON: Asset = asset!("/assets/favicon.ico"); @@ -28,25 +29,32 @@ fn main() { #[component] fn App() -> Element { - use_context_provider(|| Signal::new(IsDark("light".to_string()))); - let mut is_dark_context = use_context::>(); + let theme = get_theme(); + use_context_provider(|| Signal::new(ThemeProvider(theme.clone()))); + use_context_provider(|| { + Signal::new(NotificationProvider { + notifications: Vec::new(), + }) + }); + let mut is_dark_context = use_context::>(); use_effect(move || { - let theme = { - let storage_theme = get_local_storage_value("theme"); - match storage_theme { - Ok(s) => s, - Err(_) => { - let device_theme = get_media_theme(); - match device_theme { - Ok(s) => s, - Err(_) => "light".to_string(), - } - } - } - }; - is_dark_context.set(IsDark(theme.clone())); - let _ = set_element_class("html", &theme); + let _ = add_element_class("html", &theme); + let window = web_sys::window().unwrap(); + let media_query = window + .match_media("(prefers-color-scheme: dark)") + .unwrap() + .unwrap(); + let closure = Closure::wrap(Box::new(move |_: web_sys::MediaQueryListEvent| { + let theme = get_media_theme().unwrap_or_else(|_| "light".to_string()); + let _ = remove_element_class("html", &is_dark_context().0); + let _ = add_element_class("html", &theme); + is_dark_context.set(ThemeProvider(theme)); + }) as Box); + media_query + .add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); }); rsx! { diff --git a/frontend/src/views/home.rs b/frontend/src/views/home.rs index 6fae62b..a90e965 100644 --- a/frontend/src/views/home.rs +++ b/frontend/src/views/home.rs @@ -1,16 +1,21 @@ +use crate::components::Toast; use dioxus::prelude::*; -use crate::components::Message; #[component] pub fn Home() -> Element { rsx! { - Message{}, div { - class:"bg-yellow-200 dark:bg-sky-500", + class:"border-2 border-red-500", "hello,world" ul { li { "nihao" } - } } + button { + onclick: move |_| { + Toast::info("我是标sasadc我想二次TV要不以后牛魔题".to_string(), "我是内容".to_string()) + }, + "Show Notification" + } + } } } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 2faacb5..8cbc391 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -7,6 +7,17 @@ module.exports = { colors: { accent: 'var(--accent-color)', }, + animation:{ + progress:"5s progress linear infinite" + + }, + keyframes:{ + progress:{ + '0%':{width:'0%'}, + '100%':{width:'100%'} + } + + } }, }, plugins: [],