From 112c6487738d75d6fb48836feb2e917b2c466fd5 Mon Sep 17 00:00:00 2001 From: Harden <1915702192@qq.com> Date: Fri, 12 Dec 2025 17:06:10 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E9=87=8D=E6=9E=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=B3=BB=E7=BB=9F=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E9=A3=8E=E6=A0=BC=E4=BC=9A=E8=AF=9D=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/dev-instance2.ps1 | 23 ++ src-tauri/src/commands/chat_commands.rs | 27 +- src-tauri/src/commands/discovery_commands.rs | 19 +- src-tauri/src/commands/mod.rs | 2 + src-tauri/src/database/repository.rs | 68 +++- src-tauri/src/discovery/manager.rs | 5 + src-tauri/src/discovery/service.rs | 101 +++-- src-tauri/src/main.rs | 2 +- src-tauri/src/models/message.rs | 37 ++ src-tauri/src/websocket/handler.rs | 60 ++- src-tauri/tauri.conf.json | 2 +- src/api/index.js | 9 +- src/components/ChatInput.vue | 46 ++- src/components/DeviceCard.vue | 65 +-- src/components/FileProgress.vue | 139 ++++--- src/components/MessageBubble.vue | 38 +- src/components/Sidebar.vue | 47 ++- src/pages/ChatWindow.vue | 137 ++++--- src/pages/DeviceList.vue | 317 +++++++++++--- src/pages/FileTransfer.vue | 208 ++++++---- src/pages/Settings.vue | 408 ++++++++++--------- src/stores/chatStore.js | 23 +- src/stores/deviceStore.js | 38 +- src/styles/index.css | 97 +++-- src/types/index.js | 8 + tailwind.config.js | 79 +++- 26 files changed, 1384 insertions(+), 621 deletions(-) create mode 100644 scripts/dev-instance2.ps1 diff --git a/scripts/dev-instance2.ps1 b/scripts/dev-instance2.ps1 new file mode 100644 index 0000000..1668849 --- /dev/null +++ b/scripts/dev-instance2.ps1 @@ -0,0 +1,23 @@ +# Run second Flash Send dev instance for local testing +# Port offset: +100 + +$env:FLASH_SEND_PORT_OFFSET = "100" +$env:FLASH_SEND_INSTANCE = "LocalTest2" + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Flash Send - Dev Instance 2" -ForegroundColor White +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Port Config:" -ForegroundColor Yellow +Write-Host " UDP Broadcast: 53317 (shared)" -ForegroundColor Gray +Write-Host " UDP Listen: 53417 (53317 + 100)" -ForegroundColor Gray +Write-Host " WebSocket: 53418 (53318 + 100)" -ForegroundColor Gray +Write-Host " HTTP: 53419 (53319 + 100)" -ForegroundColor Gray +Write-Host "" +Write-Host "Make sure Instance 1 is running in another terminal" -ForegroundColor Green +Write-Host "" + +Set-Location $PSScriptRoot\.. + +pnpm tauri dev diff --git a/src-tauri/src/commands/chat_commands.rs b/src-tauri/src/commands/chat_commands.rs index a8e618d..cb8c635 100644 --- a/src-tauri/src/commands/chat_commands.rs +++ b/src-tauri/src/commands/chat_commands.rs @@ -3,7 +3,7 @@ use tauri::{AppHandle, Emitter}; use crate::database::Database; -use crate::models::{ChatMessage, event_names}; +use crate::models::{ChatMessage, Conversation, event_names}; use crate::state::AppState; use crate::utils::{CommandError, CommandResult, HEARTBEAT_INTERVAL_MS}; use crate::websocket::{HeartbeatManager, WsClient, WsConnectionEvent, WsServer}; @@ -182,6 +182,11 @@ pub async fn send_chat_message( device_id.clone(), content, ), + Some("file") => ChatMessage::new_file( + local_device_id, + device_id.clone(), + content, + ), _ => ChatMessage::new_text( local_device_id, device_id.clone(), @@ -267,3 +272,23 @@ pub async fn delete_chat_history(device_id: String) -> CommandResult { Ok(count) } + +/// 获取会话列表(所有有聊天记录的设备) +#[tauri::command] +pub async fn get_conversations() -> CommandResult> { + let state = AppState::get(); + let local_device_id = state.local_device.read().device_id.clone(); + + let mut conversations = Database::get_conversations(&local_device_id) + .map_err(|e| CommandError { + code: "DB_ERROR".to_string(), + message: e.to_string(), + })?; + + // 更新在线状态 + for conv in &mut conversations { + conv.online = state.device_manager.is_device_online(&conv.device_id); + } + + Ok(conversations) +} diff --git a/src-tauri/src/commands/discovery_commands.rs b/src-tauri/src/commands/discovery_commands.rs index 3665a4c..3d1f913 100644 --- a/src-tauri/src/commands/discovery_commands.rs +++ b/src-tauri/src/commands/discovery_commands.rs @@ -2,6 +2,7 @@ use tauri::{AppHandle, Emitter}; +use crate::database::Database; use crate::discovery::{DiscoveryEvent, DiscoveryService}; use crate::models::{DeviceInfo, event_names}; use crate::state::AppState; @@ -40,6 +41,11 @@ pub async fn start_discovery_service(app: AppHandle) -> CommandResult<()> { while let Ok(event) = rx.recv().await { match event { DiscoveryEvent::DeviceFound(device) => { + // 保存到已知设备数据库 + if let Err(e) = Database::save_known_device(&device) { + log::error!("Failed to save known device: {}", e); + } + device_manager.upsert_device(device.clone()); let _ = app_handle.emit(event_names::DEVICE_FOUND, &device); } @@ -79,10 +85,21 @@ pub async fn stop_discovery_service() -> CommandResult<()> { Ok(()) } -/// 获取已发现的设备列表 +/// 获取已发现的设备列表(在线设备) #[tauri::command] pub async fn get_discovered_devices() -> CommandResult> { let state = AppState::get(); let devices = state.device_manager.get_all_devices(); Ok(devices) } + +/// 获取历史设备列表(从数据库) +/// 这些是曾经发现过的设备,可能当前已离线 +#[tauri::command] +pub async fn get_known_devices() -> CommandResult> { + let devices = Database::get_known_devices().map_err(|e| crate::utils::CommandError { + code: "DB_ERROR".to_string(), + message: e.to_string(), + })?; + Ok(devices) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 5cb75a6..08a5804 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -20,6 +20,7 @@ pub fn get_handlers() -> impl Fn(Invoke) -> bool { start_discovery_service, stop_discovery_service, get_discovered_devices, + get_known_devices, // WebSocket 命令 start_websocket_server, stop_websocket_server, @@ -29,6 +30,7 @@ pub fn get_handlers() -> impl Fn(Invoke) -> bool { send_chat_message, get_chat_history, delete_chat_history, + get_conversations, // 文件传输命令 start_http_server, stop_http_server, diff --git a/src-tauri/src/database/repository.rs b/src-tauri/src/database/repository.rs index d44bf89..c897d71 100644 --- a/src-tauri/src/database/repository.rs +++ b/src-tauri/src/database/repository.rs @@ -5,7 +5,7 @@ use parking_lot::Mutex; use rusqlite::{params, Connection, OptionalExtension}; use std::path::PathBuf; -use crate::models::{ChatMessage, FileTransfer, MessageStatus, MessageType, TransferStatus, DeviceInfo}; +use crate::models::{ChatMessage, Conversation, FileTransfer, MessageStatus, MessageType, TransferStatus, DeviceInfo}; use crate::utils::AppError; use super::init_database; @@ -128,6 +128,72 @@ impl Database { Ok(count) } + /// 获取会话列表(所有有聊天记录的设备,按最后消息时间排序) + pub fn get_conversations(local_device_id: &str) -> Result, AppError> { + let conn = Self::conn().lock(); + + // 查询所有聊天过的设备及其最后一条消息 + let mut stmt = conn.prepare( + r#" + WITH conversation_devices AS ( + SELECT DISTINCT + CASE + WHEN from_device = ?1 THEN to_device + ELSE from_device + END as peer_device + FROM chat_messages + WHERE from_device = ?1 OR to_device = ?1 + ), + last_messages AS ( + SELECT + cd.peer_device, + m.content as last_content, + m.message_type as last_type, + m.timestamp as last_time + FROM conversation_devices cd + LEFT JOIN chat_messages m ON ( + (m.from_device = ?1 AND m.to_device = cd.peer_device) OR + (m.from_device = cd.peer_device AND m.to_device = ?1) + ) + WHERE m.timestamp = ( + SELECT MAX(m2.timestamp) + FROM chat_messages m2 + WHERE (m2.from_device = ?1 AND m2.to_device = cd.peer_device) + OR (m2.from_device = cd.peer_device AND m2.to_device = ?1) + ) + ) + SELECT + lm.peer_device, + COALESCE(kd.device_name, lm.peer_device) as device_name, + COALESCE(kd.ip, '') as ip, + lm.last_content, + lm.last_type, + lm.last_time + FROM last_messages lm + LEFT JOIN known_devices kd ON kd.device_id = lm.peer_device + ORDER BY lm.last_time DESC + "#, + )?; + + let conversations = stmt + .query_map(params![local_device_id], |row| { + let last_type_str: Option = row.get(4)?; + Ok(Conversation { + device_id: row.get(0)?, + device_name: row.get(1)?, + ip: row.get(2)?, + online: false, // 将在命令层更新 + last_message: row.get(3)?, + last_message_type: last_type_str.and_then(|s| serde_json::from_str(&s).ok()), + last_message_time: row.get(5)?, + unread_count: 0, // TODO: 实现未读计数 + }) + })? + .collect::, _>>()?; + + Ok(conversations) + } + // ==================== 文件传输操作 ==================== /// 保存文件传输记录 diff --git a/src-tauri/src/discovery/manager.rs b/src-tauri/src/discovery/manager.rs index 5118b0d..0a9a6ef 100644 --- a/src-tauri/src/discovery/manager.rs +++ b/src-tauri/src/discovery/manager.rs @@ -113,6 +113,11 @@ impl DeviceManager { } } + /// 检查设备是否在线 + pub fn is_device_online(&self, device_id: &str) -> bool { + self.devices.contains_key(device_id) + } + /// 清空所有设备 pub fn clear(&self) { let device_ids: Vec = self.devices.iter().map(|d| d.device_id.clone()).collect(); diff --git a/src-tauri/src/discovery/service.rs b/src-tauri/src/discovery/service.rs index 8e72255..ad2783d 100644 --- a/src-tauri/src/discovery/service.rs +++ b/src-tauri/src/discovery/service.rs @@ -1,4 +1,9 @@ //! UDP 广播服务实现 +//! +//! 支持同机器多实例运行: +//! - 每个实例监听不同端口 (DEFAULT_UDP_PORT + offset) +//! - 所有实例都向 DEFAULT_UDP_PORT 广播 +//! - 同时向 localhost 发送确保同机器实例能收到 use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::Arc; @@ -30,8 +35,10 @@ pub enum DiscoveryEvent { pub struct DiscoveryService { /// 本机设备信息 local_device: DeviceInfo, - /// UDP 端口 - port: u16, + /// UDP 监听端口 (可能因端口偏移而不同) + listen_port: u16, + /// UDP 广播目标端口 (始终是 DEFAULT_UDP_PORT) + broadcast_port: u16, /// 事件发送器 event_tx: broadcast::Sender, /// 停止信号 @@ -42,9 +49,18 @@ impl DiscoveryService { /// 创建新的发现服务 pub fn new(local_device: DeviceInfo) -> Self { let (event_tx, _) = broadcast::channel(100); + + // 从环境变量读取端口偏移量 (用于同机器多实例) + let port_offset: u16 = std::env::var("FLASH_SEND_PORT_OFFSET") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + Self { - // UDP 广播端口必须固定,所有实例使用同一端口才能互相发现 - port: DEFAULT_UDP_PORT, + // 监听端口 = 默认端口 + 偏移量 (避免同机器绑定冲突) + listen_port: DEFAULT_UDP_PORT + port_offset, + // 广播端口 = 始终是默认端口 (确保所有实例都向同一端口广播) + broadcast_port: DEFAULT_UDP_PORT, local_device, event_tx, stop_tx: None, @@ -58,7 +74,8 @@ impl DiscoveryService { /// 启动发现服务 pub async fn start(&mut self) -> Result<(), AppError> { - let port = self.port; + let listen_port = self.listen_port; + let broadcast_port = self.broadcast_port; let local_device = self.local_device.clone(); let event_tx = self.event_tx.clone(); @@ -66,22 +83,11 @@ impl DiscoveryService { let (stop_tx, stop_rx) = tokio::sync::oneshot::channel(); self.stop_tx = Some(stop_tx); - // 使用 socket2 创建支持端口复用的 UDP 套接字 - let bind_addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port); - let socket2_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?; - socket2_socket.set_reuse_address(true)?; - #[cfg(unix)] - socket2_socket.set_reuse_port(true)?; - socket2_socket.set_broadcast(true)?; - socket2_socket.set_nonblocking(true)?; - socket2_socket.bind(&bind_addr.into())?; + // 创建主监听 socket + let socket = Self::create_udp_socket(listen_port)?; + log::info!("Discovery service: listening on {}, broadcasting to {}", listen_port, broadcast_port); - let std_socket: std::net::UdpSocket = socket2_socket.into(); - let socket = UdpSocket::from_std(std_socket)?; - - log::info!("Discovery service started on port {}", port); let _ = event_tx.send(DiscoveryEvent::Started); - let socket = Arc::new(socket); // 启动接收任务 @@ -98,20 +104,60 @@ impl DiscoveryService { let broadcast_device = local_device.clone(); let broadcast_handle = tokio::spawn(async move { - Self::broadcast_loop(broadcast_socket, broadcast_device, port).await; + Self::broadcast_loop(broadcast_socket, broadcast_device, broadcast_port).await; }); + // 如果监听端口与广播端口不同,需要额外监听广播端口以接收其他实例的消息 + let extra_recv_handle = if listen_port != broadcast_port { + let extra_event_tx = event_tx.clone(); + let extra_local_id = local_device.device_id.clone(); + + Some(tokio::spawn(async move { + // 尝试绑定广播端口(可能被主实例占用) + match Self::create_udp_socket(broadcast_port) { + Ok(extra_socket) => { + log::info!("Discovery service: also listening on broadcast port {}", broadcast_port); + Self::receive_loop(Arc::new(extra_socket), extra_event_tx, extra_local_id).await; + } + Err(_) => { + log::warn!("Could not bind broadcast port {} (occupied by main instance)", broadcast_port); + } + } + })) + } else { + None + }; + // 等待停止信号 tokio::spawn(async move { let _ = stop_rx.await; recv_handle.abort(); broadcast_handle.abort(); + if let Some(h) = extra_recv_handle { + h.abort(); + } log::info!("Discovery service stopped"); }); Ok(()) } + /// 创建 UDP socket + fn create_udp_socket(port: u16) -> Result { + let bind_addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port); + let socket2_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?; + socket2_socket.set_reuse_address(true)?; + #[cfg(unix)] + socket2_socket.set_reuse_port(true)?; + socket2_socket.set_broadcast(true)?; + socket2_socket.set_nonblocking(true)?; + socket2_socket.bind(&bind_addr.into())?; + + let std_socket: std::net::UdpSocket = socket2_socket.into(); + let socket = UdpSocket::from_std(std_socket)?; + Ok(socket) + } + /// 停止发现服务 pub fn stop(&mut self) { if let Some(stop_tx) = self.stop_tx.take() { @@ -160,17 +206,21 @@ impl DiscoveryService { } } - /// 广播循环 - async fn broadcast_loop(socket: Arc, device: DeviceInfo, port: u16) { - let broadcast_addr = SocketAddr::V4(SocketAddrV4::new(BROADCAST_ADDR, port)); + /// 广播循环 - 向广播地址和 localhost 发送 + async fn broadcast_loop(socket: Arc, device: DeviceInfo, broadcast_port: u16) { + let broadcast_addr = SocketAddr::V4(SocketAddrV4::new(BROADCAST_ADDR, broadcast_port)); + let localhost_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), broadcast_port)); let interval = Duration::from_millis(DISCOVERY_INTERVAL_MS); loop { let packet = DiscoveryPacket::announce(device.clone()); if let Ok(data) = serde_json::to_vec(&packet) { + // 发送广播 (局域网内其他设备) if let Err(e) = socket.send_to(&data, broadcast_addr).await { log::error!("Failed to broadcast: {}", e); } + // 发送到 localhost (同机器其他实例) + let _ = socket.send_to(&data, localhost_addr).await; } tokio::time::sleep(interval).await; } @@ -182,11 +232,14 @@ impl DiscoveryService { let socket = UdpSocket::bind(bind_addr).await?; socket.set_broadcast(true)?; - let broadcast_addr = SocketAddr::V4(SocketAddrV4::new(BROADCAST_ADDR, self.port)); + let broadcast_addr = SocketAddr::V4(SocketAddrV4::new(BROADCAST_ADDR, self.broadcast_port)); + let localhost_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), self.broadcast_port)); + let packet = DiscoveryPacket::goodbye(self.local_device.clone()); let data = serde_json::to_vec(&packet)?; socket.send_to(&data, broadcast_addr).await?; + let _ = socket.send_to(&data, localhost_addr).await; Ok(()) } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5692555..162043a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -37,7 +37,7 @@ fn main() { AppState::init(app_data_dir.clone()) .expect("Failed to initialize app state"); - // 初始化数据库 + // 初始化数据库(存储在 AppData 目录下) Database::init(app_data_dir.clone()) .expect("Failed to initialize database"); diff --git a/src-tauri/src/models/message.rs b/src-tauri/src/models/message.rs index b6cf59e..b3cd3b1 100644 --- a/src-tauri/src/models/message.rs +++ b/src-tauri/src/models/message.rs @@ -11,6 +11,8 @@ pub enum MessageType { Text, /// 图片消息 (base64 指针) Image, + /// 文件消息 (文件名或描述) + File, /// 系统事件 (上线/离线等) Event, /// 消息确认回执 @@ -87,6 +89,19 @@ impl ChatMessage { } } + /// 创建文件消息 + pub fn new_file(from: String, to: String, file_info: String) -> Self { + Self { + id: Uuid::new_v4().to_string(), + message_type: MessageType::File, + content: file_info, + from, + to, + timestamp: chrono::Utc::now().timestamp(), + status: MessageStatus::Pending, + } + } + /// 创建事件消息 pub fn new_event(from: String, to: String, event: &str) -> Self { Self { @@ -151,3 +166,25 @@ pub struct ConnectionState { /// 最后心跳时间 pub last_heartbeat: i64, } + +/// 会话信息(用于会话列表显示) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Conversation { + /// 对方设备 ID + pub device_id: String, + /// 对方设备名称 + pub device_name: String, + /// 对方 IP 地址 + pub ip: String, + /// 是否在线 + pub online: bool, + /// 最后一条消息内容 + pub last_message: Option, + /// 最后一条消息类型 + pub last_message_type: Option, + /// 最后一条消息时间 + pub last_message_time: Option, + /// 未读消息数 + pub unread_count: i32, +} diff --git a/src-tauri/src/websocket/handler.rs b/src-tauri/src/websocket/handler.rs index 4a552ad..b17229f 100644 --- a/src-tauri/src/websocket/handler.rs +++ b/src-tauri/src/websocket/handler.rs @@ -5,10 +5,36 @@ use crate::models::{ChatMessage, MessageStatus, MessageType}; use super::ConnectionManager; +/// 最大重试次数 +const MAX_SAVE_RETRIES: u32 = 3; + /// 消息处理器 pub struct MessageHandler; impl MessageHandler { + /// 保存消息到数据库(带重试) + fn save_message_with_retry(message: &ChatMessage) -> bool { + for attempt in 1..=MAX_SAVE_RETRIES { + match Database::save_message(message) { + Ok(_) => { + log::debug!("Message {} saved successfully", message.id); + return true; + } + Err(e) => { + log::error!( + "Failed to save message {} (attempt {}/{}): {}", + message.id, attempt, MAX_SAVE_RETRIES, e + ); + if attempt < MAX_SAVE_RETRIES { + // 短暂延迟后重试 + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + } + } + false + } + /// 处理收到的消息 pub async fn handle_message( message: ChatMessage, @@ -16,30 +42,34 @@ impl MessageHandler { local_device_id: &str, ) { match message.message_type { - MessageType::Text | MessageType::Image => { - // 保存消息到数据库 - if let Err(e) = Database::save_message(&message) { - log::error!("Failed to save message: {}", e); + MessageType::Text | MessageType::Image | MessageType::File => { + // 保存消息到数据库(带重试机制) + let saved = Self::save_message_with_retry(&message); + if !saved { + log::error!("Message {} could not be saved after {} retries", message.id, MAX_SAVE_RETRIES); } - // 发送确认回执 - let ack = ChatMessage::new_ack( - local_device_id.to_string(), - message.from.clone(), - &message.id, - ); - - if let Err(e) = connection_manager.send_to(&message.from, &ack).await { - log::error!("Failed to send ack: {}", e); + // 发送确认回执(只有保存成功才发送) + if saved { + let ack = ChatMessage::new_ack( + local_device_id.to_string(), + message.from.clone(), + &message.id, + ); + + if let Err(e) = connection_manager.send_to(&message.from, &ack).await { + log::error!("Failed to send ack: {}", e); + } } - // 触发消息接收事件 + // 无论是否保存成功,都触发消息接收事件(让用户能看到消息) connection_manager.emit_message_received(message); } MessageType::Event => { // 处理事件消息 (如在线/离线通知) + // 这是内部握手消息,不发送到前端显示 log::info!("Received event: {} from {}", message.content, message.from); - connection_manager.emit_message_received(message); + // 可以在这里处理特定事件,如更新设备在线状态等 } MessageType::Ack => { // 处理确认回执,更新消息状态 diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 76dfc92..7f5d8f8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -4,7 +4,7 @@ "version": "1.0.0", "identifier": "com.flashsend.app", "build": { - "beforeDevCommand": "npm run dev", + "beforeDevCommand": "pnpm run dev", "devUrl": "http://localhost:5173", "beforeBuildCommand": "npm run build", "frontendDist": "../dist" diff --git a/src/api/index.js b/src/api/index.js index 70ddb36..6e5b941 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -13,8 +13,11 @@ export const discoveryApi = { /** 停止设备发现服务 */ stop: () => invoke('stop_discovery_service'), - /** 获取已发现的设备列表 */ + /** 获取已发现的设备列表(在线设备) */ getDevices: () => invoke('get_discovered_devices'), + + /** 获取历史设备列表(包含离线设备) */ + getKnownDevices: () => invoke('get_known_devices'), } /** WebSocket API */ @@ -45,6 +48,10 @@ export const chatApi = { /** 删除聊天历史 */ deleteHistory: (deviceId) => invoke('delete_chat_history', { deviceId }), + + /** 获取会话列表(所有有聊天记录的设备) */ + getConversations: () => + invoke('get_conversations'), } /** 文件传输 API */ diff --git a/src/components/ChatInput.vue b/src/components/ChatInput.vue index 5a308b7..48a7530 100644 --- a/src/components/ChatInput.vue +++ b/src/components/ChatInput.vue @@ -37,47 +37,57 @@ function autoResize(e) {