在前期工作的基础上(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类似于API
set_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;">
<input
type="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::Uint8Array
let uint8_array = Uint8Array::from(&binary_data[..]);
// 创建 Blob
let 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");
// 生成图片 URL
let 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">
<img
src=""
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_table
sqlx::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)
{
// 使用缓冲区创建 BitMapBackend
let 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 数据编码为 Base64
let 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程序的时候出错!");
}
至此基本实现数据库的写入(产品化学成分录入),内容展示(产品成分清单展示)和删除选中数据的功能。