Tauri 2.3.1+Leptos 0.7.8开发桌面应用--Sqlite数据库的写入、展示和选择删除
在前期工作的基础上(Tauri2+Leptos开发桌面应用--Sqlite数据库操作_tauri sqlite-CSDN博客),尝试制作产品化学成分录入界面,并展示数据库内容,删除选中的数据。具体效果如下:

一、前端Leptos程序
前端程序主要是实现前端产品录入界面的设计,需要实现:
1. 输入框内输入的数据和日期的合规性检测
2. 定义输入数据的值及信号,实现实时更新
3. 通过invoke调用后台tauri命令,实现数据库的写入,内容展示和删除选中数据项
4. 数据内容展示是通过生成view!视图插入到DIV中实现的,视图内容也是通过定义信号实时更新
5. 为了便于删除选中的数据,需要在展示数据内容时,在每条数据前增加选择的复选框
6. 删除数据后,还要刷新数据的展示
具体代码如下:
use leptos::task::spawn_local;
use leptos::{ev::SubmitEvent, prelude::*};
use leptos_router::hooks::use_navigate;
use serde::{Deserialize, Serialize};
use leptos::ev::Event;
use wasm_bindgen::prelude::*;
use chrono::{Local, NaiveDateTime};
use leptos::web_sys::{Blob, Url};
use web_sys::BlobPropertyBag;
use js_sys::{Array, Uint8Array};
use base64::engine::general_purpose::STANDARD; // 引入 STANDARD Engine
use base64::Engine; // 引入 Engine trait
use web_sys::HtmlInputElement;#[wasm_bindgen]
extern "C" {#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]async fn invoke_without_args(cmd: &str) -> JsValue;#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] //Tauri API 将会存储在 window.__TAURI__ 变量中,并通过 wasm-bindgen 导入。async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}//序列化后的变量作为函数invoke(cmd, args: JsValue)的参数,JsValue为序列化格式
#[derive(Serialize, Deserialize)]
struct GreetArgs<'a> {name: &'a str,
}#[derive(Serialize, Deserialize)]
struct InsertArgs<'a> { //生命周期标识符 'a 用于帮助编译器检查引用的有效性,避免悬垂引用和使用已被释放的内存。username: &'a str,email: &'a str,
}#[derive(Serialize, Deserialize)]
struct OpenArgs<'a> { //生命周期标识符 'a 用于帮助编译器检查引用的有效性,避免悬垂引用和使用已被释放的内存。title: &'a str, url: &'a str,
}#[derive(Serialize, Deserialize)]
struct UpdateArgs<'a> { //生命周期标识符 'a 用于帮助编译器检查引用的有效性,避免悬垂引用和使用已被释放的内存。label: &'a str, content: &'a str,
}#[derive(Serialize, Deserialize)]
struct SwitchArgs<'a> { //生命周期标识符 'a 用于帮助编译器检查引用的有效性,避免悬垂引用和使用已被释放的内存。label: &'a str,}#[derive(Serialize, Deserialize)]
struct User {id: u16,username: String,email: String,
}#[derive(Serialize, Deserialize)]
struct Pdt {pdt_id:i64,pdt_name:String,pdt_si:f64,pdt_al:f64,pdt_ca:f64,pdt_mg:f64,pdt_fe:f64,pdt_ti:f64,pdt_ka:f64,pdt_na:f64,pdt_mn:f64,pdt_date:String,
}#[derive(Serialize, Deserialize)]
struct PdtArgs {pdt_name:String,pdt_si:f64,pdt_al:f64,pdt_ca:f64,pdt_mg:f64,pdt_fe:f64,pdt_ti:f64,pdt_ka:f64,pdt_na:f64,pdt_mn:f64,pdt_date:String,
}#[derive(Serialize, Deserialize)]
struct WritePdtArgs {product: PdtArgs, // 将 PdtArgs 包装为一个包含 `product` 键的对象
}#[derive(Serialize, Deserialize)]
struct SelectedPdtArgs { // 将invoke调用的参数打包成结构变量再通过json传递,tauri后台invoke函数的参数名称必须根键一致(譬如此处的productlist)productlist: Vec<i64>, // 将Vec<i64>数组包装为一个包含 `productlist` 键的对象,键不能带下划线"_"
}#[component]
pub fn AcidInput() -> impl IntoView { //函数返回IntoView类型,即返回view!宏,函数名App()也是主程序view!宏中的组件名(component name)。//定义产品化学成分输入框值及信号let (pdt_Name, set_pdt_Name) = signal(String::from("产品"));let (Name_error, set_Name_error) = signal(String::new());let (pdt_Si, set_pdt_Si) = signal(0.0);let (Si_error, set_Si_error) = signal(String::new());let (pdt_Al, set_pdt_Al) = signal(0.0);let (Al_error, set_Al_error) = signal(String::new());let (pdt_Ca, set_pdt_Ca) = signal(0.0);let (Ca_error, set_Ca_error) = signal(String::new());let (pdt_Mg, set_pdt_Mg) = signal(0.0);let (Mg_error, set_Mg_error) = signal(String::new());let (pdt_Fe, set_pdt_Fe) = signal(0.0);let (Fe_error, set_Fe_error) = signal(String::new());let (pdt_Ti, set_pdt_Ti) = signal(0.0);let (Ti_error, set_Ti_error) = signal(String::new());let (pdt_Ka, set_pdt_Ka) = signal(0.0);let (Ka_error, set_Ka_error) = signal(String::new());let (pdt_Na, set_pdt_Na) = signal(0.0);let (Na_error, set_Na_error) = signal(String::new());let (pdt_Mn, set_pdt_Mn) = signal(0.0);let (Mn_error, set_Mn_error) = signal(String::new());let now = Local::now().format("%Y-%m-%dT%H:%M").to_string();let (pdt_date, set_pdt_date) = signal(now);let (date_error, set_date_error) = signal(String::new());let (sql_error, set_sql_error) = signal(String::new());//let (div_content, set_div_content) = signal(String::new());//let (div_content, set_div_content) = signal(View::new(()));let (div_content, set_div_content) = signal(view! { <div>{Vec::<View<_>>::new()}</div> });let (selected_items, set_selected_items) = signal::<Vec<i64>>(vec![]);// 创建一个信号来存储 base64 图片数据//let (pic_str, set_pic_str) = signal(String::new());//let (svg_str, set_svg_str) = signal(String::new());let update_pdt = move|ev:Event, set_value:WriteSignal<f64>, set_error:WriteSignal<String>| {match event_target_value(&ev).parse::<f64>(){Ok(num) => {//如果值在范围内,则更新信号if num >= 0.0 && num <= 100.00 {set_value.set(num);set_error.set(String::new());}else{set_error.set("数字必须在0到100之间".to_string());}}Err(_) => {set_error.set("请输入有效的数字".to_string());}}};// 定义日期时间范围let min_datetime = NaiveDateTime::parse_from_str("2011-01-01T00:00", "%Y-%m-%dT%H:%M").unwrap(); // 最小日期时间//let max_datetime = NaiveDateTime::parse_from_str("2023-12-31T18:00", "%Y-%m-%dT%H:%M").unwrap(); // 最大日期时间let update_date = move|ev| {match NaiveDateTime::parse_from_str(&event_target_value(&ev), "%Y-%m-%dT%H:%M") {Ok(parsed_datetime) => {// 检查日期时间是否在范围内if parsed_datetime >= min_datetime {set_pdt_date.set(parsed_datetime.to_string());set_date_error.set(String::new());} else {set_date_error.set(format!("日期时间必须大于{}",min_datetime.format("%Y-%m-%d %H:%M")));}}Err(_) => {set_date_error.set("请输入有效的日期时间(格式:YYYY-MM-DDTHH:MM)".to_string());}}};// 定义名称长度范围let min_length = 3;let max_length = 100;let update_Name = move|ev| {match event_target_value(&ev).parse::<String>(){Ok(name) => {//检查是否为空if name.is_empty() {set_Name_error.set("名称不能为空".to_string());return;};// 检查长度是否在范围内if name.len() < min_length {set_Name_error.set(format!("名称长度不能少于 {} 个字符", min_length));} else if name.len() > max_length {set_Name_error.set(format!("名称长度不能大于 {} 个字符", max_length));}else{set_pdt_Name.set(name.to_string());set_Name_error.set(String::new());}}Err(_) => {set_Name_error.set("请输入有效产品名称!".to_string());}}};let write_pdt_sql = move |ev: SubmitEvent| {ev.prevent_default(); //类似javascript中的Event.preventDefault(),处理<input>字段非常有用spawn_local(async move { //使用Leptos的spawn_local创建一个本地线程(local_thread)Future, 提供一个异步move闭包。let pdt_name = pdt_Name.get_untracked();let pdt_si = pdt_Si.get_untracked();let pdt_al = pdt_Al.get_untracked();let pdt_ca = pdt_Ca.get_untracked();let pdt_mg = pdt_Mg.get_untracked();let pdt_fe = pdt_Fe.get_untracked();let pdt_ti = pdt_Ti.get_untracked();let pdt_ka = pdt_Ka.get_untracked();let pdt_na = pdt_Na.get_untracked();let pdt_mn = pdt_Mn.get_untracked();let pdt_date = pdt_date.get_untracked();set_sql_error.set(String::new());let total_chem = pdt_si + pdt_al + pdt_ca + pdt_mg + pdt_fe + pdt_ti + pdt_ka + pdt_na + pdt_mn;if total_chem < 95.0 {set_sql_error.set("所有化学成分总量小于95%,请检查输入数据!".to_string());return;};if total_chem > 105.0 {set_sql_error.set("所有化学成分总量大于105%,请检查输入数据!".to_string());return;};let ca_mg = pdt_ca + pdt_mg;if ca_mg <= 0.0 {set_sql_error.set("CaO和MgO总量不能为零,请检查输入数据!".to_string());return;};let args = WritePdtArgs{product:PdtArgs { pdt_name: pdt_name, pdt_si: pdt_si, pdt_al: pdt_al, pdt_ca: pdt_ca, pdt_mg: pdt_mg, pdt_fe: pdt_fe, pdt_ti: pdt_ti, pdt_ka: pdt_ka, pdt_na: pdt_na, pdt_mn: pdt_mn, pdt_date: pdt_date },};let args_js = serde_wasm_bindgen::to_value(&args).unwrap(); //参数序列化// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/let new_msg = invoke("write_pdt_db", args_js).await.as_string().unwrap(); //使用invoke调用greet命令,greet类似于APIset_sql_error.set(new_msg);});};//处理复选框事件let check_change = move |ev:leptos::ev::Event|{//ev.prevent_default(); spawn_local(async move {let target = event_target::<HtmlInputElement>(&ev);let value_str = target.value(); // 直接获取 value// 将字符串解析为 i64(需处理可能的错误)if let Ok(value) = value_str.parse::<i64>() {set_selected_items.update(|items| {if target.checked() {items.push(value);} else {items.retain(|&x| x != value);}});};});};let receive_pdt_db = move |ev: SubmitEvent| {ev.prevent_default();spawn_local(async move { //使用Leptos的spawn_local创建一个本地线程(local_thread)Future, 提供一个异步move闭包。let pdt_js = invoke_without_args("send_pdt_db").await;let pdt_vec: Vec<Pdt> = serde_wasm_bindgen::from_value(pdt_js).map_err(|_| JsValue::from("Deserialization error")).unwrap();let mut receive_msg = String::from("读取数据库ID序列为:[");// 构建日志消息(注意:pdt_vec 已被消耗,需提前克隆或调整逻辑)let pdt_ids: Vec<i64> = pdt_vec.iter().map(|pdt| pdt.pdt_id).collect();for id in pdt_ids {receive_msg += &format!("{}, ", id);}receive_msg += "]";// 动态生成包裹在 div 中的视图let div_views = view! {<div>{pdt_vec.into_iter().map(|pdt| {let pdt_id = pdt.pdt_id;view! {<div style="margin:5px;width:1500px;"><inputtype="checkbox"name="items"value=pdt_id.to_string()prop:checked=move || selected_items.get().contains(&pdt_id)on:change=check_change/><span>// 直接使用 Unicode 下标字符"PdtID: " {pdt_id}",产品名称: " {pdt.pdt_name}",SiO₂: " {pdt.pdt_si} "%"",Al₂O₃: " {pdt.pdt_al} "%"",CaO: " {pdt.pdt_ca} "%"",MgO: " {pdt.pdt_mg} "%"",Fe₂O₃: " {pdt.pdt_fe} "%"",TiO₂: " {pdt.pdt_ti} "%"",K₂O: " {pdt.pdt_ka} "%"",Na₂O: " {pdt.pdt_na} "%"",MnO₂: " {pdt.pdt_mn} "%"",生产日期: " {pdt.pdt_date}</span></div>}}).collect_view()}</div>}; // 关键的类型擦除;// 转换为 View 类型并设置//log!("视图类型: {:?}", std::any::type_name_of_val(&div_views));set_div_content.set(div_views); set_sql_error.set(receive_msg);});};let del_selected_pdt = move|ev:SubmitEvent| {ev.prevent_default();spawn_local(async move { //使用Leptos的spawn_local创建一个本地线程(local_thread)Future, 提供一个异步move闭包。let args = SelectedPdtArgs{productlist:selected_items.get_untracked(),};let args_js = serde_wasm_bindgen::to_value(&args).unwrap(); //参数序列化let new_msg = invoke("del_selected_pdt", args_js).await.as_string().unwrap();set_sql_error.set(new_msg);set_selected_items.set(Vec::<i64>::new());// 删除完成后触发刷新操作receive_pdt_db(ev.clone()); });}; let navigate = use_navigate();let plot_image = move|ev:SubmitEvent| {ev.prevent_default();navigate("/images", Default::default());spawn_local(async move {// 调用 Tauri 的 invoke 方法获取 base64 图片数据let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_plot").await).unwrap();//log!("Received Base64 data: {}", result);let mut image = String::new();if result.len() != 0 {// 将 base64 数据存储到信号中image = result;} else {set_sql_error.set("Failed to generate plot".to_string());}// 检查 Base64 数据是否包含前缀let base64_data = if image.starts_with("data:image/png;base64,") {image.trim_start_matches("data:image/png;base64,").to_string()} else {image};// 将 Base64 字符串解码为二进制数据let binary_data = STANDARD.decode(&base64_data).expect("Failed to decode Base64");// 将二进制数据转换为 js_sys::Uint8Arraylet uint8_array = Uint8Array::from(&binary_data[..]);// 创建 Bloblet options = BlobPropertyBag::new();options.set_type("image/png");let blob = Blob::new_with_u8_array_sequence_and_options(&Array::of1(&uint8_array),&options,).expect("Failed to create Blob");// 生成图片 URLlet image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");// 打印生成的 URL,用于调试//log!("Generated Blob URL: {}", image_url);// 动态创建 <img> 元素let img = document().create_element("img").expect("Failed to create img element");img.set_attribute("src", &image_url).expect("Failed to set src");img.set_attribute("alt", "Plot").expect("Failed to set alt");// 设置宽度(例如 300px),高度会自动缩放img.set_attribute("width", "600").expect("Failed to set width");// 将 <img> 插入到 DOM 中let img_div = document().get_element_by_id("img_div").expect("img_div not found");// 清空 div 内容(避免重复插入)img_div.set_inner_html("");img_div.append_child(&img).expect("Failed to append img");});};view! { //view!宏作为App()函数的返回值返回IntoView类型<main class="container"><h1>"产品化学成分录入"</h1><form id="greet-form" on:submit=write_pdt_sql><div class="pdtinput"><div class="left"> "产品名称:"</div><div class="right"> <input style="width:350px" type="text" minlength="1" maxlength="100" placeholder="请输入产品名称..." value = move || pdt_Name.get() //将信号的值绑定到输入框on:input=move|ev|update_Name(ev) /></div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Name_error}</div></div><div class="pdtinput"><div class="left"> "二氧化硅:"</div><div class="right"> <input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入二氧化硅含量百分数..." value = move || pdt_Si.get() //将信号的值绑定到输入框on:input=move|ev|update_pdt(ev, set_pdt_Si, set_Si_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Si_error}</div></div><div class="pdtinput"><div class="left"> "三氧化二铝:"</div><div class="right"> <input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入三氧化二铝含量百分数..." value = move || pdt_Al.get() //将信号的值绑定到输入框on:input=move|ev|update_pdt(ev,set_pdt_Al, set_Al_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Al_error}</div></div><div class="pdtinput"><div class="left"> "氧化钙:"</div><div class="right"> <input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入氧化钙含量百分数..." value = move || pdt_Ca.get() //将信号的值绑定到输入框on:input=move|ev|update_pdt(ev,set_pdt_Ca, set_Ca_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Ca_error}</div></div><div class="pdtinput"><div class="left"> "氧化镁:"</div><div class="right"> <input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入氧化镁含量百分数..." value = move || pdt_Mg.get() //将信号的值绑定到输入框on:input=move|ev|update_pdt(ev,set_pdt_Mg, set_Mg_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Mg_error}</div></div><div class="pdtinput"><div class="left"> "全铁(TFe):"</div><div class="right"> <input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入全铁(Fe2O3)含量百分数..." value = move || pdt_Fe.get() //将信号的值绑定到输入框on:input=move|ev|update_pdt(ev,set_pdt_Fe, set_Fe_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Fe_error}</div></div><div class="pdtinput"><div class="left"> "二氧化钛:"</div><div class="right"> <input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入二氧化钛含量百分数..." value = move || pdt_Ti.get() //将信号的值绑定到输入框on:input=move|ev|update_pdt(ev,set_pdt_Ti, set_Ti_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Ti_error}</div></div><div class="pdtinput"><div class="left"> "氧化钾:"</div><div class="right"><input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入氧化钾含量百分数..." value = move || pdt_Ka.get() //将信号的值绑定到输入框on:input=move|ev|update_pdt(ev,set_pdt_Ka, set_Ka_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Ka_error}</div></div><div class="pdtinput"><div class="left"> "氧化钠:"</div><div class="right"><input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入氧化钠含量百分数..." value = move || pdt_Na.get() //将信号的值绑定到输入框on:input=move|ev|update_pdt(ev,set_pdt_Na, set_Na_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Na_error}</div></div><div class="pdtinput"><div class="left"> "二氧化锰:"</div><div class="right"><input style="width:320px" type="number" min="0.0" max="100.0" step="0.01" placeholder="请输入氧化锰含量百分数..." value = move || pdt_Mn.get() //将信号的值绑定到输入框on:input=move |ev|update_pdt(ev,set_pdt_Mn, set_Mn_error) />"%"</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {Mn_error}</div></div><div class="pdtinput"><div class="left"> "取样时间:"</div><div class="right"><input style="width:350px" type="datetime" min="2011-01-01T00:00:00"value = move || pdt_date.get() //将信号的值绑定到输入框on:input=update_date /></div></div><div class="errorshow"><div class="left"></div><div class="right red"> {date_error}</div></div><button style="width:300px;" type="submit" id="greet-button">"产品录入"</button></form><p class="red">{move || sql_error.get() }, "选中的项目有:"{move || selected_items.get().iter().map(|x| x.to_string()) // 将 i64 转为 String.collect::<Vec<String>>() // 收集为 Vec<String>.join(", ") // 使用标准库的 join}</p><div class="form-container"><div class="db-window" id="db-item">{move || div_content.get()}</div><div class="btn-window"><form class="row" on:submit=receive_pdt_db><button type="submit" style="margin:10px 5px 10px 5px;" id="get-button" style="margin:0 10px 0 10px;height:35px;" >"读取数据库"</button></form><form class="row" on:submit=del_selected_pdt><button type="submit" style="margin:10px 5px 10px 5px;" id="del-button" style="margin:0 10px 0 10px;height:35px;" >"删除选中项"</button></form></div></div><div><h1>"Plotters in Tauri + Leptos"</h1><form id="img_png" on:submit=plot_image><button type="submit">"Generate PNG Image"</button><p></p><div id="img_div"><imgsrc=""width="600"/></div></form></div></main>}
}
需要注意的是invoke调用,存在两种形式:一种被调用后台tauri命令没有参数,使用invoke_without_args("cmd"),一种是被调用后台tauri命令有参数,使用invoke("cmd", args_js),其中args_js是被序列化处理的自定义结构变量,结构化变量的键值就是tauri调用命令的参数值,且键值不能带下划线"_",tauri后台调用命令的参数名必须键值保持一致。
譬如前端定义的删除选中项的命令del_selected_pdt,调用的是tauri后台的del_selected_pdt命令,其要传递的参数是一个i64的数列,在后台定义del_selected_pdt命令时,其参数名为productlist,具体代码如下:
#[tauri::command]
async fn del_selected_pdt(state: tauri::State<'_, DbState>, productlist:Vec<i64>) -> Result<String, String> {// 参数名productlist必须与前端定义的结构变量SelectedPdtArgs的键值一致let db = &state.db;// 处理空数组的情况if productlist.is_empty() {return Err("删除失败:未提供有效的产品ID".into());}// 生成动态占位符(根据数组长度生成 ?, ?, ?)let placeholders = vec!["?"; productlist.len()].join(", ");let query_str = format!("DELETE FROM products WHERE pdt_id IN ({})",placeholders);// 构建查询并绑定参数let mut query = sqlx::query(&query_str);for id in &productlist {query = query.bind(id);}// 执行删除操作let result = query.execute(db).await.map_err(|e| format!("删除失败: {}", e))?;// 检查实际删除的行数if result.rows_affected() == 0 {return Err("删除失败:未找到匹配的产品".into());}Ok(format!("成功删除 {} 条数据!", result.rows_affected()))}
这样,Leptos前端在自定义结构变量时,键值也必须一致,为productlist,代码如下:
#[derive(Serialize, Deserialize)]
struct SelectedPdtArgs {productlist: Vec<i64>,
}
此处只传递一个参数,所以结构变量只有一个元素,传递几个参数值,结构变量就有几个元素。然后在invoke调用时,对包含所有传递参数的结构变量进行序列化。
let del_selected_pdt = move|ev:SubmitEvent| {ev.prevent_default();spawn_local(async move { //使用Leptos的spawn_local创建一个本地线程(local_thread)Future, 提供一个异步move闭包。let args = SelectedPdtArgs{productlist:selected_items.get_untracked(),};let args_js = serde_wasm_bindgen::to_value(&args).unwrap(); //参数序列化let new_msg = invoke("del_selected_pdt", args_js).await.as_string().unwrap();set_sql_error.set(new_msg);set_selected_items.set(Vec::<i64>::new());// 删除完成后触发刷新操作receive_pdt_db(ev.clone()); });};
二、后台tauri程序
后台tauri程序主要是定义了前端leptos需要调用的命令。具体代码如下:
use full_palette::PURPLE;
use futures::TryStreamExt;
use plotters::prelude::*;
use std::path::Path;
use sqlx::{migrate::MigrateDatabase, prelude::FromRow, sqlite::SqlitePoolOptions, Pool, Sqlite};
use tauri::{menu::{CheckMenuItem, Menu, MenuItem, Submenu}, App, Emitter, Listener, Manager, WebviewWindowBuilder};
use serde::{Deserialize, Serialize};
type Db = Pool<Sqlite>;
use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgba, DynamicImage, RgbImage};
use image::codecs::png::PngEncoder; // 引入 PngEncoder
use std::process::Command;
use std::env;struct DbState {db: Db,
}async fn setup_db(app: &App) -> Db {let mut path = app.path().app_data_dir().expect("获取程序数据文件夹路径失败!");match std::fs::create_dir_all(path.clone()) {Ok(_) => {}Err(err) => {panic!("创建文件夹错误:{}", err);}};//C:\Users\<user_name>\AppData\Roaming\com.mynewapp.app\db.sqlite path.push("db.sqlite");Sqlite::create_database(format!("sqlite:{}", path.to_str().expect("文件夹路径不能为空!")).as_str(),).await.expect("创建数据库失败!");let db = SqlitePoolOptions::new().connect(path.to_str().unwrap()).await.unwrap();//创建迁移文件位于./migrations/文件夹下 //cd src-tauri//sqlx migrate add create_users_tablesqlx::migrate!("./migrations/").run(&db).await.unwrap();db
}#[derive(Serialize, Deserialize)]
struct Product {pdt_name:String,pdt_si:f64,pdt_al:f64,pdt_ca:f64,pdt_mg:f64,pdt_fe:f64,pdt_ti:f64,pdt_ka:f64,pdt_na:f64,pdt_mn:f64,pdt_date:String,
}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct Pdt {pdt_id:i64, //sqlx 会将 SQLite 的 INTEGER 类型映射为 i64(64 位有符号整数)pdt_name:String,pdt_si:f64,pdt_al:f64,pdt_ca:f64,pdt_mg:f64,pdt_fe:f64,pdt_ti:f64,pdt_ka:f64,pdt_na:f64,pdt_mn:f64,pdt_date:String,
}#[tauri::command]
async fn send_pdt_db(state: tauri::State<'_, DbState>) -> Result<Vec<Pdt>, String> {let db = &state.db;let query_result:Vec<Pdt> = sqlx::query_as::<_, Pdt>( //查询数据以特定的格式输出"SELECT * FROM products").fetch(db).try_collect().await.unwrap();Ok(query_result)
}#[tauri::command]
async fn write_pdt_db(state: tauri::State<'_, DbState>, product:Product) -> Result<String, String> {let db = &state.db;sqlx::query("INSERT INTO products (pdt_name, pdt_si, pdt_al, pdt_ca, pdt_mg, pdt_fe, pdt_ti, pdt_ka, pdt_na, pdt_mn, pdt_date) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)").bind(product.pdt_name).bind(product.pdt_si).bind(product.pdt_al).bind(product.pdt_ca).bind(product.pdt_mg).bind(product.pdt_fe).bind(product.pdt_ti).bind(product.pdt_ka).bind(product.pdt_na).bind(product.pdt_mn).bind(product.pdt_date).execute(db).await.map_err(|e| format!("数据库插入项目错误: {}", e))?;Ok(String::from("插入数据成功!"))
}#[tauri::command]
async fn del_selected_pdt(state: tauri::State<'_, DbState>, productlist:Vec<i64>) -> Result<String, String> {// 参数名productlist必须与前端定义的结构变量SelectedPdtArgs的键值一致let db = &state.db;// 处理空数组的情况if productlist.is_empty() {return Err("删除失败:未提供有效的产品ID".into());}// 生成动态占位符(根据数组长度生成 ?, ?, ?)let placeholders = vec!["?"; productlist.len()].join(", ");let query_str = format!("DELETE FROM products WHERE pdt_id IN ({})",placeholders);// 构建查询并绑定参数let mut query = sqlx::query(&query_str);for id in &productlist {query = query.bind(id);}// 执行删除操作let result = query.execute(db).await.map_err(|e| format!("删除失败: {}", e))?;// 检查实际删除的行数if result.rows_affected() == 0 {return Err("删除失败:未找到匹配的产品".into());}Ok(format!("成功删除 {} 条数据!", result.rows_affected()))}use base64::engine::general_purpose::STANDARD;
use base64::Engine;// 生成图表并返回 Base64 编码的 PNG 图片
#[tauri::command]
async fn generate_plot() -> Result<String, String> {// 创建一个缓冲区,大小为 800x600 的 RGBA 图像let mut buffer = vec![0; 800 * 600 * 3]; // 800x600 图像,每个像素 3 字节(RGB){// 使用缓冲区创建 BitMapBackendlet root = BitMapBackend::with_buffer(&mut buffer, (800, 600)).into_drawing_area();root.fill(&WHITE).map_err(|e| e.to_string())?;// 定义绘图区域let mut chart = ChartBuilder::on(&root).caption("Sine Curve", ("sans-serif", 50).into_font()).build_cartesian_2d(-10.0..10.0, -1.5..1.5) // X 轴范围:-10 到 10,Y 轴范围:-1.5 到 1.5.map_err(|e| e.to_string())?;// 绘制正弦曲线chart.draw_series(LineSeries::new((-100..=100).map(|x| {let x_val = x as f64 * 0.1; // 将 x 转换为浮点数(x_val, x_val.sin()) // 计算正弦值}),&RED, // 使用红色绘制曲线)).map_err(|e| e.to_string())?;// 将图表写入缓冲区root.present().map_err(|e| e.to_string())?;} // 这里 `root` 离开作用域,释放对 `buffer` 的可变借用// 将 RGB 数据转换为 RGBA 数据(添加 Alpha 通道)let mut rgba_buffer = Vec::with_capacity(800 * 600 * 4);for pixel in buffer.chunks(3) {// 判断是否为背景色(RGB 值为 (255, 255, 255))let is_background = pixel[0] == 255 && pixel[1] == 255 && pixel[2] == 255;// 设置 Alpha 通道的值let alpha = if is_background {0 // 背景部分完全透明} else {255 // 其他部分完全不透明};rgba_buffer.extend_from_slice(&[pixel[0], pixel[1], pixel[2], alpha]); // 添加 Alpha 通道}// 将缓冲区的 RGBA 数据转换为 PNG 格式let image_buffer: ImageBuffer<Rgba<u8>, _> =ImageBuffer::from_raw(800, 600, rgba_buffer).ok_or("Failed to create image buffer")?;// 直接保存图片,检查是否乱码//image_buffer.save("output.png").map_err(|e| e.to_string())?;// 将 PNG 数据编码为 Base64let mut png_data = Vec::new();let encoder = PngEncoder::new(&mut png_data);encoder.write_image(&image_buffer.to_vec(),800,600,ExtendedColorType::Rgba8,).map_err(|e| e.to_string())?;// 将图片数据转换为 Base64 编码的字符串let base64_data = STANDARD.encode(&png_data);//use std::fs::File;//use std::io::Write;// 创建或打开文件//let file_path = "output.txt"; // 输出文件路径//let mut file = File::create(file_path).unwrap();// 将 base64_data 写入文件//file.write_all(base64_data.as_bytes()).unwrap();// 返回 Base64 编码的图片数据Ok(format!("data:image/png;base64,{}", base64_data))
}mod tray; //导入tray.rs模块
mod mymenu; //导入mynemu.rs模块
use mymenu::{create_menu, handle_menu_event};#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {tauri::Builder::default().plugin(tauri_plugin_opener::init()).invoke_handler(tauri::generate_handler![greet, get_db_value, send_pdt_db,del_last_pdt,del_selected_pdt,generate_plot]).menu(|app|{create_menu(app)}).setup(|app| {let main_window = app.get_webview_window("main").unwrap();main_window.on_menu_event(move |window, event| handle_menu_event(window, event));#[cfg(all(desktop))]{let handle = app.handle();tray::create_tray(handle)?; //设置app系统托盘}tauri::async_runtime::block_on(async move {let db = setup_db(&app).await; //setup_db(&app:&mut App)返回读写的数据库对象app.manage(DbState { db }); //通过app.manage(DbState{db})把数据库对象传递给state:tauri::State<'_, DbState>});Ok(())}).run(tauri::generate_context!()).expect("运行Tauri程序的时候出错!");
}
至此基本实现数据库的写入(产品化学成分录入),内容展示(产品成分清单展示)和删除选中数据的功能。
相关文章:
Tauri 2.3.1+Leptos 0.7.8开发桌面应用--Sqlite数据库的写入、展示和选择删除
在前期工作的基础上(Tauri2Leptos开发桌面应用--Sqlite数据库操作_tauri sqlite-CSDN博客),尝试制作产品化学成分录入界面,并展示数据库内容,删除选中的数据。具体效果如下: 一、前端Leptos程序 前端程序主…...
关于 Java 预先编译(AOT)技术的详细说明,涵盖 GraalVM 的配置、Spring Boot 3.x 的集成、使用示例及优缺点对比
以下是关于 Java 预先编译(AOT)技术的详细说明,涵盖 GraalVM 的配置、Spring Boot 3.x 的集成、使用示例及优缺点对比: 1. 预先编译(AOT)技术详解 1.1 核心概念 AOT(Ahead-of-Time)…...
《车辆人机工程-》实验报告
汽车驾驶操纵实验 汽车操纵装置有哪几种,各有什么特点 汽车操纵装置是驾驶员直接控制车辆行驶状态的关键部件,主要包括以下几种,其特点如下: 一、方向盘(转向操纵装置) 作用:控制车辆行驶方向…...
使用多进程和 Socket 接收解析数据并推送到 Kafka 的高性能架构
使用多进程和 Socket 接收解析数据并推送到 Kafka 的高性能架构 在现代应用程序中,实时数据处理和高并发性能是至关重要的。本文将介绍如何使用 Python 的多进程和 Socket 技术来接收和解析数据,并将处理后的数据推送到 Kafka,从而实现高效的…...
Redis 哨兵模式 搭建
1 . 哨兵模式拓扑 与 简介 本文介绍如何搭建 单主双从 多哨兵模式的搭建 哨兵有12个作用 。通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。 当哨兵监测到master宕机,会自动将slave切换成master,然后通过…...
【网络安全 | 项目开发】Web 安全响应头扫描器(提升网站安全性)
原创项目,未经许可,不得转载。 文章目录 项目简介工作流程示例输出技术栈项目代码使用说明项目简介 安全响应头是防止常见 Web 攻击(如点击劫持、跨站脚本攻击等)的有效防线,因此合理的配置这些头部信息对任何网站的安全至关重要。 Web 安全响应头扫描器(Security Head…...
构建灵活的接口抽象层:支持多种后端数据存取的实战指南
构建灵活的接口抽象层:支持多种后端数据存取的实战指南 引言 在现代软件开发中,数据存取成为业务逻辑的核心组成部分。然而,由于后端数据存储方式的多样性(如关系型数据库、NoSQL数据库和文件存储),如何设计一套能够适配多种后端数据存取的接口抽象层,成为技术团队关注…...
计算机的发展及应用
一、计算机的发展历程 计算机的发展经历了从机械计算到电子计算的跨越,其核心驱动力是 硬件技术革新 和 体系结构演进,大致可分为以下阶段: 1. 前电子计算机时代(19世纪-20世纪40年代) 机械计算装置: 16…...
深入理解linux操作系统---第4讲 用户、组和密码管理
4.1 UNIX系统的用户和组 4.1.1 用户与UID UID定义:用户身份唯一标识符,16位或32位整数,范围0-65535。系统用户UID为0(root)、1-999(系统服务),普通用户从1000开始分配特殊UID&…...
【NLP】18. Encoder 和 Decoder
1. Encoder 和 Decoder 概述 在序列到序列(sequence-to-sequence,简称 seq2seq)的模型中,整个系统通常分为两大部分:Encoder(编码器)和 Decoder(解码器)。 Encoder&…...
Npfs!NpFsdCreate函数分析之从NpCreateClientEnd函数分析到Npfs!NpSetConnectedPipeState
第一部分: 1: kd> g Breakpoint 5 hit Npfs!NpFsdCreate: baaecba6 55 push ebp 1: kd> kc # 00 Npfs!NpFsdCreate 01 nt!IofCallDriver 02 nt!IopParseDevice 03 nt!ObpLookupObjectName 04 nt!ObOpenObjectByName 05 nt!IopCreateFile 06…...
基于PySide6与pycatia的CATIA绘图比例智能调节工具开发全解析
引言:工程图纸自动化处理的技术革新 在机械设计领域,CATIA图纸的比例调整是高频且重复性极强的操作。传统手动调整方式效率低下且易出错。本文基于PySide6+pycatia技术栈,提出一种支持智能比例匹配、实时视图控制、异常自处理的图纸批处理方案,其核心突破体现在: 操作效…...
STM32硬件IIC+DMA驱动OLED显示——释放CPU资源,提升实时性
目录 前言 一、软件IIC与硬件IIC 1、软件IIC 2、硬件IIC 二、STM32CubeMX配置KEIL配置 三、OLED驱动示例 1、0.96寸OLED 2、OLED驱动程序 3、运用示例 4、效果展示 总结 前言 0.96寸OLED屏是一个很常见的显示模块,其驱动方式在用采IIC通讯时,常用软件IIC…...
Spring Bean的创建过程与三级缓存的关系详解
以下以 Bean A 和 Bean B 互相依赖为例,结合源码和流程图,详细说明 Bean 的创建过程与三级缓存的交互。 1. Bean 的完整生命周期(简化版) #mermaid-svg-uwqaB5dgOFDQ97Yd {font-family:"trebuchet ms",verdana,arial,sa…...
IDEA 调用 Generate 生成 Getter/Setter 快捷键
快捷键不会用? 快捷键:AltInsert 全选键:CtrlA IDEA 调用 Generate 生成 Getter/Setter 快捷键 - 爱吃西瓜的番茄酱 - 博客园...
泛型的二三事
泛型(Generics)是Java语言的一个重要特性,它允许在定义类、接口和方法时使用类型参数(Type Parameters),从而实现类型安全的代码重用。泛型在Java 5中被引入,极大地增强了代码的灵活性和安全性。…...
编程思想——FP、OOP、FRP、AOP、IOC、DI、MVC、DTO、DAO
个人简介 👀个人主页: 前端杂货铺 🙋♂️学习方向: 主攻前端方向,正逐渐往全干发展 📃个人状态: 研发工程师,现效力于中国工业软件事业 🚀人生格言: 积跬步…...
实现一个动态验证码生成器:Canvas与JavaScript的完美结合
验证码(CAPTCHA)是现代网站中常见的安全机制,用于区分人类用户和自动化程序。本文将详细介绍如何使用HTML5 Canvas和JavaScript创建一个美观且功能完整的验证码生成器。 一、核心功能概述 这个验证码生成器具有以下特点: 随机生…...
python中 “with” 关键字的取舍问题
自动管理资源(自动关闭文件) 当你使用 with 打开文件时,文件会在 with 代码块结束后自动关闭,无论是否发生异常。这意味着你不需要显式地调用 f.close() 来关闭文件 示例: with open("words.txt", "r…...
【区块链安全 | 第三十九篇】合约审计之delegatecall(一)
文章目录 外部调用函数calldelegatecallcall 与 delegatecall 的区别示例部署后初始状态调用B.testCall()函数调用B.testDelegatecall()函数区别总结漏洞代码代码审计攻击代码攻击原理解析攻击流程修复建议审计思路外部调用函数 在 Solidity 中,常见的两种底层外部函数调用方…...
Nginx部署spa单页面的小bug
没部署过,都是给后端干的,自己尝试部署了一个下午终于成功了 我遇到的最大的bug是进入后只有首页正常显示 其他页面全是404,于是问问问才知道,需要这个 location / { try_files $uri $uri/ /index.html; } 让…...
linux多线(进)程编程——(6)共享内存
前言 话说进程君的儿子经过父亲点播后就开始闭关,它想要开发出一种全新的传音神通。他想,如果两个人的大脑生长到了一起,那不是就可以直接知道对方在想什么了吗,这样不是可以避免通过语言传递照成的浪费吗? 下面就是它…...
【愚公系列】《Python网络爬虫从入门到精通》050-搭建 Scrapy 爬虫框架
🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…...
信息安全管理与评估2021年国赛正式卷答案截图以及十套国赛卷
2021年全国职业院校技能大赛高职组 “信息安全管理与评估”赛项 任务书1 赛项时间 共计X小时。 赛项信息 赛项内容 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 第一阶段 平台搭建与安全设备配置防护 任务1 网络平台搭建 任务2 网络安全设备配置与防护 第二…...
讲解贪心算法
贪心算法是一种常用的算法思想,其在解决问题时每一步都做出在当前状态下看起来最优的选择,从而希望最终能够获得全局最优解。C作为一种流行的编程语言,可以很好地应用于贪心算法的实现。下面我们来讲一篇关于C贪心算法的文章。 目录 贪心算法…...
高并发秒杀系统设计:关键技术解析与典型陷阱规避
电商、在线票务等众多互联网业务场景中,高并发秒杀活动屡见不鲜。这类活动往往在短时间内会涌入海量的用户请求,对系统架构的性能、稳定性和可用性提出了极高的挑战。曾经,高并发秒杀架构设计让许多开发者望而生畏,然而࿰…...
微信小程序实战案例 - 餐馆点餐系统 阶段 2 – 购物车
阶段 2 – 购物车(超详细版) 目标 把“加入购物车”做成 全局状态,任何页面都能读写在本地 持久化(关闭小程序后购物车仍在)新建 购物车页:数量增减、总价实时计算、去结算入口打 Git Tag v2.0‑cart 1. …...
Qt 元对象系统探秘:从 Q_OBJECT 到反射编程的魔法之旅
背景说明:Qt 背后的「魔法引擎」 如果你曾用 Qt 写过信号槽,或是在设计器里拖过控件改属性,一定对这个框架的“动态性”印象深刻: 无需手动调用,信号能自动连接到槽函数;无需编译重启,界面上修…...
sql 向Java的映射
优化建议,可以在SQL中控制它的类型 在 MyBatis 中,如果返回值类型设置为 java.util.Map,默认情况下可以返回 多行多列的数据...
Visual Studio未能加载相应的Package包弹窗报错
环境介绍: visulal studio 2019 问题描述: 起因:安装vs扩展插件后,重新打开Visual Studio,报了一些列如下的弹窗错误,即使选择不继续显示该错误,再次打开后任然报错; 解决思路&am…...
