# 前端结构 本文档详细介绍 Vue 3 前端的代码结构、组件功能和状态管理。 ## 目录结构 ``` src/ ├── api/ # Tauri 命令封装 │ └── index.ts ├── components/ # 可复用组件 │ ├── Sidebar.vue │ ├── DeviceCard.vue │ ├── MessageBubble.vue │ ├── ChatInput.vue │ ├── FileProgress.vue │ └── LoadingSpinner.vue ├── pages/ # 页面组件 │ ├── DeviceList.vue │ ├── ChatWindow.vue │ ├── FileTransfer.vue │ └── Settings.vue ├── stores/ # Pinia 状态管理 │ ├── deviceStore.ts │ ├── chatStore.ts │ └── settingsStore.ts ├── hooks/ # 组合式函数 │ ├── useEventListener.ts │ └── useTheme.ts ├── types/ # TypeScript 类型 │ └── index.ts ├── router/ # Vue Router │ └── index.ts ├── styles/ # 全局样式 │ └── main.css ├── App.vue # 根组件 └── main.ts # 入口文件 ``` ## 页面组件 ### DeviceList.vue 设备列表页面,展示在线设备。 **功能**: - 显示所有在线设备 - 点击设备进入聊天 - 自动刷新设备列表 **模板结构**: ```vue ``` ### ChatWindow.vue 聊天窗口页面,与指定设备聊天。 **功能**: - 显示聊天历史 - 发送/接收消息 - 发送文件 - 显示文件传输进度 - 点击文件消息打开文件位置 **关键逻辑**: ```typescript // 过滤当前设备的传输(仅显示进行中) const deviceTransfers = computed(() => { const result: FileTransfer[] = [] for (const [, transfer] of chatStore.transfers) { const isRelated = (transfer.fromDevice === localDeviceId.value && transfer.toDevice === deviceId.value) || (transfer.fromDevice === deviceId.value && transfer.toDevice === localDeviceId.value) const isInProgress = transfer.status === 'pending' || transfer.status === 'transferring' if (isRelated && isInProgress) { result.push(transfer) } } return result }) // 点击文件消息打开位置 async function handleFileClick(fileName: string) { // 查找传输记录,获取 localPath await chatStore.openFileLocation(transfer.localPath) } ``` ### FileTransfer.vue 文件传输页面,展示所有传输记录。 **功能**: - 显示所有传输(进行中/已完成) - 取消传输 - 打开文件位置 ### Settings.vue 设置页面。 **功能**: - 修改设备名称 - 切换主题(系统/浅色/深色) - 修改下载目录 ## 组件详解 ### Sidebar.vue 侧边导航栏。 ```vue ``` ### DeviceCard.vue 设备卡片组件。 **Props**: ```typescript interface Props { device: DeviceInfo } ``` **显示内容**: - 设备名称 - IP 地址 - 在线状态指示 ### MessageBubble.vue 消息气泡组件。 **Props**: ```typescript interface Props { message: ChatMessage isOwn: boolean // 是否是自己发送的 } ``` **事件**: ```typescript const emit = defineEmits<{ 'file-click': [fileName: string] }>() ``` **支持的消息类型**: - `text`: 文本消息 - `image`: 图片消息 - `file`: 文件消息(可点击) ### ChatInput.vue 聊天输入框组件。 **事件**: ```typescript const emit = defineEmits<{ 'send': [content: string] 'send-file': [] }>() ``` **功能**: - 文本输入 - Enter 发送 - 文件选择按钮 ### FileProgress.vue 文件传输进度组件。 **Props**: ```typescript interface Props { transfer: FileTransfer isReceiver: boolean // 是否是接收方 } ``` **显示内容**: - 文件名 - 进度条 - 传输速度/状态 - 操作按钮(取消/打开位置) ### LoadingSpinner.vue 加载动画组件。 ## 状态管理 ### deviceStore.ts 管理设备相关状态。 ```typescript export const useDeviceStore = defineStore('device', () => { // 状态 const devices = ref>(new Map()) const localDevice = ref(null) const loading = ref(false) // 计算属性 const deviceList = computed(() => Array.from(devices.value.values())) // 方法 async function startDiscovery() async function stopDiscovery() async function refreshDevices() function addDevice(device: DeviceInfo) function removeDevice(deviceId: string) return { devices, localDevice, loading, deviceList, ... } }) ``` ### chatStore.ts 管理聊天和文件传输状态。 ```typescript export const useChatStore = defineStore('chat', () => { // 状态 const messages = ref>(new Map()) // deviceId -> messages const transfers = ref>(new Map()) // fileId -> transfer const currentDevice = ref(null) const pendingFile = ref(null) // 方法 async function loadHistory(deviceId: string) async function sendMessage(deviceId: string, content: string) async function sendFile(deviceId: string, file: FileMetadata) function updateTransferProgress(event: TransferProgressEvent) async function loadTransferHistory(deviceId: string, force?: boolean) async function openFileLocation(path: string) async function cancelTransfer(fileId: string) return { messages, transfers, ... } }) ``` ### settingsStore.ts 管理应用设置。 ```typescript export const useSettingsStore = defineStore('settings', () => { // 状态 const theme = ref<'system' | 'light' | 'dark'>('system') const deviceName = ref('') const downloadDir = ref('') // 方法 async function loadSettings() async function updateTheme(newTheme: Theme) async function updateDeviceName(name: string) async function updateDownloadDir(path: string) return { theme, deviceName, downloadDir, ... } }) ``` ## API 封装 ### api/index.ts ```typescript import { invoke } from '@tauri-apps/api/core' // 设备发现 API export const discoveryApi = { start: () => invoke('start_discovery'), stop: () => invoke('stop_discovery'), getDevices: () => invoke('get_online_devices'), getLocalDevice: () => invoke('get_local_device'), } // 聊天 API export const chatApi = { startServer: () => invoke('start_ws_server'), connect: (deviceId: string) => invoke('connect_to_device', { deviceId }), send: (deviceId: string, content: string, messageType?: string) => invoke('send_chat_message', { deviceId, content, messageType }), getHistory: (deviceId: string, limit?: number, offset?: number) => invoke('get_chat_history', { deviceId, limit, offset }), } // 文件传输 API export const fileApi = { startServer: () => invoke('start_http_server'), selectFile: () => invoke('select_file'), send: (deviceId: string, fileId: string, filePath: string) => invoke('send_file', { deviceId, fileId, filePath }), getHistory: (deviceId: string, limit?: number) => invoke('get_transfer_history', { deviceId, limit }), openLocation: (path: string) => invoke('open_file_location', { path }), cancelTransfer: (fileId: string) => invoke('cancel_transfer', { fileId }), } // 配置 API export const configApi = { get: () => invoke('get_app_config'), updateDeviceName: (name: string) => invoke('update_device_name', { name }), updateDownloadDir: (path: string) => invoke('update_download_dir', { path }), } ``` ## 事件监听 ### hooks/useEventListener.ts ```typescript import { listen, UnlistenFn } from '@tauri-apps/api/event' import { onMounted, onUnmounted } from 'vue' export function useEventListener( eventName: string, handler: (payload: T) => void ) { let unlisten: UnlistenFn | null = null onMounted(async () => { unlisten = await listen(eventName, (event) => { handler(event.payload) }) }) onUnmounted(() => { unlisten?.() }) } ``` **使用示例**: ```typescript // 在组件中监听设备发现事件 useEventListener('device:found', (device) => { deviceStore.addDevice(device) }) // 监听文件传输进度 useEventListener('file:progress', (event) => { chatStore.updateTransferProgress(event) }) ``` ## 路由配置 ### router/index.ts ```typescript import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', redirect: '/devices' }, { path: '/devices', name: 'devices', component: () => import('../pages/DeviceList.vue') }, { path: '/chat/:deviceId', name: 'chat', component: () => import('../pages/ChatWindow.vue'), props: true }, { path: '/transfer', name: 'transfer', component: () => import('../pages/FileTransfer.vue') }, { path: '/settings', name: 'settings', component: () => import('../pages/Settings.vue') } ] export const router = createRouter({ history: createWebHistory(), routes }) ``` ## 类型定义 ### types/index.ts ```typescript // 设备信息 export interface DeviceInfo { deviceId: string deviceName: string ip: string wsPort: number httpPort: number lastSeen: number } // 聊天消息 export interface ChatMessage { id: string fromDevice: string toDevice: string content: string messageType: MessageType timestamp: number isRead: boolean } export type MessageType = 'text' | 'image' | 'file' | 'system' // 文件传输 export interface FileTransfer { fileId: string name: string size: number progress: number status: TransferStatus mimeType?: string fromDevice: string toDevice: string localPath?: string createdAt: number completedAt?: number transferredBytes: number } export type TransferStatus = 'pending' | 'transferring' | 'completed' | 'failed' | 'cancelled' // 传输进度事件 export interface TransferProgressEvent { fileId: string progress: number transferredBytes: number totalBytes: number status: TransferStatus fileName?: string localPath?: string } // 文件元数据 export interface FileMetadata { name: string path: string size: number mimeType?: string } // 应用配置 export interface AppConfig { deviceId: string deviceName: string downloadDir: string wsPort: number httpPort: number } ``` ## 样式系统 使用 TailwindCSS 进行样式管理。 ### tailwind.config.js ```javascript module.exports = { content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], darkMode: 'class', // 支持 class 模式的深色主题 theme: { extend: { colors: { primary: {...}, secondary: {...}, } } }, plugins: [] } ``` ### 深色模式 通过在 `` 标签添加 `dark` class 来切换深色模式: ```typescript // settingsStore.ts function applyTheme(theme: Theme) { if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark') } else { document.documentElement.classList.remove('dark') } } ``` **组件中使用**: ```vue
```