diff --git a/Cargo.lock b/Cargo.lock index 2e18dae..112de1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,6 +1241,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "dioxus-free-icons" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d356e0f9edad0930bc1cc76744360c0ecca020cb943acaadf42cb774f28284" +dependencies = [ + "dioxus", +] + [[package]] name = "dioxus-fullstack" version = "0.7.5" @@ -1740,6 +1749,7 @@ version = "0.1.0" dependencies = [ "anyhow", "dioxus", + "dioxus-free-icons", "futures", "polars", "rfd", diff --git a/Cargo.toml b/Cargo.toml index c7ce508..319cc1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ dioxus = { version = "0.7.1", features = ["router"] } futures = "0.3.32" polars = { version="0.53.0", features = ["lazy", "csv"] } rfd = { version = "0.17.2" } - +dioxus-free-icons = {version = "0.10.0", features=["font-awesome-solid"]} [features] default = ["desktop"] web = ["dioxus/web"] diff --git a/assets/tailwind.css b/assets/tailwind.css index 45e0724..0b6149c 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -188,12 +188,35 @@ .mb-4 { margin-bottom: calc(var(--spacing) * 4); } + .ml-3 { + margin-left: calc(var(--spacing) * 3); + } + .table { + display: table; + } + .border-collapse { + border-collapse: collapse; + } .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } + .resize { + resize: both; + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } .pt-0 { padding-top: calc(var(--spacing) * 0); } + .underline { + text-decoration-line: underline; + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } } @property --tw-rotate-x { syntax: "*"; @@ -215,6 +238,16 @@ syntax: "*"; inherits: false; } +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { @@ -223,6 +256,8 @@ --tw-rotate-z: initial; --tw-skew-x: initial; --tw-skew-y: initial; + --tw-border-style: solid; + --tw-outline-style: solid; } } } diff --git a/src/components/filters_rate_threshold.rs b/src/components/filters_rate_threshold.rs index 3a50834..0a82514 100644 --- a/src/components/filters_rate_threshold.rs +++ b/src/components/filters_rate_threshold.rs @@ -1,7 +1,7 @@ -use std::{fs::File, future::Future}; - use dioxus::prelude::*; +use dioxus_free_icons::{icons::fa_solid_icons, Icon}; use polars::prelude::*; +use std::{fs::File, future::Future}; #[derive(Clone, Copy)] pub struct FilterRateThresholdState { @@ -16,9 +16,77 @@ pub struct ThresholdItem { pub value: String, } +fn default_threshold_items() -> Vec { + vec![ + ThresholdItem { + id: 0, + label: "0".to_string(), + value: "20".to_string(), + }, + ThresholdItem { + id: 1, + label: "300".to_string(), + value: "1161.2".to_string(), + }, + ThresholdItem { + id: 2, + label: "800".to_string(), + value: "3217.337".to_string(), + }, + ThresholdItem { + id: 3, + label: "1300".to_string(), + value: "3849.787".to_string(), + }, + ThresholdItem { + id: 4, + label: "1800".to_string(), + value: "4146.974".to_string(), + }, + ThresholdItem { + id: 5, + label: "2300".to_string(), + value: "4392.644".to_string(), + }, + ThresholdItem { + id: 6, + label: "2800".to_string(), + value: "4712.814".to_string(), + }, + ThresholdItem { + id: 7, + label: "3300".to_string(), + value: "4951.606".to_string(), + }, + ThresholdItem { + id: 8, + label: "4300".to_string(), + value: "5301.445".to_string(), + }, + ThresholdItem { + id: 9, + label: "5300".to_string(), + value: "5652.705".to_string(), + }, + ThresholdItem { + id: 10, + label: "7800".to_string(), + value: "6334.89".to_string(), + }, + ThresholdItem { + id: 11, + label: "10300".to_string(), + value: "6951.119".to_string(), + }, + ] +} + #[component] pub fn FilterRateThreshold() -> Element { - let mut state = use_context::(); + let items = use_signal(default_threshold_items); + let next_id = use_signal(|| 12usize); + let mut import_dropdown_open = use_signal(|| false); + let mut state = use_context_provider(|| FilterRateThresholdState { items, next_id }); let navigator = use_navigator(); rsx! { @@ -68,18 +136,8 @@ pub fn FilterRateThreshold() -> Element { button { class: "button is-light", onclick: move |_| { - // let id = (state.next_id)(); - // state - // .items - // .write() - // .push(ThresholdItem { - // id, - // label: String::new(), - // value: String::new(), - // }); - // state.next_id.with_mut(|next_id| *next_id += 1); - state.items.write().clear(); - state.next_id.with_mut(|n| *n = 0); + *state.items.write() = default_threshold_items(); + state.next_id.set(12); }, "重置" } @@ -89,25 +147,73 @@ pub fn FilterRateThreshold() -> Element { div { class: "box", h2 { class: "title is-6 mb-4", "数据导入" } - - div { class: "buttons", - button { - class: "button is-primary", - onclick: move |_| { - load_csv_data(state.items); - }, - span { class: "icon", - i { class: "fas fa-file-import" } + div { class: if import_dropdown_open() { "dropdown is-active" } else { "dropdown" }, + div { class: "dropdown-trigger", + button { + class: "button is-primary", + style: "height: 2.5em; justify-content: space-between; align-items: center;", + "aria-haspopup": "true", + "aria-controls": "dropdown-menu", + "aria-expanded": if import_dropdown_open() { "true" } else { "false" }, + onclick: move |_| { + import_dropdown_open.set(!import_dropdown_open()); + }, + span { "导入CSV" } + span { class: "icon is-small", + Icon { + width: 18, + height: 18, + fill: "black", + icon: fa_solid_icons::FaAngleDown, + } + } } - span { "导入 CSV" } + } + div { class: "buttons", + div { + class: "dropdown-menu height: 2.5em;", + "id": "dropdown-menu", + "role": "menu", + div { class: "dropdown-content", + button { + class: "dropdown-item button is-white", + onclick: move |_| { + import_dropdown_open.set(false); + load_csv_data_old(state.items); + }, + "导入旧格式" + } + button { + class: "dropdown-item button is-white", + onclick: move |_| { + import_dropdown_open.set(false); + load_csv_data_new(state.items); + }, + "导入新格式" + } + } + } + } - button { - class: "button is-light", - onclick: move |_| navigator.go_back(), - "返回" - } } + button { + class: "button is-light ml-3", + style: "height: 2.5em;", + onclick: move |_| navigator.go_back(), + "返回" + } + // div { class: "buttons", + // button { + // class: "button is-primary", + // onclick: move |_| { + // load_csv_data(state.items); + // }, + // span { class: "icon", + // i { class: "fas fa-file-import" } + // } + // span { "导入 CSV" } + // } } } } @@ -183,7 +289,11 @@ struct SmoothRateConfig { impl Default for SmoothRateConfig { fn default() -> Self { - SmoothRateConfig { ths: Vec::new(), rates: Vec::new(), file_path: Vec::new() } + SmoothRateConfig { + ths: Vec::new(), + rates: Vec::new(), + file_path: Vec::new(), + } } } @@ -222,10 +332,12 @@ impl SmoothRateConfig { if rates.len() + 1 != ths.len() { return SmoothRateConfig::default(); } - println!("ths: {:?}", ths); - println!("rates: {:?}", rates); - SmoothRateConfig { ths, rates, file_path: files } + SmoothRateConfig { + ths, + rates, + file_path: files, + } } pub fn analysis_all_files(&self) -> anyhow::Result<()> { @@ -250,6 +362,27 @@ impl SmoothRateConfig { anyhow::Ok(()) } + pub fn analysis_all_files_old(&self) -> anyhow::Result<()> { + if self.file_path.is_empty() { + return anyhow::Ok(()); + } + + let mut failed = vec![]; + for file in &self.file_path { + match self.analysis_file_once_old(file) { + Ok(_) => {} + Err(error) => failed.push((file.clone(), error.to_string())), + } + } + + if !failed.is_empty() { + for (file, error) in failed { + println!("{file} -> {error}"); + } + } + anyhow::Ok(()) + } + fn analysis_file_once(&self, file: &str) -> anyhow::Result<()> { let df = LazyCsvReader::new(file.into()) .with_has_header(true) @@ -272,13 +405,35 @@ impl SmoothRateConfig { .include_header(true) .finish(&mut df.clone())?; - debug!("analysis finished"); + anyhow::Ok(()) + } + + fn analysis_file_once_old(&self, file: &str) -> anyhow::Result<()> { + let df = LazyCsvReader::new(file.into()) + .with_has_header(false) + .finish()? + .with_columns( + (2..=85) + .map(|i| { + let name = format!("column_{}", i); + SmoothRateConfig::analysis_col_once(&name, &self.ths, &self.rates) + .cast(DataType::Float32) + .alias(&name) + }) + .collect::>(), + ) + .collect()?; + let output = format!("{file}_processed.csv"); + let mut output_file = File::create(output)?; + CsvWriter::new(&mut output_file) + .include_header(false) + .finish(&mut df.clone())?; + anyhow::Ok(()) } fn analysis_col_once(name: &str, thresholds: &[f32], rates: &[f32]) -> Expr { - let mut expr: Expr = - col(name).cast(DataType::Float64) * lit(rates[rates.len() - 1] as f64); + let mut expr: Expr = col(name).cast(DataType::Float64) * lit(rates[rates.len() - 1] as f64); for i in (0..rates.len()).rev() { let t0 = thresholds[i] as f64; @@ -295,13 +450,41 @@ impl SmoothRateConfig { .otherwise(expr); } - when(col(name).cast(DataType::Float64).lt(lit(thresholds[0] as f64))) - .then(lit(0.0)) - .otherwise(expr) + when( + col(name) + .cast(DataType::Float64) + .lt(lit(thresholds[0] as f64)), + ) + .then(lit(0.0)) + .otherwise(expr) } } -fn load_csv_data(items: Signal>) { +fn load_csv_data_old(items: Signal>) { + let current_items = items.read().clone(); + + let task = rfd::AsyncFileDialog::new() + .add_filter("CSV", &["csv"]) + .pick_files(); + execute(async move { + let file_handles = task.await; + + if let Some(files) = file_handles { + let file_path_vec = files + .iter() + .map(|file| file.path().to_string_lossy().to_string()) + .collect::>(); + + let config = SmoothRateConfig::new(current_items, file_path_vec); + + if let Err(error) = config.analysis_all_files_old() { + eprintln!("批量处理失败: {error}"); + } + } + }); +} + +fn load_csv_data_new(items: Signal>) { let current_items = items.read().clone(); let task = rfd::AsyncFileDialog::new() diff --git a/src/components/main_page.rs b/src/components/main_page.rs index 185be78..86465df 100644 --- a/src/components/main_page.rs +++ b/src/components/main_page.rs @@ -1,5 +1,5 @@ -use dioxus::prelude::*; use crate::guide_router::Route; +use dioxus::prelude::*; #[component] pub fn MainPage() -> Element { rsx! { @@ -85,17 +85,6 @@ pub fn MainPage() -> Element { } } } - - // 底部说明区 - section { class: "section pt-0", - div { class: "container", - div { class: "notification is-light", - p { class: "has-text-grey", - "建议从“阈值配置”或“数据导入”开始。主页仅作为导航入口,具体路由逻辑可在外层统一处理。" - } - } - } - } } } } @@ -131,4 +120,4 @@ fn FeatureCard(props: FeatureCardProps) -> Element { } } } -} \ No newline at end of file +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 7614c37..c26fb87 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,2 +1,2 @@ pub mod filters_rate_threshold; -pub mod main_page; \ No newline at end of file +pub mod main_page; diff --git a/src/guide_router.rs b/src/guide_router.rs index bf65a28..df1ed81 100644 --- a/src/guide_router.rs +++ b/src/guide_router.rs @@ -1,6 +1,4 @@ -use crate::components::{main_page::MainPage, - filters_rate_threshold::FilterRateThreshold, -}; +use crate::components::{filters_rate_threshold::FilterRateThreshold, main_page::MainPage}; use dioxus::prelude::*; #[derive(Routable, Clone, PartialEq)] @@ -9,4 +7,4 @@ pub enum Route { MainPage, #[route("/filter_rate_threshold")] FilterRateThreshold, -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 2e50cc7..a765f00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,48 +1,35 @@ pub mod components; pub mod guide_router; +use dioxus::desktop::{use_window, Config}; use dioxus::prelude::*; const FAVICON: Asset = asset!("/assets/favicon.ico"); const MAIN_CSS: Asset = asset!("/assets/bulma.css"); const HEADER_SVG: Asset = asset!("/assets/header.svg"); - -use crate::components::filters_rate_threshold::FilterRateThresholdState; use crate::guide_router::Route; fn main() { - dioxus::launch(App); + LaunchBuilder::new() + .with_cfg(Config::new().with_menu(None)) + .launch(App); } #[component] fn App() -> Element { - let items = use_signal(Vec::new); - let next_id = use_signal(|| 0usize); - use_context_provider(|| FilterRateThresholdState { items, next_id }); + let window = use_window(); + + use_effect(move || { + window.window.set_always_on_top(false); + }); rsx! { document::Link { rel: "icon", href: FAVICON } document::Link { rel: "stylesheet", href: MAIN_CSS } - // // Hero {} - // FilterRateThreshold {} - Router:: {} - } - -} - -#[component] -pub fn Hero() -> Element { - rsx! { - div { id: "hero", - img { src: HEADER_SVG, id: "header" } - div { id: "links", - a { href: "https://dioxuslabs.com/learn/0.7/", "📚 Learn Dioxus" } - a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" } - a { href: "https://github.com/dioxus-community/", "📡 Community Libraries" } - a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" } - a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus", - "💫 VSCode Extension" - } - a { href: "https://discord.gg/XgGxMSkvUM", "👋 Community Discord" } - } + div { + width: "100%", + min_height: "100vh", + oncontextmenu: move |event| { + event.prevent_default(); + }, + Router:: {} } } } -