|
|
<script setup>
|
|
|
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
import { useDeviceStore } from '@/stores/deviceStore'
|
|
|
import { useChatStore } from '@/stores/chatStore'
|
|
|
import MessageBubble from '@/components/MessageBubble.vue'
|
|
|
import ChatInput from '@/components/ChatInput.vue'
|
|
|
import FileProgress from '@/components/FileProgress.vue'
|
|
|
import { ArrowLeft, MoreVertical, Trash2 } from 'lucide-vue-next'
|
|
|
|
|
|
const route = useRoute()
|
|
|
const router = useRouter()
|
|
|
const deviceStore = useDeviceStore()
|
|
|
const chatStore = useChatStore()
|
|
|
|
|
|
const deviceId = computed(() => route.params.deviceId)
|
|
|
const device = computed(() => deviceStore.getDevice(deviceId.value))
|
|
|
const messages = computed(() => chatStore.messages.get(deviceId.value) || [])
|
|
|
const localDeviceId = computed(() => deviceStore.localDevice?.deviceId || '')
|
|
|
|
|
|
// 只显示与当前对话设备相关的、进行中的传输记录(完成后隐藏)
|
|
|
const deviceTransfers = computed(() => {
|
|
|
const result = []
|
|
|
chatStore.transfers.forEach((transfer, id) => {
|
|
|
// 我发送给对方 或 对方发送给我
|
|
|
const isRelevant =
|
|
|
(transfer.fromDevice === localDeviceId.value && transfer.toDevice === deviceId.value) ||
|
|
|
(transfer.fromDevice === deviceId.value && transfer.toDevice === localDeviceId.value)
|
|
|
// 只显示进行中的传输(pending 或 transferring)
|
|
|
const isInProgress = transfer.status === 'pending' || transfer.status === 'transferring'
|
|
|
if (isRelevant && isInProgress) {
|
|
|
result.push([id, transfer])
|
|
|
}
|
|
|
})
|
|
|
return result
|
|
|
})
|
|
|
|
|
|
const messagesContainer = ref(null)
|
|
|
const showMenu = ref(false)
|
|
|
|
|
|
// 滚动到底部
|
|
|
function scrollToBottom() {
|
|
|
nextTick(() => {
|
|
|
if (messagesContainer.value) {
|
|
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 发送消息
|
|
|
async function sendMessage(content) {
|
|
|
try {
|
|
|
await chatStore.sendMessage(deviceId.value, content)
|
|
|
scrollToBottom()
|
|
|
} catch (error) {
|
|
|
console.error('Failed to send message:', error)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 选择文件
|
|
|
async function selectFile() {
|
|
|
const file = await chatStore.selectFile()
|
|
|
if (file && device.value) {
|
|
|
// 发送文件,同时发送一条文件消息到聊天记录
|
|
|
await chatStore.sendFile(deviceId.value, file)
|
|
|
// 发送文件消息,让接收方也能在聊天记录中看到
|
|
|
await chatStore.sendMessage(
|
|
|
deviceId.value,
|
|
|
`📎 ${file.name}`,
|
|
|
'file'
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 删除聊天记录
|
|
|
async function deleteHistory() {
|
|
|
showMenu.value = false
|
|
|
const { ask } = await import('@tauri-apps/plugin-dialog')
|
|
|
const confirmed = await ask('确定要删除所有聊天记录吗?', {
|
|
|
title: '删除确认',
|
|
|
kind: 'warning'
|
|
|
})
|
|
|
if (confirmed) {
|
|
|
await chatStore.deleteHistory(deviceId.value)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 返回
|
|
|
function goBack() {
|
|
|
router.push('/devices')
|
|
|
}
|
|
|
|
|
|
// 点击文件消息,打开文件位置(仅接收方有效)
|
|
|
async function handleFileClick(fileName) {
|
|
|
// 先在内存中查找
|
|
|
for (const [, transfer] of chatStore.transfers) {
|
|
|
// 接收到的文件(toDevice 是本机)
|
|
|
if (transfer.name === fileName && transfer.toDevice === localDeviceId.value && transfer.localPath) {
|
|
|
await chatStore.openFileLocation(transfer.localPath)
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 内存中没有或 localPath 为空,强制从数据库加载
|
|
|
await chatStore.loadTransferHistory(deviceId.value, true)
|
|
|
for (const [, transfer] of chatStore.transfers) {
|
|
|
if (transfer.name === fileName && transfer.toDevice === localDeviceId.value && transfer.localPath) {
|
|
|
await chatStore.openFileLocation(transfer.localPath)
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
console.log('File not found:', fileName)
|
|
|
}
|
|
|
|
|
|
// 监听新消息
|
|
|
watch(messages, () => {
|
|
|
scrollToBottom()
|
|
|
}, { deep: true })
|
|
|
|
|
|
onMounted(async () => {
|
|
|
chatStore.setCurrentDevice(deviceId.value)
|
|
|
|
|
|
// 确保已连接
|
|
|
if (!deviceStore.isConnected(deviceId.value)) {
|
|
|
try {
|
|
|
await deviceStore.connectToDevice(deviceId.value)
|
|
|
} catch (error) {
|
|
|
console.error('Connection failed:', error)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 加载历史记录
|
|
|
await chatStore.loadHistory(deviceId.value)
|
|
|
// 加载传输历史(用于点击文件消息时打开文件)
|
|
|
await chatStore.loadTransferHistory(deviceId.value)
|
|
|
scrollToBottom()
|
|
|
})
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
chatStore.setCurrentDevice(null)
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="h-full flex flex-col bg-surface-50 dark:bg-surface-900 relative overflow-hidden">
|
|
|
<!-- 背景装饰 -->
|
|
|
<div class="absolute top-0 right-0 w-[500px] h-[500px] bg-primary-200/20 dark:bg-primary-900/10 rounded-full blur-[100px] pointer-events-none -translate-y-1/2 translate-x-1/2"></div>
|
|
|
<div class="absolute bottom-0 left-0 w-[300px] h-[300px] bg-primary-300/10 dark:bg-primary-800/5 rounded-full blur-[80px] pointer-events-none translate-y-1/3 -translate-x-1/3"></div>
|
|
|
|
|
|
<!-- 头部 -->
|
|
|
<header class="relative z-20 bg-white/80 dark:bg-surface-900/80 backdrop-blur-md border-b border-surface-200/50 dark:border-surface-700/50 px-4 py-3 flex items-center gap-3 shadow-[0_2px_15px_rgba(0,0,0,0.02)]">
|
|
|
<button
|
|
|
@click="goBack"
|
|
|
class="p-2.5 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-800 text-surface-500 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 transition-colors"
|
|
|
>
|
|
|
<ArrowLeft :size="20" stroke-width="2.5" />
|
|
|
</button>
|
|
|
|
|
|
<div class="flex-1 min-w-0 flex items-center gap-3">
|
|
|
<div class="flex-1 min-w-0">
|
|
|
<h2 class="font-semibold text-surface-900 dark:text-white truncate text-base leading-tight">
|
|
|
{{ device?.deviceName || '未知设备' }}
|
|
|
</h2>
|
|
|
<div class="flex items-center gap-2 mt-0.5">
|
|
|
<span class="text-xs text-surface-500 dark:text-surface-400 font-mono opacity-80">
|
|
|
{{ device?.ip }}
|
|
|
</span>
|
|
|
<!-- 在线状态指示器 -->
|
|
|
<div
|
|
|
class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-full bg-surface-100 dark:bg-surface-800"
|
|
|
:class="deviceStore.isConnected(deviceId) ? 'text-green-600 dark:text-green-400' : 'text-surface-400'"
|
|
|
>
|
|
|
<span
|
|
|
class="w-1.5 h-1.5 rounded-full"
|
|
|
:class="[
|
|
|
deviceStore.isConnected(deviceId) ? 'bg-green-500 animate-pulse' : 'bg-surface-400',
|
|
|
'shadow-[0_0_8px_currentColor]'
|
|
|
]"
|
|
|
/>
|
|
|
<span class="text-[10px] font-bold uppercase tracking-wider">
|
|
|
{{ deviceStore.isConnected(deviceId) ? 'Online' : 'Offline' }}
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 更多菜单 -->
|
|
|
<div class="relative">
|
|
|
<button
|
|
|
@click="showMenu = !showMenu"
|
|
|
class="p-2.5 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-800 text-surface-500 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 transition-colors"
|
|
|
>
|
|
|
<MoreVertical :size="20" />
|
|
|
</button>
|
|
|
|
|
|
<transition
|
|
|
enter-active-class="transition duration-200 ease-out"
|
|
|
enter-from-class="transform scale-95 opacity-0"
|
|
|
enter-to-class="transform scale-100 opacity-100"
|
|
|
leave-active-class="transition duration-150 ease-in"
|
|
|
leave-from-class="transform scale-100 opacity-100"
|
|
|
leave-to-class="transform scale-95 opacity-0"
|
|
|
>
|
|
|
<div
|
|
|
v-if="showMenu"
|
|
|
class="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-surface-800 rounded-xl shadow-hard border border-surface-100 dark:border-surface-700 py-1.5 z-50 origin-top-right"
|
|
|
>
|
|
|
<button
|
|
|
@click="deleteHistory"
|
|
|
class="w-full px-4 py-2.5 text-left text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2.5 transition-colors"
|
|
|
>
|
|
|
<Trash2 :size="16" />
|
|
|
<span>删除聊天记录</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
</transition>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<!-- 消息列表 -->
|
|
|
<div
|
|
|
ref="messagesContainer"
|
|
|
class="flex-1 overflow-y-auto px-4 py-6 relative z-10 scroll-smooth space-y-2"
|
|
|
>
|
|
|
<!-- 空状态 -->
|
|
|
<div
|
|
|
v-if="messages.length === 0"
|
|
|
class="h-full flex flex-col items-center justify-center text-surface-400 dark:text-surface-500 animate-fade-in"
|
|
|
>
|
|
|
<div class="w-16 h-16 bg-surface-100 dark:bg-surface-800 rounded-2xl flex items-center justify-center mb-4 shadow-inner transform rotate-3">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-8 h-8 opacity-60"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
|
|
</div>
|
|
|
<p class="font-medium">暂无消息</p>
|
|
|
<p class="text-sm mt-1 opacity-75">发送消息开始聊天</p>
|
|
|
</div>
|
|
|
|
|
|
<!-- 消息列表 -->
|
|
|
<template v-else>
|
|
|
<MessageBubble
|
|
|
v-for="message in messages"
|
|
|
:key="message.id"
|
|
|
:message="message"
|
|
|
:is-sent="message.from === localDeviceId"
|
|
|
@file-click="handleFileClick"
|
|
|
/>
|
|
|
</template>
|
|
|
</div>
|
|
|
|
|
|
<!-- 文件传输进度(浮动在输入框上方) -->
|
|
|
<div class="relative z-20 px-4 pb-2">
|
|
|
<transition-group
|
|
|
enter-active-class="transition duration-300 ease-out"
|
|
|
enter-from-class="transform translate-y-4 opacity-0"
|
|
|
enter-to-class="transform translate-y-0 opacity-100"
|
|
|
leave-active-class="transition duration-200 ease-in"
|
|
|
leave-from-class="transform translate-y-0 opacity-100"
|
|
|
leave-to-class="transform translate-y-4 opacity-0"
|
|
|
>
|
|
|
<FileProgress
|
|
|
v-for="[id, transfer] in deviceTransfers"
|
|
|
:key="id"
|
|
|
:transfer="transfer"
|
|
|
:is-receiver="transfer.toDevice === localDeviceId"
|
|
|
@open="chatStore.openFileLocation(transfer.localPath || '')"
|
|
|
@cancel="chatStore.cancelTransfer"
|
|
|
class="mb-2 last:mb-0 shadow-medium"
|
|
|
/>
|
|
|
</transition-group>
|
|
|
</div>
|
|
|
|
|
|
<!-- 输入区域 -->
|
|
|
<div class="relative z-30">
|
|
|
<ChatInput
|
|
|
@send="sendMessage"
|
|
|
@select-file="selectFile"
|
|
|
@select-image="selectFile"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|