feat: 重构目录位置

This commit is contained in:
digua
2026-03-16 21:17:46 +08:00
committed by digua
parent 4b10cf21dd
commit cf7a7fccbb
12 changed files with 7208 additions and 6006 deletions
+1 -1
View File
@@ -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=""
+7102 -5956
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -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>
+18 -5
View File
@@ -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
}
+1 -1
View File
@@ -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" />
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -19,7 +19,10 @@
"quotes": "名言集",
"members": "メンバー",
"member": "メンバー",
"ai": "AIラボ"
"ai": "AIラボ",
"aiChat": "AIチャット",
"lab": "ラボ",
"sqlLab": "SQLラボ"
},
"yearFilter": {
"allTime": "全期間",
+4 -1
View File
@@ -19,7 +19,10 @@
"quotes": "语录",
"members": "成员",
"member": "成员",
"ai": "AI实验室"
"ai": "AI实验室",
"aiChat": "AI对话",
"lab": "实验室",
"sqlLab": "SQL实验室"
},
"yearFilter": {
"allTime": "全部时间",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"title": "ChatLab",
"subtitle": "聊天记录分析实验室",
"subtitle": "重构你的社交记忆",
"features": {
"privacy": {
"title": "隐私至上",
+4 -1
View File
@@ -19,7 +19,10 @@
"quotes": "金句",
"members": "成員",
"member": "成員",
"ai": "AI 實驗室"
"ai": "AI 實驗室",
"aiChat": "AI 對話",
"lab": "實驗室",
"sqlLab": "SQL 實驗室"
},
"yearFilter": {
"allTime": "全部時間",
+33 -19
View File
@@ -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>
+34 -16
View File
@@ -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>