mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-09 15:20:55 +08:00
refactor: 重构页面架构
This commit is contained in:
+44
-1
@@ -1,8 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Sidebar from '@/components/Sidebar.vue'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { isInitialized } = storeToRefs(chatStore)
|
||||
const route = useRoute()
|
||||
|
||||
// 应用启动时从数据库加载会话列表
|
||||
onMounted(async () => {
|
||||
@@ -12,6 +17,44 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<router-view />
|
||||
<div class="flex h-screen w-full overflow-hidden bg-white dark:bg-gray-950">
|
||||
<template v-if="!isInitialized">
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<div class="text-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-500" />
|
||||
<p class="mt-2 text-sm text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Sidebar />
|
||||
<main class="flex-1 overflow-hidden">
|
||||
<router-view v-slot="{ Component }">
|
||||
<Transition name="page-fade" mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</Transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</template>
|
||||
</div>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-fade-enter-active,
|
||||
.page-fade-leave-active {
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.page-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.page-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
@@ -11,7 +12,9 @@ dayjs.extend(relativeTime)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { sessions, currentSessionId } = storeToRefs(chatStore)
|
||||
const { sessions } = storeToRefs(chatStore)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const isCollapsed = ref(false)
|
||||
const deleteConfirmId = ref<string | null>(null)
|
||||
@@ -26,8 +29,8 @@ function toggleSidebar() {
|
||||
}
|
||||
|
||||
function handleImport() {
|
||||
// 清空当前会话选择,回到欢迎页(不触发导入弹窗)
|
||||
chatStore.clearSelection()
|
||||
// Navigate to home (Welcome Guide)
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
function formatTime(timestamp: number): string {
|
||||
@@ -91,7 +94,7 @@ function cancelDelete() {
|
||||
|
||||
<div class="space-y-1">
|
||||
<div v-if="!isCollapsed && sessions.length > 0" class="mb-2 px-2 text-xs font-medium text-gray-500">
|
||||
分析记录
|
||||
聊天记录
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -99,18 +102,18 @@ function cancelDelete() {
|
||||
:key="session.id"
|
||||
class="group relative flex w-full items-center rounded-full p-2 text-left transition-colors"
|
||||
:class="[
|
||||
currentSessionId === session.id && !isCollapsed
|
||||
route.params.id === session.id && !isCollapsed
|
||||
? 'bg-primary-100 text-gray-900 dark:bg-primary-900/30 dark:text-primary-100'
|
||||
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-200/60 dark:hover:bg-gray-800',
|
||||
isCollapsed ? 'justify-center cursor-pointer' : 'cursor-pointer',
|
||||
]"
|
||||
@click="chatStore.selectSession(session.id)"
|
||||
@click="router.push({ name: 'chat', params: { id: session.id } })"
|
||||
>
|
||||
<!-- Platform Icon / Text Avatar -->
|
||||
<div
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold"
|
||||
:class="[
|
||||
currentSessionId === session.id
|
||||
route.params.id === session.id
|
||||
? 'bg-primary-600 text-white dark:bg-primary-500 dark:text-white'
|
||||
: 'bg-gray-400 text-white dark:bg-gray-600 dark:text-white',
|
||||
isCollapsed ? '' : 'mr-3',
|
||||
@@ -144,7 +147,7 @@ function cancelDelete() {
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="p-3">
|
||||
<p class="mb-3 text-sm">确定删除此分析记录?</p>
|
||||
<p class="mb-3 text-sm">确定删除此记录?</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton size="xs" color="red" @click="handleDelete(session.id)">确定删除</UButton>
|
||||
</div>
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { isImporting, importProgress } = storeToRefs(chatStore)
|
||||
|
||||
const importError = ref<string | null>(null)
|
||||
const isDragOver = ref(false)
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: '🏆',
|
||||
title: '活跃度分析',
|
||||
desc: '谁是群里的潜水王?',
|
||||
color: 'text-yellow-500',
|
||||
bg: 'bg-yellow-50',
|
||||
delay: '0ms',
|
||||
},
|
||||
{
|
||||
icon: '☁️',
|
||||
title: '词云生成',
|
||||
desc: '大家最爱说什么?',
|
||||
color: 'text-blue-500',
|
||||
bg: 'bg-blue-50',
|
||||
delay: '100ms',
|
||||
},
|
||||
{
|
||||
icon: '❤️',
|
||||
title: '情感分析',
|
||||
desc: '群聊氛围怎么样?',
|
||||
color: 'text-pink-500',
|
||||
bg: 'bg-pink-50',
|
||||
delay: '200ms',
|
||||
},
|
||||
]
|
||||
|
||||
async function handleImport() {
|
||||
importError.value = null
|
||||
const result = await chatStore.importFile()
|
||||
if (!result.success && result.error && result.error !== '未选择文件') {
|
||||
importError.value = result.error
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽事件处理
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
function handleDragLeave(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
async function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
isDragOver.value = false
|
||||
|
||||
if (isImporting.value) return
|
||||
|
||||
const files = e.dataTransfer?.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
|
||||
// 使用 Electron 的 webUtils 获取文件真实路径
|
||||
let filePath: string
|
||||
try {
|
||||
filePath = window.electron.webUtils.getPathForFile(file)
|
||||
} catch (error) {
|
||||
console.error('获取文件路径失败:', error)
|
||||
importError.value = '无法读取文件路径'
|
||||
return
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
importError.value = '无法读取文件路径'
|
||||
return
|
||||
}
|
||||
|
||||
importError.value = null
|
||||
const result = await chatStore.importFileFromPath(filePath)
|
||||
if (!result.success && result.error) {
|
||||
importError.value = result.error
|
||||
}
|
||||
}
|
||||
|
||||
function openTutorial(type: 'wechat' | 'qq') {
|
||||
// TODO: 打开教程页面
|
||||
console.log('Tutorial:', type)
|
||||
}
|
||||
|
||||
function getProgressText(): string {
|
||||
if (!importProgress.value) return ''
|
||||
switch (importProgress.value.stage) {
|
||||
case 'reading':
|
||||
return '读取文件中...'
|
||||
case 'parsing':
|
||||
return '解析聊天记录...'
|
||||
case 'saving':
|
||||
return '保存数据...'
|
||||
case 'done':
|
||||
return '导入完成!'
|
||||
case 'error':
|
||||
return '导入失败'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex h-full w-full overflow-hidden bg-white dark:bg-gray-950">
|
||||
<!-- Content Container -->
|
||||
<div class="relative z-10 flex h-full w-full flex-col items-center justify-center px-4">
|
||||
<!-- Hero Section -->
|
||||
<div class="mb-12 text-center">
|
||||
<div
|
||||
class="mb-6 inline-flex items-center justify-center rounded-3xl bg-white p-4 shadow-lg shadow-pink-100 ring-1 ring-gray-100 dark:bg-gray-900 dark:shadow-pink-900/20 dark:ring-gray-800"
|
||||
:class="[isImporting ? '' : 'animate-bounce']"
|
||||
>
|
||||
<UIcon v-if="!isImporting" name="i-heroicons-sparkles" class="h-8 w-8 text-pink-500" />
|
||||
<UIcon v-else name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-500" />
|
||||
</div>
|
||||
<h1
|
||||
class="mb-4 bg-linear-to-r from-pink-600 via-pink-500 to-rose-400 bg-clip-text text-5xl font-black tracking-tight text-transparent sm:text-6xl"
|
||||
>
|
||||
ChatLab
|
||||
</h1>
|
||||
<p class="text-lg font-medium text-gray-500 dark:text-gray-400">你的AI聊天分析实验室</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature Cards -->
|
||||
<div class="mb-12 grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.title"
|
||||
class="group relative transform cursor-default rounded-2xl border border-gray-100 bg-white p-6 shadow-sm transition-all duration-300 hover:-translate-y-2 hover:shadow-xl hover:shadow-pink-500/10 dark:border-gray-800 dark:bg-gray-900"
|
||||
:style="{ animationDelay: feature.delay }"
|
||||
>
|
||||
<div class="mb-4 text-4xl transition-transform duration-300 group-hover:scale-110">
|
||||
{{ feature.icon }}
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-bold text-gray-900 dark:text-white">{{ feature.title }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ feature.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-col items-center space-y-6">
|
||||
<!-- Import Drop Zone -->
|
||||
<div
|
||||
class="group relative flex w-full max-w-2xl cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed border-pink-300 bg-white px-12 py-12 transition-all duration-300 hover:border-pink-400 hover:bg-pink-50/50 focus:outline-none focus:ring-4 focus:ring-pink-500/20 dark:border-pink-700 dark:bg-gray-900 dark:hover:border-pink-500 dark:hover:bg-pink-900/10"
|
||||
:class="{
|
||||
'border-pink-500 bg-pink-50 dark:border-pink-400 dark:bg-pink-900/20': isDragOver && !isImporting,
|
||||
'cursor-not-allowed opacity-70': isImporting,
|
||||
'hover:scale-[1.02] hover:shadow-xl hover:shadow-pink-500/10': !isImporting,
|
||||
}"
|
||||
@click="!isImporting && handleImport()"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<!-- 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 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 :value="importProgress.progress" size="md" color="pink" />
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ importProgress.message }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 默认状态 -->
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ isDragOver ? '松开鼠标导入文件' : '点击选择或拖拽文件到这里' }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">支持 QQ、微信聊天记录(JSON/TXT 格式)</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Tutorial Links -->
|
||||
<div class="flex items-center space-x-6 text-sm font-medium text-gray-400">
|
||||
<button
|
||||
class="flex items-center transition-colors hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@click="openTutorial('wechat')"
|
||||
>
|
||||
<UIcon name="i-simple-icons-wechat" class="mr-1.5 h-4 w-4" />
|
||||
微信导入教程
|
||||
</button>
|
||||
<span class="h-1 w-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
|
||||
<button
|
||||
class="flex items-center transition-colors hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@click="openTutorial('qq')"
|
||||
>
|
||||
<UIcon name="i-simple-icons-tencentqq" class="mr-1.5 h-4 w-4" />
|
||||
QQ导入教程
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { AnalysisSession, MemberActivity, HourlyActivity, DailyActivity, MessageType } from '@/types/chat'
|
||||
import UITabs from '@/components/UI/Tabs.vue'
|
||||
import OverviewTab from './analysis/OverviewTab.vue'
|
||||
import MembersTab from './analysis/MembersTab.vue'
|
||||
import TimeTab from './analysis/TimeTab.vue'
|
||||
import TimelineTab from './analysis/TimelineTab.vue'
|
||||
import OverviewTab from '@/components/analysis/OverviewTab.vue'
|
||||
import MembersTab from '@/components/analysis/MembersTab.vue'
|
||||
import TimeTab from '@/components/analysis/TimeTab.vue'
|
||||
import TimelineTab from '@/components/analysis/TimelineTab.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const chatStore = useChatStore()
|
||||
const { currentSessionId } = storeToRefs(chatStore)
|
||||
|
||||
@@ -76,6 +79,18 @@ const filteredMemberCount = computed(() => {
|
||||
return memberActivity.value.filter((m) => m.messageCount > 0).length
|
||||
})
|
||||
|
||||
// Sync route param to store
|
||||
function syncSession() {
|
||||
const id = route.params.id as string
|
||||
if (id) {
|
||||
chatStore.selectSession(id)
|
||||
// If selection failed (e.g. invalid ID), redirect to home
|
||||
if (chatStore.currentSessionId !== id) {
|
||||
router.replace('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载基础数据(不受年份筛选影响)
|
||||
async function loadBaseData() {
|
||||
if (!currentSessionId.value) return
|
||||
@@ -137,7 +152,15 @@ async function loadData() {
|
||||
isInitialLoad.value = false
|
||||
}
|
||||
|
||||
// 监听会话变化
|
||||
// 监听路由参数变化
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => {
|
||||
syncSession()
|
||||
}
|
||||
)
|
||||
|
||||
// 监听会话变化 (syncSession 会触发 currentSessionId 变化)
|
||||
watch(
|
||||
currentSessionId,
|
||||
() => {
|
||||
@@ -154,7 +177,10 @@ watch(selectedYear, () => {
|
||||
loadAnalysisData()
|
||||
})
|
||||
|
||||
onMounted(loadData)
|
||||
onMounted(() => {
|
||||
syncSession()
|
||||
// loadData is triggered by currentSessionId watch
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
+223
-18
@@ -1,30 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Sidebar from '@/components/Sidebar.vue'
|
||||
import WelcomeGuide from '@/components/WelcomeGuide.vue'
|
||||
import AnalysisDashboard from '@/components/AnalysisDashboard.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { currentSessionId, isInitialized } = storeToRefs(chatStore)
|
||||
const { isImporting, importProgress } = storeToRefs(chatStore)
|
||||
|
||||
const importError = ref<string | null>(null)
|
||||
const isDragOver = ref(false)
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: '🏆',
|
||||
title: '活跃度分析',
|
||||
desc: '谁是群里的潜水王?',
|
||||
color: 'text-yellow-500',
|
||||
bg: 'bg-yellow-50',
|
||||
delay: '0ms',
|
||||
},
|
||||
{
|
||||
icon: '☁️',
|
||||
title: '词云生成',
|
||||
desc: '大家最爱说什么?',
|
||||
color: 'text-blue-500',
|
||||
bg: 'bg-blue-50',
|
||||
delay: '100ms',
|
||||
},
|
||||
{
|
||||
icon: '❤️',
|
||||
title: '情感分析',
|
||||
desc: '群聊氛围怎么样?',
|
||||
color: 'text-pink-500',
|
||||
bg: 'bg-pink-50',
|
||||
delay: '200ms',
|
||||
},
|
||||
]
|
||||
|
||||
async function handleImport() {
|
||||
importError.value = null
|
||||
const result = await chatStore.importFile()
|
||||
if (!result.success && result.error && result.error !== '未选择文件') {
|
||||
importError.value = result.error
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽事件处理
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
function handleDragLeave(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
async function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
isDragOver.value = false
|
||||
|
||||
if (isImporting.value) return
|
||||
|
||||
const files = e.dataTransfer?.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
|
||||
// 使用 Electron 的 webUtils 获取文件真实路径
|
||||
let filePath: string
|
||||
try {
|
||||
filePath = window.electron.webUtils.getPathForFile(file)
|
||||
} catch (error) {
|
||||
console.error('获取文件路径失败:', error)
|
||||
importError.value = '无法读取文件路径'
|
||||
return
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
importError.value = '无法读取文件路径'
|
||||
return
|
||||
}
|
||||
|
||||
importError.value = null
|
||||
const result = await chatStore.importFileFromPath(filePath)
|
||||
if (!result.success && result.error) {
|
||||
importError.value = result.error
|
||||
}
|
||||
}
|
||||
|
||||
function openTutorial(type: 'wechat' | 'qq') {
|
||||
// TODO: 打开教程页面
|
||||
console.log('Tutorial:', type)
|
||||
}
|
||||
|
||||
function getProgressText(): string {
|
||||
if (!importProgress.value) return ''
|
||||
switch (importProgress.value.stage) {
|
||||
case 'reading':
|
||||
return '读取文件中...'
|
||||
case 'parsing':
|
||||
return '解析聊天记录...'
|
||||
case 'saving':
|
||||
return '保存数据...'
|
||||
case 'done':
|
||||
return '导入完成!'
|
||||
case 'error':
|
||||
return '导入失败'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen w-full overflow-hidden bg-white dark:bg-gray-950">
|
||||
<template v-if="!isInitialized">
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<div class="text-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-500" />
|
||||
<p class="mt-2 text-sm text-gray-500">加载中...</p>
|
||||
<div class="relative flex h-full w-full overflow-hidden bg-white dark:bg-gray-950">
|
||||
<!-- Content Container -->
|
||||
<div class="relative z-10 flex h-full w-full flex-col items-center justify-center px-4">
|
||||
<!-- Hero Section -->
|
||||
<div class="mb-12 text-center">
|
||||
<div
|
||||
class="mb-6 inline-flex items-center justify-center rounded-3xl bg-white p-4 shadow-lg shadow-pink-100 ring-1 ring-gray-100 dark:bg-gray-900 dark:shadow-pink-900/20 dark:ring-gray-800"
|
||||
:class="[isImporting ? '' : 'animate-bounce']"
|
||||
>
|
||||
<UIcon v-if="!isImporting" name="i-heroicons-sparkles" class="h-8 w-8 text-pink-500" />
|
||||
<UIcon v-else name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-500" />
|
||||
</div>
|
||||
<h1
|
||||
class="mb-4 bg-linear-to-r from-pink-600 via-pink-500 to-rose-400 bg-clip-text text-5xl font-black tracking-tight text-transparent sm:text-6xl"
|
||||
>
|
||||
ChatLab
|
||||
</h1>
|
||||
<p class="text-lg font-medium text-gray-500 dark:text-gray-400">你的AI聊天分析实验室</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature Cards -->
|
||||
<div class="mb-12 grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.title"
|
||||
class="group relative transform cursor-default rounded-2xl border border-gray-100 bg-white p-6 shadow-sm transition-all duration-300 hover:-translate-y-2 hover:shadow-xl hover:shadow-pink-500/10 dark:border-gray-800 dark:bg-gray-900"
|
||||
:style="{ animationDelay: feature.delay }"
|
||||
>
|
||||
<div class="mb-4 text-4xl transition-transform duration-300 group-hover:scale-110">
|
||||
{{ feature.icon }}
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-bold text-gray-900 dark:text-white">{{ feature.title }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ feature.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Sidebar />
|
||||
<main class="flex-1 overflow-hidden">
|
||||
<WelcomeGuide v-if="!currentSessionId" />
|
||||
<AnalysisDashboard v-else />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-col items-center space-y-6">
|
||||
<!-- Import Drop Zone -->
|
||||
<div
|
||||
class="group relative flex w-full max-w-2xl cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed border-pink-300 bg-white px-12 py-12 transition-all duration-300 hover:border-pink-400 hover:bg-pink-50/50 focus:outline-none focus:ring-4 focus:ring-pink-500/20 dark:border-pink-700 dark:bg-gray-900 dark:hover:border-pink-500 dark:hover:bg-pink-900/10"
|
||||
:class="{
|
||||
'border-pink-500 bg-pink-50 dark:border-pink-400 dark:bg-pink-900/20': isDragOver && !isImporting,
|
||||
'cursor-not-allowed opacity-70': isImporting,
|
||||
'hover:scale-[1.02] hover:shadow-xl hover:shadow-pink-500/10': !isImporting,
|
||||
}"
|
||||
@click="!isImporting && handleImport()"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<!-- 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 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 :value="importProgress.progress" size="md" color="pink" />
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ importProgress.message }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 默认状态 -->
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ isDragOver ? '松开鼠标导入文件' : '点击选择或拖拽文件到这里' }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">支持 QQ、微信聊天记录(JSON/TXT 格式)</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Tutorial Links -->
|
||||
<div class="flex items-center space-x-6 text-sm font-medium text-gray-400">
|
||||
<button
|
||||
class="flex items-center transition-colors hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@click="openTutorial('wechat')"
|
||||
>
|
||||
<UIcon name="i-simple-icons-wechat" class="mr-1.5 h-4 w-4" />
|
||||
微信导入教程
|
||||
</button>
|
||||
<span class="h-1 w-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
|
||||
<button
|
||||
class="flex items-center transition-colors hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@click="openTutorial('qq')"
|
||||
>
|
||||
<UIcon name="i-simple-icons-tencentqq" class="mr-1.5 h-4 w-4" />
|
||||
QQ导入教程
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+6
-1
@@ -4,9 +4,14 @@ export const router = createRouter({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
name: 'home',
|
||||
component: () => import('@/pages/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/chat/:id',
|
||||
name: 'chat',
|
||||
component: () => import('@/pages/chat.vue'),
|
||||
},
|
||||
],
|
||||
history: createWebHashHistory(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user