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. 删除数据后,还要刷新数据的展示

具体代码如下:

rust 复制代码
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,具体代码如下:

rust 复制代码
#[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,代码如下:

rust 复制代码
#[derive(Serialize, Deserialize)]
struct SelectedPdtArgs {
    productlist: Vec<i64>, 
}

此处只传递一个参数,所以结构变量只有一个元素,传递几个参数值,结构变量就有几个元素。然后在invoke调用时,对包含所有传递参数的结构变量进行序列化。

rust 复制代码
    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()); 
        });

    };  

展示数据库时,在每一条数据前面插入了一个复选框,使用value=pdt_id.to_string()传递每条数据的键值pdt_id,通过复选框prop:checked和on:change的协作,实现选中项的实时更新。prop:checked和on:change的协作机制如下:

用户操作 → 更新 target.checked → 触发on:change事件check_change → 更新状态 → prop:checked 驱动视图更新。

然后在触发事件函数check_change中,根据target.checked(选中为true,没选中为false)对selected_items(选中数据的键值组成的数列)信号进行实时更新:items.push添加勾选(添加键值),items.retain取消勾选(去除键值)。

二、后台tauri程序

后台tauri程序主要是定义了前端leptos需要调用的命令。具体代码如下:

rust 复制代码
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程序的时候出错!");
}

至此基本实现数据库的写入(产品化学成分录入),内容展示(产品成分清单展示)和删除选中数据的功能。

相关推荐
冉冰学姐5 小时前
SSM装修服务网站5ff59(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·ssm 框架·装修服务网站
库库8395 小时前
Redis分布式锁、Redisson及Redis红锁知识点总结
数据库·redis·分布式
沧澜sincerely6 小时前
Redis 缓存模式与注解缓存
数据库·redis·缓存
Elastic 中国社区官方博客6 小时前
Elasticsearch 推理 API 增加了开放的可定制服务
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
nzxzn7 小时前
MYSQL第二次作业
数据库·mysql
核桃杏仁粉7 小时前
excel拼接数据库
数据库·oracle·excel
TiAmo zhang7 小时前
SQL Server 2019实验 │ 设计数据库的完整性
数据库·sqlserver
冻咸鱼8 小时前
MySQL的CRUD
数据库·mysql·oracle
Funny Valentine-js8 小时前
团队作业——概要设计和数据库设计
数据库
CodeJourney.8 小时前
SQL提数与数据分析指南
数据库·信息可视化·数据分析