mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-06 13:06:09 +08:00
feat: 重构目录位置
This commit is contained in:
@@ -8,7 +8,7 @@ MAIN_VITE_SERVER_API=127.0.0.1
|
||||
RENDERER_VITE_SERVER_URL=
|
||||
|
||||
# 程序信息
|
||||
RENDERER_VITE_SITE_TITLE="聊天记录分析实验室"
|
||||
RENDERER_VITE_SITE_TITLE="重构你的社交记忆"
|
||||
RENDERER_VITE_SITE_KEYWORDS=""
|
||||
RENDERER_VITE_SITE_DES=""
|
||||
RENDERER_VITE_SITE_URL=""
|
||||
|
||||
Generated
+7102
-5956
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,7 @@ const shouldShowFloatingBar = computed(() => {
|
||||
const isOnSameSessionAIPage =
|
||||
route.name === expectedRouteName &&
|
||||
String(route.params.id ?? '') === activeTask.value.sessionId &&
|
||||
route.query.tab === 'ai' &&
|
||||
((route.query.aiSubTab as string | undefined) ?? 'chat-explorer') === 'chat-explorer'
|
||||
route.query.tab === 'ai-chat'
|
||||
|
||||
if (!isOnSameSessionAIPage) {
|
||||
return true
|
||||
@@ -41,7 +40,7 @@ async function handleOpenTask() {
|
||||
await router.push({
|
||||
name: activeTask.value.chatType === 'group' ? 'group-chat' : 'private-chat',
|
||||
params: { id: activeTask.value.sessionId },
|
||||
query: { tab: 'ai', aiSubTab: 'chat-explorer' },
|
||||
query: { tab: 'ai-chat' },
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -15,19 +15,32 @@ const props = defineProps<{
|
||||
sessionName: string
|
||||
timeFilter?: { startTs: number; endTs: number }
|
||||
chatType?: 'group' | 'private'
|
||||
mode?: 'full' | 'sql-only'
|
||||
}>()
|
||||
|
||||
const subTabs = computed(() => [
|
||||
{ id: 'chat-explorer', label: t('ai.tab.chatExplorer'), icon: 'i-heroicons-chat-bubble-left-ellipsis' },
|
||||
{ id: 'sql-lab', label: t('ai.tab.sqlLab'), icon: 'i-heroicons-command-line' },
|
||||
])
|
||||
const subTabs = computed(() => {
|
||||
// 实验室模式下只保留 SQL 实验室子 Tab,一级导航由外层页面承载。
|
||||
if (props.mode === 'sql-only') {
|
||||
return [{ id: 'sql-lab', label: t('ai.tab.sqlLab'), icon: 'i-heroicons-command-line' }]
|
||||
}
|
||||
|
||||
const activeSubTab = ref((route.query.aiSubTab as string) || 'chat-explorer')
|
||||
return [
|
||||
{ id: 'chat-explorer', label: t('ai.tab.chatExplorer'), icon: 'i-heroicons-chat-bubble-left-ellipsis' },
|
||||
{ id: 'sql-lab', label: t('ai.tab.sqlLab'), icon: 'i-heroicons-command-line' },
|
||||
]
|
||||
})
|
||||
|
||||
const activeSubTab = ref(props.mode === 'sql-only' ? 'sql-lab' : (route.query.aiSubTab as string) || 'chat-explorer')
|
||||
|
||||
// 悬浮任务条返回时会通过 query 指定目标子页,这里同步一次,确保能直接回到对话流。
|
||||
watch(
|
||||
() => route.query.aiSubTab,
|
||||
(nextTab) => {
|
||||
if (props.mode === 'sql-only') {
|
||||
activeSubTab.value = 'sql-lab'
|
||||
return
|
||||
}
|
||||
|
||||
if (nextTab === 'chat-explorer' || nextTab === 'sql-lab') {
|
||||
activeSubTab.value = nextTab
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible && isLoaded" class="flex items-center gap-2">
|
||||
<div v-if="isLoaded" class="flex items-center gap-2" :class="{ invisible: !visible }">
|
||||
<!-- 模式选择器 -->
|
||||
<USelect v-model="modeModel" :items="modeOptions" size="md" class="w-28 shrink-0" />
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
"quotes": "Quotes",
|
||||
"members": "Members",
|
||||
"member": "Members",
|
||||
"ai": "AI Lab"
|
||||
"ai": "AI Lab",
|
||||
"aiChat": "AI Chat",
|
||||
"lab": "Lab",
|
||||
"sqlLab": "SQL Lab"
|
||||
},
|
||||
"yearFilter": {
|
||||
"allTime": "All Time",
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
"quotes": "名言集",
|
||||
"members": "メンバー",
|
||||
"member": "メンバー",
|
||||
"ai": "AIラボ"
|
||||
"ai": "AIラボ",
|
||||
"aiChat": "AIチャット",
|
||||
"lab": "ラボ",
|
||||
"sqlLab": "SQLラボ"
|
||||
},
|
||||
"yearFilter": {
|
||||
"allTime": "全期間",
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
"quotes": "语录",
|
||||
"members": "成员",
|
||||
"member": "成员",
|
||||
"ai": "AI实验室"
|
||||
"ai": "AI实验室",
|
||||
"aiChat": "AI对话",
|
||||
"lab": "实验室",
|
||||
"sqlLab": "SQL实验室"
|
||||
},
|
||||
"yearFilter": {
|
||||
"allTime": "全部时间",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "ChatLab",
|
||||
"subtitle": "聊天记录分析实验室",
|
||||
"subtitle": "重构你的社交记忆",
|
||||
"features": {
|
||||
"privacy": {
|
||||
"title": "隐私至上",
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
"quotes": "金句",
|
||||
"members": "成員",
|
||||
"member": "成員",
|
||||
"ai": "AI 實驗室"
|
||||
"ai": "AI 實驗室",
|
||||
"aiChat": "AI 對話",
|
||||
"lab": "實驗室",
|
||||
"sqlLab": "SQL 實驗室"
|
||||
},
|
||||
"yearFilter": {
|
||||
"allTime": "全部時間",
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { MemberActivity, HourlyActivity, DailyActivity } from '@/types/anal
|
||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||
import TimeSelect from '@/components/common/TimeSelect.vue'
|
||||
import AITab from '@/components/analysis/AITab.vue'
|
||||
import { ChatExplorer } from '@/components/AIChat'
|
||||
import OverviewTab from './components/OverviewTab.vue'
|
||||
import ViewTab from './components/ViewTab.vue'
|
||||
import QuotesTab from './components/QuotesTab.vue'
|
||||
@@ -58,13 +59,19 @@ const allTabs = [
|
||||
{ id: 'view', labelKey: 'analysis.tabs.view', icon: 'i-heroicons-presentation-chart-bar' },
|
||||
{ id: 'quotes', labelKey: 'analysis.tabs.groupQuotes', icon: 'i-heroicons-chat-bubble-bottom-center-text' },
|
||||
{ id: 'members', labelKey: 'analysis.tabs.members', icon: 'i-heroicons-user-group' },
|
||||
{ id: 'ai', labelKey: 'analysis.tabs.ai', icon: 'i-heroicons-sparkles' },
|
||||
{ id: 'ai-chat', labelKey: 'analysis.tabs.aiChat', icon: 'i-heroicons-chat-bubble-left-ellipsis' },
|
||||
{ id: 'lab', labelKey: 'analysis.tabs.lab', icon: 'i-heroicons-beaker' },
|
||||
]
|
||||
|
||||
// Tab 列表
|
||||
const tabs = computed(() => allTabs)
|
||||
|
||||
const activeTab = ref((route.query.tab as string) || 'overview')
|
||||
function resolveActiveTabFromRoute(): string {
|
||||
const routeTab = route.query.tab as string | undefined
|
||||
return allTabs.some((tab) => tab.id === routeTab) ? routeTab! : 'overview'
|
||||
}
|
||||
|
||||
const activeTab = ref(resolveActiveTabFromRoute())
|
||||
|
||||
// 时间范围筛选(composable 统一管理状态、派生计算、URL 同步)
|
||||
const { timeRangeValue, fullTimeRange, availableYears, timeFilter, selectedYearForOverview, initialTimeState } =
|
||||
@@ -156,19 +163,18 @@ async function loadData() {
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => {
|
||||
// 切换会话时,重置 activeTab 为默认值(如果 URL 中没有 tab 参数)
|
||||
// 注意:sidebar 导航通常会 push 新的 URL,不带 query 参数,所以这里会自动重置
|
||||
// 但为了保险,我们可以在这里强制重置,或者依赖 activeTab 的初始化逻辑(它只在组件创建时初始化)
|
||||
// 由于组件是复用的,我们需要手动处理
|
||||
if (!route.query.tab) {
|
||||
activeTab.value = 'overview'
|
||||
} else {
|
||||
activeTab.value = route.query.tab as string
|
||||
}
|
||||
activeTab.value = resolveActiveTabFromRoute()
|
||||
syncSession()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
() => {
|
||||
activeTab.value = resolveActiveTabFromRoute()
|
||||
}
|
||||
)
|
||||
|
||||
// 监听会话变化(切换会话时由 TimeSelect 自行发出新范围,避免 Tab Content 双重重建)
|
||||
watch(
|
||||
currentSessionId,
|
||||
@@ -217,12 +223,12 @@ onMounted(() => {
|
||||
<CaptureButton />
|
||||
</template>
|
||||
<!-- Tabs -->
|
||||
<div class="mt-4 flex items-center justify-between gap-4">
|
||||
<div class="flex shrink-0 items-center gap-1 overflow-x-auto scrollbar-hide">
|
||||
<div class="mt-4 flex items-center justify-between gap-3">
|
||||
<div class="flex shrink-0 items-center gap-0.5 overflow-x-auto scrollbar-hide">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-all"
|
||||
class="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'bg-pink-500 text-white dark:bg-pink-900/30 dark:text-pink-300'
|
||||
@@ -234,11 +240,11 @@ onMounted(() => {
|
||||
<span class="whitespace-nowrap">{{ t(tab.labelKey) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 时间范围选择器靠右(AI实验室时隐藏) -->
|
||||
<!-- AI 对话、实验室和成员页都不使用这里的时间范围筛选,因此在这些一级 Tab 下隐藏。 -->
|
||||
<TimeSelect
|
||||
v-model="timeRangeValue"
|
||||
:session-id="currentSessionId ?? undefined"
|
||||
:visible="activeTab !== 'ai'"
|
||||
:visible="activeTab !== 'ai-chat' && activeTab !== 'lab' && activeTab !== 'members'"
|
||||
:initial-state="initialTimeState"
|
||||
@update:full-range="fullTimeRange = $event"
|
||||
@update:available-years="availableYears = $event"
|
||||
@@ -291,13 +297,21 @@ onMounted(() => {
|
||||
:time-filter="timeFilter"
|
||||
@data-changed="loadData"
|
||||
/>
|
||||
<AITab
|
||||
v-else-if="activeTab === 'ai'"
|
||||
:key="'ai-' + currentSessionId"
|
||||
<ChatExplorer
|
||||
v-else-if="activeTab === 'ai-chat'"
|
||||
:key="'ai-chat-' + currentSessionId"
|
||||
:session-id="currentSessionId!"
|
||||
:session-name="session.name"
|
||||
chat-type="group"
|
||||
/>
|
||||
<AITab
|
||||
v-else-if="activeTab === 'lab'"
|
||||
:key="'lab-' + currentSessionId"
|
||||
:session-id="currentSessionId!"
|
||||
:session-name="session.name"
|
||||
chat-type="group"
|
||||
mode="sql-only"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { MemberActivity, HourlyActivity, DailyActivity } from '@/types/anal
|
||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||
import TimeSelect from '@/components/common/TimeSelect.vue'
|
||||
import AITab from '@/components/analysis/AITab.vue'
|
||||
import { ChatExplorer } from '@/components/AIChat'
|
||||
import OverviewTab from './components/OverviewTab.vue'
|
||||
import ViewTab from './components/ViewTab.vue'
|
||||
import QuotesTab from './components/QuotesTab.vue'
|
||||
@@ -52,16 +53,22 @@ const dailyActivity = ref<DailyActivity[]>([])
|
||||
const messageTypes = ref<Array<{ type: MessageType; count: number }>>([])
|
||||
const isInitialLoad = ref(true)
|
||||
|
||||
// Tab 配置 - 私聊有总览、视图、语录、成员、AI实验室
|
||||
// Tab 配置 - 私聊包含总览、视图、语录、成员、AI 对话和实验室
|
||||
const tabs = [
|
||||
{ id: 'overview', labelKey: 'analysis.tabs.overview', icon: 'i-heroicons-chart-pie' },
|
||||
{ id: 'view', labelKey: 'analysis.tabs.view', icon: 'i-heroicons-presentation-chart-bar' },
|
||||
{ id: 'quotes', labelKey: 'analysis.tabs.quotes', icon: 'i-heroicons-chat-bubble-left-right' },
|
||||
{ id: 'member', labelKey: 'analysis.tabs.member', icon: 'i-heroicons-user-group' },
|
||||
{ id: 'ai', labelKey: 'analysis.tabs.ai', icon: 'i-heroicons-sparkles' },
|
||||
{ id: 'ai-chat', labelKey: 'analysis.tabs.aiChat', icon: 'i-heroicons-chat-bubble-left-ellipsis' },
|
||||
{ id: 'lab', labelKey: 'analysis.tabs.lab', icon: 'i-heroicons-beaker' },
|
||||
]
|
||||
|
||||
const activeTab = ref((route.query.tab as string) || 'overview')
|
||||
function resolveActiveTabFromRoute(): string {
|
||||
const routeTab = route.query.tab as string | undefined
|
||||
return tabs.some((tab) => tab.id === routeTab) ? routeTab! : 'overview'
|
||||
}
|
||||
|
||||
const activeTab = ref(resolveActiveTabFromRoute())
|
||||
|
||||
// 时间范围筛选(composable 统一管理状态、派生计算、URL 同步)
|
||||
const { timeRangeValue, fullTimeRange, timeFilter, selectedYearForOverview, initialTimeState } = useTimeSelect(
|
||||
@@ -149,15 +156,18 @@ async function loadData() {
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => {
|
||||
if (!route.query.tab) {
|
||||
activeTab.value = 'overview'
|
||||
} else {
|
||||
activeTab.value = route.query.tab as string
|
||||
}
|
||||
activeTab.value = resolveActiveTabFromRoute()
|
||||
syncSession()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
() => {
|
||||
activeTab.value = resolveActiveTabFromRoute()
|
||||
}
|
||||
)
|
||||
|
||||
// 监听会话变化(切换会话时由 TimeSelect 自行发出新范围,避免 Tab Content 双重重建)
|
||||
watch(
|
||||
currentSessionId,
|
||||
@@ -229,12 +239,12 @@ onMounted(() => {
|
||||
<CaptureButton />
|
||||
</template>
|
||||
<!-- Tabs -->
|
||||
<div class="mt-4 flex items-center justify-between gap-4">
|
||||
<div class="flex shrink-0 items-center gap-1 overflow-x-auto scrollbar-hide">
|
||||
<div class="mt-4 flex items-center justify-between gap-3">
|
||||
<div class="flex shrink-0 items-center gap-0.5 overflow-x-auto scrollbar-hide">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-all"
|
||||
class="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'bg-pink-500 text-white dark:bg-pink-900/30 dark:text-pink-300'
|
||||
@@ -246,11 +256,11 @@ onMounted(() => {
|
||||
<span class="whitespace-nowrap">{{ t(tab.labelKey) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 时间范围选择器靠右(AI实验室时隐藏) -->
|
||||
<!-- AI 对话、实验室和成员页都不使用这里的时间范围筛选,因此在这些一级 Tab 下隐藏。 -->
|
||||
<TimeSelect
|
||||
v-model="timeRangeValue"
|
||||
:session-id="currentSessionId ?? undefined"
|
||||
:visible="activeTab !== 'ai'"
|
||||
:visible="activeTab !== 'ai-chat' && activeTab !== 'lab' && activeTab !== 'member'"
|
||||
:initial-state="initialTimeState"
|
||||
@update:full-range="fullTimeRange = $event"
|
||||
/>
|
||||
@@ -298,13 +308,21 @@ onMounted(() => {
|
||||
:key="'member-' + currentSessionId"
|
||||
:session-id="currentSessionId!"
|
||||
/>
|
||||
<AITab
|
||||
v-else-if="activeTab === 'ai'"
|
||||
:key="'ai-' + currentSessionId"
|
||||
<ChatExplorer
|
||||
v-else-if="activeTab === 'ai-chat'"
|
||||
:key="'ai-chat-' + currentSessionId"
|
||||
:session-id="currentSessionId!"
|
||||
:session-name="session.name"
|
||||
chat-type="private"
|
||||
/>
|
||||
<AITab
|
||||
v-else-if="activeTab === 'lab'"
|
||||
:key="'lab-' + currentSessionId"
|
||||
:session-id="currentSessionId!"
|
||||
:session-name="session.name"
|
||||
chat-type="private"
|
||||
mode="sql-only"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user