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.
183 lines
8.0 KiB
Vue
183 lines
8.0 KiB
Vue
<template>
|
|
<div class="h-full flex flex-col bg-surface-50/50 dark:bg-surface-900/50 relative">
|
|
<!-- 背景装饰 -->
|
|
<div class="absolute top-0 right-0 w-[500px] h-[500px] bg-blue-200/10 dark:bg-blue-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-purple-300/10 dark:bg-purple-800/10 rounded-full blur-[80px] pointer-events-none translate-y-1/3 -translate-x-1/3"></div>
|
|
|
|
<!-- 头部 -->
|
|
<header class="relative z-10 px-8 py-6 pb-4">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">文件传输</h1>
|
|
<p class="text-sm text-surface-500 dark:text-surface-400 mt-1">
|
|
查看和管理所有文件传输记录
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 统计卡片 -->
|
|
<div class="grid grid-cols-4 gap-4 mb-6">
|
|
<div class="bg-white/80 dark:bg-surface-800/80 backdrop-blur-sm rounded-2xl p-4 border border-surface-100 dark:border-surface-700/50 shadow-soft group hover:shadow-medium transition-all duration-300">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<div class="p-2 rounded-lg bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-300">
|
|
<FileText :size="18" />
|
|
</div>
|
|
<span class="text-sm font-medium text-surface-500 dark:text-surface-400">总计</span>
|
|
</div>
|
|
<div class="text-2xl font-bold text-surface-900 dark:text-white pl-1">{{ stats.total }}</div>
|
|
</div>
|
|
|
|
<div class="bg-blue-50/80 dark:bg-blue-900/20 backdrop-blur-sm rounded-2xl p-4 border border-blue-100 dark:border-blue-800/30 shadow-soft group hover:shadow-medium transition-all duration-300">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<div class="p-2 rounded-lg bg-blue-100 dark:bg-blue-800/50 text-blue-600 dark:text-blue-300">
|
|
<ArrowUp :size="18" />
|
|
</div>
|
|
<span class="text-sm font-medium text-blue-600/70 dark:text-blue-300/70">发送</span>
|
|
</div>
|
|
<div class="text-2xl font-bold text-blue-700 dark:text-blue-100 pl-1">{{ stats.sending }}</div>
|
|
</div>
|
|
|
|
<div class="bg-green-50/80 dark:bg-green-900/20 backdrop-blur-sm rounded-2xl p-4 border border-green-100 dark:border-green-800/30 shadow-soft group hover:shadow-medium transition-all duration-300">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<div class="p-2 rounded-lg bg-green-100 dark:bg-green-800/50 text-green-600 dark:text-green-300">
|
|
<ArrowDown :size="18" />
|
|
</div>
|
|
<span class="text-sm font-medium text-green-600/70 dark:text-green-300/70">接收</span>
|
|
</div>
|
|
<div class="text-2xl font-bold text-green-700 dark:text-green-100 pl-1">{{ stats.receiving }}</div>
|
|
</div>
|
|
|
|
<div class="bg-purple-50/80 dark:bg-purple-900/20 backdrop-blur-sm rounded-2xl p-4 border border-purple-100 dark:border-purple-800/30 shadow-soft group hover:shadow-medium transition-all duration-300">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<div class="p-2 rounded-lg bg-purple-100 dark:bg-purple-800/50 text-purple-600 dark:text-purple-300">
|
|
<Activity :size="18" />
|
|
</div>
|
|
<span class="text-sm font-medium text-purple-600/70 dark:text-purple-300/70">进行中</span>
|
|
</div>
|
|
<div class="text-2xl font-bold text-purple-700 dark:text-purple-100 pl-1">{{ stats.inProgress }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 标签切换 -->
|
|
<div class="flex p-1 bg-surface-200/50 dark:bg-surface-800/50 rounded-xl w-fit backdrop-blur-sm">
|
|
<button
|
|
v-for="tab in [
|
|
{ key: 'all', label: '全部' },
|
|
{ key: 'sending', label: '发送' },
|
|
{ key: 'receiving', label: '接收' },
|
|
]"
|
|
:key="tab.key"
|
|
@click="activeTab = tab.key"
|
|
class="px-6 py-2 rounded-lg text-sm font-medium transition-all duration-300 relative"
|
|
:class="[
|
|
activeTab === tab.key
|
|
? 'text-primary-600 dark:text-primary-300 shadow-sm bg-white dark:bg-surface-700'
|
|
: 'text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-200'
|
|
]"
|
|
>
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 传输列表 -->
|
|
<div class="flex-1 overflow-y-auto px-8 py-4 relative z-0 scroll-smooth">
|
|
<!-- 空状态 -->
|
|
<div
|
|
v-if="filteredTransfers.length === 0"
|
|
class="h-64 flex flex-col items-center justify-center text-surface-400 dark:text-surface-500 animate-fade-in"
|
|
>
|
|
<div class="w-20 h-20 bg-surface-100 dark:bg-surface-800/50 rounded-3xl flex items-center justify-center mb-6 shadow-inner">
|
|
<Inbox :size="40" class="text-surface-300 dark:text-surface-600" />
|
|
</div>
|
|
<p class="text-lg font-medium text-surface-600 dark:text-surface-300">暂无传输记录</p>
|
|
<p class="text-sm mt-2 opacity-75">选择设备发送文件开始传输</p>
|
|
</div>
|
|
|
|
<!-- 传输列表 -->
|
|
<div v-else class="space-y-3 pb-8 animate-slide-up">
|
|
<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="transfer in filteredTransfers"
|
|
:key="transfer.fileId"
|
|
:transfer="transfer"
|
|
:is-receiver="transfer.toDevice === localDeviceId"
|
|
@open="openLocation(transfer.localPath || '')"
|
|
@cancel="cancelTransfer"
|
|
class="hover:scale-[1.01] transition-transform duration-200"
|
|
/>
|
|
</transition-group>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useChatStore } from '@/stores/chatStore'
|
|
import { useDeviceStore } from '@/stores/deviceStore'
|
|
import FileProgress from '@/components/FileProgress.vue'
|
|
import { FileText, ArrowUp, ArrowDown, Activity, Inbox } from 'lucide-vue-next'
|
|
|
|
const chatStore = useChatStore()
|
|
const deviceStore = useDeviceStore()
|
|
|
|
const activeTab = ref('all')
|
|
const localDeviceId = computed(() => deviceStore.localDevice?.deviceId || '')
|
|
|
|
// 过滤传输记录
|
|
const filteredTransfers = computed(() => {
|
|
const localId = deviceStore.localDevice?.deviceId || ''
|
|
const transfers = Array.from(chatStore.transfers.values())
|
|
// 倒序排列,最新的在前面
|
|
const sortedTransfers = transfers.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
|
|
|
|
switch (activeTab.value) {
|
|
case 'sending':
|
|
return sortedTransfers.filter(t => t.fromDevice === localId)
|
|
case 'receiving':
|
|
return sortedTransfers.filter(t => t.toDevice === localId)
|
|
default:
|
|
return sortedTransfers
|
|
}
|
|
})
|
|
|
|
// 统计
|
|
const stats = computed(() => {
|
|
const localId = deviceStore.localDevice?.deviceId || ''
|
|
const transfers = Array.from(chatStore.transfers.values())
|
|
|
|
return {
|
|
total: transfers.length,
|
|
sending: transfers.filter(t => t.fromDevice === localId).length,
|
|
receiving: transfers.filter(t => t.toDevice === localId).length,
|
|
completed: transfers.filter(t => t.status === 'completed').length,
|
|
inProgress: transfers.filter(t => t.status === 'transferring' || t.status === 'pending').length,
|
|
}
|
|
})
|
|
|
|
// 打开文件位置
|
|
function openLocation(path) {
|
|
chatStore.openFileLocation(path)
|
|
}
|
|
|
|
// 取消传输
|
|
async function cancelTransfer(fileId) {
|
|
await chatStore.cancelTransfer(fileId)
|
|
}
|
|
|
|
onMounted(async () => {
|
|
// 加载所有设备的传输历史
|
|
for (const device of deviceStore.deviceList) {
|
|
await chatStore.loadTransferHistory(device.deviceId)
|
|
}
|
|
})
|
|
</script>
|