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类似于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()); 
        });

    };  

展示数据库时,在每一条数据前面插入了一个复选框,使用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需要调用的命令。具体代码如下:

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值