Tauri(2.5.1)+Leptos(0.7.8)開發桌面應用--簡單的工作進度管理

在前期工作(Tauri(2.5.1)+Leptos(0.7.8)開發桌面應用--程序啟動界面_tauri 程序啟動畫面-CSDN博客)的基礎上繼續進行自用桌面小程序的開發。為了方便管理工作進度,決定自己造輪子。效果如下:

工作進度管理系統

在編寫程序過程中,使用了Roo?Code綁定的DeepSeek API 輔助編程,能力確實很強大。

??1. 數據庫結構

數據操作詳見:Tauri2+Leptos開發桌面應用--Sqlite數據庫操作_tauri sqlite-CSDN博客

cd src-tauri
sqlx migrate add create_works_table

大致的數據庫結構如下圖所示:?

?

?打開數據遷移文件:src-tauri\migrations\xxxxxx_create_works_table.sql文件,修改內容如下:

-- Add migration script here--強制啟用外鍵約束,此語句確保數據庫強制檢查外鍵關系,需在每次數據庫連接時重新執行
PRAGMA foreign_keys = ON;-- 部門表
CREATE TABLE IF NOT EXISTS departments (id INTEGER PRIMARY KEY AUTOINCREMENT,   --自增主鍵name TEXT NOT NULL UNIQUE               --唯一部門名稱
);-- 人員表
CREATE TABLE IF NOT EXISTS personnel (id INTEGER PRIMARY KEY AUTOINCREMENT,full_name TEXT NOT NULL,department_id INTEGER NOT NULL,         --所屬部門IDFOREIGN KEY (department_id) REFERENCES departments(id)  --人員必須屬于已存在的部門(department_id 外鍵約束)
);-- 工作類型表
CREATE TABLE IF NOT EXISTS work_types (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL UNIQUE
);-- 工作主表(核心字段)
CREATE TABLE IF NOT EXISTS works (id INTEGER PRIMARY KEY AUTOINCREMENT,subject TEXT NOT NULL,work_content TEXT NOT NULL,start_date DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,  -- SQLite 無原生 DATETIME 類型,實際上仍存儲為 TEXT 類型,但使用語義化類型名稱work_type_id INTEGER NOT NULL,  --關聯工作類型is_completed INTEGER NOT NULL DEFAULT 0 CHECK (is_completed IN (0, 1)),     --is_completed:完成狀態標識(0=未完成,1=已完成),CHECK 約束確保只能存儲 0 或 1FOREIGN KEY (work_type_id) REFERENCES work_types(id)
);-- 工作-部門關聯表(責任部門),多對多關系:一個工作可關聯多個部門,一個部門可參與多個工作
CREATE TABLE IF NOT EXISTS work_departments (work_id INTEGER NOT NULL,department_id INTEGER NOT NULL,PRIMARY KEY (work_id, department_id),FOREIGN KEY (work_id) REFERENCES works(id) ON DELETE CASCADE,   --當刪除工作(work_id外鍵關聯)時,自動刪除關聯記錄FOREIGN KEY (department_id) REFERENCES departments(id)
);-- 工作-人員關聯表(責任人),多對對關系
CREATE TABLE IF NOT EXISTS work_personnel (work_id INTEGER NOT NULL,personnel_id INTEGER NOT NULL,is_main_responsible INTEGER NOT NULL DEFAULT 0 CHECK (is_main_responsible IN (0, 1)),   --is_main_responsible:主負責人標識(0=普通負責人,1=主負責人)PRIMARY KEY (work_id, personnel_id),FOREIGN KEY (work_id) REFERENCES works(id) ON DELETE CASCADE,   ----當刪除工作(work_id外鍵關聯)時,自動刪除關聯記錄FOREIGN KEY (personnel_id) REFERENCES personnel(id)
);-- 進度記錄表(直接關聯工作)
CREATE TABLE IF NOT EXISTS progress_records (id INTEGER PRIMARY KEY AUTOINCREMENT,work_id INTEGER NOT NULL,progress_detail TEXT NOT NULL,recorder_id INTEGER NOT NULL,record_date DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,FOREIGN KEY (work_id) REFERENCES works(id) ON DELETE CASCADE,FOREIGN KEY (recorder_id) REFERENCES personnel(id)
);-- 索引優化
CREATE INDEX idx_works_completion ON works(is_completed);   --idx_works_completion:加速按完成狀態篩選工作
CREATE INDEX idx_work_person_main ON work_personnel(is_main_responsible); --idx_work_person_main:快速查找主負責人
CREATE INDEX idx_progress_timestamp ON progress_records(record_date);   --idx_progress_timestamp:按時間排序進度記錄

2. 前端Leptos設計

根據前面的數據結構,工作進度管理界面要具備以下功能:

1. 新建和刪除工作部門;

2. 每個部門添加和刪除員工;

3. 新建和刪除工作狀態:進行中或已完成;

4. 新建和刪除工作;

5. 每個工作添加或刪除進度記錄,并可改變工作狀態。

前端的src/app.rs文件內容如下:

mod app;use app::*;
use leptos::prelude::*;//打開trunk serve --open 以開始開發您的應用程序。 Trunk 服務器將在文件更改時重新加載您的應用程序,從而使開發相對無縫。fn main() {console_error_panic_hook::set_once();   //瀏覽器中運行 WASM 代碼發生 panic 時可以獲得一個實際的 Rust 堆棧跟蹤,其中包括 Rust 源代碼中的一行。mount_to_body(|| {view! {<App />}})
}

調用app/app.rs文件的App,在其中使用leptos_router實現標簽頁功能,具體文件內容如下:

#[warn(unused_imports)]
use leptos::prelude::*;
use leptos_router::components::{Route, Router, Routes};
use leptos_router::path;
mod acidinput;
mod schedule;use acidinput::*;
use schedule::*;#[component]
pub fn App() -> impl IntoView {view! {<Router><nav><a class="nav" href="/">"工作進度表"</a><a class="nav" href="/acidinput">"產品錄入"</a></nav><main><Routes fallback=|| "Not found.">// / just has an un-nested "Home"<Route path=path!("/") view= || view! {<WorkSchedule />} /><Route path=path!("/acidinput") view=|| view! {<AcidInput />} /></Routes>                </main></Router>}
}

工作進度表的界面設計放在了schedule.rs文件中,文件內容如下:

use leptos::task::spawn_local;
use leptos::*;
use leptos::{ev::SubmitEvent, prelude::*};
use serde::{Deserialize, Serialize};
use leptos::ev::Event;
use wasm_bindgen::prelude::*;
use web_sys;
use serde_wasm_bindgen;
use web_sys::HtmlInputElement;
use leptos::logging::log;
use chrono::{Local};
use web_sys::{HtmlSelectElement};
use std::rc::Rc;#[wasm_bindgen]
extern "C" {#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke, catch)]async fn invoke_without_args(cmd: &str) -> Result<JsValue, JsValue>;#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)]async fn invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;
}#[derive(Serialize, Deserialize, Clone, Debug)]
struct Department {id:i64,name:String,
}#[derive(Serialize, Deserialize)]
struct DepartmentSend {name:String,
}#[derive(Serialize, Deserialize)]
struct DepartmentArgs {department: DepartmentSend,
}#[derive(Serialize, Deserialize)]
struct SelectedDeptArgs {    // 將invoke調用的參數打包成結構變量再通過json傳遞,tauri后臺invoke函數的參數名稱必須根鍵一致(譬如此處的productlist)deptlist: Vec<i64>, // 將Vec<i64>數組包裝為一個包含 `productlist` 鍵的對象,鍵不能帶下劃線"_"
}#[derive(Serialize, Deserialize)]
struct SelectedItemArgs {    // 將invoke調用的參數打包成結構變量再通過json傳遞,tauri后臺invoke函數的參數名稱必須根鍵一致(譬如此處的productlist)selectedlist: Vec<i64>, // 將Vec<i64>數組包裝為一個包含 `selectedlist` 鍵的對象,鍵不能帶下劃線"_"
}#[derive(Serialize, Deserialize, Clone, Debug)]
struct Worktype {id:i64,name:String,
}#[derive(Serialize, Deserialize)]
struct WorktypeSend {name:String,
}#[derive(Serialize, Deserialize)]
struct WorktypeArgs {worktype: WorktypeSend,
}#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct Personnel {id:i64,full_name:String,department_id:i64,
}#[derive(Serialize, Deserialize)]
struct PersonnelSend {full_name:String,department_id:i64,
}#[derive(Serialize, Deserialize)]
struct PersonnelArgs {personnel: PersonnelSend,
}#[derive(Serialize, Deserialize)]
struct WorkArgs {work: WorkSend,
}#[derive(Serialize, Deserialize)]
struct WorkSend {subject: String,work_content: String,start_date: String,work_type_id: i64,is_completed: i64,
}#[derive(Serialize, Deserialize, Clone)]
struct Work {id: i64,subject: String,work_content: String,start_date: String,work_type_id: i64,is_completed: i64,
}#[derive(Serialize, Deserialize)]
struct PersonnelDeptArgs {departmentid: i64,
}#[derive(Serialize, Deserialize)]
struct WorkDeptsArgs {workdepts: Vec<WorkDeptsSend>,
}#[derive(Serialize, Deserialize)]
struct WorkDeptsSend {work_id: i64,department_id: i64,
}#[derive(Serialize, Deserialize)]
struct WorkPersonArgs {workpersonnels: Vec<WorkPersonSend>,
}#[derive(Serialize, Deserialize)]
struct WorkPersonSend {work_id: i64,personnel_id: i64,is_main_responsible: i64
}#[derive(Serialize, Deserialize)]
struct WorkAll {id: i64,subject: String,work_content: String,start_date: String,work_type_id: i64,is_completed: i64,work_departments: Vec<WorkDeptsSend>,work_personnels: Vec<WorkPersonSend>
}#[derive(Serialize, Deserialize)]
struct FetchWorkArgs {workid: i64,
}#[derive(Debug, Serialize, Deserialize)]
struct WorkBack {id: i64,subject: String,work_content: String,start_date: String,work_type_id: i64,is_completed: i64,work_departments: Vec<Department>,work_personnels: Vec<Personnel>,responsile_person: Vec<Personnel>
}#[component]
pub fn WorkSchedule() -> impl IntoView {//定義工作部門名稱及信號let (department_name, set_department_name) = signal(String::new());let (department_error, set_department_error) =  signal(String::new());let (department_content, set_department_content) = signal(view! { <div>{Vec::<View<_>>::new()}</div> });let (selected_depts, set_selected_depts) = signal::<Vec<i64>>(vec![]);let (deptsubmit_error, set_deptsubmit_error) =  signal(String::new());let (deptdb_msg, set_deptdb_msg) =  signal(String::new());let (department_list, set_department_list) =  signal::<Vec<Department>>(vec![]);//定義工作類型名稱及信號let (work_type, set_work_type) = signal(String::new());let (worktype_error, set_worktype_error) =  signal(String::new());let (worktype_content, set_worktype_content) = signal(view! { <div>{Vec::<View<_>>::new()}</div> });let (selected_worktypes, set_selected_worktypes) = signal::<Vec<i64>>(vec![]);let (typesubmit_error, set_typesubmit_error) =  signal(String::new());let (typedb_msg, set_typedb_msg) =  signal(String::new());let (worktype_list, set_worktype_list) = signal::<Vec<Worktype>>(vec![]);//定義人員及信號let (personnel_name, set_personnel_name) = signal(String::new());let (personnel_error, set_personnel_error) =  signal(String::new());let (personnel_deptid, set_personnel_deptid) = signal::<i64>(0);let (personnel_deptid_error, set_personnel_deptid_error) =  signal(String::new());let (personnel_content, set_personnel_content) = signal(view! { <div>{Vec::<View<_>>::new()}</div> });let (selected_personnels, set_selected_personnels) = signal::<Vec<i64>>(vec![]);let (personnelsubmit_error, set_personnelsubmit_error) =  signal(String::new());let (personneldb_msg, set_personneldb_msg) =  signal(String::new());//定義工作及信號let (work_subject, set_work_subject) = signal(String::new());let (subjet_error, set_subject_error) =  signal(String::new());let (work_content, set_work_content) = signal(String::new());let (workcontent_error, set_workcontent_error) =  signal(String::new());let now = Local::now().format("%Y-%m-%dT%H:%M").to_string();let (start_date, set_start_date) = signal(now.clone());let (work_state, set_work_state) = signal::<i64>(0);let (worktype_id, set_worktype_id) = signal::<i64>(0);let (typeid_error, set_typeid_error) = signal(String::new());let (workview_content, set_workview_content) = signal(view! { <div>{Vec::<View<_>>::new()}</div> });let (selected_works, set_selected_works) = signal::<Vec<i64>>(vec![]);let (worksubmit_error, set_worksubmit_error) =  signal(String::new());let (workdb_msg, set_workdb_msg) =  signal(String::new());let (work_depts, set_work_depts) = signal::<Vec<i64>>(vec![]);let (work_personnel, set_work_personnel) = signal::<Vec<Personnel>>(vec![]);let (work_responsible, set_work_responsible) = signal::<Vec<Personnel>>(vec![]);let (personnel_list, set_personnel_list) = signal::<Vec<Personnel>>(vec![]);let (dept_personnel_error, set_dept_personnel_error) =  signal(String::new());let (work_id, set_work_id) = signal::<i64>(0); // 新增work_id信號let (fetch_work_id, set_fetch_work_id) = signal::<i64>(0); // 新增work_id信號let (fetch_works_error, set_fetch_works_error) = signal(String::new());//let (fetch_work_subject, set_fetch_work_subject) = signal(String::new());let (fetch_work_content, set_fetch_work_content) = signal(String::new());let (fetch_work_startdate, set_fetch_work_startdate) = signal(String::new());let (fetch_worktype_id, set_fetch_worktype_id) =  signal::<i64>(0); let (fetch_work_state, set_fetch_work_state) =  signal::<i64>(0); let (fetch_work_list, set_fetch_work_list) =  signal::<Vec<Work>>(vec![]);let (fetch_work_personnels, set_fetch_work_personnels) = signal::<Vec<Personnel>>(vec![]);let (fetch_work_depts, set_fetch_work_depts) =  signal::<Vec<Department>>(vec![]);let (fetch_work_responsile, set_fetch_work_responsile) = signal::<Vec<Personnel>>(vec![]);let (records_date, set_records_date) = signal(now);let (progress_content, set_progress_content) = signal(String::new());let (progress_recorder, set_progress_recorder) = signal::<i64>(0);let (add_record_error, set_add_record_error) = signal(String::new());let (fetch_progress_records, set_fetch_progress_records) = signal::<Vec<ProgressRecord>>(vec![]);#[derive(Serialize, Deserialize, Clone)]struct ProgressRecord {id: i64,progress_detail: String,recorder_id: i64,record_date: String,}#[derive(Serialize, Deserialize)]struct ProgressRecordSend {work_id: i64,progress_detail: String,recorder_id: i64,record_date: String,}#[derive(Serialize, Deserialize)]struct ProgressRecordArgs {progressrecord: ProgressRecordSend,}#[derive(Serialize, Deserialize)]struct SelectedRecord {selectedrecord: i64,}let get_progress_records = move ||{spawn_local(async move {let args = FetchWorkArgs{workid: fetch_work_id.get_untracked()};let args_js = match serde_wasm_bindgen::to_value(&args) {Ok(v) => v,Err(e) => {set_fetch_works_error.set(format!("參數序列化失敗: {}", e));return;}};match invoke("send_progress_record", args_js).await {Ok(result) => {match serde_wasm_bindgen::from_value::<Vec<ProgressRecord>>(result) {Ok(work) => {set_fetch_progress_records.set(work);}Err(e) => {set_fetch_works_error.set(format!("工作數據反序列化失敗: {}", e));}}}Err(e) => {set_fetch_works_error.set(format!("獲取工作詳情失敗: {:?}", e));}}});};let write_progress_records = move |ev: SubmitEvent| {ev.prevent_default();spawn_local(async move {let work_id = fetch_work_id.get_untracked();if work_id == 0 {set_add_record_error.set("請先選擇工作".to_string());return;}let progress_detail = progress_content.get_untracked();if progress_detail.is_empty() {set_add_record_error.set("進度內容不能為空".to_string());return;}let recorder_id = progress_recorder.get_untracked();if recorder_id == 0 {set_add_record_error.set("請選擇記錄人".to_string());return;}let args = ProgressRecordArgs {progressrecord: ProgressRecordSend {work_id,progress_detail,recorder_id,record_date: records_date.get_untracked(),},};let args_js = match serde_wasm_bindgen::to_value(&args) {Ok(v) => v,Err(e) => {set_add_record_error.set(format!("參數序列化失敗: {}", e));return;}};match invoke("write_progress_record", args_js).await {Ok(result) => {if let Some(msg) = result.as_string() {set_add_record_error.set(msg.clone());if msg.contains("SUCCESS") {set_progress_content.set(String::new());// 手動清空contenteditable div的內容if let Some(window) = web_sys::window() {if let Some(document) = window.document() {if let Some(div) = document.get_element_by_id("progress-content-div") {div.set_text_content(Some(""));}}}get_progress_records();}}}Err(e) => {set_fetch_works_error.set(e.as_string().unwrap_or_else(|| format!("命令調用失敗: {:?}", e)));}}});};// 定義名稱長度范圍let min_length = 3;let max_length = 200;//處理復選框事件let check_change_dept = 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_depts.update(|items| {if target.checked() {               //target.checked與prop:checked不一樣, 是瀏覽器 DOM 的實時狀態,用于事件處理items.push(value);} else {items.retain(|&x| x != value);}});};});};let update_string = move|ev:Event, content:String, set_string:WriteSignal<String>, set_error:WriteSignal<String>| {match event_target_value(&ev).parse::<String>(){Ok(name) => {//檢查是否為空if name.is_empty() {set_error.set(format!("{}不能為空!", content));return;};// 檢查長度是否在范圍內if name.len() < min_length {set_error.set(format!("{}長度不能少于 {} 個字符", content, min_length));} else if name.len() > max_length {set_error.set(format!("{}長度不能大于 {} 個字符", content, max_length));}else{set_string.set(name.to_string());set_error.set(String::new());}}Err(_) => {set_error.set("請輸入有效字符串!".to_string());}}};let get_department_db = move |ev: SubmitEvent| {ev.prevent_default();spawn_local(async move {                //使用Leptos的spawn_local創建一個本地線程(local_thread)Future, 提供一個異步move閉包。let dept_js = match invoke_without_args("send_department_db").await {Ok(val) => val,Err(e) => {set_deptdb_msg.set(format!("獲取部門數據失敗: {:?}", e));return;}};let dept_vec: Vec<Department> = match serde_wasm_bindgen::from_value(dept_js) {Ok(vec) => vec,Err(e) => {log!("反序列化部門數據失敗: {:?}", e);set_deptdb_msg.set(format!("反序列化部門數據失敗: {}", e));return;}};// 動態生成包裹在 div 中的視圖let div_views = view! {<div>{dept_vec.into_iter().map(|dept| {let dept_id = dept.id;view! {<div style="margin:5px;width:1500px;"><inputtype="checkbox"name="items"value=dept_id.to_string()prop:checked=move || selected_depts.get().contains(&dept_id)     //Leptos 的狀態綁定,用于確保界面最終與數據同步。on:change=check_change_dept      //用戶操作 → 更新 target.checked → 觸發事件check_change → 更新狀態 → prop:checked 驅動視圖更新。/><span>"部門ID: " {dept_id}",部門名稱: " {dept.name}</span></div>}}).collect_view()}</div>}; // 關鍵的類型擦除;// 轉換為 View 類型并設置//log!("視圖類型: {:?}", std::any::type_name_of_val(&div_views));set_department_content.set(div_views); });};let del_selected_items = move|ev:SubmitEvent, selected_items:ReadSignal<Vec<i64>>,set_selected_items:WriteSignal<Vec<i64>>, cmd_invoke:String, set_error:WriteSignal<String>, refresh:Box<dyn Fn(SubmitEvent)>| {ev.prevent_default();spawn_local(async move {set_error.set(String::new());let args = SelectedItemArgs{selectedlist:selected_items.get_untracked(),};let args_js = serde_wasm_bindgen::to_value(&args).unwrap();let new_msg = match invoke(&cmd_invoke, args_js).await {Ok(val) => val.as_string().unwrap_or_else(|| "未知錯誤".to_string()),Err(e) => format!("調用命令失敗: {:?}", e)};set_error.set(new_msg.clone());set_selected_items.set(Vec::<i64>::new());// 確保刪除操作成功完成后再刷新if new_msg.contains("SUCCESS") {refresh(ev);}});};// 修改 write_dept_sql 中的調用邏輯let write_dept_sql = move |ev: SubmitEvent| {ev.prevent_default();           //類似javascript中的Event.preventDefault(),處理<input>字段非常有用spawn_local(async move {                //使用Leptos的spawn_local創建一個本地線程(local_thread)Future, 提供一個異步move閉包。let dept_name = department_name.get_untracked();set_deptsubmit_error.set(String::new());// 檢查長度是否在范圍內if dept_name.len() < min_length {set_deptsubmit_error.set(format!("部門名稱長度不能少于 {} 個字符", min_length));return;} if dept_name.len() > max_length {set_deptsubmit_error.set(format!("部門名稱長度不能大于 {} 個字符", max_length));return;}let args = DepartmentArgs{department:DepartmentSend { name: dept_name } ,};let args_js = serde_wasm_bindgen::to_value(&args).unwrap();   //參數序列化// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/let result = match invoke("write_department_db", args_js).await {Ok(result) => result,Err(e) => {let err_str = e.as_string().unwrap_or_else(|| format!("{:?}", e));set_deptsubmit_error.set(err_str.clone());//log!("調用后端命令失敗: {}", err_str);return;}};if let Some(msg) = result.as_string() {set_deptsubmit_error.set(msg.clone());if msg.contains("SUCCESS") {get_department_db(ev.clone());}} else {let err_msg = format!("ERROR: 無法解析的返回格式: {:?}", result);set_deptsubmit_error.set(err_msg.clone());log!("{}", err_msg);}});};//處理復選框事件let check_change_type = 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_worktypes.update(|items| {if target.checked() {               //target.checked與prop:checked不一樣, 是瀏覽器 DOM 的實時狀態,用于事件處理items.push(value);} else {items.retain(|&x| x != value);}});};});};let get_worktype_db = move |ev: SubmitEvent| {ev.prevent_default();spawn_local(async move {                //使用Leptos的spawn_local創建一個本地線程(local_thread)Future, 提供一個異步move閉包。let type_js = match invoke_without_args("send_worktype_db").await {Ok(val) => val,Err(e) => {set_typedb_msg.set(format!("獲取工作類型數據失敗: {:?}", e));return;}};let type_vec: Vec<Worktype> = match serde_wasm_bindgen::from_value(type_js) {Ok(vec) => vec,Err(e) => {//log!("反序列化工作類型數據失敗: {:?}", e);set_typedb_msg.set(format!("反序列化工作類型數據失敗: {}", e));return;}};// 動態生成包裹在 div 中的視圖let div_views = view! {<div>{type_vec.into_iter().map(|worktype| {let type_id = worktype.id;view! {<div style="margin:5px;width:1500px;"><inputtype="checkbox"name="items"value=type_id.to_string()prop:checked=move || selected_worktypes.get().contains(&type_id)     //Leptos 的狀態綁定,用于確保界面最終與數據同步。on:change=check_change_type      //用戶操作 → 更新 target.checked → 觸發事件check_change → 更新狀態 → prop:checked 驅動視圖更新。/><span>"類型ID: " {type_id}",工作類型: " {worktype.name}</span></div>}}).collect_view()}</div>}; // 關鍵的類型擦除;// 轉換為 View 類型并設置//log!("視圖類型: {:?}", std::any::type_name_of_val(&div_views));set_worktype_content.set(div_views); });};let write_type_sql = move |ev: SubmitEvent| {ev.prevent_default();           //類似javascript中的Event.preventDefault(),處理<input>字段非常有用spawn_local(async move {                //使用Leptos的spawn_local創建一個本地線程(local_thread)Future, 提供一個異步move閉包。let type_name = work_type.get_untracked();set_typesubmit_error.set(String::new());// 檢查長度是否在范圍內if type_name.len() < min_length {set_typesubmit_error.set(format!("工作類型長度不能少于 {} 個字符", min_length));return;} if type_name.len() > max_length {set_typesubmit_error.set(format!("工作類型長度不能大于 {} 個字符", max_length));return;}let args = WorktypeArgs{worktype:WorktypeSend { name: type_name } ,};let args_js = serde_wasm_bindgen::to_value(&args).unwrap();   //參數序列化// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/let result = match invoke("write_worktype_db", args_js).await {Ok(result) => result,Err(e) => {let err_str = e.as_string().unwrap_or_else(|| format!("{:?}", e));set_typesubmit_error.set(err_str.clone());//log!("調用后端命令失敗: {}", err_str);return;}};if let Some(msg) = result.as_string() {set_typesubmit_error.set(msg.clone());if msg.contains("SUCCESS") {get_worktype_db(ev.clone());}} else {let err_msg = format!("ERROR: 無法解析的返回格式: {:?}", result);set_typesubmit_error.set(err_msg.clone());log!("{}", err_msg);}});};let get_department_list = move || {spawn_local(async move {                //使用Leptos的spawn_local創建一個本地線程(local_thread)Future, 提供一個異步move閉包。let dept_js = match invoke_without_args("send_department_db").await {Ok(val) => val,Err(e) => {set_personnel_deptid_error.set(format!("獲取部門數據失敗: {:?}", e));return;}};let dept_vec: Vec<Department> = match serde_wasm_bindgen::from_value(dept_js) {Ok(vec) => vec,Err(e) => {log!("反序列化部門數據失敗: {:?}", e);set_personnel_deptid_error.set(format!("反序列化部門數據失敗: {}", e));return;}};if dept_vec.clone().len() == 0 {set_personnel_deptid_error.set(format!("部門數據為空,請先錄入工作部門"));return;}else{set_department_list.set(dept_vec);}});};let get_worktype_list = move || {spawn_local(async move {let type_js = match invoke_without_args("send_worktype_db").await {Ok(val) => val,Err(e) => {set_typeid_error.set(format!("獲取工作類型數據失敗: {:?}", e));return;}};let type_vec: Vec<Worktype> = match serde_wasm_bindgen::from_value(type_js) {Ok(vec) => vec,Err(e) => {set_typeid_error.set(format!("反序列化工作類型數據失敗: {}", e));return;}};if type_vec.clone().len() == 0 {set_typeid_error.set(format!("工作類型數據為空,請先錄入工作類型"));return;}else{set_worktype_list.set(type_vec);}});};let check_change_work = move |ev: leptos::ev::Event| {spawn_local(async move {let target = event_target::<HtmlInputElement>(&ev);let value_str = target.value();if let Ok(value) = value_str.parse::<i64>() {set_selected_works.update(|items| {if target.checked() {items.push(value);} else {items.retain(|&x| x != value);}});};});};let get_work_db = move |ev: SubmitEvent| {ev.prevent_default();spawn_local(async move {let work_js = match invoke_without_args("send_work_db").await {Ok(val) => val,Err(e) => {set_workdb_msg.set(format!("獲取工作數據失敗: {:?}", e));return;}};let work_vec: Vec<WorkAll> = match serde_wasm_bindgen::from_value(work_js) {Ok(vec) => vec,Err(e) => {set_workdb_msg.set(format!("反序列化工作數據失敗: {}", e));return;}};// 獲取工作類型列表用于顯示let type_js = match invoke_without_args("send_worktype_db").await {Ok(val) => val,Err(e) => {set_workdb_msg.set(format!("獲取工作類型數據失敗: {:?}", e));return;}};let type_vec: Vec<Worktype> = match serde_wasm_bindgen::from_value(type_js) {Ok(vec) => vec,Err(e) => {set_workdb_msg.set(format!("反序列化工作類型數據失敗: {}", e));return;}};set_worktype_list.set(type_vec.clone());if type_vec.is_empty() {set_typedb_msg.set(format!("獲取的工作類型列表為空!"));return;}// 獲取部門列表用于顯示部門名稱let dept_js = match invoke_without_args("send_department_db").await {Ok(val) => val,Err(e) => {set_workdb_msg.set(format!("獲取部門數據失敗: {:?}", e));return;}};let dept_vec: Vec<Department> = match serde_wasm_bindgen::from_value(dept_js) {Ok(vec) => vec,Err(e) => {set_workdb_msg.set(format!("反序列化部門數據失敗: {}", e));return;}};// 獲取人員列表用于顯示人員名稱let personnel_js = match invoke_without_args("send_personnel_db").await {Ok(val) => val,Err(e) => {set_workdb_msg.set(format!("獲取人員數據失敗: {:?}", e));return;}};let personnel_vec: Vec<Personnel> = match serde_wasm_bindgen::from_value(personnel_js) {Ok(vec) => vec,Err(e) => {set_workdb_msg.set(format!("反序列化人員數據失敗: {}", e));return;}};// 創建類型名稱信號let type_name_signal = move |type_id: i64| {type_vec.iter().find(|t| t.id == type_id).map(|t| t.name.clone()).unwrap_or_else(|| "未知類型".to_string())};// 創建部門名稱信號let dept_name_signal = move |dept_id: i64| {dept_vec.iter().find(|d| d.id == dept_id).map(|d| d.name.clone()).unwrap_or_else(|| "未知部門".to_string())};// 創建人員名稱信號let personnel_name_signal = move |person_id: i64| {personnel_vec.iter().find(|p| p.id == person_id).map(|p| p.full_name.clone()).unwrap_or_else(|| "未知人員".to_string())};// 動態生成工作列表視圖let div_views = view! {<div>{work_vec.into_iter().map(|work| {let work_id = work.id;let work_type = type_name_signal(work.work_type_id);// 獲取關聯部門名稱let dept_names: Vec<String> = work.work_departments.iter().map(|wd| dept_name_signal(wd.department_id)).collect();// 獲取關聯人員名稱let personnel_names: Vec<String> = work.work_personnels.iter().map(|wp| {let name = personnel_name_signal(wp.personnel_id);if wp.is_main_responsible == 1 {format!("{} (負責人)", name)} else {name}}).collect();view! {<div style="margin:5px;width:1500px;border:1px solid #ccc;padding:5px;"><div><inputtype="checkbox"name="items"value=work_id.to_string()prop:checked=move || selected_works.get().contains(&work_id)on:change=check_change_work/><span>"工作ID: " {work_id}",標題: " {work.subject}",類型: " {work_type}",狀態: " {if work.is_completed == 1 { "已完成" } else { "未完成" }}",開始時間: " {work.start_date}</span></div><div style="margin-left:20px;margin-top:5px;"><div>"責任部門: " {dept_names.join(", ")}</div><div>"參與人員: " {personnel_names.join(", ")}</div></div></div>}}).collect_view()}</div>};set_workview_content.set(div_views);});};let write_work_sql = move |ev: SubmitEvent| {ev.prevent_default();spawn_local(async move {let subject = work_subject.get_untracked();let content = work_content.get_untracked();let start_date = start_date.get_untracked();let work_type_id = worktype_id.get_untracked();let is_completed = work_state.get_untracked();set_worksubmit_error.set(String::new());// 驗證輸入if subject.len() < 3 {set_subject_error.set("工作標題長度不能少于3個字符".to_string());return;}if content.len() < 10 {set_workcontent_error.set("工作內容長度不能少于10個字符".to_string());return;}if work_type_id == 0 {set_worksubmit_error.set("請選擇工作類型".to_string());return;}// 獲取工作類型列表用于顯示let type_js = match invoke_without_args("send_worktype_db").await {Ok(val) => val,Err(e) => {set_worksubmit_error.set(format!("獲取工作類型數據失敗: {:?}", e));return;}};let type_vec: Vec<Worktype> = match serde_wasm_bindgen::from_value(type_js) {Ok(vec) => vec,Err(e) => {set_worksubmit_error.set(format!("反序列化工作類型數據失敗: {}", e));return;}};set_worktype_list.set(type_vec.clone());if type_vec.is_empty() {set_worksubmit_error.set(format!("獲取的工作類型列表為空!"));return;}if !type_vec.iter().any(|worktype| worktype.id == work_type_id) {set_worksubmit_error.set(format!("工作類型ID {} 不存在于工作類型列表中!",work_type_id));return;}if work_depts.get_untracked().len() == 0 {set_worksubmit_error.set(format!("工作責任部門列表為空,請選擇!"));return;}if work_personnel.get_untracked().len() == 0 {set_worksubmit_error.set(format!("工作參與人員列表為空,請選擇!"));return;}if work_responsible.get_untracked().len() == 0 {set_worksubmit_error.set(format!("該工作負責人為空,請選擇!"));return;}let args = WorkArgs {work: WorkSend {subject,                //正常為:subject:subject, key和value一致時,只寫一個即可work_content: content,start_date,work_type_id,is_completed,},};//寫入工作數據庫,返回工作IDset_work_id.set(0);// 序列化參數let args_js = match serde_wasm_bindgen::to_value(&args) {Ok(v) => v,Err(e) => {set_worksubmit_error.set(format!("參數序列化失敗: {}", e));return;}};// 調用后端命令let result = match invoke("write_work_db", args_js).await {Ok(v) => v,Err(e) => {set_worksubmit_error.set(e.as_string().unwrap_or_else(|| format!("命令調用失敗: {:?}", e)));return;}};// 解析返回結果// 先解析為JsValue,然后手動轉換為i64match result.as_f64() {Some(id) => {set_work_id.set(id as i64);}None => {set_worksubmit_error.set("無法解析工作ID".to_string());return;}}log!("成功錄入的工作任務的ID為:{}", work_id.get_untracked());// 確保work_id有效let work_id_val = work_id.get_untracked();if work_id_val == 0 {set_worksubmit_error.set("ERROR: 無效的工作ID".to_string());return;}// 創建工作責任部門參數let work_depts_args = WorkDeptsArgs {workdepts: work_depts.get_untracked().into_iter().map(|dept_id| {WorkDeptsSend {work_id: work_id_val,department_id: dept_id}}).collect()};// 創建工作參與人員參數let work_person_args = WorkPersonArgs {workpersonnels: work_personnel.get_untracked().into_iter().map(|person| {WorkPersonSend {work_id: work_id_val,personnel_id: person.id,is_main_responsible: if work_responsible.get_untracked().iter().any(|p| p.id == person.id) {1} else {0}}}).collect()};// 調用寫入工作責任部門的命令let work_depts_js = serde_wasm_bindgen::to_value(&work_depts_args).unwrap();match invoke("write_work_depts_db", work_depts_js).await {Ok(result) => {if let Some(msg) = result.as_string() {if !msg.contains("SUCCESS") {set_worksubmit_error.set(msg);return;}}}Err(e) => {let err_str = e.as_string().unwrap_or_else(|| format!("{:?}", e));set_worksubmit_error.set(err_str);return;}};// 調用寫入工作參與人員的命令let work_person_js = serde_wasm_bindgen::to_value(&work_person_args).unwrap();match invoke("write_work_personnel_db", work_person_js).await {Ok(result) => {if let Some(msg) = result.as_string() {if !msg.contains("SUCCESS") {set_worksubmit_error.set(msg);//log!("寫入工作參與人員出錯:{}",msg);return;}}}Err(e) => {let err_str = e.as_string().unwrap_or_else(|| format!("{:?}", e));set_worksubmit_error.set(err_str);//log!("寫入工作參與人員出錯:{}",err_str);return;}};get_work_db(ev);});};let get_personnel_db = move |ev: SubmitEvent| {ev.prevent_default();spawn_local(async move {// 先獲取部門列表let dept_js = match invoke_without_args("send_department_db").await {Ok(val) => val,Err(e) => {set_personneldb_msg.set(format!("獲取部門數據失敗: {:?}", e));return;}};let dept_vec: Vec<Department> = match serde_wasm_bindgen::from_value(dept_js) {Ok(vec) => vec,Err(e) => {set_personneldb_msg.set(format!("反序列化部門數據失敗: {}", e));return;}};set_department_list.set(dept_vec.clone());if dept_vec.is_empty() {set_personneldb_msg.set(format!("獲取的部門列表為空!"));return;}// 然后獲取人員列表let personnel_js = match invoke_without_args("send_personnel_db").await {Ok(val) => val,Err(e) => {set_personneldb_msg.set(format!("獲取人員數據失敗: {:?}", e));return;}};let personnel_vec: Vec<Personnel> = match serde_wasm_bindgen::from_value(personnel_js) {Ok(vec) => vec,Err(e) => {set_personneldb_msg.set(format!("反序列化人員數據失敗: {}", e));return;}};// 創建部門名稱信號let dept_name_signal = move |dept_id: i64| {department_list.with_untracked(|depts| {depts.iter().find(|d| d.id == dept_id).map(|d| d.name.clone()).unwrap_or_else(|| {log!("找不到部門ID: {}", dept_id);"未知部門".to_string()})})};// 動態生成包裹在 div 中的視圖let div_views = view! {<div>{personnel_vec.into_iter().map(|personnel| {let personnel_id = personnel.id;let dept_name = dept_name_signal(personnel.department_id);view! {<div style="margin:5px;width:1500px;"><inputtype="checkbox"name="items"value=personnel_id.to_string()prop:checked=move || selected_personnels.get().contains(&personnel_id)on:change=move |ev| {let target = event_target::<HtmlInputElement>(&ev);if let Ok(value) = target.value().parse::<i64>() {set_selected_personnels.update(|items| {if target.checked() {items.push(value);} else {items.retain(|&x| x != value);}});}}/><span>"員工ID: " {personnel_id}",員工姓名: " {personnel.full_name}", 所屬部門:" {dept_name}</span></div>}}).collect_view()}</div>};// 轉換為 View 類型并設置//log!("視圖類型: {:?}", std::any::type_name_of_val(&div_views));set_personnel_content.set(div_views); });};let write_personnel_sql = move |ev: SubmitEvent| {ev.prevent_default();           //類似javascript中的Event.preventDefault(),處理<input>字段非常有用spawn_local(async move {                //使用Leptos的spawn_local創建一個本地線程(local_thread)Future, 提供一個異步move閉包。let personnel_name = personnel_name.get_untracked();set_personnelsubmit_error.set(String::new());// 檢查長度是否在范圍內if personnel_name.len() < 2 {set_personnelsubmit_error.set(format!("員工姓名長度不能少于兩個字符"));return;} if personnel_name.len() > 50 {set_personnelsubmit_error.set(format!("員工姓名長度不能大于50個字符"));return;}if personnel_deptid.get_untracked() == 0 {set_personnelsubmit_error.set(format!("未選擇所屬部門!"));return;}//確認部門ID是否存在// 先獲取部門列表let dept_js = match invoke_without_args("send_department_db").await {Ok(val) => val,Err(e) => {set_personnelsubmit_error.set(format!("獲取部門數據失敗: {:?}", e));return;}};let dept_vec: Vec<Department> = match serde_wasm_bindgen::from_value(dept_js) {Ok(vec) => vec,Err(e) => {set_personnelsubmit_error.set(format!("反序列化部門數據失敗: {}", e));return;}};set_department_list.set(dept_vec.clone());if dept_vec.is_empty() {set_personnelsubmit_error.set(format!("獲取的部門列表為空!"));return;}let dept_id = personnel_deptid.get_untracked();if !dept_vec.iter().any(|dept| dept.id == dept_id) {set_personnelsubmit_error.set(format!("部門ID {} 不存在于部門列表中!", dept_id));return;}let args = PersonnelArgs{personnel:PersonnelSend { full_name: personnel_name, department_id: personnel_deptid.get_untracked()} ,};let args_js = serde_wasm_bindgen::to_value(&args).unwrap();   //參數序列化// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/let result = match invoke("write_personnel_db", args_js).await {Ok(result) => result,Err(e) => {let err_str = e.as_string().unwrap_or_else(|| format!("{:?}", e));set_personnelsubmit_error.set(err_str);return;}};if let Some(msg) = result.as_string() {set_personnelsubmit_error.set(msg.clone());if msg.contains("SUCCESS") {get_personnel_db(ev);}} else {let err_msg = format!("ERROR: 無法解析的返回格式: {:?}", result);set_personnelsubmit_error.set(err_msg);}});};view! {                                              //view!宏作為App()函數的返回值返回IntoView類型<main class="container"><h1>"---------※工作進度管理系統※---------"</h1><div class="pdtinput"><div class="left"  style=";margin-top:10px;margin-bottom: 10px;">"工作標題:"</div><div class="right"><select style="width:450px;font-size: 1em;"on:focus=move |_| {spawn_local(async move {match invoke_without_args("send_work_list").await {Ok(work_js) => {let works = serde_wasm_bindgen::from_value::<Vec<Work>>(work_js).unwrap_or_else(|e| {set_fetch_works_error.set(format!("工作列表反序列化失敗: {}", e));vec![]});set_fetch_work_list.set(works);}Err(e) => {set_fetch_works_error.set(format!("獲取工作列表失敗: {:?}", e));}}});}on:change=move |ev| {get_worktype_list();let value = event_target_value(&ev);if let Ok(id) = value.parse::<i64>() {set_fetch_work_id.set(id);set_fetch_works_error.set(String::new());spawn_local(async move {let args = FetchWorkArgs{workid: fetch_work_id.get_untracked()};let args_js = match serde_wasm_bindgen::to_value(&args) {Ok(v) => v,Err(e) => {set_fetch_works_error.set(format!("參數序列化失敗: {}", e));return;}};match invoke("send_one_work", args_js).await {Ok(result) => {match serde_wasm_bindgen::from_value::<WorkBack>(result) {Ok(work) => {set_fetch_work_startdate.set(work.start_date);set_fetch_work_content.set(work.work_content);set_fetch_work_state.set(work.is_completed);set_fetch_worktype_id.set(work.work_type_id);set_fetch_work_depts.set(work.work_departments);set_fetch_work_personnels.set(work.work_personnels);set_fetch_work_responsile.set(work.responsile_person);}Err(e) => {set_fetch_works_error.set(format!("工作數據反序列化失敗: {}", e));}}}Err(e) => {set_fetch_works_error.set(format!("獲取工作詳情失敗: {:?}", e));}}});}get_progress_records();}><option value="" disabled selected>"請選擇工作"</option>{move || {fetch_work_list.get().iter().map(|work| view! {<option value={work.id.to_string()}>{work.subject.clone()}</option>}).collect_view()}}</select></div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"啟動時間:"</div><div class="right">{move || fetch_work_startdate.get()}</div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"工作主要內容:"</div><div class="right">{move || fetch_work_content.get()}</div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"工作類型:"</div><div class="right">{move || {let worktype_id = fetch_worktype_id.get();worktype_list.with(|worktypes| {worktypes.iter().find(|wt| wt.id == worktype_id).map(|wt| wt.name.clone()).unwrap_or_default()})}}</div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"工作狀態:"</div><div class="right"><Show when=move || fetch_work_id.get() != 0>{move || if fetch_work_state.get() == 1 { "已完成" } else { "進行中" }}<buttonstyle="margin:0px 15px 0px 15px;height:35px;vertical-align:middle;padding:5px 10px;"on:click=move |_| {spawn_local(async move {let args = FetchWorkArgs{workid: fetch_work_id.get_untracked()};let args_js = match serde_wasm_bindgen::to_value(&args) {Ok(v) => v,Err(e) => {set_fetch_works_error.set(format!("序列化參數失敗: {}", e));return;}};match invoke("change_work_state", args_js).await {Ok(result) => {if let Some(status) = result.as_f64() {if status == 1.0 {set_fetch_work_state.set(1);} else {set_fetch_work_state.set(0);}}}Err(e) => {set_fetch_works_error.set(format!("調用change_work_state失敗: {:?}", e));}}});}>"改變工作狀態"</button></Show></div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"參與部門:"</div><div class="right">{move || {fetch_work_depts.get().iter().map(|dept| view! {<div style="margin:5px;border:1px solid #ccc;padding:5px;">{dept.name.clone()}</div>}).collect_view()}}</div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"負責人:"</div><div class="right" style="display: flex; flex-wrap: wrap; gap: 10px;">{move || {fetch_work_responsile.get().iter().map(|p| view! {<div style="flex: 1 0 43%;margin:5px;background-color:rgb(235, 89, 128);">{p.full_name.clone()}</div>}).collect_view()}}</div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"參與人員:"</div><div class="right">{move || {let responsile_ids: Vec<i64> = fetch_work_responsile.get().iter().map(|p| p.id).collect();view! {<div style="display: flex; flex-wrap: wrap; gap: 10px;">{fetch_work_personnels.get().iter().filter(|p| !responsile_ids.contains(&p.id)).map(|p| view! {<div style="flex: 1 0 30%;background-color:rgb(13, 200, 225);">{p.full_name.clone()}</div>}).collect_view()}</div>}}}</div></div><div class="errorshow"><div class="left"></div><div class="right red">{fetch_works_error}</div></div><Show when=move || (fetch_work_id.get() != 0 && fetch_work_state.get() == 0)><h2>"---------※添加新的工作進度記錄※---------"</h2><form  id="records-form" on:submit=write_progress_records><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"新的工作進度:"</div><div class="right"><div id="progress-content-div"contenteditable="true"style="width:420px; min-height:100px; border:1px solid #ccc; padding:5px;"on:input=move |ev| {let target = event_target::<web_sys::HtmlDivElement>(&ev);set_progress_content.set(target.inner_text());}></div></div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"添加時間:"</div><div class="right"><inputtype="datetime-local"value=move || records_date.get()on:input=move |ev| {let value = event_target_value(&ev);set_records_date.set(value);}/></div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style=";margin-top:10px;margin-bottom: 10px;">"記錄人:"</div><div class="right"><selectstyle="width:350px;font-size:1em"on:change=move |ev| {let value = event_target_value(&ev);if let Ok(id) = value.parse::<i64>() {set_progress_recorder.set(id);}}><option value="" disabled selected>"請選擇記錄人"</option>{move || fetch_work_personnels.get().into_iter().map(|person| {view! {<option value={person.id.to_string()}>{person.full_name.clone()}</option>}}).collect_view()}</select></div></div><div class="errorshow"><div class="left"></div><div class="right red"> {add_record_error}</div></div><button style="width:300px;" type="submit" id="dept-button">"新建工作進度記錄"</button></form></Show><div class="pdtinput" style="width:800px;background-color:rgb(182, 239, 245);margin-top:10px;margin-bottom: 10px;"><div class="left" style="margin-top:10px;margin-bottom: 10px;background-color:rgb(77, 192, 204);">"進度記錄歷史:"</div><div class="right" style="width:670px;">{move || {let mut records = fetch_progress_records.get();// 按記錄日期排序// 按記錄日期降序排序(最新記錄在前)// 使用b.cmp(&a)實現降序,a.cmp(&b)則是升序records.sort_by(|a, b| b.record_date.cmp(&a.record_date));view! {<div style="margin-top:10px;width:660px;">{records.into_iter().map(|record| {view! {<div style="display:flex; width:660px; margin:5px;background-color:rgb(77, 192, 204);"><div style="flex:1; border:1px solid #ccc; padding:5px;"><div style="font-weight:bold;">{record.record_date}</div><div>{record.progress_detail}</div><div style="text-align:right;font-style:italic;">{move || {let person = fetch_work_personnels.get().iter().find(|p| p.id == record.recorder_id).map(|p| p.full_name.clone()).unwrap_or_else(|| format!("未知人員(ID: {})", record.recorder_id));format!("記錄人: {}", person)}}</div></div><div style="width:100px; display:flex; align-items:center; justify-content:center;"><buttonstyle="width:80px;"on:click=move |_| {spawn_local(async move {let args = SelectedRecord {selectedrecord: record.id,};let args_js = serde_wasm_bindgen::to_value(&args).unwrap();match invoke("del_progress_record", args_js).await {Ok(result) => {if let Some(msg) = result.as_string() {if msg.contains("SUCCESS") {get_progress_records();}}}Err(e) => {set_fetch_works_error.set(e.as_string().unwrap_or_else(|| format!("刪除記錄失敗: {:?}", e)));}}});}>"刪除"</button></div></div>}}).collect_view()}</div>}}}</div></div><p></p><h1>"---------※工作管理系統※---------"</h1><form  id="work-form" on:submit=write_work_sql><div class="pdtinput"><div class="left"> "工作標題:"</div><div class="right"><input style="width:420px" type="text" minlength="3" maxlength="150" placeholder="請輸入工作標題..."value = move || work_subject.get()on:input=move|ev|update_string(ev, "工作標題".to_string(), set_work_subject, set_subject_error) /></div></div><div class="errorshow"><div class="left"></div><div class="right red">{subjet_error}</div></div><div class="pdtinput"><div class="left"> "工作內容:"</div><div class="right"><divcontenteditable="true"style="width:420px; min-height:100px; border:1px solid #ccc; padding:5px;"on:input=move |ev| {let target = event_target::<web_sys::HtmlDivElement>(&ev);set_work_content.set(target.inner_text());}></div></div></div><div class="errorshow"><div class="left"></div><div class="right red">{workcontent_error}</div></div><div class="pdtinput"><div class="left"> "開始時間:"</div><div class="right"><inputtype="datetime-local"value=move || start_date.get()on:input=move |ev| {let value = event_target_value(&ev);set_start_date.set(value);}/></div></div><div class="pdtinput"><div class="left" style="margin-top:7px;margin-bottom: 7px;"> "工作狀態:"</div><div class="right"><inputtype="checkbox"prop:checked=move || work_state.get() == 1on:change=move |ev| {let target = event_target::<HtmlInputElement>(&ev);set_work_state.set(if target.checked() { 1 } else { 0 });}/><span>"已完成"</span></div></div><div class="pdtinput"><div class="left" style="margin-top:7px;margin-bottom: 7px;"> "工作類型:"</div><div class="right"><select style="width:350px;margin-top:10px;margin-bottom: 10px;"on:focus=move |_| {get_worktype_list();}on:change=move |ev| {let value = event_target_value(&ev);match value.parse::<i64>() {Ok(id) => {set_worktype_id.set(id);set_typeid_error.set(String::new());}Err(_) => {set_typeid_error.set(format!("請重新選擇工作類型!"));}}}><option value="" disabled selected>"請選擇工作類型"</option>{move || worktype_list.get().into_iter().map(|worktype| {view! {<option value={worktype.id.to_string()}>{worktype.name}</option>}}).collect_view()}</select></div></div><div class="errorshow"><div class="left"></div><div class="right red">{typeid_error}</div></div>// 責任人員備選目錄<div class="pdtinput"><div class="left">"工作部門:"</div><div class="right"><select id="dept-select" style="width:350px;margin-top:10px;margin-bottom: 10px;"on:focus=move |_| {get_department_list();}on:change=move |ev| {set_dept_personnel_error.set(String::new());let select = event_target::<HtmlSelectElement>(&ev);if let Some(selected_value) = select.value().parse::<i64>().ok() {spawn_local(async move {let args = PersonnelDeptArgs{departmentid: selected_value};let args_js = serde_wasm_bindgen::to_value(&args).unwrap();let result = invoke("get_personnel_by_department", args_js).await;match result {Ok(val) => {let personnel: Vec<Personnel> = serde_wasm_bindgen::from_value(val).unwrap();set_personnel_list.set(personnel);}Err(e) => {set_dept_personnel_error.set(format!("獲取人員失敗: {:?}", e));}}});}}><option value="" disabled selected>"請選擇責任部門"</option>{move || department_list.get().into_iter().map(|dept| {view! {<option value={dept.id.to_string()}>{dept.name}</option>}}).collect_view()}</select></div></div><div class="errorshow"><div class="left"></div><div class="right red">{dept_personnel_error}</div></div><div class="pdtinput"><div class="left" style="margin-top:10px;margin-bottom: 10px;">"部門人員:"</div><div class="right"><div style="display: flex; flex-wrap: wrap; gap: 5px;">{move || personnel_list.get().into_iter().map(|person| {view! {<div style="flex: 1 0 30%; min-width: 30px; margin: 5px;"><inputtype="checkbox"value={person.id.to_string()}prop:checked=move || work_personnel.get().iter().any(|p| p.id == person.id)on:change=move |ev| {let target = event_target::<HtmlInputElement>(&ev);if let Some(person) = personnel_list.get_untracked().iter().find(|p| p.id.to_string() == target.value()).cloned() {if target.checked() {set_work_personnel.update(|ids| ids.push(person.clone()));// 添加部門ID到work_deptsset_work_depts.update(|depts| {if !depts.contains(&person.department_id) {depts.push(person.department_id);}});} else {set_work_personnel.update(|ids| ids.retain(|p| p.id != person.id));// 檢查是否需要從work_depts中移除部門IDset_work_depts.update(|depts| {let work_personnel = work_personnel.get_untracked();if !work_personnel.iter().any(|p| p.department_id == person.department_id) {depts.retain(|&did| did != person.department_id);}});}}}/>{person.full_name}</div>}}).collect_view()}</div></div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);"><div class="left" style="margin-top:10px;margin-bottom:10px;">"責任部門:"</div><div class="right">{move || {// 獲取所有涉及的部門let departments = work_depts.get().clone();view! {// 部門顯示區域<div style="margin-bottom: 10px;">{departments.into_iter().map(move |dept| {let dept_id = dept;let dept_name = department_list.get_untracked().iter().find(|d| d.id == dept_id).map(|d| d.name.clone()).unwrap_or_else(|| "未知部門".to_string());view! {<div style="margin:5px;border:1px solid #ccc;padding:5px;;background-color:rgb(225, 168, 13)"><span>{dept_name}</span></div>}}).collect_view()}</div>}}}</div></div><div class="pdtinput"  style="background-color:rgb(182, 239, 245);"><div class="left">"項目參與人員:"<br/>"(請勾選負責人)"</div><div class="right">// 人員顯示區域{move || {// 獲取所有涉及的部門view! {<div style="display: flex; flex-wrap: wrap; gap: 5px;">{move || {{move || {let personnel = work_personnel.get().clone();personnel.into_iter().map(|person| {let person_rc = Rc::new(person);let person_id = person_rc.id;let full_name = person_rc.full_name.clone();let person_clone = person_rc.clone();view! {<div style="flex: 1 0 40%; min-width:40px;margin:5px;border:1px solid #ccc;padding:5px;background-color:rgb(13, 200, 225);">{full_name}<inputtype="checkbox"prop:checked=move || work_responsible.with(|r| r.iter().any(|p| p.id == person_id))on:change=move |ev| {let target = event_target::<HtmlInputElement>(&ev);if target.checked() {set_work_responsible.update(|personnel| personnel.push((*person_clone).clone()));} else {set_work_responsible.update(|personnel| personnel.retain(|p| p.id != person_id));}}/></div>}}).collect_view()}}}}</div>}}}</div></div><div class="pdtinput" style="background-color:rgb(182, 239, 245);"><div class="left" style="margin-top: 10px; margin-bottom: 10px;">"項目負責人:"</div><div class="right" >// 人員顯示區域{move || {// 獲取所有涉及的部門view! {<div style="display: flex; flex-wrap: wrap; gap: 5px;">{move || {{move || {let res_person = work_responsible.get().clone();res_person.into_iter().map(|person| {let person_rc = Rc::new(person);let full_name = person_rc.full_name.clone();view! {<div style="flex: 1 0 40%; min-width: 40px; margin:5px;border:1px solid #ccc;padding:5px;background-color:rgb(235, 89, 128);"><span>{full_name}</span></div>}}).collect_view()}}}}</div>}}}</div></div><div class="errorshow"><div class="left"></div><div class="right red">{worksubmit_error}</div></div><button style="width:300px;" type="submit" id="work-button">"添加新工作"</button></form><p></p><div class="errorshow"><div class="left"></div><div class="right red">{workdb_msg}</div></div><div class="form-container"><div class="db-window" id="work-item">{move || workview_content.get()}</div><div class="btn-window"><form class="row" on:submit=get_work_db><button type="submit" style="margin:10px 5px 10px 5px;" id="get-button">"讀取工作列表"</button></form><form class="row" on:submit=move|ev|{del_selected_items(ev, selected_works, set_selected_works, String::from("del_work_item"), set_workdb_msg, Box::new(get_work_db))}><button type="submit" style="margin:10px 5px 10px 5px;" id="del-button">"刪除選中項"</button></form></div></div><p></p><h1>"---------※工作部門管理※---------"</h1><form  id="dept-form" on:submit=write_dept_sql><div class="pdtinput"><div class="left"> "部門名稱:"</div><div class="right"> <input style="width:420px" type="text" minlength="3" maxlength="150" placeholder="請輸入部門名稱..." value = move || department_name.get()  //將信號的值綁定到輸入框on:input=move|ev|update_string(ev, "部門名稱".to_string(), set_department_name, set_department_error) /></div></div><div class="errorshow"><div class="left"></div><div class="right red"> {department_error}</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {deptsubmit_error}</div></div><button style="width:300px;" type="submit" id="dept-button">"新建工作部門"</button></form><p></p><div class="errorshow"><div class="left"></div><div class="right red"> {deptdb_msg}</div></div><div class="form-container"><div class="db-window" id="department-item">{move || department_content.get()}</div><div class="btn-window"><form class="row" on:submit=get_department_db><button type="submit" id="get-button" style="margin:10px 5px 10px 5px;height:45px;" >"讀取數據庫"</button></form><form class="row" on:submit=move|ev|{del_selected_items(ev, selected_depts, set_selected_depts, String::from("del_department_item"), set_deptdb_msg, Box::new(get_department_db))}><button type="submit" style="margin:10px 5px 10px 5px;height:45px;" id="del-button" >"刪除選中項"</button></form></div></div><h1>"---------※工作類型管理※---------"</h1><form  id="type-form" on:submit=write_type_sql><div class="pdtinput"><div class="left"> "工作類型:"</div><div class="right"> <input style="width:420px" type="text" minlength="3" maxlength="150" placeholder="請輸入部門名稱..." value = move || work_type.get()  //將信號的值綁定到輸入框on:input=move|ev|update_string(ev, "工作類型".to_string(), set_work_type, set_worktype_error) /></div></div><div class="errorshow"><div class="left"></div><div class="right red"> {worktype_error}</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {typesubmit_error}</div></div><button style="width:300px;" type="submit" id="type-button">"新建工作類型"</button></form><p></p><div class="errorshow"><div class="left"></div><div class="right red"> {typedb_msg}</div></div><div class="form-container"><div class="db-window" id="worktype-item">{move || worktype_content.get()}</div><div class="btn-window"><form class="row" on:submit=get_worktype_db><button type="submit" style="margin:10px 5px 10px 5px;height:45px;" id="get-button" >"讀取數據庫"</button></form><form class="row" on:submit=move|ev|{del_selected_items(ev, selected_worktypes, set_selected_worktypes, String::from("del_worktype_item"), set_typedb_msg, Box::new(get_worktype_db))}><button type="submit" style="margin:10px 5px 10px 5px;height:45px;" id="del-button">"刪除選中項"</button></form></div></div><h1>"---------※部門人員管理※---------"</h1><form  id="personnel-form" on:submit=write_personnel_sql><div class="pdtinput"><div class="left"> "員工姓名:"</div><div class="right"> <input style="width:420px" type="text" minlength="2" maxlength="50" placeholder="請輸入部門員工全名..." value = move || personnel_name.get()  //將信號的值綁定到輸入框on:input=move|ev|update_string(ev, "員工全名".to_string(), set_personnel_name, set_personnel_error) /></div></div><div class="errorshow"><div class="left"></div><div class="right red"> {personnel_error}</div></div><div class="pdtinput"><div class="left" style="margin-top:7px;margin-bottom: 7px;"> "所屬部門:"</div><div class="right"> <select style="width:350px;margin-top:10px;margin-bottom: 10px;"on:focus=move |_| {get_department_list();}on:change=move |ev| {let value = event_target_value(&ev);match value.parse::<i64>() {Ok(id) => {set_personnel_deptid.set(id);set_personnel_deptid_error.set(String::new());}Err(_) => {set_personnel_deptid_error.set(format!("請重新選擇工作部門!"));}}}><option value="" disabled selected>"請選擇部門"</option>{move || department_list.get().into_iter().map(|dept| {view! {<option value={dept.id.to_string()}>{dept.name}</option>}}).collect_view()}</select></div></div><div class="errorshow"><div class="left"></div><div class="right red"> {personnel_deptid_error}</div></div><div class="errorshow"><div class="left"></div><div class="right red"> {personnelsubmit_error}</div></div><button style="width:300px;" type="submit" id="type-button">"添加部門員工"</button></form><p></p><div class="errorshow"><div class="left"></div><div class="right red"> {personneldb_msg}</div></div><div class="form-container"><div class="db-window" id="personnel-item">{move || personnel_content.get()}</div><div class="btn-window"><form class="row" on:submit=get_personnel_db><button type="submit" style="margin:10px 5px 10px 5px;height:45px;" id="get-button" >"讀取員工名單"</button></form><form class="row" on:submit=move|ev|{del_selected_items(ev, selected_personnels, set_selected_personnels, String::from("del_personnel_item"), set_personneldb_msg, Box::new(get_personnel_db))}><button type="submit" style="margin:10px 5px 10px 5px;height:45px;" id="del-button" >"刪除選中項"</button></form></div></div></main>}
}

3. 后端Tauri命令

?對數據庫的讀寫、刪除、更新操作主要是通過前端Leptos調用(invoke)后臺Tauri命令完成的,在schedule.rs中需要對invoke調用傳遞的參數和返回的數據的格式進行規定。

#[wasm_bindgen]
extern "C" {#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke, catch)]async fn invoke_without_args(cmd: &str) -> Result<JsValue, JsValue>;#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)]async fn invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;
}
/*
在tauri后臺,命令:fn function()-> Result<String, String>
使用match處理Reslut輸出,Ok()及Err()輸出如下:
match ().await{Ok(_) => Ok(String::from("SUCCESS! 插入數據成功!")),Err(e) => {let err_msg = e.to_string();if err_msg.contains("UNIQUE constraint failed") {Err(format!("ERROR! 部門名稱 '{}' 已存在!!", department.name))} else {Err(format!("數據庫錯誤: {}", err_msg))}}}在前端leptos的invoke調用中,同樣通過match來處理調用后臺命令返回的Ok和Err信息:
let result = match invoke("write_department_db", args_js).await {Ok(result) => result,Err(e) => {let err_str = e.as_string().unwrap_or_else(|| format!("{:?}", e));set_deptdb_msg.set(err_str.clone());//log!("調用后端命令失敗: {}", err_str);return;}};if let Some(msg) = result.as_string() {set_deptdb_msg.set(msg.clone());if msg.contains("SUCCESS") {get_department_db(ev.clone());}} else {let err_msg = format!("ERROR: 無法解析的返回格式: {:?}", result);set_deptdb_msg.set(err_msg.clone());log!("{}", err_msg);}
*/

傳遞給后臺命令的參數首先要轉換成結構體,然后其轉換成JsValue格式后,傳遞給后臺命令,后臺命令返回的值也是JsValue格式,也需要格式轉換。具體例子如下:

#[derive(Serialize, Deserialize)]
struct FetchWorkArgs {workid: i64,
}......spawn_local(async move {let args = FetchWorkArgs{workid: fetch_work_id.get_untracked()};let args_js = match serde_wasm_bindgen::to_value(&args) {Ok(v) => v,Err(e) => {set_fetch_works_error.set(format!("參數序列化失敗: {}", e));return;}};match invoke("send_progress_record", args_js).await {Ok(result) => {match serde_wasm_bindgen::from_value::<Vec<ProgressRecord>>(result) {Ok(work) => {set_fetch_progress_records.set(work);}Err(e) => {set_fetch_works_error.set(format!("工作數據反序列化失敗: {}", e));}}}Err(e) => {set_fetch_works_error.set(format!("獲取工作詳情失敗: {:?}", e));}}});

而對應的后臺send_progress_record命令如下,其中參數workid是與Leptos傳遞的參數結構體的鍵workid保持一直的,且不能有下劃線等符號。

#[tauri::command]
async fn send_progress_record(state: tauri::State<'_, DbState>, workid:i64) -> Result<Vec<ProgressRecord>, String> {let db = &state.db;let records: Vec<ProgressRecord> = sqlx::query_as::<_, ProgressRecord>("SELECT id, progress_detail, recorder_id, record_dateFROM progress_recordsWHERE work_id = ?ORDER BY record_date ASC").bind(workid).fetch_all(db).await.map_err(|e| format!("查詢進度記錄失敗: {}", e))?;Ok(records)
}

所有后臺命令均放在src-tauri\src\lib.rs文件中,具體內容如下:

use std::io::Write;
use futures::TryStreamExt;
use plotters::prelude::*;
use sqlx::{migrate::MigrateDatabase, prelude::FromRow, sqlite::SqlitePoolOptions, Pool, Sqlite};
//use tauri::{App, Manager, WebviewWindowBuilder, Emitter};
use tauri::{App, Emitter, Manager};
use serde::{Deserialize, Serialize};
type Db = Pool<Sqlite>;
use std::process::Command;
use std::env;struct DbState {db: Db,
}mod tray;       //導入tray.rs模塊
mod mymenu;     //導入mynemu.rs模塊
use mymenu::{create_menu, handle_menu_event};async fn setup_db(app: &App) -> Db {let mut path = app.path().app_data_dir().expect("獲取程序數據文件夾路徑失敗!");match std::fs::create_dir_all(path.clone()) {Ok(_) => {}Err(err) => {panic!("創建文件夾錯誤:{}", err);}};//C:\Users\<user_name>\AppData\Roaming\com.mynewapp.app\db.sqlite path.push("db.sqlite");Sqlite::create_database(format!("sqlite:{}", path.to_str().expect("文件夾路徑不能為空!")).as_str(),).await.expect("創建數據庫失敗!");let db = SqlitePoolOptions::new().connect(path.to_str().unwrap()).await.unwrap();//創建遷移文件位于./migrations/文件夾下    //cd src-tauri//sqlx migrate add create_users_tablesqlx::migrate!("./migrations/").run(&db).await.unwrap();db
}// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
async fn show_splashscreen_window(app: tauri::AppHandle) {if let Some(splashscreen) = app.get_webview_window("splashscreen") {splashscreen.show().unwrap();}
}
#[tauri::command]
async fn close_splashscreen(app: tauri::AppHandle) {// 獲取主窗口let main_window = app.get_webview_window("main").unwrap();// 延遲創建菜單并附加到窗口let menu = create_menu(&app).unwrap();main_window.set_menu(menu).unwrap();main_window.on_menu_event(move |window, event| handle_menu_event(window, event));if let Some(splashscreen) = app.get_webview_window("splashscreen") {splashscreen.close().unwrap();}// 顯示主窗口main_window.show().unwrap();
}//導航到指定頁面
#[tauri::command]
async fn navigate_to(app: tauri::AppHandle, path: String) -> Result<(), String> {if let Some(window) = app.get_webview_window("main") {window.emit("navigate", path).map_err(|e| e.to_string())?;      //window.emit_to(label, event, content)向特定label頁面發送event}Ok(())
}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct User {id: u16,username: String,email: String,
}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct UserId {id: u16,
}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct ProductId {pdt_id: i64,
}#[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(Clone, Serialize, Deserialize)]struct DataPoint {x: f64,y: f64,}#[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,
}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct Department {id:i64,name:String,
}#[derive(Serialize, Deserialize)]
struct DepartmentSend {name:String,
}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct Worktype {id:i64,name:String,
}#[derive(Serialize, Deserialize)]
struct WorktypeSend {name:String,
}#[derive(Debug, Serialize, Deserialize, FromRow, Clone)]
struct Personnel {id:i64,full_name:String,department_id:i64,
}#[derive(Serialize, Deserialize)]
struct PersonnelSend {full_name:String,department_id:i64,
}#[derive(Serialize, Deserialize,  FromRow)]
struct ProgressRecord {id: i64,progress_detail: String,recorder_id: i64,record_date: String,
}#[derive(Serialize, Deserialize,  FromRow)]
struct ProgressRecordSend {work_id: i64,progress_detail: String,recorder_id: i64,record_date: String,
}#[tauri::command]
async fn get_personnel_by_department(state: tauri::State<'_, DbState>,departmentid: i64
) -> Result<Vec<Personnel>, String> {let db = &state.db;if departmentid <= 0 {return Ok(Vec::new());}let query_result:Vec<Personnel> = sqlx::query_as::<_, Personnel>("SELECT id, full_name, department_id FROM personnel WHERE department_id = ?1").bind(&departmentid).fetch(db).try_collect().await.map_err(|e| format!("查詢人員失敗: {}", e))?;Ok(query_result)
}#[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 update_user(state: tauri::State<'_, DbState>, user: User) -> Result<(), String> {let db = &state.db;sqlx::query("UPDATE users SET username = ?1, email = ?2 WHERE id = ?3").bind(user.username).bind(user.email).bind(user.id).execute(db).await.map_err(|e| format!("不能更新user:{}", e))?;Ok(())
}#[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(String::from("刪除失敗:未提供有效的產品ID"));}// 生成動態占位符(根據數組長度生成 ?, ?, ?)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(String::from("刪除失敗:未找到匹配的產品"));}Ok(format!("成功刪除 {} 條數據!", result.rows_affected()))}#[tauri::command]
async fn send_selected_pdt(state: tauri::State<'_, DbState>, productlist:Vec<i64>) -> Result<Vec<Pdt>, String> {// 參數名productlist必須與前端定義的結構變量SelectedPdtArgs的鍵值一致let db = &state.db;// 處理空數組的情況if productlist.is_empty() {return Err(String::from("讀取失敗:未提供有效的產品ID"));}// 生成動態占位符(根據數組長度生成 ?, ?, ?)let placeholders = vec!["?"; productlist.len()].join(", ");let query_str = format!("SELECT * FROM products WHERE pdt_id IN ({})",placeholders);// 構建查詢并綁定參數let mut query = sqlx::query_as::<_, Pdt>(&query_str);for id in &productlist {query = query.bind(id);}// 執行讀取操作let query_result = query.fetch_all(db).await.map_err(|e| format!("查詢失敗: {}", e))?;Ok(query_result)}#[tauri::command]
async fn close_main_window(app: tauri::AppHandle) -> Result<(), String>{if let Some(window) = app.get_webview_window("main"){window.close().unwrap();}Ok(())
}use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use tauri::path::BaseDirectory;#[tauri::command]
fn python_plot(app: tauri::AppHandle) -> Result<String, String> {let resource_path = app.path().resolve("resources/plot.py", BaseDirectory::Resource) // 解析資源文件路徑.expect("Failed to resolve resource");// 調用 Python 腳本let output = Command::new("E:/python_envs/eric7/python.exe").arg(resource_path) // Python 腳本路徑.output().map_err(|e| e.to_string())?;// 調用打包后的 Python 可執行文件/*let output = Command::new("E:/Rust_Program/tauri-app/acid-index/src-tauri/dist/plot.exe").output().map_err(|e| e.to_string())?;*/// 檢查 Python 腳本是否成功運行if output.status.success() {// 獲取 Python 腳本的輸出(Base64 圖像數據)let image_data = String::from_utf8(output.stdout).map_err(|e| e.to_string())?;// 去除多余的換行符let image_data = image_data.trim().to_string();Ok(image_data)} else {// 獲取 Python 腳本的錯誤輸出let error_message = String::from_utf8(output.stderr).map_err(|e| e.to_string())?;Err(error_message)}
}#[tauri::command]
fn python_acid_plot(app: tauri::AppHandle, productdata: Vec<Pdt>) -> Result<String, String> {use std::collections::HashMap;let resource_path = app.path().resolve("resources/views.py", BaseDirectory::Resource).expect("Failed to resolve resource");// 將Pdt結構體轉換為HashMaplet data: Vec<HashMap<&str, serde_json::Value>> = productdata.iter().map(|pdt| {let mut map = HashMap::new();map.insert("pdt_id", serde_json::json!(pdt.pdt_id));map.insert("pdt_name", serde_json::json!(pdt.pdt_name));map.insert("pdt_si", serde_json::json!(pdt.pdt_si));map.insert("pdt_al", serde_json::json!(pdt.pdt_al));map.insert("pdt_ca", serde_json::json!(pdt.pdt_ca));map.insert("pdt_mg", serde_json::json!(pdt.pdt_mg));map.insert("pdt_fe", serde_json::json!(pdt.pdt_fe));map.insert("pdt_ti", serde_json::json!(pdt.pdt_ti));map.insert("pdt_ka", serde_json::json!(pdt.pdt_ka));map.insert("pdt_na", serde_json::json!(pdt.pdt_na));map.insert("pdt_mn", serde_json::json!(pdt.pdt_mn));map.insert("pdt_date", serde_json::json!(pdt.pdt_date));map}).collect();// 將HashMap序列化為JSON字符串// 添加調試日志//println!("Input data to Python script: {:?}", data);let input_data = serde_json::to_string(&data).map_err(|e| e.to_string())?;// 添加調試日志//println!("JSON input data: {}", input_data);// 創建Python進程并將數據通過標準輸入傳遞let mut command = Command::new("E:/python_envs/eric7/python.exe").arg(resource_path).stdin(std::process::Stdio::piped()).stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped()).spawn().map_err(|e| e.to_string())?;// 將JSON數據寫入Python進程的標準輸入if let Some(stdin) = command.stdin.as_mut() {Write::write_all(stdin, input_data.as_bytes()).map_err(|e| e.to_string())?;}// 等待命令完成并獲取輸出let output = command.wait_with_output().map_err(|e| e.to_string())?;if output.status.success() {let image_data = String::from_utf8(output.stdout).map_err(|e| e.to_string())?.trim().to_string();Ok(image_data)} else {let error_message = String::from_utf8(output.stderr).map_err(|e| e.to_string())?;Err(error_message)}
}#[tauri::command]
fn plotters_acid_rust(productdata: Vec<Pdt>) -> Result<String, String> {// 參數列表let para_list = [[1375.76, 122.29, 1.06247, 1.57233, 1.61648, 1.44738, 1.92899, 1.47337],[1272.64, 117.64, 1.05336, 1.42246, 1.48036, 1.51099, 1.86207, 1.36590],[1192.44, 112.99, 1.03567, 1.27336, 1.43136, 1.41448, 1.65966, 1.20929]];// 使用SVG后端實現抗鋸齒let mut svg_buffer = String::new();{let root = SVGBackend::with_string(&mut svg_buffer, (2400, 2000)).into_drawing_area();root.fill(&WHITE).map_err(|e| e.to_string())?;let mut ymax = vec![0.0; productdata.len()];// 存儲每個產品的t0, b1, b0值let mut params = Vec::with_capacity(productdata.len());// 處理每個產品數據 - 第一次循環計算并存儲參數for (idx, pdt) in productdata.iter().enumerate() {// 計算T1, T2, T3let mut t = [0.0; 3];for (i, para) in para_list.iter().enumerate() {let [a, b0, b1, b2, b3, b4, b5, b6] = para;// 計算溫度參數公式let numerator = b0 - pdt.pdt_si - b1 * pdt.pdt_al;let denominator = b2 * pdt.pdt_ca+ b3 * pdt.pdt_mg+ b4 * (pdt.pdt_na + pdt.pdt_ka)+ b5 * pdt.pdt_fe * 2.0 / 3.0 * 71.8444 * 2.0 / 159.6882+ b6 * pdt.pdt_fe / 3.0;t[i] = a * (numerator / denominator);}// 計算T0, B1, B0并存儲let t0 = (t[0] * t[1] + t[1] * t[2] - 2.0 * t[0] * t[2]) / (t[0] - 2.0 * t[1] + t[2]);let b1 = (t[0] + t0) * (t[1] + t0) / (t[0] - t[1]) / 2.0;let b0 = 1.5 - b1 / (t[0] + t0);params.push((t0, b1, b0));// 計算當前產品的ymax并存儲let x_min = 1300.0;let exponent = b0 + b1 / (x_min + t0);ymax[idx] = (exponent * std::f64::consts::LN_10).exp() / 10.0;}// 計算ymax的最大值并向上取偶let max_value = *ymax.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();let max_even = ((max_value.ceil() as i32 + 1) & !1) as f64;// 創建圖表let mut chart = ChartBuilder::on(&root).caption("巖礦棉溫粘曲線", ("微軟雅黑", 96).into_font()) // 字體放大到140.margin(80)  // 增大邊距.x_label_area_size(120)  // 增大X軸標簽區域.y_label_area_size(120)  // 增大Y軸標簽區域.build_cartesian_2d(1300f64..1600f64, 0f64..max_even).map_err(|e| e.to_string())?;// 配置網格chart.configure_mesh().x_labels(16).y_labels((0..=max_even as usize).count()) // 根據Y軸范圍設置標簽數量.x_desc("溫度T/℃").y_desc("動力粘度η/Pa·s").x_label_style(("微軟雅黑", 48).into_font()) // 字體放大到80.y_label_style(("微軟雅黑", 48).into_font()).light_line_style(BLACK.mix(0.15)).bold_line_style({let style = BLACK.mix(0.5).stroke_width(2);style}) // 加粗刻度線.x_label_formatter(&|x| format!("{:.0}", x))    //標簽格式,小數位數0.y_label_formatter(&|y| format!("{:.0}", y)).draw().map_err(|e| e.to_string())?;// 手動繪制紅色網格線let drawing_area = chart.plotting_area();for y in [2.0, 5.0] {if y <= max_even {drawing_area.draw(&PathElement::new(vec![(1300.0, y), (1600.0, y)],ShapeStyle {color: RED.to_rgba(),filled: false,stroke_width: 5,})).map_err(|e| e.to_string())?;}}// 處理每個產品數據 - 第二次循環使用存儲的參數for (idx, pdt) in productdata.iter().enumerate() {// 從存儲的參數中獲取t0, b1, b0let (t0, b1, b0) = params[idx];// 繪制曲線let color = Palette99::pick(idx);// 使用PathElement繪制更平滑的曲線let points: Vec<_> = (1300000..1600000).step_by(1).map(|x| {let x_val = x as f64 / 1000.0;let exponent = b0 + b1 / (x_val + t0);let y_val = (exponent * std::f64::consts::LN_10).exp() / 10.0;(x_val, y_val)}).collect();chart.draw_series(vec![PathElement::new(points,ShapeStyle {color: color.to_rgba(),filled: false,stroke_width: 8,})]).map_err(|e| e.to_string())?.label(&pdt.pdt_name).legend(move |(x, y)| {Rectangle::new([(x - 35, y - 5), (x + 35, y + 5)], color.filled())  // 將圖例線段寬度從20px增加到70px});}// 繪制圖例let legend_bg_style = WHITE.mix(0.8);let legend_border_style = BLACK.stroke_width(4);  // 邊框調整為5pxchart.configure_series_labels().position(SeriesLabelPosition::UpperRight).background_style(legend_bg_style).border_style(legend_border_style).label_font(("微軟雅黑", 48))  // 字體調整為72pt.margin(75)  // 邊距調整為75px.legend_area_size(90)  // 圖例區調整為90px.draw().map_err(|e| e.to_string())?;// 將圖表寫入緩沖區root.present().map_err(|e| e.to_string())?;}// 將 SVG 數據轉換為 Base64 編碼的字符串let base64_data = STANDARD.encode(&svg_buffer);// 返回 Base64 編碼的 SVG 數據Ok(format!("data:image/svg+xml;base64,{}", base64_data))
}#[tauri::command]
async fn send_department_db(state: tauri::State<'_, DbState>) -> Result<Vec<Department>, String> {let db = &state.db;let query_result:Vec<Department> = sqlx::query_as::<_, Department>(       //查詢數據以特定的格式輸出"SELECT * FROM departments").fetch(db).try_collect().await.unwrap();Ok(query_result)
}#[tauri::command]
async fn write_department_db(state: tauri::State<'_, DbState>, department:DepartmentSend) -> Result<String, String> {let db = &state.db;match sqlx::query("INSERT INTO departments (name) VALUES (?1)").bind(&department.name).execute(db).await{Ok(_) => Ok(String::from("SUCCESS! 插入數據成功!")),Err(e) => {let err_msg = e.to_string();if err_msg.contains("UNIQUE constraint failed") {Err(format!("ERROR! 部門名稱 '{}' 已存在!!", department.name))} else {Err(format!("數據庫錯誤: {}", err_msg))}}}
}#[tauri::command]
async fn del_department_item(state: tauri::State<'_, DbState>, selectedlist:Vec<i64>) -> Result<String, String> {// 參數名productlist必須與前端定義的結構變量SelectedPdtArgs的鍵值一致let db = &state.db;// 處理空數組的情況if selectedlist.is_empty() {return Err(String::from("刪除失敗:未提供有效的產品ID"));}// 生成動態占位符(根據數組長度生成 ?, ?, ?)let placeholders = vec!["?"; selectedlist.len()].join(", ");let query_str = format!("DELETE FROM departments WHERE id IN ({})",placeholders);// 構建查詢并綁定參數let mut query = sqlx::query(&query_str);for id in &selectedlist {query = query.bind(id);}// 執行刪除操作let result = query.execute(db).await.map_err(|e| format!("刪除失敗: {}", e))?;// 檢查實際刪除的行數if result.rows_affected() == 0 {return Err(String::from("刪除失敗:未找到匹配的產品"));}Ok(format!("SUCCESS! 成功刪除 {} 條數據!", result.rows_affected()))}#[tauri::command]
async fn send_worktype_db(state: tauri::State<'_, DbState>) -> Result<Vec<Worktype>, String> {let db = &state.db;let query_result:Vec<Worktype> = sqlx::query_as::<_, Worktype>(       //查詢數據以特定的格式輸出"SELECT * FROM work_types").fetch(db).try_collect().await.unwrap();Ok(query_result)
}#[tauri::command]
async fn write_worktype_db(state: tauri::State<'_, DbState>, worktype:WorktypeSend) -> Result<String, String> {let db = &state.db;match sqlx::query("INSERT INTO work_types (name) VALUES (?1)").bind(&worktype.name).execute(db).await{Ok(_) => Ok(String::from("SUCCESS! 插入數據成功!")),Err(e) => {let err_msg = e.to_string();if err_msg.contains("UNIQUE constraint failed") {Err(format!("ERROR! 工作類型 '{}' 已存在!!", worktype.name))} else {Err(format!("數據庫錯誤: {}", err_msg))}}}
}#[tauri::command]
async fn write_progress_record(state: tauri::State<'_, DbState>, progressrecord: ProgressRecordSend) -> Result<String, String> {let db = &state.db;// 檢查工作是否已完成let is_completed: i64 = sqlx::query_scalar("SELECT is_completed FROM works WHERE id = ?").bind(&progressrecord.work_id).fetch_one(db).await.map_err(|e| format!("ERROR! 查詢工作狀態失敗: {}", e))?;if is_completed == 1 {return Err("ERROR! 該工作已完成,不能再添加進度記錄".to_string());}sqlx::query("INSERT INTO progress_records (work_id, progress_detail, recorder_id, record_date)VALUES (?1, ?2, ?3, ?4)").bind(&progressrecord.work_id).bind(&progressrecord.progress_detail).bind(&progressrecord.recorder_id).bind(&progressrecord.record_date).execute(db).await.map_err(|e| format!("ERROR! 進度記錄失敗: {}", e))?;Ok("SUCCESS! 工作進度記錄已保存!".to_string())
}#[tauri::command]
async fn del_worktype_item(state: tauri::State<'_, DbState>, selectedlist:Vec<i64>) -> Result<String, String> {// 參數名productlist必須與前端定義的結構變量SelectedPdtArgs的鍵值一致let db = &state.db;// 處理空數組的情況if selectedlist.is_empty() {return Err(String::from("刪除失敗:未提供有效的ID清單"));}// 生成動態占位符(根據數組長度生成 ?, ?, ?)let placeholders = vec!["?"; selectedlist.len()].join(", ");let query_str = format!("DELETE FROM work_types WHERE id IN ({})",placeholders);// 構建查詢并綁定參數let mut query = sqlx::query(&query_str);for id in &selectedlist {query = query.bind(id);}// 執行刪除操作let result = query.execute(db).await.map_err(|e| format!("刪除失敗: {}", e))?;// 檢查實際刪除的行數if result.rows_affected() == 0 {return Err(String::from("刪除失敗:未找到匹配的工作類型"));}Ok(format!("SUCCESS! 成功刪除 {} 種工作類型!", result.rows_affected()))}#[tauri::command]
async fn write_personnel_db(state: tauri::State<'_, DbState>, personnel:PersonnelSend) -> Result<String, String> {let db = &state.db;match sqlx::query("INSERT INTO personnel (full_name, department_id) VALUES (?1,?2)").bind(&personnel.full_name).bind(&personnel.department_id).execute(db).await{Ok(_) => Ok(format!("SUCCESS! 員工({})已成功錄入系統!", &personnel.full_name)),Err(e) => {let err_msg = e.to_string();Err(format!("數據庫寫入錯誤: {}", err_msg))}}
}#[tauri::command]
async fn send_personnel_db(state: tauri::State<'_, DbState>) -> Result<Vec<Personnel>, String> {let db = &state.db;let query_result:Vec<Personnel> = sqlx::query_as::<_, Personnel>(       //查詢數據以特定的格式輸出"SELECT * FROM personnel").fetch(db).try_collect().await.unwrap();Ok(query_result)
}#[tauri::command]
async fn del_personnel_item(state: tauri::State<'_, DbState>, selectedlist:Vec<i64>) -> Result<String, String> {// 參數名productlist必須與前端定義的結構變量SelectedPdtArgs的鍵值一致let db = &state.db;// 處理空數組的情況if selectedlist.is_empty() {return Err(String::from("刪除失敗:未提供有效員工ID清單"));}// 生成動態占位符(根據數組長度生成 ?, ?, ?)let placeholders = vec!["?"; selectedlist.len()].join(", ");let query_str = format!("DELETE FROM personnel WHERE id IN ({})",placeholders);// 構建查詢并綁定參數let mut query = sqlx::query(&query_str);for id in &selectedlist {query = query.bind(id);}// 執行刪除操作let result = query.execute(db).await.map_err(|e| format!("刪除失敗: {}", e))?;// 檢查實際刪除的行數if result.rows_affected() == 0 {return Err(String::from("刪除失敗:未找到匹配的崗位員工"));}Ok(format!("SUCCESS! 成功刪除 {} 個崗位員工!", result.rows_affected()))}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct Work {id: i64,subject: String,work_content: String,start_date: String,work_type_id: i64,is_completed: i64,
}#[derive(Serialize, Deserialize)]
struct WorkSend {subject: String,work_content: String,start_date: String,work_type_id: i64,is_completed: i64,
}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct WorkDeptsSend {work_id: i64,department_id: i64,
}#[derive(Debug, Serialize, Deserialize, FromRow)]
struct WorkPersonSend {work_id: i64,personnel_id: i64,is_main_responsible: i64
}#[derive(Debug, Serialize, Deserialize)]
struct WorkAll {id: i64,subject: String,work_content: String,start_date: String,work_type_id: i64,is_completed: i64,work_departments: Vec<WorkDeptsSend>,work_personnels: Vec<WorkPersonSend>
}#[derive(Debug, Serialize, Deserialize)]
struct WorkBack {id: i64,subject: String,work_content: String,start_date: String,work_type_id: i64,is_completed: i64,work_departments: Vec<Department>,work_personnels: Vec<Personnel>,responsile_person: Vec<Personnel>
}#[tauri::command]
async fn send_work_db(state: tauri::State<'_, DbState>) -> Result<Vec<WorkAll>, String> {let db = &state.db;// 查詢works表獲取工作基本信息let works: Vec<Work> = sqlx::query_as::<_, Work>("SELECT * FROM works").fetch(db).try_collect().await.map_err(|e| format!("查詢工作數據失敗: {}", e))?;let mut result = Vec::new();for work in works {// 查詢關聯部門let departments: Vec<WorkDeptsSend> = sqlx::query_as::<_, WorkDeptsSend>("SELECT work_id, department_id FROM work_departments WHERE work_id = ?").bind(work.id).fetch(db).try_collect().await.map_err(|e| format!("查詢工作關聯部門失敗: {}", e))?;// 查詢關聯人員let personnels: Vec<WorkPersonSend> = sqlx::query_as::<_, WorkPersonSend>("SELECT work_id, personnel_id, is_main_responsible FROM work_personnel WHERE work_id = ?").bind(work.id).fetch(db).try_collect().await.map_err(|e| format!("查詢工作關聯人員失敗: {}", e))?;result.push(WorkAll {id: work.id,subject: work.subject,work_content: work.work_content,start_date: work.start_date,work_type_id: work.work_type_id,is_completed: work.is_completed,work_departments: departments,work_personnels: personnels});}Ok(result)
}#[tauri::command]
async fn send_work_list(state: tauri::State<'_, DbState>) -> Result<Vec<Work>, String> {let db = &state.db;// 查詢works表獲取工作基本信息let works: Vec<Work> = sqlx::query_as::<_, Work>("SELECT * FROM works").fetch(db).try_collect().await.map_err(|e| format!("查詢工作數據失敗: {}", e))?;Ok(works)
}#[tauri::command]
async fn send_one_work(state: tauri::State<'_, DbState>, workid:i64) -> Result<WorkBack, String> {let db = &state.db;// 開始事務let mut tx = db.begin().await.map_err(|e| format!("事務開始失敗: {}", e))?;// 查詢works表獲取工作基本信息let work: Work = sqlx::query_as::<_, Work>("SELECT * FROM works WHERE id = ?").bind(workid).fetch_one(&mut *tx).await.map_err(|e| format!("查詢工作數據失敗: {}", e))?;// 查詢關聯部門IDlet department_ids: Vec<WorkDeptsSend> = sqlx::query_as::<_, WorkDeptsSend>("SELECT work_id, department_id FROM work_departments WHERE work_id = ?").bind(workid).fetch_all(&mut *tx).await.map_err(|e| format!("查詢工作關聯部門失敗: {}", e))?;// 查詢部門詳細信息let mut work_departments = Vec::new();for dept in &department_ids {let department: Department = sqlx::query_as::<_, Department>("SELECT * FROM departments WHERE id = ?").bind(dept.department_id).fetch_one(&mut *tx).await.map_err(|e| format!("查詢部門詳細信息失敗: {}", e))?;work_departments.push(department);}// 查詢關聯人員IDlet personnel_ids: Vec<WorkPersonSend> = sqlx::query_as::<_, WorkPersonSend>("SELECT work_id, personnel_id, is_main_responsible FROM work_personnel WHERE work_id = ?").bind(workid).fetch_all(&mut *tx).await.map_err(|e| format!("查詢工作關聯人員失敗: {}", e))?;// 查詢人員詳細信息let mut work_personnels = Vec::new();let mut responsile_person = Vec::new();for person in &personnel_ids {let personnel: Personnel = sqlx::query_as::<_, Personnel>("SELECT * FROM personnel WHERE id = ?").bind(person.personnel_id).fetch_one(&mut *tx).await.map_err(|e| format!("查詢人員詳細信息失敗: {}", e))?;work_personnels.push(personnel.clone());if person.is_main_responsible == 1 {responsile_person.push(personnel);}}// 提交事務tx.commit().await.map_err(|e| format!("事務提交失敗: {}", e))?;let result = WorkBack {id: work.id,subject: work.subject,work_content: work.work_content,start_date: work.start_date,work_type_id: work.work_type_id,is_completed: work.is_completed,work_departments,work_personnels,responsile_person};Ok(result)
}#[tauri::command]
async fn send_progress_record(state: tauri::State<'_, DbState>, workid:i64) -> Result<Vec<ProgressRecord>, String> {let db = &state.db;let records: Vec<ProgressRecord> = sqlx::query_as::<_, ProgressRecord>("SELECT id, progress_detail, recorder_id, record_dateFROM progress_recordsWHERE work_id = ?ORDER BY record_date ASC").bind(workid).fetch_all(db).await.map_err(|e| format!("查詢進度記錄失敗: {}", e))?;Ok(records)
}#[tauri::command]
async fn del_progress_record(state: tauri::State<'_, DbState>, selectedrecord: i64) -> Result<String, String> {let db = &state.db;if selectedrecord <= 0 {return Err(String::from("刪除失敗:未提供有效的記錄ID"));}let result = sqlx::query("DELETE FROM progress_records WHERE id = ?").bind(selectedrecord).execute(db).await.map_err(|e| format!("刪除失敗: {}", e))?;if result.rows_affected() == 0 {return Err(String::from("刪除失敗:未找到匹配的記錄"));}Ok(format!("SUCCESS! 成功刪除進度記錄!"))
}#[tauri::command]
async fn change_work_state(state: tauri::State<'_, DbState>, workid: i64) -> Result<i64, String> {let db = &state.db;if workid <= 0 {return Err("ERROR! 無效的工作ID".to_string());}// 獲取當前狀態let current_state: i64 = sqlx::query_scalar("SELECT is_completed FROM works WHERE id = ?").bind(workid).fetch_one(db).await.map_err(|e| format!("ERROR! 查詢工作狀態失敗: {}", e))?;// 切換狀態let new_state = if current_state == 1 { 0 } else { 1 };// 更新狀態sqlx::query("UPDATE works SET is_completed = ? WHERE id = ?").bind(new_state).bind(workid).execute(db).await.map_err(|e| format!("ERROR! 更新工作狀態失敗: {}", e))?;Ok(new_state)
}#[tauri::command]
async fn write_work_db(state: tauri::State<'_, DbState>, work: WorkSend) -> Result<i64, String> {let db = &state.db;sqlx::query("INSERT INTO works (subject, work_content, start_date, work_type_id, is_completed)VALUES (?1, ?2, ?3, ?4, ?5)").bind(&work.subject).bind(&work.work_content).bind(&work.start_date).bind(&work.work_type_id).bind(&work.is_completed).execute(db).await.map_err(|e| format!("數據庫寫入錯誤: {}", e))?;// 獲取最后插入的ID - 查詢works表中的最大id并確保返回整數let id: i64 = sqlx::query_scalar("SELECT CAST(MAX(id) AS INTEGER) FROM works").fetch_one(db).await.map_err(|e| format!("獲取ID失敗: {}", e))?;Ok(id)
}#[tauri::command]
async fn del_work_item(state: tauri::State<'_, DbState>, selectedlist: Vec<i64>) -> Result<String, String> {let db = &state.db;if selectedlist.is_empty() {return Err(String::from("刪除失敗:未提供有效工作ID清單"));}let placeholders = vec!["?"; selectedlist.len()].join(", ");let query_str = format!("DELETE FROM works WHERE id IN ({})", placeholders);let mut query = sqlx::query(&query_str);for id in &selectedlist {query = query.bind(id);}let result = query.execute(db).await.map_err(|e| format!("刪除失敗: {}", e))?;if result.rows_affected() == 0 {return Err(String::from("刪除失敗:未找到匹配的工作"));}Ok(format!("SUCCESS! 成功刪除 {} 個工作!", result.rows_affected()))
}#[tauri::command]
async fn write_work_depts_db(state: tauri::State<'_, DbState>, workdepts: Vec<WorkDeptsSend>) -> Result<String, String> {let db = &state.db;if workdepts.is_empty() {return Err("ERROR! 未提供有效的部門關聯數據".to_string());}// 開始事務let mut tx = db.begin().await.map_err(|e| format!("ERROR! 事務開始失敗: {}", e))?;// 批量插入for workdept in &workdepts {match sqlx::query("INSERT INTO work_departments (work_id, department_id) VALUES (?1, ?2)").bind(workdept.work_id).bind(workdept.department_id).execute(&mut *tx).await {Ok(_) => (),Err(e) => {if let Err(rollback_err) = tx.rollback().await {return Err(format!("ERROR! 關聯部門失敗: {}, 回滾失敗: {}", e, rollback_err));}return Err(format!("ERROR! 關聯部門失敗: {}", e));}}}// 提交事務tx.commit().await.map_err(|e| format!("ERROR! 事務提交失敗: {}", e))?;Ok(format!("SUCCESS! 成功關聯 {} 個工作與部門!", workdepts.len()))
}#[tauri::command]
async fn write_work_personnel_db(state: tauri::State<'_, DbState>, workpersonnels: Vec<WorkPersonSend>) -> Result<String, String> {let db = &state.db;if workpersonnels.is_empty() {return Err("ERROR! 未提供有效的人員關聯數據".to_string());}// 開始事務let mut tx = match db.begin().await {Ok(tx) => tx,Err(e) => return Err(format!("ERROR! 事務開始失敗: {}", e)),};// 批量插入for workperson in &workpersonnels {if let Err(e) = sqlx::query("INSERT INTO work_personnel (work_id, personnel_id, is_main_responsible) VALUES (?1, ?2, ?3)").bind(workperson.work_id).bind(workperson.personnel_id).bind(workperson.is_main_responsible).execute(&mut *tx).await {if let Err(rollback_err) = tx.rollback().await {return Err(format!("ERROR! 關聯人員失敗: {}, 回滾失敗: {}", e, rollback_err));}return Err(format!("ERROR! 關聯人員失敗: {}", e));}}// 提交事務match tx.commit().await {Ok(_) => Ok(format!("SUCCESS! 成功關聯 {} 個人員!", workpersonnels.len())),Err(e) => Err(format!("ERROR! 事務提交失敗: {}", e)),}
}#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {tauri::Builder::default().plugin(tauri_plugin_opener::init()).invoke_handler(tauri::generate_handler![show_splashscreen_window,close_splashscreen,navigate_to,update_user,close_main_window,write_pdt_db,send_pdt_db,del_selected_pdt,python_plot,python_acid_plot,plotters_acid_rust,send_selected_pdt,send_department_db,write_department_db,del_department_item,send_worktype_db,write_worktype_db,del_worktype_item,write_personnel_db,send_personnel_db,del_personnel_item,write_work_db,send_work_db,send_one_work,send_work_list,del_work_item,get_personnel_by_department,write_work_depts_db,write_work_personnel_db,write_progress_record,send_progress_record,del_progress_record,change_work_state]).setup(|app| {#[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.pswp.cn/bicheng/83656.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/83656.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/83656.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

java面試 網絡編程與 Java I/O:技術要點解析

java面試 網絡編程與 Java I/O&#xff1a;技術要點解析 網絡編程與 Java I/O&#xff1a;技術要點解析一、TCP 和 UDP 的區別TCP&#xff08;Transfer Control Protocol&#xff09;UDP&#xff08;User Datagram Protocol&#xff09;TCP 的三次握手與四次揮手 二、Java 的幾…

PhpStorm設置中文

環境信息 系統版本&#xff1a;Windows11 22H2 PhpStorm版本&#xff1a;2025.1.1【Build #PS-251.25410.148】 設置中文 PhpStorm并不需要安裝插件或下載相應的漢化包進行漢化 依次點擊點擊&#xff1a; file或右上角設置按鈕→ 進入Settings→ 找到Appearance & Behav…

【監控】Spring Boot 應用監控

這段配置是 Spring Boot 應用中對 Actuator 和 Micrometer 監控系統的配置&#xff0c;用于將應用的指標暴露給 Prometheus 進行收集。下面我將詳細介紹這種配置方式及其提供的指標。 配置說明 這個配置主要涉及 Spring Boot Actuator 和 Micrometer 兩個核心組件&#xff1a…

學習筆記(23): 機器學習之數據預處理Pandas和轉換成張量格式[1]

學習筆記(23): 機器學習之數據預處理Pandas和轉換成張量格式[1] 學習機器學習&#xff0c;需要學習如何預處理原始數據&#xff0c;這里用到pandas&#xff0c;將原始數據轉換為張量格式的數據。 1、安裝pandas pip install pandas 2、寫入和讀取數據 >>創建一個人工…

一臺電腦聯網如何共享另一臺電腦?網線方式

前言 公司內網一個人只能申請一個賬號和一個主機設備&#xff1b;會檢測MAC地址&#xff1b;如果有兩臺設備&#xff0c;另一臺就沒有網&#xff1b;因為是聯想老電腦&#xff0c;共享熱點用不了&#xff0c;但是有一根網線&#xff0c;現在解決網線方式共享網絡&#xff1b; …

Spring Boot 基礎知識全面解析:快速構建企業級應用的核心指南

一、Spring Boot 概述&#xff1a;重新定義 Java 開發 1.1 什么是 Spring Boot&#xff1f; Spring Boot 是基于 Spring 框架的快速開發框架&#xff0c;旨在簡化 Spring 應用的初始搭建及開發過程。它通過 「約定優于配置」&#xff08;Convention Over Configuration&#…

CentOS-stream-9 Zabbix的安裝與配置

一、Web環境搭建部署Zabbix時&#xff0c;選擇合適的MariaDB、PHP和Nginx版本非常重要&#xff0c;以確保兼容性和最佳性能。以下是建議版本&#xff1a;Zabbix 6.4 MariaDB&#xff1a;官方文檔推薦使用MariaDB 10.3或更高版本。對于CentOS Stream 9&#xff0c;建議使用Maria…

CppCon 2014 學習:Adventures in Updating a Legacy Vintage Codebase

“VINTAGE” 部分是對現實中飛行模擬系統中遺留系統復雜性的描述。以下是對關鍵點的理解與拆解&#xff1a; 飛行模擬系統的背景 多環境、多語言、多硬件&#xff1a; 編程語言&#xff1a; 混用的“遺留語言”&#xff1a;Ada, C, C, Fortran, Jovial, PL/M, Pascal不同語言…

【計算機】計算機存儲器的分類與特性

文章目錄 一、按作用層次分類1. 主存儲器&#xff08;內存&#xff09;2. 輔助存儲器&#xff08;外存&#xff09;3. 高速緩沖存儲器&#xff08;Cache&#xff09; 二、按存儲介質分類1. 半導體存儲器2. 磁存儲技術3. 光存儲發展 三、按存取方式分類1. 隨機存儲器技術細節2. …

Redisson - 實現延遲隊列

Redisson 延遲隊列 Redisson 是基于 Redis 的一款功能強大的 Java 客戶端。它提供了諸如分布式鎖、限流器、阻塞隊列、延遲隊列等高可用、高并發組件。 其中&#xff0c;RDelayedQueue 是對 Redis 數據結構的高階封裝&#xff0c;能讓你將消息延遲一定時間后再進入消費隊列。…

上門服務小程序訂單系統框架設計

一、邏輯分析 上門服務小程序訂單系統主要涉及服務展示、用戶下單、訂單處理、服務人員接單與服務完成反饋等核心流程。 服務展示&#xff1a;不同類型的上門服務&#xff08;如家政、維修等&#xff09;需要在小程序中展示詳細信息&#xff0c;包括服務名稱、價格、服務內容介…

Android apk裝機編譯類型: verify、speed-profile, speed與啟動耗時

Android apk裝機編譯類型: verify、speed-profile, speed與啟動耗時 Dex2oat (dalvik excutable file to optimized art file) &#xff0c;對 dex 文件進行編譯優化&#xff0c;Android 虛擬機可識別的是dex文件&#xff0c;應用運行過程如果每次都將dex文件加載內存&#xff…

winrm登錄失敗,指定的憑據被服務器拒絕

winrm登錄失敗&#xff0c;指定的憑據被服務器拒絕。 異常提示&#xff1a;the specified credentials were rejected by the server 在windows power shell執行 set-executionpolicy remotesigned winrm quickconfig winrm set winrm/config/service/auth {Basic"true…

Unity3D ET框架游戲腳本系統解析

前言 ET框架在Unity3D中實現的GamePlay腳本系統是一種革命性的、基于ECS&#xff08;實體-組件-系統&#xff09;架構的設計&#xff0c;它徹底改變了傳統的基于MonoBehaviour的游戲邏輯編寫方式。其核心思想是追求高性能、高解耦、易熱更新&#xff0c;特別適合大型復雜的網絡…

android與Qt類比

一、概念對應關系 Android RecyclerView 組件類比描述Qt 模型 - 視圖組件Qt 類比描述RecyclerView畫板&#xff08;容器&#xff09;QAbstractItemView視圖&#xff08;展示數據的容器&#xff0c;如列表、表格&#xff09;RecyclerView.Adapter畫布&#xff08;數據橋梁&…

Jenkins 2.479.1安裝和郵箱配置教程

1.安裝 在JDK安裝并設置環境變量完成后&#xff0c;下載官網對應的war版本&#xff0c;在對應目錄下打開命令行窗口并輸入 java -jar jenkins.war其余參數感興趣可以自行查閱&#xff0c;這里啟動的 jenkins 服務默認占用8080端口&#xff0c;在瀏覽器輸入 localhost:8080進入…

多分辨率 LCD 的 GUI 架構設計與實現

1.1多分辨率顯示系統的挑戰與解決方案 1.1.1 分辨率適配的核心問題 在嵌入式系統中,同時支持不同分辨率的 LCD(如 240160、320480 等)面臨以下挑戰: 布局適配:同一界面元素在不同分辨率下需要調整大小和位置 字體顯示:小分辨率屏幕需要更小的字體,而大分辨率需要更清…

11. MySQL事務管理(上)

1. CURD不加控制&#xff0c;會有什么問題&#xff1f; 火車票售票系統tickets表 id name nums 10 西安<->蘭州 1 客戶端A 客戶端B if (nums > 0) { if (nums > 0) { 賣票 賣票 // update numsnums - 1 update numsnums - 1 } } 當客戶端A檢查還有一張票時&#xf…

Beta分布Dirichlet分布

目錄 Beta分布Dirichlet分布Beta分布&Dirichlet分布從Dirichlet分布生成Beta樣本Beta分布&Dirichlet分布應用 Beta分布 Beta分布是定義在區間 [ 0 , 1 ] [0, 1] [0,1]上的連續概率分布&#xff0c;通常用于模擬概率或比例的隨機變量。Beta分布的概率密度函數&#xff…

嵌入式系統中常用的開源協議

目錄 1、GNU通用公共許可證&#xff08;GPL&#xff09; 2、GNU寬松通用公共許可證&#xff08;LGPL&#xff09; 3、MIT許可證 4、Apache許可證2.0 5、BSD許可證 6、如何選擇合適的協議 在嵌入式系統開發中&#xff0c;開源軟件的使用已成為主流趨勢。從物聯網設備到汽車…