You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
12 KiB
12 KiB
前端结构
本文档详细介绍 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
设备列表页面,展示在线设备。
功能:
- 显示所有在线设备
- 点击设备进入聊天
- 自动刷新设备列表
模板结构:
<template>
<div class="device-list">
<header>在线设备</header>
<div v-if="loading">
<LoadingSpinner />
</div>
<div v-else-if="devices.length === 0">
<EmptyState message="暂无在线设备" />
</div>
<div v-else class="device-grid">
<DeviceCard
v-for="device in devices"
:key="device.deviceId"
:device="device"
@click="goToChat(device)"
/>
</div>
</div>
</template>
ChatWindow.vue
聊天窗口页面,与指定设备聊天。
功能:
- 显示聊天历史
- 发送/接收消息
- 发送文件
- 显示文件传输进度
- 点击文件消息打开文件位置
关键逻辑:
// 过滤当前设备的传输(仅显示进行中)
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
侧边导航栏。
<template>
<aside class="sidebar">
<nav>
<router-link to="/devices">
<MonitorSmartphone />
<span>设备</span>
</router-link>
<router-link to="/transfer">
<FolderSync />
<span>传输</span>
</router-link>
<router-link to="/settings">
<Settings />
<span>设置</span>
</router-link>
</nav>
<div class="device-info">
<span>{{ deviceName }}</span>
</div>
</aside>
</template>
DeviceCard.vue
设备卡片组件。
Props:
interface Props {
device: DeviceInfo
}
显示内容:
- 设备名称
- IP 地址
- 在线状态指示
MessageBubble.vue
消息气泡组件。
Props:
interface Props {
message: ChatMessage
isOwn: boolean // 是否是自己发送的
}
事件:
const emit = defineEmits<{
'file-click': [fileName: string]
}>()
支持的消息类型:
text: 文本消息image: 图片消息file: 文件消息(可点击)
ChatInput.vue
聊天输入框组件。
事件:
const emit = defineEmits<{
'send': [content: string]
'send-file': []
}>()
功能:
- 文本输入
- Enter 发送
- 文件选择按钮
FileProgress.vue
文件传输进度组件。
Props:
interface Props {
transfer: FileTransfer
isReceiver: boolean // 是否是接收方
}
显示内容:
- 文件名
- 进度条
- 传输速度/状态
- 操作按钮(取消/打开位置)
LoadingSpinner.vue
加载动画组件。
状态管理
deviceStore.ts
管理设备相关状态。
export const useDeviceStore = defineStore('device', () => {
// 状态
const devices = ref<Map<string, DeviceInfo>>(new Map())
const localDevice = ref<DeviceInfo | null>(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
管理聊天和文件传输状态。
export const useChatStore = defineStore('chat', () => {
// 状态
const messages = ref<Map<string, ChatMessage[]>>(new Map()) // deviceId -> messages
const transfers = ref<Map<string, FileTransfer>>(new Map()) // fileId -> transfer
const currentDevice = ref<string | null>(null)
const pendingFile = ref<FileMetadata | null>(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
管理应用设置。
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
import { invoke } from '@tauri-apps/api/core'
// 设备发现 API
export const discoveryApi = {
start: () => invoke('start_discovery'),
stop: () => invoke('stop_discovery'),
getDevices: () => invoke<DeviceInfo[]>('get_online_devices'),
getLocalDevice: () => invoke<DeviceInfo>('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<ChatMessage>('send_chat_message', { deviceId, content, messageType }),
getHistory: (deviceId: string, limit?: number, offset?: number) =>
invoke<ChatMessage[]>('get_chat_history', { deviceId, limit, offset }),
}
// 文件传输 API
export const fileApi = {
startServer: () => invoke('start_http_server'),
selectFile: () => invoke<FileMetadata | null>('select_file'),
send: (deviceId: string, fileId: string, filePath: string) =>
invoke<FileTransfer>('send_file', { deviceId, fileId, filePath }),
getHistory: (deviceId: string, limit?: number) =>
invoke<FileTransfer[]>('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<AppConfig>('get_app_config'),
updateDeviceName: (name: string) => invoke('update_device_name', { name }),
updateDownloadDir: (path: string) => invoke('update_download_dir', { path }),
}
事件监听
hooks/useEventListener.ts
import { listen, UnlistenFn } from '@tauri-apps/api/event'
import { onMounted, onUnmounted } from 'vue'
export function useEventListener<T>(
eventName: string,
handler: (payload: T) => void
) {
let unlisten: UnlistenFn | null = null
onMounted(async () => {
unlisten = await listen<T>(eventName, (event) => {
handler(event.payload)
})
})
onUnmounted(() => {
unlisten?.()
})
}
使用示例:
// 在组件中监听设备发现事件
useEventListener<DeviceInfo>('device:found', (device) => {
deviceStore.addDevice(device)
})
// 监听文件传输进度
useEventListener<TransferProgressEvent>('file:progress', (event) => {
chatStore.updateTransferProgress(event)
})
路由配置
router/index.ts
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
// 设备信息
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
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: 'class', // 支持 class 模式的深色主题
theme: {
extend: {
colors: {
primary: {...},
secondary: {...},
}
}
},
plugins: []
}
深色模式
通过在 <html> 标签添加 dark class 来切换深色模式:
// 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')
}
}
组件中使用:
<div class="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100">
<!-- 内容 -->
</div>