mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-15 19:09:24 +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 { useScreenCapture, type ScreenCaptureOptions } from './useScreenCapture'
|
||||
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",
|
||||
"loading": "Loading analysis 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": {
|
||||
"title": "Private Chat Analysis",
|
||||
"loading": "Loading analysis data...",
|
||||
"loadError": "Unable to load session data",
|
||||
"description": "{dateRange}, {messageCount} messages in total"
|
||||
"description": "You've exchanged {messageCount} messages between {startDate} and {endDate}"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": "Overview",
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"title": "グループ分析",
|
||||
"loading": "分析データを読み込み中...",
|
||||
"loadError": "セッションデータを読み込めません",
|
||||
"description": "{dateRange}、{memberCount} 人のメンバーが合計 {messageCount} 件のメッセージをやり取りしました"
|
||||
"description": "{startDate} から {endDate} までの間に、{messageCount} 件のメッセージをやり取りしています"
|
||||
},
|
||||
"privateChat": {
|
||||
"title": "個人チャット分析",
|
||||
"loading": "分析データを読み込み中...",
|
||||
"loadError": "セッションデータを読み込めません",
|
||||
"description": "{dateRange}、合計 {messageCount} 件のメッセージ"
|
||||
"description": "{startDate} から {endDate} までの間に、{messageCount} 件のメッセージをやり取りしています"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": "概要",
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"title": "群聊分析",
|
||||
"loading": "加载分析数据...",
|
||||
"loadError": "无法加载会话数据",
|
||||
"description": "{dateRange},{memberCount} 位成员共聊了 {messageCount} 条消息"
|
||||
"description": "从 {startDate} 到 {endDate},你们共聊了 {messageCount} 条消息"
|
||||
},
|
||||
"privateChat": {
|
||||
"title": "私聊分析",
|
||||
"loading": "加载分析数据...",
|
||||
"loadError": "无法加载会话数据",
|
||||
"description": "{dateRange},共 {messageCount} 条消息"
|
||||
"description": "从 {startDate} 到 {endDate},你们共聊了 {messageCount} 条消息"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": "总览",
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"title": "群聊分析",
|
||||
"loading": "載入分析資料...",
|
||||
"loadError": "無法載入聊天資料",
|
||||
"description": "{dateRange},{memberCount} 位成員共聊了 {messageCount} 條訊息"
|
||||
"description": "從 {startDate} 到 {endDate},你們一共聊了 {messageCount} 則訊息"
|
||||
},
|
||||
"privateChat": {
|
||||
"title": "私聊分析",
|
||||
"loading": "載入分析資料...",
|
||||
"loadError": "無法載入聊天資料",
|
||||
"description": "{dateRange},共 {messageCount} 條訊息"
|
||||
"description": "從 {startDate} 到 {endDate},你們一共聊了 {messageCount} 則訊息"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": "總覽",
|
||||
|
||||
+32
-123
@@ -1,10 +1,8 @@
|
||||
<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 { storeToRefs } from 'pinia'
|
||||
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 TimeSelect from '@/components/common/TimeSelect.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 { useLayoutStore } from '@/stores/layout'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useTimeSelect } from '@/composables'
|
||||
import { useSessionAnalysisPageBase, useSessionHeaderDescription } from '@/composables'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -50,15 +48,6 @@ function openChatRecordViewer() {
|
||||
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 配置
|
||||
const allTabs = [
|
||||
{ id: 'overview', labelKey: 'analysis.tabs.overview', icon: 'i-heroicons-chart-pie' },
|
||||
@@ -71,22 +60,30 @@ const allTabs = [
|
||||
// Tab 列表
|
||||
const tabs = computed(() => allTabs)
|
||||
|
||||
function resolveActiveTabFromRoute(): string {
|
||||
const routeTab = route.query.tab as string | undefined
|
||||
if (routeTab && allTabs.some((tab) => tab.id === routeTab)) return routeTab
|
||||
return settingsStore.defaultSessionTab
|
||||
}
|
||||
|
||||
const activeTab = ref(resolveActiveTabFromRoute())
|
||||
|
||||
// 时间范围筛选(composable 统一管理状态、派生计算、URL 同步)
|
||||
const { timeRangeValue, fullTimeRange, availableYears, timeFilter, selectedYearForOverview, initialTimeState } =
|
||||
useTimeSelect(route, router, {
|
||||
activeTab,
|
||||
isInitialLoad,
|
||||
currentSessionId,
|
||||
onTimeRangeChange: () => loadAnalysisData(),
|
||||
})
|
||||
const {
|
||||
activeTab,
|
||||
isLoading,
|
||||
isInitialLoad,
|
||||
session,
|
||||
memberActivity,
|
||||
hourlyActivity,
|
||||
dailyActivity,
|
||||
messageTypes,
|
||||
timeRangeValue,
|
||||
fullTimeRange,
|
||||
availableYears,
|
||||
timeFilter,
|
||||
selectedYearForOverview,
|
||||
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))
|
||||
@@ -105,93 +102,11 @@ 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) {
|
||||
sessionStore.selectSession(id)
|
||||
// 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()
|
||||
const { headerDescription } = useSessionHeaderDescription({
|
||||
session,
|
||||
fullTimeRange,
|
||||
timeRangeValue,
|
||||
descriptionKey: 'analysis.groupChat.description',
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -205,13 +120,7 @@ onMounted(() => {
|
||||
<!-- Header -->
|
||||
<PageHeader
|
||||
:title="session.name"
|
||||
:description="
|
||||
t('analysis.groupChat.description', {
|
||||
dateRange: timeRangeValue?.displayLabel ?? '',
|
||||
memberCount: timeRangeValue?.isFullRange !== false ? session.memberCount : filteredMemberCount,
|
||||
messageCount: timeRangeValue?.isFullRange !== false ? session.messageCount : filteredMessageCount,
|
||||
})
|
||||
"
|
||||
:description="headerDescription"
|
||||
:avatar="session.groupAvatar"
|
||||
icon="i-heroicons-chat-bubble-left-right"
|
||||
icon-class="bg-primary-600 text-white dark:bg-primary-500 dark:text-white"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<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 { storeToRefs } from 'pinia'
|
||||
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 TimeSelect from '@/components/common/TimeSelect.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 { useLayoutStore } from '@/stores/layout'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useTimeSelect } from '@/composables'
|
||||
import { useSessionAnalysisPageBase, useSessionHeaderDescription } from '@/composables'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -49,15 +47,6 @@ function openChatRecordViewer() {
|
||||
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 对话和实验室
|
||||
const tabs = [
|
||||
{ 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' },
|
||||
]
|
||||
|
||||
function resolveActiveTabFromRoute(): string {
|
||||
const routeTab = route.query.tab as string | undefined
|
||||
if (routeTab && tabs.some((tab) => tab.id === routeTab)) return routeTab
|
||||
return settingsStore.defaultSessionTab
|
||||
}
|
||||
|
||||
const activeTab = ref(resolveActiveTabFromRoute())
|
||||
|
||||
// 时间范围筛选(composable 统一管理状态、派生计算、URL 同步)
|
||||
const { timeRangeValue, fullTimeRange, timeFilter, selectedYearForOverview, initialTimeState } = useTimeSelect(
|
||||
const {
|
||||
activeTab,
|
||||
isLoading,
|
||||
isInitialLoad,
|
||||
session,
|
||||
memberActivity,
|
||||
hourlyActivity,
|
||||
dailyActivity,
|
||||
messageTypes,
|
||||
timeRangeValue,
|
||||
fullTimeRange,
|
||||
timeFilter,
|
||||
selectedYearForOverview,
|
||||
initialTimeState,
|
||||
loadAnalysisData,
|
||||
} = useSessionAnalysisPageBase({
|
||||
route,
|
||||
router,
|
||||
{
|
||||
activeTab,
|
||||
isInitialLoad,
|
||||
currentSessionId,
|
||||
onTimeRangeChange: () => loadAnalysisData(),
|
||||
}
|
||||
)
|
||||
currentSessionId,
|
||||
selectSession: sessionStore.selectSession,
|
||||
defaultTab: settingsStore.defaultSessionTab,
|
||||
validTabIds: tabs.map((tab) => tab.id),
|
||||
})
|
||||
|
||||
// 当前筛选后的消息总数
|
||||
const filteredMessageCount = computed(() => {
|
||||
@@ -97,90 +90,12 @@ 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) {
|
||||
sessionStore.selectSession(id)
|
||||
// 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 { headerDescription } = useSessionHeaderDescription({
|
||||
session,
|
||||
fullTimeRange,
|
||||
timeRangeValue,
|
||||
descriptionKey: 'analysis.privateChat.description',
|
||||
})
|
||||
|
||||
// 获取对方头像
|
||||
const otherMemberAvatar = computed(() => {
|
||||
@@ -206,9 +121,6 @@ const otherMemberAvatar = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
syncSession()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -221,12 +133,7 @@ onMounted(() => {
|
||||
<!-- Header -->
|
||||
<PageHeader
|
||||
:title="session.name"
|
||||
:description="
|
||||
t('analysis.privateChat.description', {
|
||||
dateRange: timeRangeValue?.displayLabel ?? '',
|
||||
messageCount: timeRangeValue?.isFullRange !== false ? session.messageCount : filteredMessageCount,
|
||||
})
|
||||
"
|
||||
:description="headerDescription"
|
||||
:avatar="otherMemberAvatar"
|
||||
icon="i-heroicons-user"
|
||||
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}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 按指定语言环境格式化日期(默认长日期)
|
||||
* @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