mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-27 09:11:48 +08:00
feat: 支持导入微信默认数据库
This commit is contained in:
@@ -341,21 +341,9 @@ onMounted(() => {
|
||||
<div class="mt-4 flex items-start gap-3 rounded-xl bg-amber-50 p-4 dark:bg-amber-900/20">
|
||||
<UIcon name="i-heroicons-exclamation-triangle" class="mt-0.5 h-5 w-5 shrink-0 text-amber-500" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-amber-800 dark:text-amber-200">字段说明</p>
|
||||
<ul class="mt-1 list-inside list-disc text-sm text-amber-700 dark:text-amber-300">
|
||||
<li>
|
||||
<strong>账号名称</strong>
|
||||
:用户的 QQ 原始昵称
|
||||
</li>
|
||||
<li>
|
||||
<strong>群昵称</strong>
|
||||
:用户在本群的专属备注名称
|
||||
</li>
|
||||
<li>
|
||||
<strong>自定义别名</strong>
|
||||
:您为用户添加的备注,用于搜索和 AI 分析
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||
提示:添加别名可以更好地识别聊天记录中的对话对象,别名将用于搜索和 AI 分析中。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import type { MemberWithStats } from '@/types/chat'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
}>()
|
||||
|
||||
// 成员列表
|
||||
const members = ref<MemberWithStats[]>([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 正在保存别名的成员ID
|
||||
const savingAliasesId = ref<number | null>(null)
|
||||
|
||||
// 获取成员显示名称
|
||||
function getDisplayName(member: MemberWithStats): string {
|
||||
return member.groupNickname || member.accountName || member.platformId
|
||||
}
|
||||
|
||||
// 获取成员首字符(用于头像)
|
||||
function getFirstChar(member: MemberWithStats): string {
|
||||
const name = getDisplayName(member)
|
||||
return name.slice(0, 1)
|
||||
}
|
||||
|
||||
// 计算消息总数
|
||||
const totalMessageCount = computed(() => {
|
||||
return members.value.reduce((sum, m) => sum + m.messageCount, 0)
|
||||
})
|
||||
|
||||
// 计算每个成员的消息占比
|
||||
function getPercentage(count: number): number {
|
||||
if (totalMessageCount.value === 0) return 0
|
||||
return Math.round((count / totalMessageCount.value) * 100)
|
||||
}
|
||||
|
||||
// 加载成员列表
|
||||
async function loadMembers() {
|
||||
if (!props.sessionId) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
members.value = await window.chatApi.getMembers(props.sessionId)
|
||||
} catch (error) {
|
||||
console.error('加载成员列表失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新别名
|
||||
async function updateAliases(member: MemberWithStats, newAliases: string[]) {
|
||||
const aliasesToSave = JSON.parse(JSON.stringify(newAliases)) as string[]
|
||||
|
||||
const currentAliases = JSON.stringify(member.aliases)
|
||||
const newAliasesStr = JSON.stringify(aliasesToSave)
|
||||
if (currentAliases === newAliasesStr) return
|
||||
|
||||
savingAliasesId.value = member.id
|
||||
try {
|
||||
const success = await window.chatApi.updateMemberAliases(props.sessionId, member.id, aliasesToSave)
|
||||
if (success) {
|
||||
const idx = members.value.findIndex((m) => m.id === member.id)
|
||||
if (idx !== -1) {
|
||||
members.value[idx] = {
|
||||
...members.value[idx],
|
||||
aliases: aliasesToSave,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存别名失败:', error)
|
||||
} finally {
|
||||
savingAliasesId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 sessionId 变化
|
||||
watch(
|
||||
() => props.sessionId,
|
||||
() => {
|
||||
loadMembers()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadMembers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-4xl p-6">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">对话成员</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
共 {{ members.length }} 位成员,可为成员添加别名备注用于搜索和 AI 分析
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading" class="flex h-60 items-center justify-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-500" />
|
||||
</div>
|
||||
|
||||
<!-- 成员卡片列表 -->
|
||||
<div v-else class="grid gap-4 md:grid-cols-2">
|
||||
<div
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<!-- 成员头部信息 -->
|
||||
<div class="flex items-start gap-4">
|
||||
<!-- 头像 -->
|
||||
<div
|
||||
class="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-pink-400 to-pink-600 text-lg font-medium text-white"
|
||||
>
|
||||
{{ getFirstChar(member) }}
|
||||
</div>
|
||||
|
||||
<!-- 名称和ID -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate">
|
||||
{{ getDisplayName(member) }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">ID: {{ member.platformId }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息统计 -->
|
||||
<div class="mt-4 flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">消息数</span>
|
||||
<span class="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{{ member.messageCount.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="mt-2 h-2 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div
|
||||
class="h-full rounded-full bg-gradient-to-r from-pink-400 to-pink-600 transition-all duration-500"
|
||||
:style="{ width: `${getPercentage(member.messageCount)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">占比 {{ getPercentage(member.messageCount) }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 别名编辑 -->
|
||||
<div class="mt-4 border-t border-gray-100 pt-4 dark:border-gray-800">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">自定义别名</label>
|
||||
<div class="relative">
|
||||
<UInputTags
|
||||
:model-value="member.aliases"
|
||||
@update:model-value="(val) => updateAliases(member, val)"
|
||||
placeholder="输入后回车添加别名"
|
||||
class="w-full"
|
||||
/>
|
||||
<!-- 保存中指示器 -->
|
||||
<div v-if="savingAliasesId === member.id" class="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-4 w-4 animate-spin text-pink-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!isLoading && members.length === 0" class="flex h-60 flex-col items-center justify-center">
|
||||
<UIcon name="i-heroicons-user-group" class="mb-3 h-12 w-12 text-gray-300 dark:text-gray-600" />
|
||||
<p class="text-gray-500 dark:text-gray-400">暂无成员数据</p>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div v-if="members.length > 0" class="mt-6 flex items-start gap-3 rounded-xl bg-blue-50 p-4 dark:bg-blue-900/20">
|
||||
<UIcon name="i-heroicons-information-circle" class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">提示</p>
|
||||
<p class="mt-1 text-sm text-blue-700 dark:text-blue-300">
|
||||
添加别名可以更好地识别聊天记录中的对话对象,别名将用于搜索和 AI 分析中。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { SubTabs } from '@/components/UI'
|
||||
import { CatchphraseTab, KeywordAnalysis } from './quotes'
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
// 子 Tab 配置(私聊只保留口头禅和关键词分析)
|
||||
const subTabs = [
|
||||
{ id: 'catchphrase', label: '口头禅', icon: 'i-heroicons-chat-bubble-bottom-center-text' },
|
||||
{ id: 'keyword', label: '关键词分析', icon: 'i-heroicons-magnifying-glass' },
|
||||
]
|
||||
|
||||
const activeSubTab = ref('catchphrase')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- 子 Tab 导航 -->
|
||||
<SubTabs v-model="activeSubTab" :items="subTabs" />
|
||||
|
||||
<!-- 子 Tab 内容 -->
|
||||
<div class="flex-1 min-h-0 overflow-auto">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<!-- 口头禅分析 -->
|
||||
<CatchphraseTab
|
||||
v-if="activeSubTab === 'catchphrase'"
|
||||
:session-id="props.sessionId"
|
||||
:time-filter="props.timeFilter"
|
||||
/>
|
||||
|
||||
<!-- 关键词分析 -->
|
||||
<div v-else-if="activeSubTab === 'keyword'" class="mx-auto max-w-3xl p-6">
|
||||
<KeywordAnalysis :session-id="props.sessionId" :time-filter="props.timeFilter" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,10 @@ import { formatDateRange } from '@/utils'
|
||||
import UITabs from '@/components/UI/Tabs.vue'
|
||||
import PrivateOverviewTab from '@/components/analysis/PrivateOverviewTab.vue'
|
||||
import PrivateTimelineTab from '@/components/analysis/PrivateTimelineTab.vue'
|
||||
import PrivateQuotesTab from '@/components/analysis/PrivateQuotesTab.vue'
|
||||
import PrivateMemberTab from '@/components/analysis/PrivateMemberTab.vue'
|
||||
import AITab from '@/components/analysis/AITab.vue'
|
||||
import SQLLabTab from '@/components/analysis/SQLLabTab.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -29,11 +32,14 @@ const availableYears = ref<number[]>([])
|
||||
const selectedYear = ref<number>(0) // 0 表示全部
|
||||
const isInitialLoad = ref(true) // 用于跳过初始加载时的 watch 触发,并控制首屏加载状态
|
||||
|
||||
// Tab 配置 - 私聊有总览、趋势和 AI
|
||||
// Tab 配置 - 私聊有总览、趋势、语录、成员、AI 和 SQL
|
||||
const tabs = [
|
||||
{ id: 'overview', label: '总览', icon: 'i-heroicons-chart-pie' },
|
||||
{ id: 'timeline', label: '趋势', icon: 'i-heroicons-chart-bar' },
|
||||
{ id: 'quotes', label: '语录', icon: 'i-heroicons-chat-bubble-left-right' },
|
||||
{ id: 'member', label: '成员', icon: 'i-heroicons-user-group' },
|
||||
{ id: 'ai', label: 'AI实验室', icon: 'i-heroicons-sparkles' },
|
||||
{ id: 'sql', label: 'SQL实验室', icon: 'i-heroicons-command-line' },
|
||||
]
|
||||
|
||||
const activeTab = ref((route.query.tab as string) || 'overview')
|
||||
@@ -292,6 +298,17 @@ onMounted(() => {
|
||||
:time-range="timeRange"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<PrivateQuotesTab
|
||||
v-else-if="activeTab === 'quotes'"
|
||||
:key="'quotes-' + selectedYear"
|
||||
:session-id="currentSessionId!"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<PrivateMemberTab
|
||||
v-else-if="activeTab === 'member'"
|
||||
:key="'member'"
|
||||
:session-id="currentSessionId!"
|
||||
/>
|
||||
<AITab
|
||||
v-else-if="activeTab === 'ai'"
|
||||
:key="'ai-' + selectedYear"
|
||||
@@ -300,6 +317,11 @@ onMounted(() => {
|
||||
:time-filter="timeFilter"
|
||||
chat-type="private"
|
||||
/>
|
||||
<SQLLabTab
|
||||
v-else-if="activeTab === 'sql'"
|
||||
:key="'sql-' + selectedYear"
|
||||
:session-id="currentSessionId!"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user