mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-06-14 03:26:37 +08:00
refactor: 抽取会话分析页公共逻辑并统一头部文案
This commit is contained in:
@@ -6,3 +6,4 @@ export { usePageAnchors, type AnchorItem } from './usePageAnchors'
|
|||||||
export { useAIChat, type ChatMessage, type SourceMessage } from './useAIChat'
|
export { useAIChat, type ChatMessage, type SourceMessage } from './useAIChat'
|
||||||
export { useScreenCapture, type ScreenCaptureOptions } from './useScreenCapture'
|
export { useScreenCapture, type ScreenCaptureOptions } from './useScreenCapture'
|
||||||
export { useTimeSelect } from './useTimeSelect'
|
export { useTimeSelect } from './useTimeSelect'
|
||||||
|
export { useSessionAnalysisPageBase, useSessionHeaderDescription } from './useSessionAnalysisPageBase'
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { ref, watch, onMounted, computed } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
|
||||||
|
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||||
|
import type { MemberActivity, HourlyActivity, DailyActivity } from '@/types/analysis'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { formatLocalizedDate } from '@/utils'
|
||||||
|
import { useTimeSelect } from './useTimeSelect'
|
||||||
|
|
||||||
|
interface UseSessionAnalysisPageBaseOptions {
|
||||||
|
route: RouteLocationNormalizedLoaded
|
||||||
|
router: Router
|
||||||
|
currentSessionId: Ref<string | null>
|
||||||
|
selectSession: (id: string) => void
|
||||||
|
defaultTab: string
|
||||||
|
validTabIds: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseSessionHeaderDescriptionOptions {
|
||||||
|
session: Ref<AnalysisSession | null>
|
||||||
|
fullTimeRange: Ref<{ start: number; end: number } | null>
|
||||||
|
timeRangeValue: Ref<{ startTs: number } | null>
|
||||||
|
descriptionKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSessionAnalysisPageBase(options: UseSessionAnalysisPageBaseOptions) {
|
||||||
|
const { route, router, currentSessionId, selectSession, defaultTab, validTabIds } = options
|
||||||
|
|
||||||
|
const isLoading = ref(true)
|
||||||
|
const isInitialLoad = ref(true)
|
||||||
|
const session = ref<AnalysisSession | null>(null)
|
||||||
|
const memberActivity = ref<MemberActivity[]>([])
|
||||||
|
const hourlyActivity = ref<HourlyActivity[]>([])
|
||||||
|
const dailyActivity = ref<DailyActivity[]>([])
|
||||||
|
const messageTypes = ref<Array<{ type: MessageType; count: number }>>([])
|
||||||
|
|
||||||
|
function resolveActiveTabFromRoute(): string {
|
||||||
|
const routeTab = route.query.tab as string | undefined
|
||||||
|
if (routeTab && validTabIds.includes(routeTab)) return routeTab
|
||||||
|
return defaultTab
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTab = ref(resolveActiveTabFromRoute())
|
||||||
|
|
||||||
|
const { timeRangeValue, fullTimeRange, availableYears, timeFilter, selectedYearForOverview, initialTimeState } =
|
||||||
|
useTimeSelect(route, router, {
|
||||||
|
activeTab,
|
||||||
|
isInitialLoad,
|
||||||
|
currentSessionId,
|
||||||
|
onTimeRangeChange: () => loadAnalysisData(),
|
||||||
|
})
|
||||||
|
|
||||||
|
function syncSession() {
|
||||||
|
const id = route.params.id as string
|
||||||
|
if (id) {
|
||||||
|
selectSession(id)
|
||||||
|
if (currentSessionId.value !== id) {
|
||||||
|
router.replace('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBaseData() {
|
||||||
|
if (!currentSessionId.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionData = await window.chatApi.getSession(currentSessionId.value)
|
||||||
|
session.value = sessionData
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载基础数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAnalysisData() {
|
||||||
|
if (!currentSessionId.value) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filter = timeFilter.value
|
||||||
|
|
||||||
|
const [members, hourly, daily, types] = await Promise.all([
|
||||||
|
window.chatApi.getMemberActivity(currentSessionId.value, filter),
|
||||||
|
window.chatApi.getHourlyActivity(currentSessionId.value, filter),
|
||||||
|
window.chatApi.getDailyActivity(currentSessionId.value, filter),
|
||||||
|
window.chatApi.getMessageTypeDistribution(currentSessionId.value, filter),
|
||||||
|
])
|
||||||
|
|
||||||
|
memberActivity.value = members
|
||||||
|
hourlyActivity.value = hourly
|
||||||
|
dailyActivity.value = daily
|
||||||
|
messageTypes.value = types
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分析数据失败:', error)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
if (!currentSessionId.value) return
|
||||||
|
|
||||||
|
isInitialLoad.value = true
|
||||||
|
await loadBaseData()
|
||||||
|
isInitialLoad.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
() => {
|
||||||
|
activeTab.value = resolveActiveTabFromRoute()
|
||||||
|
syncSession()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.tab,
|
||||||
|
() => {
|
||||||
|
activeTab.value = resolveActiveTabFromRoute()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
currentSessionId,
|
||||||
|
() => {
|
||||||
|
loadData()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncSession()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeTab,
|
||||||
|
isLoading,
|
||||||
|
isInitialLoad,
|
||||||
|
session,
|
||||||
|
memberActivity,
|
||||||
|
hourlyActivity,
|
||||||
|
dailyActivity,
|
||||||
|
messageTypes,
|
||||||
|
timeRangeValue,
|
||||||
|
fullTimeRange,
|
||||||
|
availableYears,
|
||||||
|
timeFilter,
|
||||||
|
selectedYearForOverview,
|
||||||
|
initialTimeState,
|
||||||
|
syncSession,
|
||||||
|
loadData,
|
||||||
|
loadAnalysisData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSessionHeaderDescription(options: UseSessionHeaderDescriptionOptions) {
|
||||||
|
const { session, fullTimeRange, timeRangeValue, descriptionKey } = options
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
|
const headerStartDate = computed(() => {
|
||||||
|
const startTs = fullTimeRange.value?.start ?? timeRangeValue.value?.startTs
|
||||||
|
const fallbackTs = Math.floor(Date.now() / 1000)
|
||||||
|
return formatLocalizedDate(startTs ?? fallbackTs, locale.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerEndDate = computed(() => formatLocalizedDate(Math.floor(Date.now() / 1000), locale.value))
|
||||||
|
|
||||||
|
const headerDescription = computed(() =>
|
||||||
|
t(descriptionKey, {
|
||||||
|
startDate: headerStartDate.value,
|
||||||
|
endDate: headerEndDate.value,
|
||||||
|
messageCount: session.value?.messageCount ?? 0,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
headerDescription,
|
||||||
|
headerStartDate,
|
||||||
|
headerEndDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
"title": "Group Chat Analysis",
|
"title": "Group Chat Analysis",
|
||||||
"loading": "Loading analysis data...",
|
"loading": "Loading analysis data...",
|
||||||
"loadError": "Unable to load session data",
|
"loadError": "Unable to load session data",
|
||||||
"description": "{dateRange}, {memberCount} members chatted {messageCount} messages"
|
"description": "You've exchanged {messageCount} messages between {startDate} and {endDate}"
|
||||||
},
|
},
|
||||||
"privateChat": {
|
"privateChat": {
|
||||||
"title": "Private Chat Analysis",
|
"title": "Private Chat Analysis",
|
||||||
"loading": "Loading analysis data...",
|
"loading": "Loading analysis data...",
|
||||||
"loadError": "Unable to load session data",
|
"loadError": "Unable to load session data",
|
||||||
"description": "{dateRange}, {messageCount} messages in total"
|
"description": "You've exchanged {messageCount} messages between {startDate} and {endDate}"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
"title": "グループ分析",
|
"title": "グループ分析",
|
||||||
"loading": "分析データを読み込み中...",
|
"loading": "分析データを読み込み中...",
|
||||||
"loadError": "セッションデータを読み込めません",
|
"loadError": "セッションデータを読み込めません",
|
||||||
"description": "{dateRange}、{memberCount} 人のメンバーが合計 {messageCount} 件のメッセージをやり取りしました"
|
"description": "{startDate} から {endDate} までの間に、{messageCount} 件のメッセージをやり取りしています"
|
||||||
},
|
},
|
||||||
"privateChat": {
|
"privateChat": {
|
||||||
"title": "個人チャット分析",
|
"title": "個人チャット分析",
|
||||||
"loading": "分析データを読み込み中...",
|
"loading": "分析データを読み込み中...",
|
||||||
"loadError": "セッションデータを読み込めません",
|
"loadError": "セッションデータを読み込めません",
|
||||||
"description": "{dateRange}、合計 {messageCount} 件のメッセージ"
|
"description": "{startDate} から {endDate} までの間に、{messageCount} 件のメッセージをやり取りしています"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"overview": "概要",
|
"overview": "概要",
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
"title": "群聊分析",
|
"title": "群聊分析",
|
||||||
"loading": "加载分析数据...",
|
"loading": "加载分析数据...",
|
||||||
"loadError": "无法加载会话数据",
|
"loadError": "无法加载会话数据",
|
||||||
"description": "{dateRange},{memberCount} 位成员共聊了 {messageCount} 条消息"
|
"description": "从 {startDate} 到 {endDate},你们共聊了 {messageCount} 条消息"
|
||||||
},
|
},
|
||||||
"privateChat": {
|
"privateChat": {
|
||||||
"title": "私聊分析",
|
"title": "私聊分析",
|
||||||
"loading": "加载分析数据...",
|
"loading": "加载分析数据...",
|
||||||
"loadError": "无法加载会话数据",
|
"loadError": "无法加载会话数据",
|
||||||
"description": "{dateRange},共 {messageCount} 条消息"
|
"description": "从 {startDate} 到 {endDate},你们共聊了 {messageCount} 条消息"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"overview": "总览",
|
"overview": "总览",
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
"title": "群聊分析",
|
"title": "群聊分析",
|
||||||
"loading": "載入分析資料...",
|
"loading": "載入分析資料...",
|
||||||
"loadError": "無法載入聊天資料",
|
"loadError": "無法載入聊天資料",
|
||||||
"description": "{dateRange},{memberCount} 位成員共聊了 {messageCount} 條訊息"
|
"description": "從 {startDate} 到 {endDate},你們一共聊了 {messageCount} 則訊息"
|
||||||
},
|
},
|
||||||
"privateChat": {
|
"privateChat": {
|
||||||
"title": "私聊分析",
|
"title": "私聊分析",
|
||||||
"loading": "載入分析資料...",
|
"loading": "載入分析資料...",
|
||||||
"loadError": "無法載入聊天資料",
|
"loadError": "無法載入聊天資料",
|
||||||
"description": "{dateRange},共 {messageCount} 條訊息"
|
"description": "從 {startDate} 到 {endDate},你們一共聊了 {messageCount} 則訊息"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"overview": "總覽",
|
"overview": "總覽",
|
||||||
|
|||||||
+32
-123
@@ -1,10 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue'
|
import { ref, computed, defineAsyncComponent } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
|
||||||
import type { MemberActivity, HourlyActivity, DailyActivity } from '@/types/analysis'
|
|
||||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||||
import TimeSelect from '@/components/common/TimeSelect.vue'
|
import TimeSelect from '@/components/common/TimeSelect.vue'
|
||||||
import AITab from '@/components/analysis/AITab.vue'
|
import AITab from '@/components/analysis/AITab.vue'
|
||||||
@@ -22,7 +20,7 @@ import LoadingState from '@/components/UI/LoadingState.vue'
|
|||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { useLayoutStore } from '@/stores/layout'
|
import { useLayoutStore } from '@/stores/layout'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useTimeSelect } from '@/composables'
|
import { useSessionAnalysisPageBase, useSessionHeaderDescription } from '@/composables'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -50,15 +48,6 @@ function openChatRecordViewer() {
|
|||||||
layoutStore.openChatRecordDrawer({})
|
layoutStore.openChatRecordDrawer({})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据状态
|
|
||||||
const isLoading = ref(true)
|
|
||||||
const session = ref<AnalysisSession | null>(null)
|
|
||||||
const memberActivity = ref<MemberActivity[]>([])
|
|
||||||
const hourlyActivity = ref<HourlyActivity[]>([])
|
|
||||||
const dailyActivity = ref<DailyActivity[]>([])
|
|
||||||
const messageTypes = ref<Array<{ type: MessageType; count: number }>>([])
|
|
||||||
const isInitialLoad = ref(true)
|
|
||||||
|
|
||||||
// Tab 配置
|
// Tab 配置
|
||||||
const allTabs = [
|
const allTabs = [
|
||||||
{ id: 'overview', labelKey: 'analysis.tabs.overview', icon: 'i-heroicons-chart-pie' },
|
{ id: 'overview', labelKey: 'analysis.tabs.overview', icon: 'i-heroicons-chart-pie' },
|
||||||
@@ -71,22 +60,30 @@ const allTabs = [
|
|||||||
// Tab 列表
|
// Tab 列表
|
||||||
const tabs = computed(() => allTabs)
|
const tabs = computed(() => allTabs)
|
||||||
|
|
||||||
function resolveActiveTabFromRoute(): string {
|
const {
|
||||||
const routeTab = route.query.tab as string | undefined
|
activeTab,
|
||||||
if (routeTab && allTabs.some((tab) => tab.id === routeTab)) return routeTab
|
isLoading,
|
||||||
return settingsStore.defaultSessionTab
|
isInitialLoad,
|
||||||
}
|
session,
|
||||||
|
memberActivity,
|
||||||
const activeTab = ref(resolveActiveTabFromRoute())
|
hourlyActivity,
|
||||||
|
dailyActivity,
|
||||||
// 时间范围筛选(composable 统一管理状态、派生计算、URL 同步)
|
messageTypes,
|
||||||
const { timeRangeValue, fullTimeRange, availableYears, timeFilter, selectedYearForOverview, initialTimeState } =
|
timeRangeValue,
|
||||||
useTimeSelect(route, router, {
|
fullTimeRange,
|
||||||
activeTab,
|
availableYears,
|
||||||
isInitialLoad,
|
timeFilter,
|
||||||
currentSessionId,
|
selectedYearForOverview,
|
||||||
onTimeRangeChange: () => loadAnalysisData(),
|
initialTimeState,
|
||||||
})
|
loadData,
|
||||||
|
} = useSessionAnalysisPageBase({
|
||||||
|
route,
|
||||||
|
router,
|
||||||
|
currentSessionId,
|
||||||
|
selectSession: sessionStore.selectSession,
|
||||||
|
defaultTab: settingsStore.defaultSessionTab,
|
||||||
|
validTabIds: allTabs.map((tab) => tab.id),
|
||||||
|
})
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const topMembers = computed(() => memberActivity.value.slice(0, 3))
|
const topMembers = computed(() => memberActivity.value.slice(0, 3))
|
||||||
@@ -105,93 +102,11 @@ const filteredMemberCount = computed(() => {
|
|||||||
return memberActivity.value.filter((m) => m.messageCount > 0).length
|
return memberActivity.value.filter((m) => m.messageCount > 0).length
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sync route param to store
|
const { headerDescription } = useSessionHeaderDescription({
|
||||||
function syncSession() {
|
session,
|
||||||
const id = route.params.id as string
|
fullTimeRange,
|
||||||
if (id) {
|
timeRangeValue,
|
||||||
sessionStore.selectSession(id)
|
descriptionKey: 'analysis.groupChat.description',
|
||||||
// If selection failed (e.g. invalid ID), redirect to home
|
|
||||||
if (sessionStore.currentSessionId !== id) {
|
|
||||||
router.replace('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载基础数据(仅会话信息,时间范围由 TimeSelect 内部拉取)
|
|
||||||
async function loadBaseData() {
|
|
||||||
if (!currentSessionId.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sessionData = await window.chatApi.getSession(currentSessionId.value)
|
|
||||||
session.value = sessionData
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载基础数据失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载分析数据(受年份筛选影响)
|
|
||||||
async function loadAnalysisData() {
|
|
||||||
if (!currentSessionId.value) return
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filter = timeFilter.value
|
|
||||||
|
|
||||||
const [members, hourly, daily, types] = await Promise.all([
|
|
||||||
window.chatApi.getMemberActivity(currentSessionId.value, filter),
|
|
||||||
window.chatApi.getHourlyActivity(currentSessionId.value, filter),
|
|
||||||
window.chatApi.getDailyActivity(currentSessionId.value, filter),
|
|
||||||
window.chatApi.getMessageTypeDistribution(currentSessionId.value, filter),
|
|
||||||
])
|
|
||||||
|
|
||||||
memberActivity.value = members
|
|
||||||
hourlyActivity.value = hourly
|
|
||||||
dailyActivity.value = daily
|
|
||||||
messageTypes.value = types
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载分析数据失败:', error)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载所有数据
|
|
||||||
async function loadData() {
|
|
||||||
if (!currentSessionId.value) return
|
|
||||||
|
|
||||||
isInitialLoad.value = true
|
|
||||||
await loadBaseData()
|
|
||||||
isInitialLoad.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听路由参数变化
|
|
||||||
watch(
|
|
||||||
() => route.params.id,
|
|
||||||
() => {
|
|
||||||
activeTab.value = resolveActiveTabFromRoute()
|
|
||||||
syncSession()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.query.tab,
|
|
||||||
() => {
|
|
||||||
activeTab.value = resolveActiveTabFromRoute()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听会话变化(切换会话时由 TimeSelect 自行发出新范围,避免 Tab Content 双重重建)
|
|
||||||
watch(
|
|
||||||
currentSessionId,
|
|
||||||
() => {
|
|
||||||
loadData()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
syncSession()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -205,13 +120,7 @@ onMounted(() => {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<PageHeader
|
<PageHeader
|
||||||
:title="session.name"
|
:title="session.name"
|
||||||
:description="
|
:description="headerDescription"
|
||||||
t('analysis.groupChat.description', {
|
|
||||||
dateRange: timeRangeValue?.displayLabel ?? '',
|
|
||||||
memberCount: timeRangeValue?.isFullRange !== false ? session.memberCount : filteredMemberCount,
|
|
||||||
messageCount: timeRangeValue?.isFullRange !== false ? session.messageCount : filteredMessageCount,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
:avatar="session.groupAvatar"
|
:avatar="session.groupAvatar"
|
||||||
icon="i-heroicons-chat-bubble-left-right"
|
icon="i-heroicons-chat-bubble-left-right"
|
||||||
icon-class="bg-primary-600 text-white dark:bg-primary-500 dark:text-white"
|
icon-class="bg-primary-600 text-white dark:bg-primary-500 dark:text-white"
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue'
|
import { ref, computed, defineAsyncComponent } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
|
||||||
import type { MemberActivity, HourlyActivity, DailyActivity } from '@/types/analysis'
|
|
||||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||||
import TimeSelect from '@/components/common/TimeSelect.vue'
|
import TimeSelect from '@/components/common/TimeSelect.vue'
|
||||||
import AITab from '@/components/analysis/AITab.vue'
|
import AITab from '@/components/analysis/AITab.vue'
|
||||||
@@ -21,7 +19,7 @@ import LoadingState from '@/components/UI/LoadingState.vue'
|
|||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { useLayoutStore } from '@/stores/layout'
|
import { useLayoutStore } from '@/stores/layout'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useTimeSelect } from '@/composables'
|
import { useSessionAnalysisPageBase, useSessionHeaderDescription } from '@/composables'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -49,15 +47,6 @@ function openChatRecordViewer() {
|
|||||||
layoutStore.openChatRecordDrawer({})
|
layoutStore.openChatRecordDrawer({})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据状态
|
|
||||||
const isLoading = ref(true)
|
|
||||||
const session = ref<AnalysisSession | null>(null)
|
|
||||||
const memberActivity = ref<MemberActivity[]>([])
|
|
||||||
const hourlyActivity = ref<HourlyActivity[]>([])
|
|
||||||
const dailyActivity = ref<DailyActivity[]>([])
|
|
||||||
const messageTypes = ref<Array<{ type: MessageType; count: number }>>([])
|
|
||||||
const isInitialLoad = ref(true)
|
|
||||||
|
|
||||||
// Tab 配置 - 私聊包含总览、视图、语录、AI 对话和实验室
|
// Tab 配置 - 私聊包含总览、视图、语录、AI 对话和实验室
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'overview', labelKey: 'analysis.tabs.overview', icon: 'i-heroicons-chart-pie' },
|
{ id: 'overview', labelKey: 'analysis.tabs.overview', icon: 'i-heroicons-chart-pie' },
|
||||||
@@ -67,25 +56,29 @@ const tabs = [
|
|||||||
{ id: 'lab', labelKey: 'analysis.tabs.lab', icon: 'i-heroicons-beaker' },
|
{ id: 'lab', labelKey: 'analysis.tabs.lab', icon: 'i-heroicons-beaker' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function resolveActiveTabFromRoute(): string {
|
const {
|
||||||
const routeTab = route.query.tab as string | undefined
|
activeTab,
|
||||||
if (routeTab && tabs.some((tab) => tab.id === routeTab)) return routeTab
|
isLoading,
|
||||||
return settingsStore.defaultSessionTab
|
isInitialLoad,
|
||||||
}
|
session,
|
||||||
|
memberActivity,
|
||||||
const activeTab = ref(resolveActiveTabFromRoute())
|
hourlyActivity,
|
||||||
|
dailyActivity,
|
||||||
// 时间范围筛选(composable 统一管理状态、派生计算、URL 同步)
|
messageTypes,
|
||||||
const { timeRangeValue, fullTimeRange, timeFilter, selectedYearForOverview, initialTimeState } = useTimeSelect(
|
timeRangeValue,
|
||||||
|
fullTimeRange,
|
||||||
|
timeFilter,
|
||||||
|
selectedYearForOverview,
|
||||||
|
initialTimeState,
|
||||||
|
loadAnalysisData,
|
||||||
|
} = useSessionAnalysisPageBase({
|
||||||
route,
|
route,
|
||||||
router,
|
router,
|
||||||
{
|
currentSessionId,
|
||||||
activeTab,
|
selectSession: sessionStore.selectSession,
|
||||||
isInitialLoad,
|
defaultTab: settingsStore.defaultSessionTab,
|
||||||
currentSessionId,
|
validTabIds: tabs.map((tab) => tab.id),
|
||||||
onTimeRangeChange: () => loadAnalysisData(),
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 当前筛选后的消息总数
|
// 当前筛选后的消息总数
|
||||||
const filteredMessageCount = computed(() => {
|
const filteredMessageCount = computed(() => {
|
||||||
@@ -97,90 +90,12 @@ const filteredMemberCount = computed(() => {
|
|||||||
return memberActivity.value.filter((m) => m.messageCount > 0).length
|
return memberActivity.value.filter((m) => m.messageCount > 0).length
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sync route param to store
|
const { headerDescription } = useSessionHeaderDescription({
|
||||||
function syncSession() {
|
session,
|
||||||
const id = route.params.id as string
|
fullTimeRange,
|
||||||
if (id) {
|
timeRangeValue,
|
||||||
sessionStore.selectSession(id)
|
descriptionKey: 'analysis.privateChat.description',
|
||||||
// If selection failed (e.g. invalid ID), redirect to home
|
})
|
||||||
if (sessionStore.currentSessionId !== id) {
|
|
||||||
router.replace('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载基础数据(仅会话信息,时间范围由 TimeSelect 内部拉取)
|
|
||||||
async function loadBaseData() {
|
|
||||||
if (!currentSessionId.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sessionData = await window.chatApi.getSession(currentSessionId.value)
|
|
||||||
session.value = sessionData
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载基础数据失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载分析数据(受时间范围筛选影响)
|
|
||||||
async function loadAnalysisData() {
|
|
||||||
if (!currentSessionId.value) return
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filter = timeFilter.value
|
|
||||||
|
|
||||||
const [members, hourly, daily, types] = await Promise.all([
|
|
||||||
window.chatApi.getMemberActivity(currentSessionId.value, filter),
|
|
||||||
window.chatApi.getHourlyActivity(currentSessionId.value, filter),
|
|
||||||
window.chatApi.getDailyActivity(currentSessionId.value, filter),
|
|
||||||
window.chatApi.getMessageTypeDistribution(currentSessionId.value, filter),
|
|
||||||
])
|
|
||||||
|
|
||||||
memberActivity.value = members
|
|
||||||
hourlyActivity.value = hourly
|
|
||||||
dailyActivity.value = daily
|
|
||||||
messageTypes.value = types
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载分析数据失败:', error)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载所有数据
|
|
||||||
async function loadData() {
|
|
||||||
if (!currentSessionId.value) return
|
|
||||||
|
|
||||||
isInitialLoad.value = true
|
|
||||||
await loadBaseData()
|
|
||||||
isInitialLoad.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听路由参数变化
|
|
||||||
watch(
|
|
||||||
() => route.params.id,
|
|
||||||
() => {
|
|
||||||
activeTab.value = resolveActiveTabFromRoute()
|
|
||||||
syncSession()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.query.tab,
|
|
||||||
() => {
|
|
||||||
activeTab.value = resolveActiveTabFromRoute()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听会话变化(切换会话时由 TimeSelect 自行发出新范围,避免 Tab Content 双重重建)
|
|
||||||
watch(
|
|
||||||
currentSessionId,
|
|
||||||
() => {
|
|
||||||
loadData()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 获取对方头像
|
// 获取对方头像
|
||||||
const otherMemberAvatar = computed(() => {
|
const otherMemberAvatar = computed(() => {
|
||||||
@@ -206,9 +121,6 @@ const otherMemberAvatar = computed(() => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
syncSession()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -221,12 +133,7 @@ onMounted(() => {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<PageHeader
|
<PageHeader
|
||||||
:title="session.name"
|
:title="session.name"
|
||||||
:description="
|
:description="headerDescription"
|
||||||
t('analysis.privateChat.description', {
|
|
||||||
dateRange: timeRangeValue?.displayLabel ?? '',
|
|
||||||
messageCount: timeRangeValue?.isFullRange !== false ? session.messageCount : filteredMessageCount,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
:avatar="otherMemberAvatar"
|
:avatar="otherMemberAvatar"
|
||||||
icon="i-heroicons-user"
|
icon="i-heroicons-user"
|
||||||
icon-class="bg-pink-600 text-white dark:bg-pink-500 dark:text-white"
|
icon-class="bg-pink-600 text-white dark:bg-pink-500 dark:text-white"
|
||||||
|
|||||||
@@ -97,3 +97,16 @@ export function formatDateRange(
|
|||||||
}
|
}
|
||||||
return `${start}${separator}${end}`
|
return `${start}${separator}${end}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按指定语言环境格式化日期(默认长日期)
|
||||||
|
* @param ts Unix 时间戳(秒)
|
||||||
|
* @param locale 语言环境(如 zh-CN / en-US / ja-JP)
|
||||||
|
*/
|
||||||
|
export function formatLocalizedDate(ts: number, locale: string): string {
|
||||||
|
return new Intl.DateTimeFormat(locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
}).format(new Date(ts * 1000))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user