feat: 优化导入逻辑,错误时会显示导入日志

This commit is contained in:
digua
2025-12-28 18:02:17 +08:00
parent 84408507ec
commit 1172a0977f
5 changed files with 270 additions and 160 deletions
+182
View File
@@ -0,0 +1,182 @@
<script setup lang="ts">
import { FileDropZone } from '@/components/UI'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useSessionStore } from '@/stores/session'
const sessionStore = useSessionStore()
const { isImporting, importProgress } = storeToRefs(sessionStore)
const importError = ref<string | null>(null)
const hasImportLog = ref(false)
const router = useRouter()
// 根据会话类型导航到对应页面
async function navigateToSession(sessionId: string) {
const session = await window.chatApi.getSession(sessionId)
if (session) {
const routeName = session.type === 'private' ? 'private-chat' : 'group-chat'
router.push({ name: routeName, params: { id: sessionId } })
}
}
// 检查是否有导入日志
async function checkImportLog() {
const result = await window.cacheApi.getLatestImportLog()
hasImportLog.value = result.success && !!result.path
}
// 处理文件选择(点击选择)
async function handleClickImport() {
importError.value = null
hasImportLog.value = false
const result = await sessionStore.importFile()
if (!result.success && result.error && result.error !== '未选择文件') {
importError.value = result.error
await checkImportLog()
} else if (result.success && sessionStore.currentSessionId) {
await navigateToSession(sessionStore.currentSessionId)
}
}
// 处理文件拖拽
async function handleFileDrop({ paths }: { files: File[]; paths: string[] }) {
if (paths.length === 0) {
importError.value = '无法读取文件路径'
return
}
importError.value = null
hasImportLog.value = false
const result = await sessionStore.importFileFromPath(paths[0])
if (!result.success && result.error) {
importError.value = result.error
await checkImportLog()
} else if (result.success && sessionStore.currentSessionId) {
await navigateToSession(sessionStore.currentSessionId)
}
}
function openTutorial() {
window.open('https://chatlab.fun/usage/how-to-export.html?utm_source=app', '_blank')
}
// 打开最新的导入日志文件
async function openLatestImportLog() {
const result = await window.cacheApi.getLatestImportLog()
if (result.success && result.path) {
await window.cacheApi.showInFolder(result.path)
} else {
// 没有日志文件时,打开日志目录
await window.cacheApi.openDir('logs')
}
}
function getProgressText(): string {
if (!importProgress.value) return ''
switch (importProgress.value.stage) {
case 'detecting':
return '正在检测格式...'
case 'reading':
return '正在读取文件...'
case 'parsing':
return '正在解析消息...'
case 'saving':
return '正在写入数据库...'
case 'done':
return '导入完成'
case 'error':
return '导入中断'
default:
return ''
}
}
function getProgressDetail(): string {
if (!importProgress.value) return ''
const { messagesProcessed, totalBytes, bytesRead } = importProgress.value
if (messagesProcessed && messagesProcessed > 0) {
return `已处理 ${messagesProcessed.toLocaleString()} 条消息`
}
if (totalBytes && bytesRead) {
const percent = Math.round((bytesRead / totalBytes) * 100)
const mbRead = (bytesRead / 1024 / 1024).toFixed(1)
const mbTotal = (totalBytes / 1024 / 1024).toFixed(1)
return `${mbRead} MB / ${mbTotal} MB (${percent}%)`
}
return importProgress.value.message || ''
}
</script>
<template>
<div class="flex flex-col items-center space-y-6">
<!-- Import Drop Zone -->
<FileDropZone
:accept="['.json', '.jsonl', '.txt']"
:disabled="isImporting"
class="w-full max-w-4xl"
@files="handleFileDrop"
>
<template #default="{ isDragOver }">
<div
class="group relative flex w-full cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed border-pink-300/50 bg-white/50 px-8 py-8 backdrop-blur-sm transition-all duration-300 hover:border-pink-400 hover:bg-white/80 hover:shadow-lg hover:shadow-pink-500/10 focus:outline-none focus:ring-4 focus:ring-pink-500/20 sm:px-12 sm:py-12 dark:border-pink-700/50 dark:bg-gray-900/50 dark:hover:border-pink-500 dark:hover:bg-gray-900/80"
:class="{
'border-pink-500 bg-pink-50/50 dark:border-pink-400 dark:bg-pink-900/20': isDragOver && !isImporting,
'cursor-not-allowed opacity-70': isImporting,
'hover:scale-[1.02]': !isImporting,
}"
@click="!isImporting && handleClickImport()"
>
<!-- Icon -->
<div
class="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-linear-to-br from-pink-100 to-rose-100 transition-transform duration-300 dark:from-pink-900/30 dark:to-rose-900/30"
:class="{ 'scale-110': isDragOver && !isImporting, 'animate-pulse': isImporting }"
>
<UIcon
v-if="!isImporting"
name="i-heroicons-arrow-up-tray"
class="h-8 w-8 text-pink-600 transition-transform group-hover:-translate-y-1 dark:text-pink-400"
/>
<UIcon v-else name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-600 dark:text-pink-400" />
</div>
<!-- Text -->
<div class="w-full min-w-80 text-center">
<template v-if="isImporting && importProgress">
<!-- 导入中显示进度 -->
<p class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">{{ getProgressText() }}</p>
<div class="mx-auto w-full max-w-md">
<UProgress v-model="importProgress.progress" size="md" />
</div>
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">
{{ getProgressDetail() }}
</p>
</template>
<template v-else>
<!-- 默认状态 -->
<p class="text-lg font-semibold text-gray-900 dark:text-white">
{{ isDragOver ? '松开鼠标导入文件' : '点击选择或拖拽聊天记录导入' }}
</p>
</template>
</div>
</div>
</template>
</FileDropZone>
<!-- Error Message -->
<div v-if="importError" class="flex flex-col items-center gap-3 rounded-lg bg-red-50 px-4 py-4 dark:bg-red-900/20">
<div class="flex items-center gap-2 text-sm text-red-600 dark:text-red-400">
<UIcon name="i-heroicons-exclamation-circle" class="h-5 w-5 shrink-0" />
<span>{{ importError }}</span>
</div>
<UButton v-if="hasImportLog" size="xs" @click="openLatestImportLog">查看导入日志</UButton>
</div>
<UButton @click="openTutorial">查看聊天记录导入教程 </UButton>
</div>
</template>
+3 -160
View File
@@ -1,16 +1,7 @@
<script setup lang="ts">
import { FileDropZone } from '@/components/UI'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import AgreementModal from './components/AgreementModal.vue'
import MigrationModal from './components/MigrationModal.vue'
import { useSessionStore } from '@/stores/session'
const sessionStore = useSessionStore()
const { isImporting, importProgress } = storeToRefs(sessionStore)
const importError = ref<string | null>(null)
import ImportArea from './components/ImportArea.vue'
const features = [
{
@@ -38,86 +29,6 @@ const features = [
delay: '300ms',
},
]
const router = useRouter()
// 根据会话类型导航到对应页面
async function navigateToSession(sessionId: string) {
const session = await window.chatApi.getSession(sessionId)
if (session) {
const routeName = session.type === 'private' ? 'private-chat' : 'group-chat'
router.push({ name: routeName, params: { id: sessionId } })
}
}
// 处理文件选择(点击选择)
async function handleClickImport() {
importError.value = null
const result = await sessionStore.importFile()
if (!result.success && result.error && result.error !== '未选择文件') {
importError.value = result.error
} else if (result.success && sessionStore.currentSessionId) {
await navigateToSession(sessionStore.currentSessionId)
}
}
// 处理文件拖拽
async function handleFileDrop({ paths }: { files: File[]; paths: string[] }) {
if (paths.length === 0) {
importError.value = '无法读取文件路径'
return
}
importError.value = null
const result = await sessionStore.importFileFromPath(paths[0])
if (!result.success && result.error) {
importError.value = result.error
} else if (result.success && sessionStore.currentSessionId) {
await navigateToSession(sessionStore.currentSessionId)
}
}
function openTutorial() {
window.open('https://chatlab.fun/usage/how-to-export.html?utm_source=app', '_blank')
}
function getProgressText(): string {
if (!importProgress.value) return ''
switch (importProgress.value.stage) {
case 'detecting':
return '正在检测格式...'
case 'reading':
return '正在读取文件...'
case 'parsing':
return '正在解析消息...'
case 'saving':
return '正在写入数据库...'
case 'done':
return '导入完成'
case 'error':
return '导入中断'
default:
return ''
}
}
function getProgressDetail(): string {
if (!importProgress.value) return ''
const { messagesProcessed, totalBytes, bytesRead } = importProgress.value
if (messagesProcessed && messagesProcessed > 0) {
return `已处理 ${messagesProcessed.toLocaleString()} 条消息`
}
if (totalBytes && bytesRead) {
const percent = Math.round((bytesRead / totalBytes) * 100)
const mbRead = (bytesRead / 1024 / 1024).toFixed(1)
const mbTotal = (totalBytes / 1024 / 1024).toFixed(1)
return `${mbRead} MB / ${mbTotal} MB (${percent}%)`
}
return importProgress.value.message || ''
}
</script>
<template>
@@ -176,76 +87,8 @@ function getProgressDetail(): string {
</div>
</div>
<!-- Actions -->
<div class="flex flex-col items-center space-y-6">
<!-- Import Drop Zone -->
<FileDropZone
:accept="['.json', '.jsonl', '.txt']"
:disabled="isImporting"
class="w-full max-w-4xl"
@files="handleFileDrop"
>
<template #default="{ isDragOver }">
<div
class="group relative flex w-full cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed border-pink-300/50 bg-white/50 px-8 py-8 backdrop-blur-sm transition-all duration-300 hover:border-pink-400 hover:bg-white/80 hover:shadow-lg hover:shadow-pink-500/10 focus:outline-none focus:ring-4 focus:ring-pink-500/20 sm:px-12 sm:py-12 dark:border-pink-700/50 dark:bg-gray-900/50 dark:hover:border-pink-500 dark:hover:bg-gray-900/80"
:class="{
'border-pink-500 bg-pink-50/50 dark:border-pink-400 dark:bg-pink-900/20': isDragOver && !isImporting,
'cursor-not-allowed opacity-70': isImporting,
'hover:scale-[1.02]': !isImporting,
}"
@click="!isImporting && handleClickImport()"
>
<!-- Icon -->
<div
class="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-linear-to-br from-pink-100 to-rose-100 transition-transform duration-300 dark:from-pink-900/30 dark:to-rose-900/30"
:class="{ 'scale-110': isDragOver && !isImporting, 'animate-pulse': isImporting }"
>
<UIcon
v-if="!isImporting"
name="i-heroicons-arrow-up-tray"
class="h-8 w-8 text-pink-600 transition-transform group-hover:-translate-y-1 dark:text-pink-400"
/>
<UIcon
v-else
name="i-heroicons-arrow-path"
class="h-8 w-8 animate-spin text-pink-600 dark:text-pink-400"
/>
</div>
<!-- Text -->
<div class="w-full min-w-80 text-center">
<template v-if="isImporting && importProgress">
<!-- 导入中显示进度 -->
<p class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">{{ getProgressText() }}</p>
<div class="mx-auto w-full max-w-md">
<UProgress v-model="importProgress.progress" size="md" />
</div>
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">
{{ getProgressDetail() }}
</p>
</template>
<template v-else>
<!-- 默认状态 -->
<p class="text-lg font-semibold text-gray-900 dark:text-white">
{{ isDragOver ? '松开鼠标导入文件' : '点击选择或拖拽聊天记录导入' }}
</p>
</template>
</div>
</div>
</template>
</FileDropZone>
<!-- Error Message -->
<div
v-if="importError"
class="flex items-center gap-2 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400"
>
<UIcon name="i-heroicons-exclamation-circle" class="h-5 w-5 shrink-0" />
<span>{{ importError }}</span>
</div>
<UButton @click="openTutorial">查看聊天记录导入教程 </UButton>
</div>
<!-- Import Area -->
<ImportArea />
</div>
</div>