企业网站名是什么意思快速搭建网站框架
在前期工作的基础上(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程序的时候出错!");
}
至此基本实现数据库的写入(产品化学成分录入),内容展示(产品成分清单展示)和删除选中数据的功能。