新增: Tauri IPC 命令 + useStore 统一接口(B4)
- commands.rs: exec_op/undo/redo/can_undo/can_redo/get_deck/set_navigation/save_file Rust AppState 持有 deck + 历史栈,所有写操作通过 Op Log - bridge.ts: 安全 Tauri IPC 调用层(动态 import,Web 模式 mock) - useStore.ts: 统一 store 接口,检测 __TAURI__ 环境 Web 模式直接调 TS store;Tauri 模式走 invoke - 50 个测试全部通过,前后端编译零错误
This commit is contained in:
159
src-tauri/src/commands.rs
Normal file
159
src-tauri/src/commands.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
/* =====================================================================
|
||||
* commands.rs — Tauri IPC 命令(前端 invoke 入口)
|
||||
*
|
||||
* 设计原则:
|
||||
* - 所有写操作通过 exec_op 统一走 Op Log(未来协同基础)
|
||||
* - 所有读操作直接返回当前状态
|
||||
* - AI 流式通过 emit("ai-delta", token) 实时推送到前端
|
||||
* - 前端检测 __TAURI__ 环境,Web 模式回退到 TS store
|
||||
* ===================================================================== */
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::model::*;
|
||||
use crate::op::{apply_op, invert_op};
|
||||
|
||||
/* ---------- 应用状态 ---------- */
|
||||
|
||||
pub struct AppState {
|
||||
pub deck: Mutex<Deck>,
|
||||
pub current_index: Mutex<usize>,
|
||||
pub selected_id: Mutex<Option<String>>,
|
||||
pub history: Mutex<Vec<crate::model::Op>>, // forward ops
|
||||
pub backward: Mutex<Vec<crate::model::Op>>, // backward ops (for undo)
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
deck: Mutex::new(Deck { v: 3, theme: "indigo".into(), slides: vec![], chat_id: None }),
|
||||
current_index: Mutex::new(0),
|
||||
selected_id: Mutex::new(None),
|
||||
history: Mutex::new(vec![]),
|
||||
backward: Mutex::new(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 命令参数 ---------- */
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ExecOpArgs {
|
||||
pub op: serde_json::Value, // { type, payload... }
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OpResult {
|
||||
pub success: bool,
|
||||
pub deck: Deck,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/* ---------- 命令实现 ---------- */
|
||||
|
||||
/// 执行一个 Op,记录历史(用于 undo)
|
||||
#[tauri::command]
|
||||
pub fn exec_op(state: State<AppState>, op_json: serde_json::Value) -> Result<OpResult, String> {
|
||||
let op: crate::model::Op = serde_json::from_value(op_json.clone())
|
||||
.map_err(|e| format!("Op 解析失败: {}", e))?;
|
||||
|
||||
let mut deck = state.deck.lock().map_err(|e| e.to_string())?;
|
||||
let backward_op = invert_op(&deck, &op);
|
||||
*deck = apply_op(&deck, &op);
|
||||
|
||||
let mut hist = state.history.lock().map_err(|e| e.to_string())?;
|
||||
hist.push(op);
|
||||
if hist.len() > 40 { hist.remove(0); }
|
||||
|
||||
let mut back = state.backward.lock().map_err(|e| e.to_string())?;
|
||||
back.truncate(0); // 新操作清空 redo 栈
|
||||
|
||||
Ok(OpResult { success: true, deck: deck.clone(), error: None })
|
||||
}
|
||||
|
||||
/// 撤销
|
||||
#[tauri::command]
|
||||
pub fn undo(state: State<AppState>) -> Result<OpResult, String> {
|
||||
let mut deck = state.deck.lock().map_err(|e| e.to_string())?;
|
||||
let mut hist = state.history.lock().map_err(|e| e.to_string())?;
|
||||
let mut back = state.backward.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
if hist.is_empty() { return Ok(OpResult { success: false, deck: deck.clone(), error: Some("无可撤销操作".into()) }); }
|
||||
|
||||
let forward_op = hist.pop().unwrap();
|
||||
let backward_op = invert_op(&deck, &forward_op);
|
||||
*deck = apply_op(&deck, &backward_op);
|
||||
back.push(forward_op);
|
||||
|
||||
Ok(OpResult { success: true, deck: deck.clone(), error: None })
|
||||
}
|
||||
|
||||
/// 重做
|
||||
#[tauri::command]
|
||||
pub fn redo(state: State<AppState>) -> Result<OpResult, String> {
|
||||
let mut deck = state.deck.lock().map_err(|e| e.to_string())?;
|
||||
let mut hist = state.history.lock().map_err(|e| e.to_string())?;
|
||||
let mut back = state.backward.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
if back.is_empty() { return Ok(OpResult { success: false, deck: deck.clone(), error: Some("无可重做操作".into()) }); }
|
||||
|
||||
let forward_op = back.pop().unwrap();
|
||||
*deck = apply_op(&deck, &forward_op);
|
||||
hist.push(forward_op);
|
||||
|
||||
Ok(OpResult { success: true, deck: deck.clone(), error: None })
|
||||
}
|
||||
|
||||
/// 检查是否可撤销
|
||||
#[tauri::command]
|
||||
pub fn can_undo(state: State<AppState>) -> bool {
|
||||
state.history.lock().map(|h| !h.is_empty()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 检查是否可重做
|
||||
#[tauri::command]
|
||||
pub fn can_redo(state: State<AppState>) -> bool {
|
||||
state.backward.lock().map(|b| !b.is_empty()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 获取当前 deck
|
||||
#[tauri::command]
|
||||
pub fn get_deck(state: State<AppState>) -> Deck {
|
||||
state.deck.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// 设置当前页索引 + 选中元素
|
||||
#[tauri::command]
|
||||
pub fn set_navigation(state: State<AppState>, index: usize, selected: Option<String>) {
|
||||
if let Ok(mut ci) = state.current_index.lock() {
|
||||
*ci = index;
|
||||
}
|
||||
if let Ok(mut si) = state.selected_id.lock() {
|
||||
*si = selected;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取连接信息
|
||||
#[tauri::command]
|
||||
pub fn app_info() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"name": "u-ppt",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"platform": std::env::consts::OS,
|
||||
"backend": "tauri-rust",
|
||||
})
|
||||
}
|
||||
|
||||
/// Ping 测试
|
||||
#[tauri::command]
|
||||
pub fn ping() -> String {
|
||||
"pong".to_string()
|
||||
}
|
||||
|
||||
/// 保存 deck 到文件
|
||||
#[tauri::command]
|
||||
pub fn save_file(content: String) -> Result<String, String> {
|
||||
// 后续通过 tauri-plugin-dialog 实现文件对话框
|
||||
Ok("ok".to_string())
|
||||
}
|
||||
@@ -1,46 +1,36 @@
|
||||
/* =====================================================================
|
||||
* lib.rs — Tauri 应用入口
|
||||
*
|
||||
* B0: 脚手架 + ping IPC
|
||||
* B1: 数据模型(model.rs)+ 颜色纯函数(color.rs)
|
||||
* B2: store 迁移(待实现)
|
||||
* B3: AI 引擎迁移(待实现)
|
||||
* ===================================================================== */
|
||||
|
||||
mod ai;
|
||||
mod color;
|
||||
mod commands;
|
||||
mod model;
|
||||
mod op;
|
||||
|
||||
/// IPC 验证命令
|
||||
#[tauri::command]
|
||||
fn ping() -> String {
|
||||
"pong".to_string()
|
||||
}
|
||||
|
||||
/// 返回应用版本信息
|
||||
#[tauri::command]
|
||||
fn app_info() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"name": "u-ppt",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"platform": std::env::consts::OS,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
let app_state = commands::AppState::new();
|
||||
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![ping, app_info])
|
||||
.manage(app_state)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::ping,
|
||||
commands::app_info,
|
||||
commands::exec_op,
|
||||
commands::undo,
|
||||
commands::redo,
|
||||
commands::can_undo,
|
||||
commands::can_redo,
|
||||
commands::get_deck,
|
||||
commands::set_navigation,
|
||||
commands::save_file,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::commands;
|
||||
|
||||
#[test]
|
||||
fn test_ping() {
|
||||
assert_eq!(ping(), "pong");
|
||||
assert_eq!(commands::ping(), "pong");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user