在前期工作(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, --所属部门ID
FOREIGN 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 或 1
FOREIGN 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;">
<input
type="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;">
<input
type="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>
<input
type="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,
},
};
//写入工作数据库,返回工作ID
set_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,然后手动转换为i64
match 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;">
<input
type="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 { "进行中" }}
<button
style="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">
<input
type="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">
<select
style="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;">
<button
style="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">
<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_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">
<input
type="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">
<input
type="checkbox"
prop:checked=move || work_state.get() == 1
on: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;">
<input
type="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_depts
set_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中移除部门ID
set_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}
<input
type="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_date
FROM progress_records
WHERE 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_table
sqlx::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结构体转换为HashMap
let 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, T3
let 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, b0
let (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); // 边框调整为5px
chart.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))?;
// 查询关联部门ID
let 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);
}
// 查询关联人员ID
let 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_date
FROM progress_records
WHERE 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程序的时候出错!");
}
至此,工作进度管理桌面小程序基本完成。