添加界面优化

This commit is contained in:
lenn
2026-04-16 18:00:54 +08:00
parent ccfc77b734
commit 408cd66c77
6 changed files with 244 additions and 94 deletions

1
Cargo.lock generated
View File

@@ -1738,6 +1738,7 @@ dependencies = [
name = "eskin-data-analysis" name = "eskin-data-analysis"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"dioxus", "dioxus",
"futures", "futures",
"polars", "polars",

View File

@@ -7,6 +7,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1.0.102"
dioxus = { version = "0.7.1", features = ["router"] } dioxus = { version = "0.7.1", features = ["router"] }
futures = "0.3.32" futures = "0.3.32"
polars = { version="0.53.0", features = ["lazy", "csv"] } polars = { version="0.53.0", features = ["lazy", "csv"] }
@@ -17,3 +18,4 @@ default = ["desktop"]
web = ["dioxus/web"] web = ["dioxus/web"]
desktop = ["dioxus/desktop"] desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"] mobile = ["dioxus/mobile"]

View File

@@ -19,3 +19,7 @@ script = []
# Javascript code file # Javascript code file
# serve: [dev-server] only # serve: [dev-server] only
script = [] script = []
[bundle]
identifier = "com.qunin"
publisher = "JOYSONQUNIN"

View File

@@ -182,6 +182,9 @@
max-width: 96rem; max-width: 96rem;
} }
} }
.mr-3 {
margin-right: calc(var(--spacing) * 3);
}
.mb-4 { .mb-4 {
margin-bottom: calc(var(--spacing) * 4); margin-bottom: calc(var(--spacing) * 4);
} }

View File

@@ -1,45 +1,92 @@
use dioxus::{Ok, html::{div, script::r#async, text}, prelude::*}; use std::{fs::File, future::Future};
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::guide_router::Route;
use rfd::AsyncFileDialog;
use polars::prelude::*; use polars::prelude::*;
const RATE_LABELS: [i32; 11] = [60, 160, 260, 360, 460, 560, 660, 860, 1060, 1560, 2060];
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct FilterRateThresholdState { pub struct FilterRateThresholdState {
pub thresholds: Signal<Vec<f32>>, pub items: Signal<Vec<ThresholdItem>>,
pub next_id: Signal<usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ThresholdItem {
pub id: usize,
pub label: String,
pub value: String,
} }
#[component] #[component]
pub fn FilterRateThreshold() -> Element { pub fn FilterRateThreshold() -> Element {
let mut state = use_context::<FilterRateThresholdState>(); let mut state = use_context::<FilterRateThresholdState>();
let mut navigator = use_navigator(); let navigator = use_navigator();
rsx! { rsx! {
div { class: "section", div { class: "section",
div { class: "container", div { class: "container",
div { class: "card", div { class: "card",
div { class: "card-header", div { class: "card-header",
p { class: "card-header-title", "过滤阈值配置" } p { class: "card-header-title", "阈值配置" }
} }
div { class: "card-content", div { class: "card-content",
p { class: "subtitle is-6 has-text-grey", p { class: "subtitle is-6 has-text-grey",
"支持手动输入CSV 导入,后续可扩展其他输入方式" "支持手动输入CSV 导入。手动输入时可以随时新增、删除和修改条目"
} }
// 手动输入
div { class: "box", div { class: "box",
h2 { class: "title is-6 mb-4", "手动输入" } h2 { class: "title is-6 mb-4", "手动输入" }
div { class: "columns is-multiline", div { class: "columns is-multiline",
for (index , label) in RATE_LABELS.iter().enumerate() { for item in state.items.read().iter() {
div { class: "column is-half", div { class: "column is-half",
ThresholdField { label: *label, index } ThresholdField {
id: item.id,
label: item.label.clone(),
value: item.value.clone(),
}
}
}
div { class: "column is-full",
button {
class: "button is-primary mr-3",
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);
},
"添加"
}
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);
},
"重置"
} }
} }
} }
} }
// 数据导入
div { class: "box", div { class: "box",
h2 { class: "title is-6 mb-4", "数据导入" } h2 { class: "title is-6 mb-4", "数据导入" }
@@ -47,7 +94,7 @@ pub fn FilterRateThreshold() -> Element {
button { button {
class: "button is-primary", class: "button is-primary",
onclick: move |_| { onclick: move |_| {
load_csv_data(&mut state.thresholds); load_csv_data(state.items);
}, },
span { class: "icon", span { class: "icon",
i { class: "fas fa-file-import" } i { class: "fas fa-file-import" }
@@ -55,39 +102,13 @@ pub fn FilterRateThreshold() -> Element {
span { "导入 CSV" } span { "导入 CSV" }
} }
button {
class: "button is-link is-light",
onclick: move |_| {
paste_data(state.thresholds);
},
"批量粘贴"
}
button { button {
class: "button is-light", class: "button is-light",
onclick: move |_| { onclick: move |_| navigator.go_back(),
navigator.go_back();
}, // navigator.go_back();, // back();,
"返回" "返回"
} }
} }
} }
// 当前预览
div { class: "box",
h2 { class: "title is-6 mb-4", "当前预览" }
div { class: "content",
ul {
for (index , label) in RATE_LABELS.iter().enumerate() {
li {
strong { "{label}: " }
"{state.thresholds.read()[index]}"
}
}
}
}
}
} }
} }
} }
@@ -96,24 +117,57 @@ pub fn FilterRateThreshold() -> Element {
} }
#[component] #[component]
pub fn ThresholdField(label: i32, index: usize) -> Element { fn ThresholdField(id: usize, label: String, value: String) -> Element {
let mut state = use_context::<FilterRateThresholdState>(); let mut state = use_context::<FilterRateThresholdState>();
rsx! { rsx! {
div { class: "field", div { class: "box",
label { class: "label", "{label}" } div { class: "columns is-vcentered",
div { class: "control", div { class: "column",
input { input {
class: "input is-info", class: "input",
r#type: "text", r#type: "text",
placeholder: "输入阈", placeholder: "边界",
value: "{state.thresholds.read()[index]}", value: label,
oninput: move |evt| { oninput: move |evt| {
// state.thresholds.write()[index] = evt as i32; let new_label = evt.value();
if let Ok(v) = evt.value().parse::<f32>() { state
state.thresholds.write()[index] = v; .items
} .with_mut(|items| {
}, if let Some(item) = items.iter_mut().find(|item| item.id == id) {
item.label = new_label;
}
});
},
}
}
div { class: "column",
input {
class: "input is-info",
r#type: "text",
placeholder: "阈值",
value,
oninput: move |evt| {
let new_value = evt.value();
state
.items
.with_mut(|items| {
if let Some(item) = items.iter_mut().find(|item| item.id == id) {
item.value = new_value;
}
});
},
}
}
div { class: "column is-narrow",
button {
class: "delete is-medium",
onclick: move |_| {
state.items.with_mut(|items| items.retain(|item| item.id != id));
},
}
} }
} }
} }
@@ -122,76 +176,162 @@ pub fn ThresholdField(label: i32, index: usize) -> Element {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct SmoothRateConfig { struct SmoothRateConfig {
thresholds: Vec<f32>, ths: Vec<f32>,
rates: Vec<f32>, rates: Vec<f32>,
file_path: Vec<String>, file_path: Vec<String>,
} }
impl Default for SmoothRateConfig { impl Default for SmoothRateConfig {
fn default() -> Self { fn default() -> Self {
SmoothRateConfig { thresholds: Vec::new(), rates: Vec::new(), file_path: Vec::new() } SmoothRateConfig { ths: Vec::new(), rates: Vec::new(), file_path: Vec::new() }
} }
} }
impl SmoothRateConfig { impl SmoothRateConfig {
fn new(ths: Vec<f32>, files: Vec<String>) -> Self { pub fn new(items: Vec<ThresholdItem>, files: Vec<String>) -> Self {
if ths.len() != 11 { let mut points = items
.into_iter()
.filter_map(|item| {
let label = item.label.trim().parse::<usize>().ok()?;
let value = item.value.trim().parse::<f32>().ok()?;
Some((label, value))
})
.collect::<Vec<_>>();
points.sort_by_key(|(label, _)| *label);
if points.len() < 2 {
return SmoothRateConfig::default(); return SmoothRateConfig::default();
} }
let rates = ths.chunks(2)
.map(|ck| ck[1] - ck[0]).collect::<Vec<f32>>(); // let labels = points.iter().map(|(label, _)| *label).collect::<Vec<_>>();
let ths: Vec<f32> = points.iter().map(|(_, v)| *v).collect::<Vec<f32>>();
SmoothRateConfig { thresholds: ths, rates, file_path: files } let rates = points
.windows(2)
.filter_map(|window| {
let (start_label, start_value) = window[0];
let (end_label, end_value) = window[1];
let distance = end_label.checked_sub(start_label)?;
if distance == 0 {
return None;
}
Some((end_value - start_value) / distance as f32)
})
.collect::<Vec<_>>();
if rates.len() + 1 != ths.len() {
return SmoothRateConfig::default();
}
println!("ths: {:?}", ths);
println!("rates: {:?}", rates);
SmoothRateConfig { ths, rates, file_path: files }
} }
fn analysis_file_once(file: &str) -> PolarsResult<()> { pub fn analysis_all_files(&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(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()) let df = LazyCsvReader::new(file.into())
.with_has_header(true) .with_has_header(true)
.finish()? .finish()?
.with_columns(
(1..=84)
.map(|i| {
let name = format!("channel{i}");
SmoothRateConfig::analysis_col_once(&name, &self.ths, &self.rates)
.cast(DataType::Float32)
.alias(&name)
})
.collect::<Vec<_>>(),
)
.collect()?; .collect()?;
let df = df.with_column( let output = format!("{file}_processed.csv");
(1..=84).map(|i| { let mut output_file = File::create(output)?;
let name = format!("channel{}", i); CsvWriter::new(&mut output_file)
.include_header(true)
}) .finish(&mut df.clone())?;
)
debug!("analysis finished");
anyhow::Ok(())
} }
fn analysis_col_once(name: &str, thresholds: &[f32], rates: &[f32]) -> Expr { 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);
for i in (0..rates.len()).rev() {
let t0 = thresholds[i] as f64;
let t1 = thresholds[i + 1] as f64;
let rate = rates[i] as f64;
expr = when(
col(name)
.cast(DataType::Float64)
.gt_eq(lit(t0))
.and(col(name).cast(DataType::Float64).lt(lit(t1))),
)
.then(col(name).cast(DataType::Float64) * lit(rate))
.otherwise(expr);
}
when(col(name).cast(DataType::Float64).lt(lit(thresholds[0] as f64)))
.then(lit(0.0))
.otherwise(expr)
} }
} }
fn load_csv_data(thresholds: &mut Signal<Vec<f32>>) { fn load_csv_data(items: Signal<Vec<ThresholdItem>>) {
let current_items = items.read().clone();
let task = rfd::AsyncFileDialog::new() let task = rfd::AsyncFileDialog::new()
.add_filter("CSV", &["csv"]) .add_filter("CSV", &["csv"])
.pick_files(); .pick_files();
execute(async {
let file_handles = task.await;
if let Some(files) = file_handles {
for file in files {
let path = file.path();
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::<Vec<_>>();
let config = SmoothRateConfig::new(current_items, file_path_vec);
if let Err(error) = config.analysis_all_files() {
eprintln!("批量处理失败: {error}");
} }
} }
}); });
} }
fn paste_data(thresholds: Signal<Vec<f32>>) { #[cfg(not(target_arch = "wasm32"))]
todo!() fn execute<F: Future<Output = ()> + Send + 'static>(future: F) {
std::thread::spawn(move || futures::executor::block_on(future));
} }
use std::{fs::FileTimes, future::Future};
#[cfg(not(target_arch = "wasm32"))]
fn execute<F: Future<Output = ()> + Send + 'static>(f: F) {
// this is stupid... use any executor of your choice instead
std::thread::spawn(move || futures::executor::block_on(f));
}
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
fn execute<F: Future<Output = ()> + 'static>(f: F) { fn execute<F: Future<Output = ()> + 'static>(future: F) {
wasm_bindgen_futures::spawn_local(f); wasm_bindgen_futures::spawn_local(future);
} }

View File

@@ -1,13 +1,12 @@
pub mod components; pub mod components;
pub mod guide_router; pub mod guide_router;
use dioxus::{html::{div, text}, prelude::*};
use dioxus::prelude::*; use dioxus::prelude::*;
const FAVICON: Asset = asset!("/assets/favicon.ico"); const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/bulma.css"); const MAIN_CSS: Asset = asset!("/assets/bulma.css");
const HEADER_SVG: Asset = asset!("/assets/header.svg"); const HEADER_SVG: Asset = asset!("/assets/header.svg");
use crate::components::filters_rate_threshold::{FilterRateThreshold, FilterRateThresholdState}; use crate::components::filters_rate_threshold::FilterRateThresholdState;
use crate::guide_router::Route; use crate::guide_router::Route;
fn main() { fn main() {
dioxus::launch(App); dioxus::launch(App);
@@ -15,8 +14,9 @@ fn main() {
#[component] #[component]
fn App() -> Element { fn App() -> Element {
let thresholds = use_signal(|| vec![0.0; 11]); let items = use_signal(Vec::new);
use_context_provider(|| FilterRateThresholdState { thresholds }); let next_id = use_signal(|| 0usize);
use_context_provider(|| FilterRateThresholdState { items, next_id });
rsx! { rsx! {
document::Link { rel: "icon", href: FAVICON } document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS } document::Link { rel: "stylesheet", href: MAIN_CSS }