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程序的时候出错!");
}
至此,工作进度管理桌面小程序基本完成。