|
@@ -1,7 +1,9 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { NInput, NButton, NEmpty, NScrollbar, NPopconfirm, NSpin, NTag, NModal, NCard } from 'naive-ui'
|
|
|
|
|
|
|
+import { NInput, NButton, NEmpty, NScrollbar, NPopconfirm, NSpin, NTag, NModal, NCard, NDrawer, NDrawerContent } from 'naive-ui'
|
|
|
import { ref, nextTick, watch, onMounted, computed } from 'vue'
|
|
import { ref, nextTick, watch, onMounted, computed } from 'vue'
|
|
|
import { useAiChat } from './useAiChat'
|
|
import { useAiChat } from './useAiChat'
|
|
|
|
|
+import { listAiFiles, downloadAiFile } from '@/service/aiChat'
|
|
|
|
|
+import type { AiFileItem } from './interface'
|
|
|
import MessageItem from './MessageItem.vue'
|
|
import MessageItem from './MessageItem.vue'
|
|
|
|
|
|
|
|
const {
|
|
const {
|
|
@@ -141,6 +143,50 @@ function handleCreateNewSessionFromDialog() {
|
|
|
dismissSessionLoadInfo()
|
|
dismissSessionLoadInfo()
|
|
|
createNewSession()
|
|
createNewSession()
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// ============ 文件历史 Drawer ============
|
|
|
|
|
+const fileDrawerVisible = ref(false)
|
|
|
|
|
+const fileDrawerType = ref<'upload' | 'download'>('upload')
|
|
|
|
|
+const fileList = ref<AiFileItem[]>([])
|
|
|
|
|
+const isLoadingFiles = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+async function showFileDrawer(type: 'upload' | 'download') {
|
|
|
|
|
+ fileDrawerType.value = type
|
|
|
|
|
+ fileDrawerVisible.value = true
|
|
|
|
|
+ isLoadingFiles.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await listAiFiles(type)
|
|
|
|
|
+ fileList.value = data || []
|
|
|
|
|
+ }
|
|
|
|
|
+ catch {
|
|
|
|
|
+ fileList.value = []
|
|
|
|
|
+ }
|
|
|
|
|
+ finally {
|
|
|
|
|
+ isLoadingFiles.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleDownloadFile(key: string, fileName: string) {
|
|
|
|
|
+ downloadAiFile(key, fileName)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatFileSize(bytes: number): string {
|
|
|
|
|
+ if (bytes < 1024) return `${bytes} B`
|
|
|
|
|
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
|
|
|
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatExpiry(expiresAt: number): string {
|
|
|
|
|
+ if (!expiresAt) return '不过期'
|
|
|
|
|
+ const now = Date.now()
|
|
|
|
|
+ const diff = expiresAt - now
|
|
|
|
|
+ if (diff <= 0) return '已过期'
|
|
|
|
|
+ const hours = Math.floor(diff / 3600000)
|
|
|
|
|
+ if (hours < 1) return '即将过期'
|
|
|
|
|
+ if (hours < 24) return `${hours} 小时后过期`
|
|
|
|
|
+ const days = Math.floor(hours / 24)
|
|
|
|
|
+ return `${days} 天后过期`
|
|
|
|
|
+}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
@@ -154,6 +200,12 @@ function handleCreateNewSessionFromDialog() {
|
|
|
<span class="text-xs text-neutral-400">AI 助手</span>
|
|
<span class="text-xs text-neutral-400">AI 助手</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="flex items-center gap-1">
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
+ <NButton text size="small" title="上传文件历史" @click="showFileDrawer('upload')">
|
|
|
|
|
+ <span class="i-ph:upload-simple text-sm" />
|
|
|
|
|
+ </NButton>
|
|
|
|
|
+ <NButton text size="small" title="AI生成文件" @click="showFileDrawer('download')">
|
|
|
|
|
+ <span class="i-ph:download-simple text-sm" />
|
|
|
|
|
+ </NButton>
|
|
|
<NButton text size="small" title="新建对话" @click="handleNewSession">
|
|
<NButton text size="small" title="新建对话" @click="handleNewSession">
|
|
|
<span class="i-ph:plus text-sm" />
|
|
<span class="i-ph:plus text-sm" />
|
|
|
</NButton>
|
|
</NButton>
|
|
@@ -356,6 +408,37 @@ function handleCreateNewSessionFromDialog() {
|
|
|
</template>
|
|
</template>
|
|
|
</NCard>
|
|
</NCard>
|
|
|
</NModal>
|
|
</NModal>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 文件历史 Drawer -->
|
|
|
|
|
+ <NDrawer v-model:show="fileDrawerVisible" :width="320" placement="right">
|
|
|
|
|
+ <NDrawerContent :title="fileDrawerType === 'upload' ? '上传文件历史' : 'AI 生成文件'">
|
|
|
|
|
+ <div v-if="isLoadingFiles" class="flex justify-center py-8">
|
|
|
|
|
+ <NSpin size="medium" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else-if="fileList.length === 0" class="py-8">
|
|
|
|
|
+ <NEmpty :description="fileDrawerType === 'upload' ? '暂无上传文件' : '暂无生成文件'" size="small" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="space-y-2">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="file in fileList"
|
|
|
|
|
+ :key="file.key"
|
|
|
|
|
+ class="flex items-center gap-2 rounded-md border p-2 dark:border-neutral-600"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="flex-1 min-w-0">
|
|
|
|
|
+ <div class="truncate text-xs font-medium text-neutral-700 dark:text-neutral-200">
|
|
|
|
|
+ {{ file.fileName }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="mt-0.5 text-[10px] text-neutral-400">
|
|
|
|
|
+ {{ formatFileSize(file.size) }} · {{ formatExpiry(file.expiresAt) }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <NButton text size="tiny" title="下载" @click="handleDownloadFile(file.key, file.fileName)">
|
|
|
|
|
+ <span class="i-ph:download text-sm text-blue-500" />
|
|
|
|
|
+ </NButton>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </NDrawerContent>
|
|
|
|
|
+ </NDrawer>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|