Tauri 2.3.1+Leptos 0.7.8开发桌面应用--Sqlite数据库的写入、展示和选择删除

news2025/4/15 23:21:25

 在前期工作的基础上(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程序的时候出错!");
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2334713.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《车辆人机工程-》实验报告

汽车驾驶操纵实验 汽车操纵装置有哪几种&#xff0c;各有什么特点 汽车操纵装置是驾驶员直接控制车辆行驶状态的关键部件&#xff0c;主要包括以下几种&#xff0c;其特点如下&#xff1a; 一、方向盘&#xff08;转向操纵装置&#xff09; 作用&#xff1a;控制车辆行驶方向…

使用多进程和 Socket 接收解析数据并推送到 Kafka 的高性能架构

使用多进程和 Socket 接收解析数据并推送到 Kafka 的高性能架构 在现代应用程序中&#xff0c;实时数据处理和高并发性能是至关重要的。本文将介绍如何使用 Python 的多进程和 Socket 技术来接收和解析数据&#xff0c;并将处理后的数据推送到 Kafka&#xff0c;从而实现高效的…

Redis 哨兵模式 搭建

1 . 哨兵模式拓扑 与 简介 本文介绍如何搭建 单主双从 多哨兵模式的搭建 哨兵有12个作用 。通过发送命令&#xff0c;让Redis服务器返回监控其运行状态&#xff0c;包括主服务器和从服务器。 当哨兵监测到master宕机&#xff0c;会自动将slave切换成master&#xff0c;然后通过…

【网络安全 | 项目开发】Web 安全响应头扫描器(提升网站安全性)

原创项目,未经许可,不得转载。 文章目录 项目简介工作流程示例输出技术栈项目代码使用说明项目简介 安全响应头是防止常见 Web 攻击(如点击劫持、跨站脚本攻击等)的有效防线,因此合理的配置这些头部信息对任何网站的安全至关重要。 Web 安全响应头扫描器(Security Head…

基于PySide6与pycatia的CATIA绘图比例智能调节工具开发全解析

引言:工程图纸自动化处理的技术革新 在机械设计领域,CATIA图纸的比例调整是高频且重复性极强的操作。传统手动调整方式效率低下且易出错。本文基于PySide6+pycatia技术栈,提出一种支持智能比例匹配、实时视图控制、异常自处理的图纸批处理方案,其核心突破体现在: ​操作效…

STM32硬件IIC+DMA驱动OLED显示——释放CPU资源,提升实时性

目录 前言 一、软件IIC与硬件IIC 1、软件IIC 2、硬件IIC 二、STM32CubeMX配置KEIL配置 三、OLED驱动示例 1、0.96寸OLED 2、OLED驱动程序 3、运用示例 4、效果展示 总结 前言 0.96寸OLED屏是一个很常见的显示模块&#xff0c;其驱动方式在用采IIC通讯时&#xff0c;常用软件IIC…

泛型的二三事

泛型&#xff08;Generics&#xff09;是Java语言的一个重要特性&#xff0c;它允许在定义类、接口和方法时使用类型参数&#xff08;Type Parameters&#xff09;&#xff0c;从而实现类型安全的代码重用。泛型在Java 5中被引入&#xff0c;极大地增强了代码的灵活性和安全性。…

编程思想——FP、OOP、FRP、AOP、IOC、DI、MVC、DTO、DAO

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

【区块链安全 | 第三十九篇】合约审计之delegatecall(一)

文章目录 外部调用函数calldelegatecallcall 与 delegatecall 的区别示例部署后初始状态调用B.testCall()函数调用B.testDelegatecall()函数区别总结漏洞代码代码审计攻击代码攻击原理解析攻击流程修复建议审计思路外部调用函数 在 Solidity 中,常见的两种底层外部函数调用方…

linux多线(进)程编程——(6)共享内存

前言 话说进程君的儿子经过父亲点播后就开始闭关&#xff0c;它想要开发出一种全新的传音神通。他想&#xff0c;如果两个人的大脑生长到了一起&#xff0c;那不是就可以直接知道对方在想什么了吗&#xff0c;这样不是可以避免通过语言传递照成的浪费吗&#xff1f; 下面就是它…

信息安全管理与评估2021年国赛正式卷答案截图以及十套国赛卷

2021年全国职业院校技能大赛高职组 “信息安全管理与评估”赛项 任务书1 赛项时间 共计X小时。 赛项信息 赛项内容 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 第一阶段 平台搭建与安全设备配置防护 任务1 网络平台搭建 任务2 网络安全设备配置与防护 第二…

高并发秒杀系统设计:关键技术解析与典型陷阱规避

电商、在线票务等众多互联网业务场景中&#xff0c;高并发秒杀活动屡见不鲜。这类活动往往在短时间内会涌入海量的用户请求&#xff0c;对系统架构的性能、稳定性和可用性提出了极高的挑战。曾经&#xff0c;高并发秒杀架构设计让许多开发者望而生畏&#xff0c;然而&#xff0…

微信小程序实战案例 - 餐馆点餐系统 阶段 2 – 购物车

阶段 2 – 购物车&#xff08;超详细版&#xff09; 目标 把“加入购物车”做成 全局状态&#xff0c;任何页面都能读写在本地 持久化&#xff08;关闭小程序后购物车仍在&#xff09;新建 购物车页&#xff1a;数量增减、总价实时计算、去结算入口打 Git Tag v2.0‑cart 1. …

sql 向Java的映射

优化建议&#xff0c;可以在SQL中控制它的类型 在 MyBatis 中&#xff0c;如果返回值类型设置为 java.util.Map&#xff0c;默认情况下可以返回 多行多列的数据

Visual Studio未能加载相应的Package包弹窗报错

环境介绍&#xff1a; visulal studio 2019 问题描述&#xff1a; 起因&#xff1a;安装vs扩展插件后&#xff0c;重新打开Visual Studio&#xff0c;报了一些列如下的弹窗错误&#xff0c;即使选择不继续显示该错误&#xff0c;再次打开后任然报错&#xff1b; 解决思路&am…

【HD-RK3576-PI】Docker搭建与使用

硬件&#xff1a;HD-RK3576-PI 软件&#xff1a;Linux6.1Ubuntu22.04 1.Docker 简介 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言开发&#xff0c;遵循 Apache 2.0 协议。它可以让开发者将应用程序及其依赖项打包到一个轻量级、可移植的容器中&#xff0c;并在任…

【websocket】使用案例( ​JSR 356 标准)

目录 一、JSR 356方式&#xff1a;简单示例 1、引入依赖 2、注册端点扫描器 3、编写通过注解处理生命周期和消息 4、细节解读 5、总结 二、聊天室案例 方案流程 1、引入依赖 2、注册端点扫描器 3、编写一个配置类&#xff0c;读取httpsession 4、编写通过注解处理生…

IS-IS中特殊字段——OL过载

文章目录 OL 过载位 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Datacom专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2025年04月13日20点12分 OL 过载位 路由过载 使用 IS-IS 的过载标记来标识过载状态 对设备设置过载标记后&#xff…

【时频谱分析】快速谱峭度

算法配置页面&#xff0c;也可以一键导出结果数据 报表自定义绘制 获取和下载【PHM学习软件PHM源码】的方式 获取方式&#xff1a;Docshttps://jcn362s9p4t8.feishu.cn/wiki/A0NXwPxY3ie1cGkOy08cru6vnvc

Spring Boot 支持的内嵌服务器(Tomcat、Jetty、Undertow、Netty(用于 WebFlux 响应式应用))详解

Spring Boot 支持的内嵌服务器详解 1. 支持的内嵌服务器 Spring Boot 默认支持以下内嵌服务器&#xff1a; Tomcat&#xff08;默认&#xff09;JettyUndertowNetty&#xff08;用于 WebFlux 响应式应用&#xff09; 2. 各服务器使用示例 (1) Tomcat&#xff08;默认&#xf…