Initial commit: Update project structure and add new features
This commit is contained in:
@@ -0,0 +1 @@
|
||||
v24.15.0
|
||||
@@ -1,7 +0,0 @@
|
||||
# Tauri + Vue + TypeScript
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
|
||||
+7
-4
@@ -11,13 +11,16 @@
|
||||
"tdev": "tauri dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lucide/vue": "^1.6.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-notification": "~2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-sql": "~2.3.2",
|
||||
"daisyui": "^5.5.19",
|
||||
"lucide-vue-next": "^0.577.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^5.0.3"
|
||||
"vue-router": "^5.0.3",
|
||||
"vue3-hot-toast": "^0.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
@@ -25,8 +28,8 @@
|
||||
"@types/node": "^25.3.5",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.3",
|
||||
"vitest": "^4.0.18",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.1.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+510
-369
File diff suppressed because it is too large
Load Diff
Generated
+2943
-31
File diff suppressed because it is too large
Load Diff
@@ -22,4 +22,10 @@ tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
tauri-plugin-sql = { version = "2", features = ['sqlite'] }
|
||||
tauri-plugin-notification = "2"
|
||||
iroh = { version = "0.97", features = ["address-lookup-mdns"] }
|
||||
iroh-tickets = "0.4"
|
||||
tokio = "1.50.0"
|
||||
anyhow = "1.0.102"
|
||||
chrono = "0.4"
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# LocalFinder 实现说明
|
||||
|
||||
## 📋 概述
|
||||
|
||||
已成功构建 `LocalFinder` 结构的基础框架,用于 Iroh 的局域网设备发现功能。由于 Iroh v0.97 API 发生重大变更,当前实现为占位符版本。
|
||||
|
||||
## ✅ 已完成的工作
|
||||
|
||||
### 1. 文件结构
|
||||
|
||||
创建了以下文件:
|
||||
|
||||
- **`src/iroh/local_find.rs`** - LocalFinder 核心实现
|
||||
- **`src/iroh/mod.rs`** - Iroh 模块导出
|
||||
- **Tauri Commands** - 在 `handlers/tauri_handlers.rs` 中添加了 5 个新的命令处理器
|
||||
|
||||
### 2. LocalFinder 结构
|
||||
|
||||
```rust
|
||||
pub struct LocalFinder {
|
||||
node_id: String,
|
||||
}
|
||||
```
|
||||
|
||||
#### 已实现的方法:
|
||||
|
||||
- `new()` - 创建局域网发现管理器(占位符)
|
||||
- `local_id()` - 获取本地节点 ID
|
||||
|
||||
### 3. Tauri Commands
|
||||
|
||||
已添加以下命令供前端调用:
|
||||
|
||||
1. **`initialize_local_finder()`** - 初始化局域网发现管理器
|
||||
2. **`start_discovery()`** - 开始设备发现(占位符)
|
||||
3. **`stop_discovery()`** - 停止设备发现(占位符)
|
||||
4. **`get_local_node_id()`** - 获取本地节点 ID
|
||||
5. **`get_discovered_peers()`** - 获取发现的设备列表(占位符)
|
||||
|
||||
### 4. 集成到应用
|
||||
|
||||
- ✅ 更新了 `lib.rs` 导出 `LocalFinder`
|
||||
- ✅ 在 Tauri 应用中注册了 `LocalFinder` 状态管理
|
||||
- ✅ 将所有新 commands 添加到 invoke_handler
|
||||
|
||||
## ⚠️ 当前限制
|
||||
|
||||
### Iroh API 变更问题
|
||||
|
||||
Iroh v0.97 的 API 与早期版本有很大不同,主要问题包括:
|
||||
|
||||
1. **`Endpoint::builder()`** - 现在需要 `Preset` trait 参数
|
||||
2. **`SecretKey::generate()`** - 需要特定类型的 RNG
|
||||
3. **`MdnsAddressLookup`** - MDNS 发现 API 已变更
|
||||
4. **依赖冲突** - `rand` 和 `rand_core` 版本兼容性问题
|
||||
|
||||
### 占位符实现
|
||||
|
||||
当前代码使用占位符实现,仅用于演示结构和接口设计。
|
||||
|
||||
## 🔧 下一步工作
|
||||
|
||||
### 需要查阅的文档
|
||||
|
||||
1. **Iroh v0.97 官方文档** - https://docs.rs/iroh/latest/iroh/
|
||||
2. **Iroh GitHub 仓库** - https://github.com/n0-computer/iroh
|
||||
3. **Iroh Examples** - 查看最新的示例代码
|
||||
|
||||
### 待实现的功能
|
||||
|
||||
1. **正确的 Endpoint 初始化**
|
||||
```rust
|
||||
// 需要找到正确的方式
|
||||
let endpoint = Endpoint::builder(preset).bind().await?;
|
||||
```
|
||||
|
||||
2. **SecretKey 生成**
|
||||
```rust
|
||||
// 需要使用正确的 RNG
|
||||
let secret_key = SecretKey::generate(/* correct rng */);
|
||||
```
|
||||
|
||||
3. **MDNS 发现集成**
|
||||
```rust
|
||||
// 需要查阅新的 API
|
||||
let mdns = MdnsAddressLookup::new()?;
|
||||
endpoint.discovery().add(mdns)?;
|
||||
```
|
||||
|
||||
4. **设备连接和管理**
|
||||
- `connect_to_peer()` - 连接到发现的设备
|
||||
- `disconnect_peer()` - 断开连接
|
||||
- `get_known_peers()` - 获取已知设备列表
|
||||
|
||||
## 📝 使用示例(当前占位符)
|
||||
|
||||
### Rust 代码
|
||||
|
||||
```rust
|
||||
use crate::iroh::LocalFinder;
|
||||
|
||||
// 创建 LocalFinder
|
||||
let finder = LocalFinder::new().await?;
|
||||
|
||||
// 获取节点 ID
|
||||
let node_id = finder.local_id();
|
||||
println!("Local node ID: {}", node_id);
|
||||
```
|
||||
|
||||
### TypeScript/JavaScript 调用(前端)
|
||||
|
||||
```typescript
|
||||
// 初始化
|
||||
const nodeId = await invoke('initialize_local_finder');
|
||||
console.log(nodeId);
|
||||
|
||||
// 获取本地节点 ID
|
||||
const localId = await invoke('get_local_node_id');
|
||||
console.log(localId);
|
||||
|
||||
// 开始发现(占位符)
|
||||
await invoke('start_discovery', { durationSecs: 60 });
|
||||
|
||||
// 获取发现的设备(占位符)
|
||||
const peers = await invoke('get_discovered_peers');
|
||||
console.log(peers);
|
||||
```
|
||||
|
||||
## 🎯 建议
|
||||
|
||||
由于 Iroh API 频繁变更,建议:
|
||||
|
||||
1. **暂时移除 Iroh 依赖** - 避免编译错误
|
||||
2. **使用条件编译** - 仅在需要时启用 Iroh 功能
|
||||
3. **关注 Iroh 更新** - 订阅 Iroh 的 release notes
|
||||
4. **考虑替代方案** - 如果 Iroh 不稳定,可以考虑其他 P2P 库
|
||||
|
||||
## 📦 依赖配置
|
||||
|
||||
当前 `Cargo.toml` 中的相关依赖:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
iroh = { version = "0.97", features = ["address-lookup-mdns"] }
|
||||
iroh-tickets = "0.4"
|
||||
tokio = "1.50.0"
|
||||
anyhow = "1.0.102"
|
||||
```
|
||||
|
||||
## 📊 项目状态
|
||||
|
||||
- ✅ **LocalFinder 结构** - 完成
|
||||
- ✅ **Tauri 集成** - 完成
|
||||
- ✅ **Commands 实现** - 完成(占位符)
|
||||
- ⚠️ **实际功能** - 等待 Iroh API 研究
|
||||
|
||||
---
|
||||
|
||||
**创建时间**: 2026-04-03
|
||||
**最后更新**: 2026-04-03
|
||||
**状态**: 占位符实现,等待 Iroh API 研究
|
||||
Binary file not shown.
@@ -2,9 +2,13 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"sql:default",
|
||||
"notification:default"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||
|
||||
/// 获取所有数据库迁移
|
||||
pub fn get_migrations() -> Vec<Migration> {
|
||||
vec![
|
||||
// 版本 1: 创建初始表
|
||||
Migration {
|
||||
version: 1,
|
||||
description: "create_initial_tables",
|
||||
sql: r#"
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 工作流表
|
||||
CREATE TABLE IF NOT EXISTS workflows (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'draft',
|
||||
creator TEXT NOT NULL,
|
||||
definition TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 流程节点表
|
||||
CREATE TABLE IF NOT EXISTS workflow_nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workflow_id INTEGER NOT NULL,
|
||||
node_type TEXT NOT NULL,
|
||||
position_x REAL NOT NULL,
|
||||
position_y REAL NOT NULL,
|
||||
data TEXT,
|
||||
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 流程边表
|
||||
CREATE TABLE IF NOT EXISTS workflow_edges (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workflow_id INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
label TEXT,
|
||||
data TEXT,
|
||||
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 消息表
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
workflow_id INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL
|
||||
);
|
||||
"#,
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod migrations;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod tauri_handlers;
|
||||
|
||||
pub use tauri_handlers::TauriMessageHandler;
|
||||
@@ -0,0 +1,159 @@
|
||||
use crate::router::MessageRouter;
|
||||
use crate::iroh::LocalFinder;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub struct TauriMessageHandler {
|
||||
#[allow(dead_code)]
|
||||
router: Arc<Mutex<MessageRouter>>,
|
||||
}
|
||||
|
||||
impl TauriMessageHandler {
|
||||
pub fn new(router: MessageRouter) -> Self {
|
||||
TauriMessageHandler {
|
||||
router: Arc::new(Mutex::new(router)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_message(state: tauri::State<'_, Arc<Mutex<MessageRouter>>>, message: Value) -> Result<String, String> {
|
||||
let router = state.lock().await;
|
||||
|
||||
match router.send(message).await {
|
||||
Ok(_) => Ok("Message sent successfully".to_string()),
|
||||
Err(e) => Err(format!("Failed to send message: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_supported_message_types() -> Result<Vec<String>, String> {
|
||||
Ok(vec!["text".to_string(), "file".to_string()])
|
||||
}
|
||||
|
||||
/// 初始化局域网发现
|
||||
#[tauri::command]
|
||||
pub async fn initialize_local_finder(
|
||||
state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
|
||||
) -> Result<String, String> {
|
||||
let mut finder_opt = state.lock().await;
|
||||
|
||||
if finder_opt.is_some() {
|
||||
return Ok("Local finder already initialized".to_string());
|
||||
}
|
||||
|
||||
match LocalFinder::new().await {
|
||||
Ok(finder) => {
|
||||
let node_id = finder.local_id();
|
||||
*finder_opt = Some(finder);
|
||||
Ok(format!("Local finder initialized successfully. Node ID: {}", node_id))
|
||||
}
|
||||
Err(e) => Err(format!("Failed to initialize local finder: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始设备发现
|
||||
#[tauri::command]
|
||||
pub async fn start_discovery(
|
||||
_state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
|
||||
_duration_secs: u64,
|
||||
) -> Result<String, String> {
|
||||
Ok("Discovery functionality will be implemented in future versions".to_string())
|
||||
}
|
||||
|
||||
/// 停止设备发现
|
||||
#[tauri::command]
|
||||
pub async fn stop_discovery(
|
||||
_state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
|
||||
) -> Result<String, String> {
|
||||
Ok("Stop discovery functionality will be implemented in future versions".to_string())
|
||||
}
|
||||
|
||||
/// 获取本地节点 ID
|
||||
#[tauri::command]
|
||||
pub async fn get_local_node_id(
|
||||
state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
|
||||
) -> Result<String, String> {
|
||||
let finder_opt = state.lock().await;
|
||||
|
||||
if let Some(finder) = finder_opt.as_ref() {
|
||||
Ok(finder.local_id())
|
||||
} else {
|
||||
Err("Local finder not initialized".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取发现的设备列表
|
||||
#[tauri::command]
|
||||
pub async fn get_discovered_peers(
|
||||
state: tauri::State<'_, Arc<Mutex<Option<LocalFinder>>>>,
|
||||
) -> Result<String, String> {
|
||||
let finder_opt = state.lock().await;
|
||||
|
||||
if let Some(finder) = finder_opt.as_ref() {
|
||||
Ok(format!("Local node ID: {}", finder.local_id()))
|
||||
} else {
|
||||
Err("Local finder not initialized".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::plugins::TextMessagePlugin;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_message_success() {
|
||||
let (sender, _receiver) = mpsc::channel(100);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.with_iroh_sender(sender)
|
||||
.build();
|
||||
|
||||
let handler = TauriMessageHandler::new(router);
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"content": "Test"
|
||||
});
|
||||
|
||||
let result = handler.send_message(message).await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "Message sent successfully");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_message_failure() {
|
||||
// 不设置 iroh_sender,导致发送失败
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.build();
|
||||
|
||||
let handler = TauriMessageHandler::new(router);
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"content": "Test"
|
||||
});
|
||||
|
||||
let result = handler.send_message(message).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_supported_message_types() {
|
||||
let router = MessageRouter::builder().build();
|
||||
let handler = TauriMessageHandler::new(router);
|
||||
|
||||
let result = handler.get_supported_message_types();
|
||||
assert!(result.is_ok());
|
||||
let types = result.unwrap();
|
||||
assert!(types.contains(&"text".to_string()));
|
||||
assert!(types.contains(&"file".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/// 局域网发现管理器(占位符实现)
|
||||
///
|
||||
/// TODO: 需要查阅最新的 Iroh 文档来正确实现
|
||||
/// Iroh v0.97 的 API 与早期版本有很大不同
|
||||
pub struct LocalFinder {
|
||||
node_id: String,
|
||||
}
|
||||
|
||||
impl LocalFinder {
|
||||
/// 创建新的局域网发现管理器
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
// 占位符实现 - 仅用于演示结构
|
||||
Ok(LocalFinder {
|
||||
node_id: "placeholder-node-id".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取本地节点 ID
|
||||
pub fn local_id(&self) -> String {
|
||||
self.node_id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LocalFinder {
|
||||
fn drop(&mut self) {
|
||||
// 清理资源(占位符)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_finder_creation() {
|
||||
let finder = LocalFinder::new().await;
|
||||
assert!(finder.is_ok());
|
||||
|
||||
let finder = finder.unwrap();
|
||||
assert!(!finder.local_id().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod local_find;
|
||||
|
||||
pub use local_find::LocalFinder;
|
||||
+58
-25
@@ -1,35 +1,68 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
|
||||
// #[tauri::command]
|
||||
// fn something(state: tauri::State<String>) -> String {
|
||||
// state.inner().clone()
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::something;
|
||||
// use tauri::Manager;
|
||||
|
||||
// #[test]
|
||||
// pub fn test_something() {
|
||||
// let app = tauri::test::mock_app();
|
||||
// app.manage("something".to_string());
|
||||
// assert_eq!(&something(app.state::<String>()), "something");
|
||||
// }
|
||||
// }
|
||||
mod db;
|
||||
mod iroh;
|
||||
mod plugins;
|
||||
mod router;
|
||||
mod handlers;
|
||||
|
||||
pub use plugins::{MessagePlugin, PluginResult};
|
||||
pub use router::MessageRouter;
|
||||
pub use handlers::TauriMessageHandler;
|
||||
pub use plugins::{FileMessagePlugin, TextMessagePlugin};
|
||||
pub use iroh::LocalFinder;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let migrations = db::migrations::get_migrations();
|
||||
|
||||
// 创建消息路由器
|
||||
let (iroh_sender, iroh_receiver) = tokio::sync::mpsc::channel(100);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(FileMessagePlugin::new())
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.with_iroh_sender(iroh_sender)
|
||||
.build();
|
||||
|
||||
// 创建 Tauri 处理器和管理状态
|
||||
let handler_state = Arc::new(Mutex::new(router));
|
||||
|
||||
// 创建局域网发现管理器
|
||||
let local_finder_state: Arc<Mutex<Option<LocalFinder>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(
|
||||
tauri_plugin_sql::Builder::new()
|
||||
.add_migrations("sqlite:app.db", migrations)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
handlers::tauri_handlers::send_message,
|
||||
handlers::tauri_handlers::get_supported_message_types,
|
||||
handlers::tauri_handlers::initialize_local_finder,
|
||||
handlers::tauri_handlers::start_discovery,
|
||||
handlers::tauri_handlers::stop_discovery,
|
||||
handlers::tauri_handlers::get_local_node_id,
|
||||
handlers::tauri_handlers::get_discovered_peers,
|
||||
])
|
||||
.manage(handler_state)
|
||||
.manage(local_finder_state)
|
||||
.setup(|app| {
|
||||
// 启动 Iroh 接收处理任务
|
||||
let _app_handle = app.handle();
|
||||
let mut iroh_receiver = iroh_receiver;
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(_binary_data) = iroh_receiver.recv().await {
|
||||
println!("Received binary data from Iroh: {} bytes", _binary_data.len());
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
use super::{BinaryData, MessagePlugin, PluginResult};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// 文件消息插件
|
||||
pub struct FileMessagePlugin;
|
||||
|
||||
impl FileMessagePlugin {
|
||||
pub fn new() -> Self {
|
||||
FileMessagePlugin {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileMessagePlugin {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagePlugin for FileMessagePlugin {
|
||||
fn get_plugin_type(&self) -> &str {
|
||||
"file"
|
||||
}
|
||||
|
||||
fn before_send(&self, json_data: &Value) -> PluginResult<BinaryData> {
|
||||
let to = json_data.get("to")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing 'to' field")?;
|
||||
|
||||
let from = json_data.get("from")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing 'from' field")?;
|
||||
|
||||
let processed_data = json!({
|
||||
"type": "file",
|
||||
"to": to,
|
||||
"from": from,
|
||||
"timestamp": chrono::Utc::now().timestamp(),
|
||||
});
|
||||
|
||||
Ok(serde_json::to_vec(&processed_data)?)
|
||||
}
|
||||
|
||||
fn before_receive(&self, binary_data: &BinaryData) -> PluginResult<Value> {
|
||||
let json_value: Value = serde_json::from_slice(binary_data)?;
|
||||
Ok(json_value)
|
||||
}
|
||||
|
||||
fn after_send(&self, _json_data: &Value) -> PluginResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn after_receive(&self, _binary_data: &BinaryData) -> PluginResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_get_plugin_type() {
|
||||
let plugin = FileMessagePlugin::new();
|
||||
assert_eq!(plugin.get_plugin_type(), "file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_send_valid_message() {
|
||||
let plugin = FileMessagePlugin::new();
|
||||
let message = json!({
|
||||
"type": "file",
|
||||
"to": "user123",
|
||||
"from": "user456"
|
||||
});
|
||||
|
||||
let result = plugin.before_send(&message);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let binary_data = result.unwrap();
|
||||
let parsed: Value = serde_json::from_slice(&binary_data).unwrap();
|
||||
|
||||
assert_eq!(parsed["type"], "file");
|
||||
assert_eq!(parsed["to"], "user123");
|
||||
assert_eq!(parsed["from"], "user456");
|
||||
assert!(parsed["timestamp"].is_number());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_send_missing_to_field() {
|
||||
let plugin = FileMessagePlugin::new();
|
||||
let message = json!({
|
||||
"type": "file",
|
||||
"from": "user456"
|
||||
});
|
||||
|
||||
let result = plugin.before_send(&message);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_send_missing_from_field() {
|
||||
let plugin = FileMessagePlugin::new();
|
||||
let message = json!({
|
||||
"type": "file",
|
||||
"to": "user123"
|
||||
});
|
||||
|
||||
let result = plugin.before_send(&message);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_receive() {
|
||||
let plugin = FileMessagePlugin::new();
|
||||
let original_message = json!({
|
||||
"type": "file",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"timestamp": 1234567890
|
||||
});
|
||||
|
||||
let binary_data = serde_json::to_vec(&original_message).unwrap();
|
||||
let result = plugin.before_receive(&binary_data);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let received = result.unwrap();
|
||||
assert_eq!(received["type"], "file");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use serde_json::Value;
|
||||
use std::error::Error;
|
||||
|
||||
pub mod file_plugin;
|
||||
pub mod text_plugin;
|
||||
|
||||
pub use file_plugin::FileMessagePlugin;
|
||||
pub use text_plugin::TextMessagePlugin;
|
||||
|
||||
pub type PluginResult<T> = Result<T, Box<dyn Error>>;
|
||||
pub type BinaryData = Vec<u8>;
|
||||
|
||||
pub trait MessagePlugin: Send + Sync {
|
||||
/// 获取插件类型标识
|
||||
fn get_plugin_type(&self) -> &str;
|
||||
|
||||
/// 发送消息前的处理
|
||||
fn before_send(&self, json_data: &Value) -> PluginResult<BinaryData>;
|
||||
|
||||
/// 接收消息前的处理
|
||||
fn before_receive(&self, binary_data: &BinaryData) -> PluginResult<Value>;
|
||||
|
||||
/// 发送消息后的处理
|
||||
fn after_send(&self, json_data: &Value) -> PluginResult<()>;
|
||||
|
||||
/// 接收消息后的处理
|
||||
fn after_receive(&self, binary_data: &BinaryData) -> PluginResult<()>;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
use super::{BinaryData, MessagePlugin, PluginResult};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// 文本消息插件
|
||||
pub struct TextMessagePlugin;
|
||||
|
||||
impl TextMessagePlugin {
|
||||
pub fn new() -> Self {
|
||||
TextMessagePlugin {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextMessagePlugin {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagePlugin for TextMessagePlugin {
|
||||
fn get_plugin_type(&self) -> &str {
|
||||
"text"
|
||||
}
|
||||
|
||||
fn before_send(&self, json_data: &Value) -> PluginResult<BinaryData> {
|
||||
let to = json_data.get("to")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing 'to' field")?;
|
||||
|
||||
let from = json_data.get("from")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing 'from' field")?;
|
||||
|
||||
let content = json_data.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing 'content' field")?;
|
||||
|
||||
let processed_data = json!({
|
||||
"type": "text",
|
||||
"to": to,
|
||||
"from": from,
|
||||
"content": content,
|
||||
"timestamp": chrono::Utc::now().timestamp(),
|
||||
});
|
||||
|
||||
Ok(serde_json::to_vec(&processed_data)?)
|
||||
}
|
||||
|
||||
fn before_receive(&self, binary_data: &BinaryData) -> PluginResult<Value> {
|
||||
let json_value: Value = serde_json::from_slice(binary_data)?;
|
||||
Ok(json_value)
|
||||
}
|
||||
|
||||
fn after_send(&self, _json_data: &Value) -> PluginResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn after_receive(&self, _binary_data: &BinaryData) -> PluginResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_get_plugin_type() {
|
||||
let plugin = TextMessagePlugin::new();
|
||||
assert_eq!(plugin.get_plugin_type(), "text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_send_valid_message() {
|
||||
let plugin = TextMessagePlugin::new();
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"content": "Hello, World!"
|
||||
});
|
||||
|
||||
let result = plugin.before_send(&message);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let binary_data = result.unwrap();
|
||||
let parsed: Value = serde_json::from_slice(&binary_data).unwrap();
|
||||
|
||||
assert_eq!(parsed["type"], "text");
|
||||
assert_eq!(parsed["to"], "user123");
|
||||
assert_eq!(parsed["from"], "user456");
|
||||
assert_eq!(parsed["content"], "Hello, World!");
|
||||
assert!(parsed["timestamp"].is_number());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_send_missing_to_field() {
|
||||
let plugin = TextMessagePlugin::new();
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"from": "user456",
|
||||
"content": "Hello"
|
||||
});
|
||||
|
||||
let result = plugin.before_send(&message);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Missing 'to' field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_send_missing_from_field() {
|
||||
let plugin = TextMessagePlugin::new();
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"content": "Hello"
|
||||
});
|
||||
|
||||
let result = plugin.before_send(&message);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_send_missing_content_field() {
|
||||
let plugin = TextMessagePlugin::new();
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456"
|
||||
});
|
||||
|
||||
let result = plugin.before_send(&message);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_receive() {
|
||||
let plugin = TextMessagePlugin::new();
|
||||
let original_message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"content": "Test message",
|
||||
"timestamp": 1234567890
|
||||
});
|
||||
|
||||
let binary_data = serde_json::to_vec(&original_message).unwrap();
|
||||
let result = plugin.before_receive(&binary_data);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let received = result.unwrap();
|
||||
assert_eq!(received["type"], "text");
|
||||
assert_eq!(received["content"], "Test message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_after_send_and_receive() {
|
||||
let plugin = TextMessagePlugin::new();
|
||||
let message = json!({"test": "data"});
|
||||
let binary_data = vec![1, 2, 3];
|
||||
|
||||
assert!(plugin.after_send(&message).is_ok());
|
||||
assert!(plugin.after_receive(&binary_data).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
use crate::plugins::{BinaryData, MessagePlugin};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
type MessageCallback = Box<dyn Fn(Value) + Send + Sync + 'static>;
|
||||
|
||||
/// 消息路由器
|
||||
pub struct MessageRouter {
|
||||
plugins: Arc<RwLock<HashMap<String, Arc<dyn MessagePlugin>>>>,
|
||||
message_callback: Arc<RwLock<Option<MessageCallback>>>,
|
||||
iroh_sender: Option<mpsc::Sender<BinaryData>>,
|
||||
}
|
||||
|
||||
pub struct MessageRouterBuilder {
|
||||
router: MessageRouter,
|
||||
}
|
||||
|
||||
impl MessageRouter {
|
||||
pub fn builder() -> MessageRouterBuilder {
|
||||
MessageRouterBuilder {
|
||||
router: MessageRouter {
|
||||
plugins: Arc::new(RwLock::new(HashMap::new())),
|
||||
message_callback: Arc::new(RwLock::new(None)),
|
||||
iroh_sender: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送消息
|
||||
pub async fn send(&self, message: Value) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let message_type = message.get("type")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Message must have a 'type' field")?;
|
||||
|
||||
// 先获取插件并释放锁
|
||||
let plugin = {
|
||||
let plugins = self.plugins.read().map_err(|_| "Failed to read plugins")?;
|
||||
let plugin = plugins.get(message_type)
|
||||
.ok_or_else(|| format!("No plugin found for message type: {}", message_type))?;
|
||||
Arc::clone(plugin)
|
||||
};
|
||||
|
||||
// 在锁释放后处理消息
|
||||
let processed_data = plugin.before_send(&message)?;
|
||||
|
||||
if let Some(sender) = &self.iroh_sender {
|
||||
sender.send(processed_data).await?;
|
||||
} else {
|
||||
return Err("Iroh sender not initialized".into());
|
||||
}
|
||||
|
||||
plugin.after_send(&message)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 处理接收到的消息
|
||||
pub fn handle_received_message(&self, binary_data: BinaryData) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let json_value: Value = serde_json::from_slice(&binary_data)?;
|
||||
let message_type = json_value.get("type")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Received message must have a 'type' field")?;
|
||||
|
||||
// 先获取插件并释放锁
|
||||
let plugin = {
|
||||
let plugins = self.plugins.read().map_err(|_| "Failed to read plugins")?;
|
||||
let plugin = plugins.get(message_type)
|
||||
.ok_or_else(|| format!("No plugin found for message type: {}", message_type))?;
|
||||
Arc::clone(plugin)
|
||||
};
|
||||
|
||||
let processed_message = plugin.before_receive(&binary_data)?;
|
||||
|
||||
if let Some(callback) = &*self.message_callback.read().map_err(|_| "Failed to read callback")? {
|
||||
callback(processed_message);
|
||||
}
|
||||
|
||||
plugin.after_receive(&binary_data)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageRouterBuilder {
|
||||
/// 注册插件
|
||||
pub fn register_plugin<P>(self, plugin: P) -> Self
|
||||
where
|
||||
P: MessagePlugin + 'static,
|
||||
{
|
||||
let plugin_type = plugin.get_plugin_type().to_string();
|
||||
self.router.plugins.write()
|
||||
.expect("Failed to write plugins")
|
||||
.insert(plugin_type, Arc::new(plugin));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置消息回调
|
||||
pub fn on_message<F>(self, callback: F) -> Self
|
||||
where
|
||||
F: Fn(Value) + Send + Sync + 'static,
|
||||
{
|
||||
*self.router.message_callback.write()
|
||||
.expect("Failed to write callback") = Some(Box::new(callback));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置 Iroh 发送器
|
||||
pub fn with_iroh_sender(mut self, sender: mpsc::Sender<BinaryData>) -> Self {
|
||||
self.router.iroh_sender = Some(sender);
|
||||
self
|
||||
}
|
||||
|
||||
/// 构建路由器
|
||||
pub fn build(self) -> MessageRouter {
|
||||
self.router
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::plugins::TextMessagePlugin;
|
||||
use serde_json::json;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
fn test_router_builder() {
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.build();
|
||||
|
||||
let plugins = router.plugins.read().unwrap();
|
||||
assert!(plugins.contains_key("text"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_multiple_plugins() {
|
||||
use crate::plugins::FileMessagePlugin;
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.register_plugin(FileMessagePlugin::new())
|
||||
.build();
|
||||
|
||||
let plugins = router.plugins.read().unwrap();
|
||||
assert!(plugins.contains_key("text"));
|
||||
assert!(plugins.contains_key("file"));
|
||||
assert_eq!(plugins.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_text_message() {
|
||||
let (sender, _receiver) = mpsc::channel(100);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.with_iroh_sender(sender)
|
||||
.build();
|
||||
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"content": "Test message"
|
||||
});
|
||||
|
||||
let result = router.send(message).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_message_without_iroh_sender() {
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.build();
|
||||
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"content": "Test"
|
||||
});
|
||||
|
||||
let result = router.send(message).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Iroh sender not initialized"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_message_with_unknown_type() {
|
||||
let (sender, _receiver) = mpsc::channel(100);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.with_iroh_sender(sender)
|
||||
.build();
|
||||
|
||||
let message = json!({
|
||||
"type": "unknown",
|
||||
"to": "user123",
|
||||
"from": "user456"
|
||||
});
|
||||
|
||||
let result = router.send(message).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("No plugin found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_received_message() {
|
||||
let received_messages = Arc::new(Mutex::new(Vec::new()));
|
||||
let received_clone = Arc::clone(&received_messages);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.on_message(move |msg| {
|
||||
received_clone.lock().unwrap().push(msg);
|
||||
})
|
||||
.build();
|
||||
|
||||
let original_message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"content": "Incoming message"
|
||||
});
|
||||
|
||||
let binary_data = serde_json::to_vec(&original_message).unwrap();
|
||||
let result = router.handle_received_message(binary_data);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
let messages = received_messages.lock().unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0]["type"], "text");
|
||||
assert_eq!(messages[0]["content"], "Incoming message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_received_message_invalid_type() {
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.build();
|
||||
|
||||
// 发送一个没有 type 字段的消息
|
||||
let invalid_message = vec![1, 2, 3]; // 无效的 JSON
|
||||
let result = router.handle_received_message(invalid_message);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod message_router;
|
||||
|
||||
pub use message_router::MessageRouter;
|
||||
@@ -14,7 +14,9 @@
|
||||
{
|
||||
"title": "rolegram",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
"height": 600,
|
||||
"minWidth": 600,
|
||||
"minHeight": 500
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,213 @@
|
||||
// 消息路由系统测试示例
|
||||
// 这些代码展示了如何测试各个组件
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration_tests {
|
||||
use serde_json::json;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// 导入被测试的模块
|
||||
use crate::plugins::{MessagePlugin, TextMessagePlugin, FileMessagePlugin};
|
||||
use crate::router::MessageRouter;
|
||||
use crate::handlers::TauriMessageHandler;
|
||||
|
||||
/// 集成测试:完整的消息发送流程
|
||||
#[tokio::test]
|
||||
async fn test_full_message_flow() {
|
||||
// 1. 创建 channel
|
||||
let (sender, mut receiver) = mpsc::channel(100);
|
||||
|
||||
// 2. 记录接收到的消息
|
||||
let received_messages = Arc::new(Mutex::new(Vec::new()));
|
||||
let received_clone = Arc::clone(&received_messages);
|
||||
|
||||
// 3. 创建路由器
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.on_message(move |msg| {
|
||||
received_clone.lock().unwrap().push(msg);
|
||||
})
|
||||
.with_iroh_sender(sender)
|
||||
.build();
|
||||
|
||||
// 4. 发送消息
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user123",
|
||||
"from": "user456",
|
||||
"content": "Integration test message"
|
||||
});
|
||||
|
||||
let result = router.send(message.clone()).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// 5. 验证消息被发送到 channel
|
||||
let sent_data = receiver.recv().await.unwrap();
|
||||
let sent_json: serde_json::Value = serde_json::from_slice(&sent_data).unwrap();
|
||||
assert_eq!(sent_json["type"], "text");
|
||||
assert_eq!(sent_json["content"], "Integration test message");
|
||||
|
||||
// 6. 模拟接收消息
|
||||
let binary_data = serde_json::to_vec(&sent_json).unwrap();
|
||||
let result = router.handle_received_message(binary_data);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// 7. 验证回调被调用
|
||||
let messages = received_messages.lock().unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0]["content"], "Integration test message");
|
||||
}
|
||||
|
||||
/// 测试多插件协作
|
||||
#[tokio::test]
|
||||
async fn test_multiple_plugins() {
|
||||
let (sender, _receiver) = mpsc::channel(100);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.register_plugin(FileMessagePlugin::new())
|
||||
.with_iroh_sender(sender)
|
||||
.build();
|
||||
|
||||
// 发送文本消息
|
||||
let text_msg = json!({
|
||||
"type": "text",
|
||||
"to": "user1",
|
||||
"from": "user2",
|
||||
"content": "Text"
|
||||
});
|
||||
assert!(router.send(text_msg).await.is_ok());
|
||||
|
||||
// 发送文件消息
|
||||
let file_msg = json!({
|
||||
"type": "file",
|
||||
"to": "user1",
|
||||
"from": "user2"
|
||||
});
|
||||
assert!(router.send(file_msg).await.is_ok());
|
||||
}
|
||||
|
||||
/// 测试错误处理
|
||||
#[tokio::test]
|
||||
async fn test_error_handling() {
|
||||
let (sender, _receiver) = mpsc::channel(100);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.with_iroh_sender(sender)
|
||||
.build();
|
||||
|
||||
// 测试缺失必要字段
|
||||
let invalid_msg = json!({
|
||||
"type": "text",
|
||||
"content": "Missing to and from"
|
||||
});
|
||||
let result = router.send(invalid_msg).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// 测试未知消息类型
|
||||
let unknown_type = json!({
|
||||
"type": "unknown_type",
|
||||
"to": "user1",
|
||||
"from": "user2"
|
||||
});
|
||||
let result = router.send(unknown_type).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// 测试 Handler 层
|
||||
#[tokio::test]
|
||||
async fn test_handler_layer() {
|
||||
let (sender, _receiver) = mpsc::channel(100);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.with_iroh_sender(sender)
|
||||
.build();
|
||||
|
||||
let handler = TauriMessageHandler::new(router);
|
||||
|
||||
// 测试成功的命令调用
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user1",
|
||||
"from": "user2",
|
||||
"content": "Handler test"
|
||||
});
|
||||
|
||||
let result = handler.send_message(message).await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "Message sent successfully");
|
||||
|
||||
// 测试获取支持的消息类型
|
||||
let types = handler.get_supported_message_types().unwrap();
|
||||
assert!(types.contains(&"text".to_string()));
|
||||
}
|
||||
|
||||
/// 性能测试:大量消息发送
|
||||
#[tokio::test]
|
||||
async fn test_performance_many_messages() {
|
||||
let (sender, mut receiver) = mpsc::channel(1000);
|
||||
|
||||
let router = MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.with_iroh_sender(sender)
|
||||
.build();
|
||||
|
||||
// 发送 100 条消息
|
||||
for i in 0..100 {
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user1",
|
||||
"from": "user2",
|
||||
"content": format!("Message {}", i)
|
||||
});
|
||||
|
||||
let result = router.send(message).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// 验证所有消息都被发送
|
||||
let mut count = 0;
|
||||
while let Ok(_) = receiver.try_recv() {
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(count, 100);
|
||||
}
|
||||
|
||||
/// 测试并发安全性
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_safety() {
|
||||
let (sender, _receiver) = mpsc::channel(100);
|
||||
|
||||
let router = Arc::new(
|
||||
MessageRouter::builder()
|
||||
.register_plugin(TextMessagePlugin::new())
|
||||
.with_iroh_sender(sender)
|
||||
.build()
|
||||
);
|
||||
|
||||
// 创建多个并发任务
|
||||
let mut handles = vec![];
|
||||
for i in 0..10 {
|
||||
let router_clone = Arc::clone(&router);
|
||||
let handle = tokio::spawn(async move {
|
||||
let message = json!({
|
||||
"type": "text",
|
||||
"to": "user1",
|
||||
"from": "user2",
|
||||
"content": format!("Concurrent message {}", i)
|
||||
});
|
||||
router_clone.send(message).await
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
for handle in handles {
|
||||
let result = handle.await.unwrap();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -1,5 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
|
||||
// tauri
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
onMounted(()=>{
|
||||
console.log('App mounted!');
|
||||
invoke('get_supported_message_types').then((appName) => {
|
||||
console.log(appName);
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { Workflow } from '@/composables/useWorkflow'
|
||||
import { getDatabase, initWorkflowTable } from '@/utils/database.js'
|
||||
|
||||
// 获取所有工作流
|
||||
export async function getWorkflows(): Promise<Workflow[]> {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
const results: any[] = await db.select('SELECT * FROM workflows ORDER BY createTime DESC')
|
||||
return results as Workflow[]
|
||||
} catch (error) {
|
||||
console.error('获取工作流失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 根据 ID 获取工作流
|
||||
export async function getWorkflowById(id: number): Promise<Workflow | null> {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
const results: any[] = await db.select('SELECT * FROM workflows WHERE id = ?', [id])
|
||||
if (results.length > 0) {
|
||||
return results[0] as Workflow
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('获取工作流详情失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 创建工作流
|
||||
export async function createWorkflow(workflow: Omit<Workflow, 'id'>): Promise<number | null> {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
await initWorkflowTable()
|
||||
const result = await db.execute(
|
||||
'INSERT INTO workflows (name, status, creator, createTime, definition) VALUES (?, ?, ?, ?, ?)',
|
||||
[workflow.name, workflow.status, workflow.creator, workflow.createTime, workflow.definition || null]
|
||||
)
|
||||
return result.lastInsertId as number
|
||||
} catch (error) {
|
||||
console.error('创建工作流失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 更新工作流
|
||||
export async function updateWorkflow(id: number, workflow: Partial<Workflow>): Promise<boolean> {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
const fields: string[] = []
|
||||
const values: any[] = []
|
||||
|
||||
if (workflow.name !== undefined) {
|
||||
fields.push('name = ?')
|
||||
values.push(workflow.name)
|
||||
}
|
||||
if (workflow.status !== undefined) {
|
||||
fields.push('status = ?')
|
||||
values.push(workflow.status)
|
||||
}
|
||||
if (workflow.definition !== undefined) {
|
||||
fields.push('definition = ?')
|
||||
values.push(workflow.definition)
|
||||
}
|
||||
if (fields.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
fields.push('updatedTime = ?')
|
||||
values.push(new Date().toISOString())
|
||||
values.push(id)
|
||||
|
||||
await db.execute(
|
||||
`UPDATE workflows SET ${fields.join(', ')} WHERE id = ?`,
|
||||
values
|
||||
)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('更新工作流失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除工作流
|
||||
export async function deleteWorkflow(id: number): Promise<boolean> {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
await db.execute('DELETE FROM workflows WHERE id = ?', [id])
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除工作流失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -3,9 +3,13 @@
|
||||
|
||||
|
||||
@utility input {
|
||||
@apply w-full outline-none ;
|
||||
@apply w-full outline-none;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
@apply my-1.5 size-4;
|
||||
}
|
||||
|
||||
#app {
|
||||
@apply h-screen w-screen;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- 任务列表 -->
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
@click="$emit('select', task)"
|
||||
:class="[
|
||||
'p-3 rounded-lg cursor-pointer transition-colors border-l-4',
|
||||
selectedTaskId === task.id
|
||||
? 'bg-primary text-primary-content border-primary'
|
||||
: 'bg-base-100 hover:bg-base-300 border-transparent'
|
||||
]"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="font-semibold text-sm">{{ task.name }}</div>
|
||||
<span :class="getStatusBadgeClass(task.status)">{{ task.status }}</span>
|
||||
</div>
|
||||
<div class="text-xs opacity-70 mt-1">
|
||||
{{ getTriggerTypeLabel(task.triggerType) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tasks.length === 0" class="text-center opacity-50 py-8">
|
||||
<p>暂无自动化任务</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AutomationTask } from '@/composables/useAutomation'
|
||||
|
||||
const props = defineProps<{
|
||||
tasks: AutomationTask[]
|
||||
selectedTaskId: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [task: AutomationTask]
|
||||
}>()
|
||||
|
||||
const getStatusBadgeClass = (status: string) => {
|
||||
const baseClass = 'badge badge-sm'
|
||||
switch (status) {
|
||||
case '运行中':
|
||||
return `${baseClass} badge-info`
|
||||
case '已暂停':
|
||||
return `${baseClass} badge-warning`
|
||||
case '草稿':
|
||||
return `${baseClass} badge-ghost`
|
||||
default:
|
||||
return baseClass
|
||||
}
|
||||
}
|
||||
|
||||
const getTriggerTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
cron: '定时触发',
|
||||
event: '事件触发',
|
||||
manual: '手动触发',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="card bg-base-100 p-4">
|
||||
<h3 class="font-bold mb-3">自动化规则</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">规则名称</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue?.name"
|
||||
@input="updateName($event)"
|
||||
type="text"
|
||||
placeholder="请输入规则名称"
|
||||
class="input input-bordered input-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">触发条件</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue?.trigger"
|
||||
@input="updateTrigger($event)"
|
||||
class="select select-bordered select-sm"
|
||||
>
|
||||
<option value="">选择触发条件</option>
|
||||
<option v-for="trigger in triggers" :key="trigger" :value="trigger">
|
||||
{{ trigger }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">执行动作</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue?.action"
|
||||
@input="updateAction($event)"
|
||||
class="select select-bordered select-sm"
|
||||
>
|
||||
<option value="">选择执行动作</option>
|
||||
<option v-for="action in actions" :key="action" :value="action">
|
||||
{{ action }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
:checked="modelValue?.enabled"
|
||||
@change="updateEnabled(($event.target as HTMLInputElement).checked)"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-sm">启用此规则</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface AutomationRule {
|
||||
name?: string
|
||||
trigger?: string
|
||||
action?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: AutomationRule
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: AutomationRule]
|
||||
}>()
|
||||
|
||||
const triggers = [
|
||||
'流程启动时',
|
||||
'流程完成时',
|
||||
'流程失败时',
|
||||
'节点执行前',
|
||||
'节点执行后',
|
||||
'数据变更时',
|
||||
'定时触发',
|
||||
]
|
||||
|
||||
const actions = [
|
||||
'发送邮件',
|
||||
'发送消息',
|
||||
'执行脚本',
|
||||
'调用 API',
|
||||
'更新数据',
|
||||
'创建任务',
|
||||
'记录日志',
|
||||
]
|
||||
|
||||
const updateName = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', { ...props.modelValue, name: target.value })
|
||||
}
|
||||
|
||||
const updateTrigger = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
emit('update:modelValue', { ...props.modelValue, trigger: target.value })
|
||||
}
|
||||
|
||||
const updateAction = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
emit('update:modelValue', { ...props.modelValue, action: target.value })
|
||||
}
|
||||
|
||||
const updateEnabled = (value: boolean) => {
|
||||
emit('update:modelValue', { ...props.modelValue, enabled: value })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Cron 表达式</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue"
|
||||
@input="updateValue($event)"
|
||||
type="text"
|
||||
placeholder="0 0 12 * * ?"
|
||||
class="input input-bordered font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 快捷设置 -->
|
||||
<div class="space-y-2">
|
||||
<label class="label">
|
||||
<span class="label-text">快捷设置</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="(expr, label) in presetExpressions"
|
||||
:key="label"
|
||||
@click="selectPreset(expr)"
|
||||
class="btn btn-xs btn-outline"
|
||||
>
|
||||
{{ label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表达式说明 -->
|
||||
<div class="text-xs opacity-70 bg-base-200 p-3 rounded">
|
||||
<div class="font-semibold mb-2">Cron 表达式格式:</div>
|
||||
<div class="grid grid-cols-6 gap-2 text-center">
|
||||
<div>秒</div>
|
||||
<div>分钟</div>
|
||||
<div>小时</div>
|
||||
<div>日期</div>
|
||||
<div>月份</div>
|
||||
<div>星期</div>
|
||||
</div>
|
||||
<div class="mt-2 text-center font-mono">{{ modelValue || '0 0 12 * * ?' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 下次执行时间预览 -->
|
||||
<div v-if="nextRunTime" class="text-sm">
|
||||
<span class="opacity-70">下次执行时间:</span>
|
||||
<span class="font-semibold">{{ nextRunTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const presetExpressions: Record<string, string> = {
|
||||
'每分钟': '0 * * * * ?',
|
||||
'每小时': '0 0 * * * ?',
|
||||
'每天零点': '0 0 0 * * ?',
|
||||
'每天中午': '0 0 12 * * ?',
|
||||
'每周一': '0 0 9 ? * MON',
|
||||
'每月 1 号': '0 0 0 1 * ?',
|
||||
}
|
||||
|
||||
const updateValue = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const selectPreset = (expr: string) => {
|
||||
emit('update:modelValue', expr)
|
||||
}
|
||||
|
||||
// 计算下次执行时间(简化版本)
|
||||
const nextRunTime = computed(() => {
|
||||
if (!props.modelValue) return ''
|
||||
// TODO: 实现真实的 Cron 解析
|
||||
return '2026-03-12 00:00:00'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">触发事件</span>
|
||||
</label>
|
||||
<select :value="modelValue?.event" @input="updateEvent($event)" class="select select-bordered">
|
||||
<option value="">选择事件</option>
|
||||
<option v-for="event in availableEvents" :key="event" :value="event">
|
||||
{{ event }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">触发条件</span>
|
||||
</label>
|
||||
<select :value="modelValue?.condition" @input="updateCondition($event)" class="select select-bordered">
|
||||
<option value="equals">等于</option>
|
||||
<option value="contains">包含</option>
|
||||
<option value="greaterThan">大于</option>
|
||||
<option value="lessThan">小于</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">条件值</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue?.value"
|
||||
@input="updateValue($event)"
|
||||
type="text"
|
||||
placeholder="请输入条件值"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-xs opacity-70 bg-base-200 p-3 rounded">
|
||||
<div class="font-semibold mb-2">可用事件:</div>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>流程完成 - 当某个流程执行完成时触发</li>
|
||||
<li>数据更新 - 当数据库记录更新时触发</li>
|
||||
<li>文件上传 - 当有新文件上传时触发</li>
|
||||
<li>API 调用 - 当收到特定 API 请求时触发</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface EventConfig {
|
||||
event?: string
|
||||
condition?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: EventConfig
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: EventConfig]
|
||||
}>()
|
||||
|
||||
const availableEvents = [
|
||||
'流程完成',
|
||||
'数据更新',
|
||||
'文件上传',
|
||||
'API 调用',
|
||||
'系统启动',
|
||||
'用户登录',
|
||||
]
|
||||
|
||||
const updateEvent = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
emit('update:modelValue', { ...props.modelValue, event: target.value })
|
||||
}
|
||||
|
||||
const updateCondition = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
emit('update:modelValue', { ...props.modelValue, condition: target.value })
|
||||
}
|
||||
|
||||
const updateValue = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', { ...props.modelValue, value: target.value })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">循环类型</span>
|
||||
</label>
|
||||
<select :value="modelValue?.loopType" @input="updateLoopType($event)" class="select select-bordered">
|
||||
<option value="once">仅执行一次</option>
|
||||
<option value="daily">每天</option>
|
||||
<option value="weekly">每周</option>
|
||||
<option value="monthly">每月</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="modelValue?.loopType === 'custom'" class="space-y-3">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">执行间隔</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
:value="modelValue?.interval"
|
||||
@input="updateInterval($event)"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input input-bordered w-24"
|
||||
/>
|
||||
<select
|
||||
:value="modelValue?.intervalUnit"
|
||||
@input="updateIntervalUnit($event)"
|
||||
class="select select-bordered flex-1"
|
||||
>
|
||||
<option value="minutes">分钟</option>
|
||||
<option value="hours">小时</option>
|
||||
<option value="days">天</option>
|
||||
<option value="weeks">周</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">超时设置(秒)</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue?.timeout"
|
||||
@input="updateTimeout($event)"
|
||||
type="number"
|
||||
min="0"
|
||||
class="input input-bordered"
|
||||
placeholder="0 表示不限制"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">失败重试次数</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue?.retryCount"
|
||||
@input="updateRetryCount($event)"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
:checked="modelValue?.enabled"
|
||||
@change="updateEnabled(($event.target as HTMLInputElement).checked)"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-sm">启用此调度</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Schedule {
|
||||
loopType?: string
|
||||
interval?: number
|
||||
intervalUnit?: string
|
||||
timeout?: number
|
||||
retryCount?: number
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: Schedule
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: Schedule]
|
||||
}>()
|
||||
|
||||
const updateLoopType = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
emit('update:modelValue', { ...props.modelValue, loopType: target.value })
|
||||
}
|
||||
|
||||
const updateInterval = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', { ...props.modelValue, interval: Number(target.value) })
|
||||
}
|
||||
|
||||
const updateIntervalUnit = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
emit('update:modelValue', { ...props.modelValue, intervalUnit: target.value })
|
||||
}
|
||||
|
||||
const updateTimeout = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', { ...props.modelValue, timeout: Number(target.value) })
|
||||
}
|
||||
|
||||
const updateRetryCount = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', { ...props.modelValue, retryCount: Number(target.value) })
|
||||
}
|
||||
|
||||
const updateEnabled = (value: boolean) => {
|
||||
emit('update:modelValue', { ...props.modelValue, enabled: value })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="chat" :class="isSelf ? 'chat-end' : 'chat-start'">
|
||||
<!-- 头像 -->
|
||||
<div v-if="avatar" class="chat-image avatar">
|
||||
<div class="w-10 rounded-full">
|
||||
<img :alt="avatar" :src="avatar" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息气泡 -->
|
||||
<div class="chat-header mb-1">
|
||||
<time v-if="time" class="text-xs opacity-50">{{ time }}</time>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="chat-bubble"
|
||||
:class="isSelf ? 'chat-bubble-primary' : 'chat-bubble-neutral'"
|
||||
>
|
||||
{{ content }}
|
||||
</div>
|
||||
|
||||
<div v-if="showFooter" class="chat-footer mt-1">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
// 消息内容
|
||||
content: string
|
||||
// 是否为自已发送的消息
|
||||
isSelf?: boolean
|
||||
// 显示时间
|
||||
time?: string
|
||||
// 头像 URL
|
||||
avatar?: string
|
||||
// 是否显示底部区域(用于插槽)
|
||||
showFooter?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
max-width: 70%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="w-full border-t border-base-300 p-4 bg-base-100">
|
||||
<div class="flex gap-2">
|
||||
<!-- 输入框 -->
|
||||
<textarea
|
||||
ref="inputRef"
|
||||
v-model="message"
|
||||
class="textarea textarea-bordered flex-1"
|
||||
placeholder="输入消息..."
|
||||
rows="1"
|
||||
@keydown.enter.exact.prevent="handleSend"
|
||||
@input="autoResize"
|
||||
></textarea>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!message.trim()"
|
||||
@click="handleSend"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
send: [content: string]
|
||||
}>()
|
||||
|
||||
const message = ref('')
|
||||
const inputRef = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// 自动调整文本框高度
|
||||
const autoResize = () => {
|
||||
const el = inputRef.value
|
||||
if (el) {
|
||||
el.style.height = 'auto'
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const handleSend = () => {
|
||||
const content = message.value.trim()
|
||||
if (!content) return
|
||||
|
||||
emit('send', content)
|
||||
message.value = ''
|
||||
|
||||
// 重置高度
|
||||
const el = inputRef.value
|
||||
if (el) {
|
||||
el.style.height = 'auto'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
textarea {
|
||||
resize: none;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="flex flex-col w-full h-full bg-base-200 overflow-hidden">
|
||||
<!-- 消息气泡列表 -->
|
||||
<div ref="bubbleListRef" class="flex-1 overflow-y-auto p-4 ">
|
||||
<!-- 空状态 -->
|
||||
<div v-if="bubbles.length === 0" class="flex items-center justify-center h-full">
|
||||
<p class="text-base-content opacity-50">暂无消息,发送第一条消息开始对话吧~</p>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<template v-else>
|
||||
<ChatBubble v-for="bubble in bubbles" :key="bubble.id" :content="bubble.content" :is-self="bubble.isSelf"
|
||||
:time="bubble.time" :avatar="getAvatar(bubble.isSelf)" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 输入框区域 -->
|
||||
<ChatInput @send="handleSend" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import ChatBubble from './ChatBubble.vue'
|
||||
import ChatInput from './ChatInput.vue'
|
||||
import type { ChatBubble as ChatBubbleType } from '@/composables/useChat'
|
||||
|
||||
const props = defineProps<{
|
||||
// 消息气泡列表
|
||||
bubbles: ChatBubbleType[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
send: [content: string]
|
||||
}>()
|
||||
|
||||
const bubbleListRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
// 获取头像(根据是否为自己发送的消息)
|
||||
const getAvatar = (isSelf: boolean): string => {
|
||||
return isSelf
|
||||
? 'https://api.dicebear.com/7.x/avataaars/svg?seed=User'
|
||||
: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Assistant'
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
const el = bubbleListRef.value
|
||||
if (el) {
|
||||
// 使用平滑滚动
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 监听消息变化,自动滚动到底部
|
||||
watch(
|
||||
() => props.bubbles.length,
|
||||
() => {
|
||||
// 下一帧滚动,确保 DOM 已更新
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 0)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 处理发送消息
|
||||
const handleSend = (content: string) => {
|
||||
emit('send', content)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条样式 */
|
||||
.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">{{ item.title }}</h3>
|
||||
<p class="text-sm text-base-content/70 mt-1">{{ item.time }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-sm">{{ item.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button @click="emit('action', 'reply', item.id)" class="btn btn-sm btn-primary">
|
||||
回复
|
||||
</button>
|
||||
<button @click="emit('action', 'markRead', item.id)" class="btn btn-sm btn-outline">
|
||||
标记已读
|
||||
</button>
|
||||
<button @click="emit('action', 'delete', item.id)" class="btn btn-sm btn-ghost text-error">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Message } from '@/composables/useHome'
|
||||
|
||||
defineProps<{
|
||||
item: Message
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [action: string, id: number]
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<ul class="list bg-base-100 rounded-box shadow-md">
|
||||
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide flex items-center justify-between">
|
||||
<span class="mr-auto">本周任务</span>
|
||||
<button class="cursor-pointer" title="新增">
|
||||
<SquarePlus />
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="list-row" v-for="item in items" :class="{
|
||||
'bg-primary': selectedId == item.id
|
||||
}" @click="emit('select', item.id)">
|
||||
<div class="list-col-grow">
|
||||
<div>{{ item.title }}</div>
|
||||
<div class="text-xs uppercase font-semibold opacity-60">{{ item.content }}</div>
|
||||
</div>
|
||||
<button class="btn btn-square btn-ghost">
|
||||
<Trash></Trash>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Message } from '@/composables/useHome'
|
||||
import { Trash, SquarePlus } from 'lucide-vue-next';
|
||||
|
||||
defineProps<{
|
||||
items: Message[]
|
||||
selectedId: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [id: number]
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">{{ item.name }}</h3>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<span class="badge badge-sm" :class="{
|
||||
'badge-info': item.status === '进行中',
|
||||
'badge-ghost': item.status === '规划中',
|
||||
'badge-success': item.status === '已完成',
|
||||
'badge-warning': item.status === '已暂停'
|
||||
}">
|
||||
{{ item.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-4 space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-base-content/70">项目负责人</span>
|
||||
<span class="text-sm font-medium">{{ item.manager }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-base-content/70">当前进度</span>
|
||||
<span class="font-medium">{{ item.progress }}%</span>
|
||||
</div>
|
||||
<progress class="progress progress-primary w-full" :value="item.progress" max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button @click="emit('action', 'view', item.id)" class="btn btn-sm btn-outline">
|
||||
查看详情
|
||||
</button>
|
||||
<button @click="emit('action', 'edit', item.id)" class="btn btn-sm btn-primary">
|
||||
编辑项目
|
||||
</button>
|
||||
<button @click="emit('action', 'addMember', item.id)" class="btn btn-sm btn-secondary">
|
||||
添加成员
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Project } from '@/composables/useHome'
|
||||
|
||||
defineProps<{
|
||||
item: Project
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [action: string, id: number]
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<ul class="list bg-base-100 rounded-box shadow-md">
|
||||
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide flex items-center justify-between">
|
||||
<span class="mr-auto">项目列表</span>
|
||||
<button class="cursor-pointer" title="新增">
|
||||
<SquarePlus />
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="list-row" v-for="item in items" :class="{
|
||||
'bg-primary': selectedId == item.id
|
||||
}" @click="emit('select', item.id)">
|
||||
<div class="list-col-grow">
|
||||
<div>{{ item.name }}</div>
|
||||
<div class="text-xs uppercase font-semibold opacity-60">{{ item.manager }}</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="badge badge-sm" :class="{
|
||||
'badge-info': item.status === '进行中',
|
||||
'badge-ghost': item.status === '规划中',
|
||||
'badge-success': item.status === '已完成',
|
||||
'badge-warning': item.status === '已暂停'
|
||||
}">
|
||||
{{ item.status }}
|
||||
</span>
|
||||
<progress class="progress progress-primary w-24" :value="item.progress" max="100"></progress>
|
||||
</div>
|
||||
<button class="btn btn-square btn-ghost">
|
||||
<Trash></Trash>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Project } from '@/composables/useHome'
|
||||
import { Trash, SquarePlus } from 'lucide-vue-next';
|
||||
|
||||
defineProps<{
|
||||
items: Project[]
|
||||
selectedId: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [id: number]
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">{{ item.name }}</h3>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<span class="badge badge-sm" :class="{
|
||||
'badge-info': item.status === '进行中',
|
||||
'badge-success': item.status === '已完成',
|
||||
'badge-ghost': item.status === '草稿',
|
||||
'badge-warning': item.status === '已暂停'
|
||||
}">
|
||||
{{ item.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-4 space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-base-content/70">创建人</span>
|
||||
<span class="text-sm font-medium">{{ item.creator }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-base-content/70">创建时间</span>
|
||||
<span class="text-sm font-medium">{{ item.createTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button @click="emit('action', 'edit', item.id)" class="btn btn-sm btn-primary">
|
||||
编辑流程
|
||||
</button>
|
||||
<button @click="emit('action', 'view', item.id)" class="btn btn-sm btn-outline">
|
||||
查看流程图
|
||||
</button>
|
||||
<button @click="emit('action', 'start', item.id)" class="btn btn-sm btn-success" :disabled="item.status !== '草稿'">
|
||||
启动流程
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Workflow } from '@/composables/useHome'
|
||||
|
||||
defineProps<{
|
||||
item: Workflow
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [action: string, id: number]
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<ul class="list bg-base-100 rounded-box shadow-md">
|
||||
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide flex items-center justify-between">
|
||||
<span class="mr-auto">流程列表</span>
|
||||
<button class="cursor-pointer" title="新增">
|
||||
<SquarePlus />
|
||||
</button>
|
||||
</li>
|
||||
<li class="list-row" v-for="item in items" :class="{
|
||||
'bg-primary': selectedId == item.id
|
||||
}" @click="emit('select', item.id)">
|
||||
<div class="list-col-grow">
|
||||
<div>{{ item.name }}</div>
|
||||
<div class="text-xs uppercase font-semibold opacity-60">{{ item.creator }}</div>
|
||||
</div>
|
||||
<span class="badge badge-sm" :class="{
|
||||
'badge-info': item.status === '进行中',
|
||||
'badge-success': item.status === '已完成',
|
||||
'badge-ghost': item.status === '草稿',
|
||||
'badge-warning': item.status === '已暂停'
|
||||
}">
|
||||
{{ item.status }}
|
||||
</span>
|
||||
<button class="btn btn-square btn-ghost">
|
||||
<Trash></Trash>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Workflow } from '@/composables/useHome'
|
||||
import { Trash, SquarePlus } from 'lucide-vue-next';
|
||||
|
||||
defineProps<{
|
||||
items: Workflow[]
|
||||
selectedId: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [id: number]
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<section class="card bg-base-100 shadow-sm h-full overflow-hidden">
|
||||
<div class="card-body p-0 h-full">
|
||||
<MessageList
|
||||
v-if="currentFunction === 'message'"
|
||||
:items="messages"
|
||||
:selected-id="selectedMessageId"
|
||||
@select="emit('selectMessage', $event)"
|
||||
/>
|
||||
|
||||
<WorkflowList
|
||||
v-else-if="currentFunction === 'workflow'"
|
||||
:items="workflows"
|
||||
:selected-id="selectedWorkflowId"
|
||||
@select="emit('selectWorkflow', $event)"
|
||||
/>
|
||||
|
||||
<ProjectList
|
||||
v-else
|
||||
:items="projects"
|
||||
:selected-id="selectedProjectId"
|
||||
@select="emit('selectProject', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FunctionType, Message, Workflow, Project } from '@/composables/useHome'
|
||||
import MessageList from '../common/MessageList.vue'
|
||||
import WorkflowList from '../common/WorkflowList.vue'
|
||||
import ProjectList from '../common/ProjectList.vue'
|
||||
|
||||
defineProps<{
|
||||
currentFunction: FunctionType
|
||||
messages: Message[]
|
||||
workflows: Workflow[]
|
||||
projects: Project[]
|
||||
selectedMessageId: number | null
|
||||
selectedWorkflowId: number | null
|
||||
selectedProjectId: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectMessage: [id: number]
|
||||
selectWorkflow: [id: number]
|
||||
selectProject: [id: number]
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<ul class="menu w-full grow">
|
||||
<!-- List item -->
|
||||
<li v-for="item in buttons" class="mb-1">
|
||||
<button class="is-drawer-close:tooltip is-drawer-close:tooltip-right" :class="{
|
||||
'bg-primary': currentFunction == item.type
|
||||
}" :data-tip="item.label" @click="emit('select', item.type)">
|
||||
<component :is="item.icon"></component>
|
||||
<span class="is-drawer-close:hidden">{{ item.label }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FunctionType } from '@/composables/useHome'
|
||||
import { Mail, Workflow, FolderKanban } from 'lucide-vue-next'
|
||||
|
||||
defineProps<{
|
||||
currentFunction: FunctionType
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [func: FunctionType]
|
||||
}>()
|
||||
|
||||
const buttons = [
|
||||
{ type: 'message' as FunctionType, label: '消息列表', icon: Mail },
|
||||
{ type: 'workflow' as FunctionType, label: '流程设计', icon: Workflow },
|
||||
{ type: 'project' as FunctionType, label: '项目管理', icon: FolderKanban },
|
||||
] as const
|
||||
</script>
|
||||
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<section class="card bg-base-100 shadow-sm h-full overflow-hidden">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="font-bold text-base mb-3">操作空间</h2>
|
||||
|
||||
<div class="overflow-y-auto h-[calc(100%-40px)]">
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="!selectedMessage && !selectedWorkflow && !selectedProject"
|
||||
class="flex flex-col items-center justify-center h-full text-base-content/50"
|
||||
>
|
||||
<div class="text-4xl mb-2">👈</div>
|
||||
<p class="text-sm">请从列表中选择一个项目查看详情</p>
|
||||
</div>
|
||||
|
||||
<!-- 消息详情 -->
|
||||
<MessageDetail
|
||||
v-if="currentFunction === 'message' && selectedMessage"
|
||||
:item="selectedMessage"
|
||||
@action="(action, id) => emit('messageAction', action, id)"
|
||||
/>
|
||||
|
||||
<!-- 流程详情 -->
|
||||
<WorkflowDetail
|
||||
v-else-if="currentFunction === 'workflow' && selectedWorkflow"
|
||||
:item="selectedWorkflow"
|
||||
@action="(action, id) => emit('workflowAction', action, id)"
|
||||
/>
|
||||
|
||||
<!-- 项目详情 -->
|
||||
<ProjectDetail
|
||||
v-else-if="currentFunction === 'project' && selectedProject"
|
||||
:item="selectedProject"
|
||||
@action="(action, id) => emit('projectAction', action, id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FunctionType, Message, Workflow, Project } from '@/composables/useHome'
|
||||
import MessageDetail from '../common/MessageDetail.vue'
|
||||
import WorkflowDetail from '../common/WorkflowDetail.vue'
|
||||
import ProjectDetail from '../common/ProjectDetail.vue'
|
||||
|
||||
defineProps<{
|
||||
currentFunction: FunctionType
|
||||
selectedMessage: Message | undefined
|
||||
selectedWorkflow: Workflow | undefined
|
||||
selectedProject: Project | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
messageAction: [action: string, id: number]
|
||||
workflowAction: [action: string, id: number]
|
||||
projectAction: [action: string, id: number]
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<ul class="menu w-full grow">
|
||||
<!-- List item -->
|
||||
<li v-for="item in buttons" class="mb-1">
|
||||
<RouterLink class="is-drawer-close:tooltip is-drawer-close:tooltip-right" :class="{
|
||||
'bg-primary': currentFunction == item.type
|
||||
}" :data-tip="item.label" :to="item.path" active-class="bg-primary">
|
||||
<component :is="item.icon"></component>
|
||||
<span class="is-drawer-close:hidden">{{ item.label }}</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { MessageCircle, Workflow, Clock } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export type FunctionType = 'chat' | 'workflow' | 'automation'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 根据路由路径判断当前功能类型
|
||||
const currentFunction = computed<FunctionType>(() => {
|
||||
const path = route.path
|
||||
if (path.includes('/chat')) return 'chat'
|
||||
if (path.includes('/workflow')) return 'workflow'
|
||||
if (path.includes('/automation')) return 'automation'
|
||||
return 'chat' // 默认值
|
||||
})
|
||||
|
||||
interface NavItem {
|
||||
type: FunctionType
|
||||
label: string
|
||||
icon: any
|
||||
path: string
|
||||
}
|
||||
|
||||
const buttons: NavItem[] = [
|
||||
{ type: 'chat', label: '消息', icon: MessageCircle, path: '/chat' },
|
||||
{ type: 'workflow', label: '流程', icon: Workflow, path: '/workflow' },
|
||||
{ type: 'automation', label: '自动化', icon: Clock, path: '/automation' },
|
||||
]
|
||||
</script>
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
|
||||
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input id="my-drawer-4" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar w-full bg-base-300">
|
||||
<label for="my-drawer-4" aria-label="打开侧边栏" class="btn btn-square btn-ghost">
|
||||
<!-- Sidebar toggle icon -->
|
||||
<PanelLeftOpen class="[.drawer-toggle:checked~.drawer-content_.navbar_&]:hidden" />
|
||||
<PanelLeftClose class="[.drawer-toggle:checked~.drawer-content_.navbar_&]:block hidden" />
|
||||
</label>
|
||||
<div class="px-4">{{ currentFunction }}</div>
|
||||
</nav>
|
||||
<!-- Page content here -->
|
||||
<div class="p-4 flex-1 flex flex-row gap-1">
|
||||
<div class="col-span-5 min-w-[300px]">
|
||||
<FeatureList :current-function="currentFunction" :messages="messages" :workflows="workflows"
|
||||
:projects="projects" :selected-message-id="selectedMessageId" :selected-workflow-id="selectedWorkflowId"
|
||||
:selected-project-id="selectedProjectId" @select-message="selectMessage" @select-workflow="selectWorkflow"
|
||||
@select-project="selectProject" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作空间 -->
|
||||
<div class="col-span-5 flex-1">
|
||||
<OperationSpace :current-function="currentFunction" :selected-message="selectedMessage"
|
||||
:selected-workflow="selectedWorkflow" :selected-project="selectedProject"
|
||||
@message-action="handleMessageAction" @workflow-action="handleWorkflowAction"
|
||||
@project-action="handleProjectAction" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side is-drawer-close:overflow-visible">
|
||||
<label for="my-drawer-4" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<div class="flex min-h-full flex-col items-start bg-base-200 is-drawer-close:w-14 is-drawer-open:w-64">
|
||||
<!-- Sidebar content here -->
|
||||
<FunctionNav :current-function="currentFunction" @select="selectFunction" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useHome } from '@/composables/useHome'
|
||||
import FunctionNav from './FunctionNav.vue'
|
||||
import FeatureList from './FeatureList.vue'
|
||||
import OperationSpace from './OperationSpace.vue'
|
||||
import { PanelLeftClose, PanelLeftOpen } from 'lucide-vue-next'
|
||||
|
||||
const {
|
||||
currentFunction,
|
||||
messages,
|
||||
workflows,
|
||||
projects,
|
||||
selectedMessageId,
|
||||
selectedWorkflowId,
|
||||
selectedProjectId,
|
||||
selectedMessage,
|
||||
selectedWorkflow,
|
||||
selectedProject,
|
||||
selectFunction,
|
||||
selectMessage,
|
||||
selectWorkflow,
|
||||
selectProject,
|
||||
handleMessageAction,
|
||||
handleWorkflowAction,
|
||||
handleProjectAction,
|
||||
} = useHome()
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './useChat'
|
||||
export * from './useWorkflow'
|
||||
export * from './useAutomation'
|
||||
@@ -0,0 +1,134 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface AutomationTask {
|
||||
id: number
|
||||
name: string
|
||||
workflowId: number
|
||||
status: '运行中' | '已暂停' | '草稿'
|
||||
triggerType: 'cron' | 'event' | 'manual'
|
||||
cronExpression?: string
|
||||
eventConfig?: any
|
||||
schedule?: any
|
||||
nextRun?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export function useAutomation() {
|
||||
const tasks = ref<AutomationTask[]>([])
|
||||
const selectedTaskId = ref<number | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// 计算属性:获取当前选中的任务
|
||||
const selectedTask = computed(() =>
|
||||
tasks.value.find(t => t.id === selectedTaskId.value)
|
||||
)
|
||||
|
||||
// 加载任务列表
|
||||
const loadTasks = async () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
// TODO: 调用 API 获取任务列表
|
||||
// const response = await fetch('/api/automation/tasks')
|
||||
// tasks.value = await response.json()
|
||||
|
||||
// 模拟数据
|
||||
tasks.value = [
|
||||
{
|
||||
id: 1,
|
||||
name: '每日数据同步',
|
||||
workflowId: 1,
|
||||
status: '运行中',
|
||||
triggerType: 'cron',
|
||||
cronExpression: '0 0 2 * * ?',
|
||||
nextRun: '2026-03-12 02:00:00',
|
||||
createdAt: '2026-03-01 10:00:00',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '周报生成',
|
||||
workflowId: 2,
|
||||
status: '已暂停',
|
||||
triggerType: 'cron',
|
||||
cronExpression: '0 0 9 ? * MON',
|
||||
nextRun: '未安排',
|
||||
createdAt: '2026-03-05 14:30:00',
|
||||
},
|
||||
]
|
||||
} catch (e) {
|
||||
error.value = '加载任务列表失败'
|
||||
console.error(e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
const createTask = async (task: Omit<AutomationTask, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const newTask: AutomationTask = {
|
||||
...task,
|
||||
id: Date.now(),
|
||||
createdAt: new Date().toLocaleString(),
|
||||
updatedAt: new Date().toLocaleString(),
|
||||
}
|
||||
tasks.value.push(newTask)
|
||||
// TODO: 调用 API 创建任务
|
||||
return newTask
|
||||
}
|
||||
|
||||
// 更新任务
|
||||
const updateTask = async (id: number, updates: Partial<AutomationTask>) => {
|
||||
const index = tasks.value.findIndex(t => t.id === id)
|
||||
if (index === -1) return
|
||||
|
||||
const updated = { ...tasks.value[index], ...updates, updatedAt: new Date().toLocaleString() }
|
||||
tasks.value[index] = updated
|
||||
// TODO: 调用 API 更新任务
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const deleteTask = async (id: number) => {
|
||||
tasks.value = tasks.value.filter(t => t.id !== id)
|
||||
if (selectedTaskId.value === id) {
|
||||
selectedTaskId.value = null
|
||||
}
|
||||
// TODO: 调用 API 删除任务
|
||||
}
|
||||
|
||||
// 启动任务
|
||||
const startTask = async (id: number) => {
|
||||
await updateTask(id, { status: '运行中' })
|
||||
}
|
||||
|
||||
// 暂停任务
|
||||
const pauseTask = async (id: number) => {
|
||||
await updateTask(id, { status: '已暂停' })
|
||||
}
|
||||
|
||||
// 选择任务
|
||||
const selectTask = (id: number) => {
|
||||
selectedTaskId.value = id
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
tasks,
|
||||
selectedTaskId,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// 计算属性
|
||||
selectedTask,
|
||||
|
||||
// 方法
|
||||
loadTasks,
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
startTask,
|
||||
pauseTask,
|
||||
selectTask,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface ChatMessage {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
time: string
|
||||
pending?: boolean
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
export interface ChatBubble {
|
||||
id: number
|
||||
content: string
|
||||
time: string
|
||||
isSelf: boolean
|
||||
}
|
||||
|
||||
export type FilterType = 'all' | 'pending' | 'processed'
|
||||
|
||||
export function useChat() {
|
||||
const selectedChatId = ref<number | null>(null)
|
||||
|
||||
// 模拟对话列表数据
|
||||
const chats = ref<ChatMessage[]>([
|
||||
{ id: 1, title: '系统助手', content: '欢迎使用角色管理系统,祝您工作顺利!', time: '10:00', pending: false, avatar: '系统' },
|
||||
{ id: 2, title: '任务提醒', content: '您有一个待处理的任务需要在今天完成。', time: '11:30', pending: true, avatar: '任务' },
|
||||
{ id: 3, title: '审批助手', content: '新的流程审批等待您的处理,请及时查看。', time: '14:20', pending: true, avatar: '审批' },
|
||||
{ id: 4, title: '会议助手', content: '明天上午 10 点召开项目进度会议,请准时参加。', time: '15:00', pending: false, avatar: '会议' },
|
||||
])
|
||||
|
||||
// 模拟聊天泡泡数据
|
||||
const chatBubbles = ref<Record<number, ChatBubble[]>>({
|
||||
1: [
|
||||
{ id: 1, content: '欢迎使用角色管理系统,祝您工作顺利!', time: '10:00', isSelf: false },
|
||||
{ id: 2, content: '谢谢,我该如何开始?', time: '10:01', isSelf: true },
|
||||
{ id: 3, content: '您可以从左侧菜单开始探索各项功能', time: '10:02', isSelf: false },
|
||||
],
|
||||
2: [
|
||||
{ id: 1, content: '您有一个待处理的任务需要在今天完成。', time: '11:30', isSelf: false },
|
||||
],
|
||||
3: [
|
||||
{ id: 1, content: '新的流程审批等待您的处理,请及时查看。', time: '14:20', isSelf: false },
|
||||
{ id: 2, content: '好的,我马上处理', time: '14:25', isSelf: true },
|
||||
],
|
||||
4: [
|
||||
{ id: 1, content: '明天上午 10 点召开项目进度会议,请准时参加。', time: '15:00', isSelf: false },
|
||||
],
|
||||
})
|
||||
|
||||
// 计算属性:获取当前选中的对话
|
||||
const selectedChat = computed(() =>
|
||||
chats.value.find(c => c.id === selectedChatId.value)
|
||||
)
|
||||
|
||||
// 计算属性:获取当前对话的泡泡列表
|
||||
const currentBubbles = computed(() =>
|
||||
selectedChatId.value ? (chatBubbles.value[selectedChatId.value] || []) : []
|
||||
)
|
||||
|
||||
// 选择对话
|
||||
const selectChat = (id: number) => {
|
||||
selectedChatId.value = id
|
||||
// 标记为已处理
|
||||
const chat = chats.value.find(c => c.id === id)
|
||||
if (chat) chat.pending = false
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = (content: string) => {
|
||||
if (!selectedChatId.value || !content.trim()) return
|
||||
|
||||
const newBubble: ChatBubble = {
|
||||
id: Date.now(),
|
||||
content: content.trim(),
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
isSelf: true,
|
||||
}
|
||||
|
||||
if (!chatBubbles.value[selectedChatId.value]) {
|
||||
chatBubbles.value[selectedChatId.value] = []
|
||||
}
|
||||
chatBubbles.value[selectedChatId.value].push(newBubble)
|
||||
}
|
||||
|
||||
// 操作处理函数
|
||||
const handleChatAction = (action: string, chatId: number) => {
|
||||
console.log(`处理对话 ${chatId}: ${action}`)
|
||||
// TODO: 实现具体业务逻辑
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
selectedChatId,
|
||||
|
||||
// 数据
|
||||
chats,
|
||||
chatBubbles,
|
||||
|
||||
// 计算属性
|
||||
selectedChat,
|
||||
currentBubbles,
|
||||
|
||||
// 方法
|
||||
selectChat,
|
||||
sendMessage,
|
||||
handleChatAction,
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export type FunctionType = 'message' | 'workflow' | 'project'
|
||||
|
||||
export interface Message {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
time: string
|
||||
unread?: boolean
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
id: number
|
||||
name: string
|
||||
status: '进行中' | '已完成' | '草稿' | '已暂停'
|
||||
creator: string
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: number
|
||||
name: string
|
||||
status: '进行中' | '规划中' | '已完成' | '已暂停'
|
||||
progress: number
|
||||
manager: string
|
||||
}
|
||||
|
||||
export function useHome() {
|
||||
const currentFunction = ref<FunctionType>('message')
|
||||
const selectedMessageId = ref<number | null>(null)
|
||||
const selectedWorkflowId = ref<number | null>(null)
|
||||
const selectedProjectId = ref<number | null>(null)
|
||||
|
||||
// 模拟数据
|
||||
const messages= ref<Message[]>([
|
||||
{ id: 1, title: '系统通知', content: '欢迎使用角色管理系统,祝您工作顺利!', time: '2026-03-10 10:00', unread: true },
|
||||
{ id: 2, title: '任务提醒', content: '您有一个待处理的任务需要在今天完成。', time: '2026-03-10 11:30', unread: true },
|
||||
{ id: 3, title: '审批请求', content: '新的流程审批等待您的处理,请及时查看。', time: '2026-03-10 14:20', unread: false },
|
||||
{ id: 4, title: '会议通知', content: '明天上午 10 点召开项目进度会议,请准时参加。', time: '2026-03-10 15:00', unread: false },
|
||||
])
|
||||
|
||||
const workflows = ref<Workflow[]>([
|
||||
{ id: 1, name: '请假审批流程', status: '进行中', creator: '张三', createTime: '2026-03-09' },
|
||||
{ id: 2, name: '采购申请流程', status: '已完成', creator: '李四', createTime: '2026-03-08' },
|
||||
{ id: 3, name: '项目立项流程', status: '草稿', creator: '王五', createTime: '2026-03-10' },
|
||||
{ id: 4, name: '费用报销流程', status: '进行中', creator: '赵六', createTime: '2026-03-07' },
|
||||
])
|
||||
|
||||
const projects = ref<Project[]>([
|
||||
{ id: 1, name: 'ERP 系统升级', status: '进行中', progress: 65, manager: '张三' },
|
||||
{ id: 2, name: 'CRM 客户管理系统', status: '规划中', progress: 20, manager: '李四' },
|
||||
{ id: 3, name: '数据分析平台', status: '已完成', progress: 100, manager: '王五' },
|
||||
{ id: 4, name: '移动办公 APP', status: '进行中', progress: 45, manager: '赵六' },
|
||||
])
|
||||
|
||||
// 计算属性:获取当前选中的项目
|
||||
const selectedMessage = computed(() =>
|
||||
messages.value.find(m => m.id === selectedMessageId.value)
|
||||
)
|
||||
|
||||
const selectedWorkflow = computed(() =>
|
||||
workflows.value.find(w => w.id === selectedWorkflowId.value)
|
||||
)
|
||||
|
||||
const selectedProject = computed(() =>
|
||||
projects.value.find(p => p.id === selectedProjectId.value)
|
||||
)
|
||||
|
||||
// 切换功能
|
||||
const selectFunction = (func: FunctionType) => {
|
||||
currentFunction.value = func
|
||||
// 清空其他选中项
|
||||
selectedMessageId.value = null
|
||||
selectedWorkflowId.value = null
|
||||
selectedProjectId.value = null
|
||||
}
|
||||
|
||||
// 选择列表项
|
||||
const selectMessage = (id: number) => {
|
||||
selectedMessageId.value = id
|
||||
// 标记为已读
|
||||
const msg = messages.value.find(m => m.id === id)
|
||||
if (msg) msg.unread = false
|
||||
}
|
||||
|
||||
const selectWorkflow = (id: number) => {
|
||||
selectedWorkflowId.value = id
|
||||
}
|
||||
|
||||
const selectProject = (id: number) => {
|
||||
selectedProjectId.value = id
|
||||
}
|
||||
|
||||
// 操作处理函数
|
||||
const handleMessageAction = (action: string, messageId: number) => {
|
||||
console.log(`处理消息 ${messageId}: ${action}`)
|
||||
// TODO: 实现具体业务逻辑
|
||||
}
|
||||
|
||||
const handleWorkflowAction = (action: string, workflowId: number) => {
|
||||
console.log(`处理流程 ${workflowId}: ${action}`)
|
||||
// TODO: 实现具体业务逻辑
|
||||
}
|
||||
|
||||
const handleProjectAction = (action: string, projectId: number) => {
|
||||
console.log(`处理项目 ${projectId}: ${action}`)
|
||||
// TODO: 实现具体业务逻辑
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentFunction,
|
||||
selectedMessageId,
|
||||
selectedWorkflowId,
|
||||
selectedProjectId,
|
||||
|
||||
// 数据
|
||||
messages,
|
||||
workflows,
|
||||
projects,
|
||||
|
||||
// 计算属性
|
||||
selectedMessage,
|
||||
selectedWorkflow,
|
||||
selectedProject,
|
||||
|
||||
// 方法
|
||||
selectFunction,
|
||||
selectMessage,
|
||||
selectWorkflow,
|
||||
selectProject,
|
||||
handleMessageAction,
|
||||
handleWorkflowAction,
|
||||
handleProjectAction,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
isPermissionGranted,
|
||||
Options as NotificationOptions,
|
||||
requestPermission,
|
||||
sendNotification,
|
||||
} from '@tauri-apps/plugin-notification';
|
||||
|
||||
export function useNotification(options: NotificationOptions) {
|
||||
|
||||
const send = async (replaceOptions?: Partial<NotificationOptions>) => {
|
||||
// 你有发送通知的权限吗?
|
||||
let permissionGranted = await isPermissionGranted();
|
||||
|
||||
// 如果没有,我们需要请求它
|
||||
if (!permissionGranted) {
|
||||
const permission = await requestPermission();
|
||||
permissionGranted = permission === 'granted';
|
||||
}
|
||||
|
||||
// 一旦获得许可,我们就可以发送通知
|
||||
if (permissionGranted) {
|
||||
sendNotification({ ...options, ...replaceOptions });
|
||||
}
|
||||
};
|
||||
|
||||
return send
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
getWorkflows as apiGetWorkflows,
|
||||
createWorkflow as apiCreateWorkflow,
|
||||
updateWorkflow as apiUpdateWorkflow,
|
||||
deleteWorkflow as apiDeleteWorkflow
|
||||
} from '@/api/workflow'
|
||||
|
||||
export interface Workflow {
|
||||
id: number
|
||||
name: string
|
||||
status: '进行中' | '已完成' | '草稿' | '已暂停'
|
||||
creator: string
|
||||
createTime: string
|
||||
definition?: string
|
||||
updatedTime?: string
|
||||
}
|
||||
|
||||
export function useWorkflow() {
|
||||
const selectedWorkflowId = ref<number | null>(null)
|
||||
const workflows = ref<Workflow[]>([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 加载工作流列表
|
||||
const loadWorkflows = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
workflows.value = await apiGetWorkflows()
|
||||
} catch (error) {
|
||||
console.error('加载工作流失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建工作流
|
||||
const createWorkflow = async (workflowData: Omit<Workflow, 'id'>) => {
|
||||
const id = await apiCreateWorkflow(workflowData)
|
||||
if (id) {
|
||||
await loadWorkflows()
|
||||
return id
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 更新工作流
|
||||
const updateWorkflow = async (id: number, workflowData: Partial<Workflow>) => {
|
||||
const success = await apiUpdateWorkflow(id, workflowData)
|
||||
if (success) {
|
||||
await loadWorkflows()
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
// 删除工作流
|
||||
const deleteWorkflow = async (id: number) => {
|
||||
const success = await apiDeleteWorkflow(id)
|
||||
if (success) {
|
||||
if (selectedWorkflowId.value === id) {
|
||||
selectedWorkflowId.value = null
|
||||
}
|
||||
await loadWorkflows()
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
// 计算属性:获取当前选中的工作流
|
||||
const selectedWorkflow = computed(() =>
|
||||
workflows.value.find(w => w.id === selectedWorkflowId.value)
|
||||
)
|
||||
|
||||
// 选择工作流
|
||||
const selectWorkflow = (id: number) => {
|
||||
selectedWorkflowId.value = id
|
||||
}
|
||||
|
||||
// 操作处理函数
|
||||
const handleWorkflowAction = async (action: string, workflowId: number) => {
|
||||
console.log(`处理流程 ${workflowId}: ${action}`)
|
||||
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
if (confirm('确定要删除这个工作流吗?')) {
|
||||
await deleteWorkflow(workflowId)
|
||||
}
|
||||
break
|
||||
case 'edit':
|
||||
// TODO: 打开编辑对话框
|
||||
console.log('编辑工作流', workflowId)
|
||||
break
|
||||
case 'start':
|
||||
// TODO: 启动工作流
|
||||
console.log('启动工作流', workflowId)
|
||||
break
|
||||
default:
|
||||
console.log('未知操作:', action)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
onMounted(() => {
|
||||
loadWorkflows()
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
selectedWorkflowId,
|
||||
isLoading,
|
||||
|
||||
// 数据
|
||||
workflows,
|
||||
|
||||
// 计算属性
|
||||
selectedWorkflow,
|
||||
|
||||
// 方法
|
||||
selectWorkflow,
|
||||
handleWorkflowAction,
|
||||
loadWorkflows,
|
||||
createWorkflow,
|
||||
updateWorkflow,
|
||||
deleteWorkflow,
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
definePage({
|
||||
name: 'About',
|
||||
meta: {
|
||||
title: 'About Page'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="container">
|
||||
<h1>About Page</h1>
|
||||
<p>This is the about page.</p>
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink> |
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
</nav>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: none;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="drawer h-full md:drawer-open">
|
||||
<input id="my-drawer-4" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<nav class="navbar bg-base-300">
|
||||
<label for="my-drawer-4" aria-label="打开侧边栏" class="btn btn-square btn-ghost">
|
||||
<!-- Sidebar toggle icon -->
|
||||
<PanelLeftOpen class="[.drawer-toggle:checked~.drawer-content_.navbar_&]:hidden" />
|
||||
<PanelLeftClose class="[.drawer-toggle:checked~.drawer-content_.navbar_&]:block hidden" />
|
||||
</label>
|
||||
<div class="px-4">{{ route.name }}</div>
|
||||
</nav>
|
||||
<!-- Navbar -->
|
||||
<!-- Page content here -->
|
||||
<main class="p-4 flex-1 flex flex-row gap-1">
|
||||
<RouterView></RouterView>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side is-drawer-close:overflow-visible">
|
||||
<label for="my-drawer-4" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<div class="flex min-h-full flex-col items-start bg-base-200 is-drawer-close:w-14 is-drawer-open:w-64">
|
||||
<!-- Sidebar content here -->
|
||||
<SideNav />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SideNav from '@/components/home/SideNav.vue'
|
||||
import { PanelLeftClose, PanelLeftOpen } from '@lucide/vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
|
||||
definePage({
|
||||
name: 'Home',
|
||||
path: '/',
|
||||
redirect: '/chat'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<div class="flex flex-1 gap-4 w-full h-full overflow-hidden">
|
||||
<!-- 左侧自动化任务列表 -->
|
||||
<div class="w-80 bg-base-200 p-4 rounded-lg flex flex-col overflow-hidden">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-bold text-lg">自动化任务</h3>
|
||||
<button class="btn btn-primary btn-sm" @click="showCreateModal = true">
|
||||
<Plus :size="16" />
|
||||
新建
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="form-control mb-4">
|
||||
<input
|
||||
v-model="searchText"
|
||||
type="text"
|
||||
placeholder="搜索任务..."
|
||||
class="input input-bordered input-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div
|
||||
v-for="task in filteredTasks"
|
||||
:key="task.id"
|
||||
@click="selectTask(task)"
|
||||
:class="[
|
||||
'p-3 mb-2 rounded-lg cursor-pointer transition-colors border-l-4',
|
||||
selectedTask?.id === task.id
|
||||
? 'bg-primary text-primary-content border-primary'
|
||||
: 'bg-base-100 hover:bg-base-300 border-transparent'
|
||||
]"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="font-semibold text-sm">{{ task.name }}</div>
|
||||
<span :class="getStatusBadgeClass(task.status)">{{ task.status }}</span>
|
||||
</div>
|
||||
<div class="text-xs opacity-70 mt-1">
|
||||
{{ getTriggerTypeLabel(task.triggerType) }}
|
||||
</div>
|
||||
<div class="text-xs opacity-50 mt-1">
|
||||
下次执行:{{ task.nextRun || '未安排' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧任务详情 -->
|
||||
<div class="flex-1 bg-base-200 p-6 rounded-lg overflow-y-auto">
|
||||
<div v-if="selectedTask" class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold">{{ selectedTask.name }}</h2>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-success btn-sm" @click="toggleTask(true)" :disabled="selectedTask.status === '运行中'">
|
||||
<Play :size="16" />
|
||||
启动
|
||||
</button>
|
||||
<button class="btn btn-warning btn-sm" @click="toggleTask(false)" :disabled="selectedTask.status !== '运行中'">
|
||||
<Pause :size="16" />
|
||||
暂停
|
||||
</button>
|
||||
<button class="btn btn-error btn-sm" @click="deleteTask">
|
||||
<Trash2 :size="16" />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="card bg-base-100 p-4">
|
||||
<h3 class="font-bold mb-3">基本信息</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">任务名称</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="selectedTask.name"
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">关联流程</span>
|
||||
</label>
|
||||
<select v-model="selectedTask.workflowId" class="select select-bordered select-sm">
|
||||
<option value="">选择流程</option>
|
||||
<option v-for="wf in workflows" :key="wf.id" :value="wf.id">
|
||||
{{ wf.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 触发器配置 -->
|
||||
<div class="card bg-base-100 p-4">
|
||||
<h3 class="font-bold mb-3">触发器配置</h3>
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">触发类型</span>
|
||||
</label>
|
||||
<select v-model="selectedTask.triggerType" class="select select-bordered select-sm">
|
||||
<option value="cron">定时触发</option>
|
||||
<option value="event">事件触发</option>
|
||||
<option value="manual">手动触发</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Cron 表达式配置 -->
|
||||
<div v-if="selectedTask.triggerType === 'cron'" class="space-y-4">
|
||||
<CronEditor v-model="selectedTask.cronExpression" />
|
||||
</div>
|
||||
|
||||
<!-- 事件触发配置 -->
|
||||
<div v-if="selectedTask.triggerType === 'event'" class="space-y-4">
|
||||
<EventTrigger v-model="selectedTask.eventConfig" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 调度规则 -->
|
||||
<div class="card bg-base-100 p-4">
|
||||
<h3 class="font-bold mb-3">调度规则</h3>
|
||||
<ScheduleManager v-model="selectedTask.schedule" />
|
||||
</div>
|
||||
|
||||
<!-- 执行历史 -->
|
||||
<div class="card bg-base-100 p-4">
|
||||
<h3 class="font-bold mb-3">执行历史</h3>
|
||||
<div class="text-sm opacity-70">
|
||||
暂无执行记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex-1 flex items-center justify-center h-full">
|
||||
<div class="text-center opacity-50">
|
||||
<Clock :size="48" class="mx-auto mb-4 opacity-50" />
|
||||
<p>请选择一个自动化任务</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建任务弹窗 -->
|
||||
<dialog :class="['modal', { 'modal-open': showCreateModal }]">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">创建自动化任务</h3>
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">任务名称</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="newTaskName"
|
||||
type="text"
|
||||
placeholder="请输入任务名称"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">关联流程</span>
|
||||
</label>
|
||||
<select v-model="newTaskWorkflowId" class="select select-bordered">
|
||||
<option value="">选择流程</option>
|
||||
<option v-for="wf in workflows" :key="wf.id" :value="wf.id">
|
||||
{{ wf.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button class="btn" @click="showCreateModal = false">取消</button>
|
||||
<button class="btn btn-primary" @click="createTask">创建</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Plus, Play, Pause, Trash2, Clock } from '@lucide/vue'
|
||||
import CronEditor from '@/components/automation/CronEditor.vue'
|
||||
import EventTrigger from '@/components/automation/EventTrigger.vue'
|
||||
import ScheduleManager from '@/components/automation/ScheduleManager.vue'
|
||||
import { getWorkflows } from '@/api/workflow'
|
||||
import type { Workflow } from '@/composables/useWorkflow'
|
||||
|
||||
definePage({
|
||||
name: '自动化',
|
||||
})
|
||||
|
||||
interface AutomationTask {
|
||||
id: number
|
||||
name: string
|
||||
workflowId: number
|
||||
status: '运行中' | '已暂停' | '草稿'
|
||||
triggerType: 'cron' | 'event' | 'manual'
|
||||
cronExpression?: string
|
||||
eventConfig?: any
|
||||
schedule?: any
|
||||
nextRun?: string
|
||||
}
|
||||
|
||||
const workflows = ref<Workflow[]>([])
|
||||
const selectedTask = ref<AutomationTask | null>(null)
|
||||
const searchText = ref('')
|
||||
const showCreateModal = ref(false)
|
||||
const newTaskName = ref('')
|
||||
const newTaskWorkflowId = ref<number | ''>('')
|
||||
|
||||
const tasks = ref<AutomationTask[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '每日数据同步',
|
||||
workflowId: 1,
|
||||
status: '运行中',
|
||||
triggerType: 'cron',
|
||||
cronExpression: '0 0 2 * * ?',
|
||||
nextRun: '2026-03-12 02:00:00',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '周报生成',
|
||||
workflowId: 2,
|
||||
status: '已暂停',
|
||||
triggerType: 'cron',
|
||||
cronExpression: '0 0 9 ? * MON',
|
||||
nextRun: '未安排',
|
||||
},
|
||||
])
|
||||
|
||||
// 过滤后的任务列表
|
||||
const filteredTasks = computed(() => {
|
||||
if (!searchText.value) return tasks.value
|
||||
return tasks.value.filter(task =>
|
||||
task.name.toLowerCase().includes(searchText.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 获取触发类型标签
|
||||
const getTriggerTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
cron: '定时触发',
|
||||
event: '事件触发',
|
||||
manual: '手动触发',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
// 获取状态徽章样式
|
||||
const getStatusBadgeClass = (status: string) => {
|
||||
const baseClass = 'badge badge-sm'
|
||||
switch (status) {
|
||||
case '运行中':
|
||||
return `${baseClass} badge-info`
|
||||
case '已暂停':
|
||||
return `${baseClass} badge-warning`
|
||||
case '草稿':
|
||||
return `${baseClass} badge-ghost`
|
||||
default:
|
||||
return baseClass
|
||||
}
|
||||
}
|
||||
|
||||
// 选择任务
|
||||
const selectTask = (task: AutomationTask) => {
|
||||
selectedTask.value = { ...task }
|
||||
}
|
||||
|
||||
// 切换任务状态
|
||||
const toggleTask = (start: boolean) => {
|
||||
if (!selectedTask.value) return
|
||||
selectedTask.value.status = start ? '运行中' : '已暂停'
|
||||
// TODO: 调用 API
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const deleteTask = () => {
|
||||
if (!selectedTask.value) return
|
||||
if (confirm(`确定要删除任务 "${selectedTask.value.name}" 吗?`)) {
|
||||
tasks.value = tasks.value.filter(t => t.id !== selectedTask.value!.id)
|
||||
selectedTask.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
const createTask = () => {
|
||||
if (!newTaskName.value || !newTaskWorkflowId.value) {
|
||||
alert('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
const newTask: AutomationTask = {
|
||||
id: Date.now(),
|
||||
name: newTaskName.value,
|
||||
workflowId: Number(newTaskWorkflowId.value),
|
||||
status: '草稿',
|
||||
triggerType: 'manual',
|
||||
}
|
||||
|
||||
tasks.value.push(newTask)
|
||||
showCreateModal.value = false
|
||||
newTaskName.value = ''
|
||||
newTaskWorkflowId.value = ''
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
workflows.value = await getWorkflows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal {
|
||||
&.modal-open {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="flex flex-1 w-full h-full overflow-hidden">
|
||||
<!-- 聊天窗口 -->
|
||||
<ChatWindow :bubbles="currentBubbles" @send="handleSendMessage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChatWindow from '@/components/chat/ChatWindow.vue';
|
||||
import { useChat } from '@/composables/useChat';
|
||||
import { useNotification } from '@/composables/useNotifition';
|
||||
|
||||
definePage({
|
||||
name: '消息',
|
||||
})
|
||||
|
||||
const {
|
||||
selectedChat,
|
||||
currentBubbles,
|
||||
sendMessage,
|
||||
} = useChat()
|
||||
|
||||
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = (content: string) => {
|
||||
sendMessage(content)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<!-- 流程列表 -->
|
||||
<div class="w-full bg-base-200 border-r border-base-300 p-4 flex flex-col">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-bold text-lg">工作流列表</h3>
|
||||
<button class="btn btn-sm btn-primary" @click="createNewWorkflow">
|
||||
<Plus :size="16" />
|
||||
新建
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="form-control mb-4">
|
||||
<input v-model="searchText" type="text" placeholder="搜索流程..." class="w-100 input input-bordered input-sm" />
|
||||
</div>
|
||||
|
||||
<!-- 流程列表 -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<!-- 表头 -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-8"></th>
|
||||
<th>流程名称</th>
|
||||
<th>状态</th>
|
||||
<th>创建人</th>
|
||||
<th>创建时间</th>
|
||||
<th class="text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- 表体 -->
|
||||
<tbody>
|
||||
<tr v-for="workflow in paginatedWorkflows" :key="workflow.id" @click="handleWorkflowClick(workflow)" :class="[
|
||||
'cursor-pointer hover:bg-base-300',
|
||||
selectedWorkflowId === workflow.id ? 'bg-primary text-primary-content' : ''
|
||||
]">
|
||||
<td class="text-xs">
|
||||
<div class="badge badge-xs"
|
||||
:class="selectedWorkflowId === workflow.id ? 'badge-primary' : 'badge-ghost'"></div>
|
||||
</td>
|
||||
<td class="font-semibold">{{ workflow.name }}</td>
|
||||
<td>
|
||||
<span :class="getStatusBadgeClass(workflow.status)" class="badge badge-xs">
|
||||
{{ workflow.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-sm opacity-70">{{ workflow.creator }}</td>
|
||||
<td class="text-sm opacity-70">{{ formatDate(workflow.createTime) }}</td>
|
||||
<td class="text-right">
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-xs btn-ghost" @click.stop="editWorkflow(workflow)" title="编辑">
|
||||
<Edit :size="14" />
|
||||
编辑
|
||||
</button>
|
||||
<button class="join-item btn btn-xs btn-ghost text-error hover:text-error"
|
||||
@click.stop="confirmDeleteWorkflow(workflow)" title="删除">
|
||||
<Trash2 :size="14" />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<tr v-if="filteredWorkflows.length === 0">
|
||||
<td colspan="6" class="text-center py-8 opacity-50">
|
||||
<p>暂无工作流</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mt-4 flex justify-center gap-2">
|
||||
<div class="join">
|
||||
<button class="join-item btn" :disabled="currentPage === 1" @click="currentPage--">上一页</button>
|
||||
<button class="join-item btn">{{ currentPage }} / {{ totalPages }}</button>
|
||||
<button class="join-item btn" :disabled="currentPage === totalPages" @click="currentPage++">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { deleteWorkflow as apiDeleteWorkflow, getWorkflows } from '@/api/workflow'
|
||||
import type { Workflow } from '@/composables/useWorkflow'
|
||||
import { Edit, Plus, Trash2 } from '@lucide/vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import toast from 'vue3-hot-toast'
|
||||
|
||||
definePage({
|
||||
name: '流程',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const workflows = ref<Workflow[]>([])
|
||||
const selectedWorkflowId = ref<number | null>(null)
|
||||
const searchText = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
// 过滤后的流程列表
|
||||
const filteredWorkflows = computed(() => {
|
||||
if (!searchText.value) return workflows.value
|
||||
return workflows.value.filter(wf =>
|
||||
wf.name.toLowerCase().includes(searchText.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 分页后的流程列表
|
||||
const paginatedWorkflows = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
return filteredWorkflows.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 总页数
|
||||
const totalPages = computed(() => Math.ceil(filteredWorkflows.value.length / pageSize))
|
||||
|
||||
// 点击流程项(选中效果)
|
||||
const handleWorkflowClick = (workflow: Workflow) => {
|
||||
selectedWorkflowId.value = workflow.id
|
||||
}
|
||||
|
||||
// 编辑流程
|
||||
const editWorkflow = (workflow: Workflow) => {
|
||||
router.push(`/workflow/${workflow.id}`)
|
||||
}
|
||||
|
||||
// 确认删除流程
|
||||
const confirmDeleteWorkflow = (workflow: Workflow) => {
|
||||
if (confirm(`确定要删除工作流 "${workflow.name}" 吗?`)) {
|
||||
deleteWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除流程
|
||||
const deleteWorkflow = async (workflow: Workflow) => {
|
||||
const success = await apiDeleteWorkflow(workflow.id)
|
||||
if (success) {
|
||||
toast.success('删除成功')
|
||||
if (selectedWorkflowId.value === workflow.id) {
|
||||
selectedWorkflowId.value = null
|
||||
}
|
||||
await loadWorkflows()
|
||||
} else {
|
||||
toast.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新流程
|
||||
const createNewWorkflow = () => {
|
||||
router.push('/workflow/new')
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取状态徽章样式
|
||||
const getStatusBadgeClass = (status: string) => {
|
||||
const classes: Record<string, string> = {
|
||||
'进行中': 'badge-info',
|
||||
'已完成': 'badge-success',
|
||||
'草稿': 'badge-ghost',
|
||||
'已暂停': 'badge-warning',
|
||||
}
|
||||
return `badge ${classes[status] || ''}`
|
||||
}
|
||||
|
||||
// 加载流程列表
|
||||
const loadWorkflows = async () => {
|
||||
workflows.value = await getWorkflows()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadWorkflows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,7 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import HomePage from '@/components/home/index.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HomePage />
|
||||
</template>
|
||||
+2
-2
@@ -58,9 +58,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
import { Lock, User } from '@lucide/vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { User, Lock } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
Vendored
+40
-9
@@ -26,16 +26,32 @@ declare module 'vue-router/auto-routes' {
|
||||
* Route name map generated by vue-router
|
||||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<
|
||||
'/',
|
||||
'Home': RouteRecordInfo<
|
||||
'Home',
|
||||
'/',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| '流程'
|
||||
| '消息'
|
||||
| '自动化'
|
||||
>,
|
||||
'自动化': RouteRecordInfo<
|
||||
'自动化',
|
||||
'/automation',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'About': RouteRecordInfo<
|
||||
'About',
|
||||
'/about',
|
||||
'消息': RouteRecordInfo<
|
||||
'消息',
|
||||
'/chat',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'流程': RouteRecordInfo<
|
||||
'流程',
|
||||
'/workflow',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
@@ -60,15 +76,30 @@ declare module 'vue-router/auto-routes' {
|
||||
* @internal
|
||||
*/
|
||||
export interface _RouteFileInfoMap {
|
||||
'src/pages/index.vue': {
|
||||
'src/pages/home.vue': {
|
||||
routes:
|
||||
| '/'
|
||||
| 'Home'
|
||||
| '流程'
|
||||
| '消息'
|
||||
| '自动化'
|
||||
views:
|
||||
| 'default'
|
||||
}
|
||||
'src/pages/home/automation/index.vue': {
|
||||
routes:
|
||||
| '自动化'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/about.vue': {
|
||||
'src/pages/home/chat/index.vue': {
|
||||
routes:
|
||||
| 'About'
|
||||
| '消息'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/home/workflow/index.vue': {
|
||||
routes:
|
||||
| '流程'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 基础节点数据接口
|
||||
export interface NodeData {
|
||||
label: string
|
||||
description?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 表单节点数据
|
||||
export interface FormNodeData extends NodeData {
|
||||
formId: string
|
||||
formName: string
|
||||
formFields: FormField[]
|
||||
}
|
||||
|
||||
export interface FormField {
|
||||
id: string
|
||||
name: string
|
||||
type: 'input' | 'select' | 'textarea' | 'checkbox' | 'radio' | 'date'
|
||||
label: string
|
||||
required?: boolean
|
||||
options?: { label: string; value: any }[]
|
||||
defaultValue?: any
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
// 定时节点数据
|
||||
export interface CronNodeData extends NodeData {
|
||||
cronExpression: string
|
||||
timezone?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
loopType: 'once' | 'daily' | 'weekly' | 'monthly' | 'custom'
|
||||
}
|
||||
|
||||
// SQL 节点数据
|
||||
export interface SqlNodeData extends NodeData {
|
||||
databaseType: 'mysql' | 'postgresql' | 'sqlite' | 'sqlserver'
|
||||
connectionString?: string
|
||||
sqlQuery: string
|
||||
parameters?: SqlParameter[]
|
||||
outputMapping?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface SqlParameter {
|
||||
name: string
|
||||
type: string
|
||||
value: any
|
||||
}
|
||||
|
||||
// 脚本节点数据
|
||||
export interface ScriptNodeData extends NodeData {
|
||||
scriptType: 'shell' | 'python' | 'javascript' | 'powershell'
|
||||
scriptContent: string
|
||||
workingDirectory?: string
|
||||
environmentVariables?: Record<string, string>
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
// 节点类型联合
|
||||
export type CustomNodeData = FormNodeData | CronNodeData | SqlNodeData | ScriptNodeData
|
||||
|
||||
// 节点类型枚举
|
||||
export type CustomNodeType = 'form' | 'cron' | 'sql' | 'script'
|
||||
@@ -0,0 +1,58 @@
|
||||
import Database from '@tauri-apps/plugin-sql'
|
||||
|
||||
let dbInstance: Database | null = null
|
||||
|
||||
export async function getDatabase(): Promise<Database> {
|
||||
if (!dbInstance) {
|
||||
dbInstance = await Database.load('sqlite:rolegram.db')
|
||||
}
|
||||
return dbInstance
|
||||
}
|
||||
|
||||
// 初始化工作流表
|
||||
export async function initWorkflowTable() {
|
||||
const db = await getDatabase()
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS workflows (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT '草稿',
|
||||
creator TEXT NOT NULL,
|
||||
createTime TEXT NOT NULL,
|
||||
definition TEXT,
|
||||
updatedTime TEXT
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
// 初始化节点表
|
||||
export async function initNodesTable() {
|
||||
const db = await getDatabase()
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS workflow_nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workflow_id INTEGER NOT NULL,
|
||||
node_type TEXT NOT NULL,
|
||||
position_x REAL NOT NULL,
|
||||
position_y REAL NOT NULL,
|
||||
data TEXT,
|
||||
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
// 初始化边表
|
||||
export async function initEdgesTable() {
|
||||
const db = await getDatabase()
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS workflow_edges (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workflow_id INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
type TEXT,
|
||||
label TEXT,
|
||||
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ const host = process.env.TAURI_DEV_HOST;
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [vue(), VueRouter({
|
||||
dts: 'src/route-map.d.ts',
|
||||
logs: true,
|
||||
}), tailwindcss()],
|
||||
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user