mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-12 17:21:01 +08:00
feat: 优化消息类型分布
This commit is contained in:
@@ -1,203 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { HourlyActivity, WeekdayActivity, MonthlyActivity } from '@/types/analysis'
|
||||
import { EChartBar } from '@/components/charts'
|
||||
import type { EChartBarData } from '@/components/charts'
|
||||
import { SectionCard } from '@/components/UI'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
hourlyActivity: HourlyActivity[]
|
||||
weekdayActivity: WeekdayActivity[]
|
||||
monthlyActivity: MonthlyActivity[]
|
||||
isLoadingWeekday: boolean
|
||||
isLoadingMonthly: boolean
|
||||
weekdayNames: string[]
|
||||
weekdayVsWeekend: { weekday: number; weekend: number }
|
||||
}>()
|
||||
|
||||
// --- 24小时分布逻辑 ---
|
||||
// 24小时分布图数据
|
||||
const hourlyChartData = computed<EChartBarData>(() => {
|
||||
return {
|
||||
labels: props.hourlyActivity.map((h) => `${h.hour}:00`),
|
||||
values: props.hourlyActivity.map((h) => h.messageCount),
|
||||
}
|
||||
})
|
||||
|
||||
// 时段占比计算
|
||||
const totalMessages = computed(() => props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0))
|
||||
|
||||
const lateNightRatio = computed(() => {
|
||||
const lateNight = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 0 && h.hour < 6)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return totalMessages.value > 0 ? Math.round((lateNight / totalMessages.value) * 100) : 0
|
||||
})
|
||||
|
||||
const morningRatio = computed(() => {
|
||||
const morning = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 6 && h.hour < 12)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return totalMessages.value > 0 ? Math.round((morning / totalMessages.value) * 100) : 0
|
||||
})
|
||||
|
||||
const afternoonRatio = computed(() => {
|
||||
const afternoon = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 12 && h.hour < 18)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return totalMessages.value > 0 ? Math.round((afternoon / totalMessages.value) * 100) : 0
|
||||
})
|
||||
|
||||
const eveningRatio = computed(() => {
|
||||
const evening = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 18 && h.hour < 24)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return totalMessages.value > 0 ? Math.round((evening / totalMessages.value) * 100) : 0
|
||||
})
|
||||
|
||||
// --- 星期分布逻辑 ---
|
||||
// 星期分布图数据
|
||||
const weekdayChartData = computed<EChartBarData>(() => {
|
||||
return {
|
||||
labels: props.weekdayActivity.map((w) => props.weekdayNames[w.weekday - 1]),
|
||||
values: props.weekdayActivity.map((w) => w.messageCount),
|
||||
}
|
||||
})
|
||||
|
||||
// --- 月份分布逻辑 ---
|
||||
// 月份分布图数据
|
||||
const monthlyChartData = computed<EChartBarData>(() => {
|
||||
return {
|
||||
labels: props.monthlyActivity.map((m) => {
|
||||
// 中文用 X月,英文用 Jan, Feb 等
|
||||
if (locale.value === 'zh-CN') {
|
||||
return `${m.month}月`
|
||||
}
|
||||
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
return monthNames[m.month - 1] || `${m.month}`
|
||||
}),
|
||||
values: props.monthlyActivity.map((m) => m.messageCount),
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2 2xl:grid-cols-3">
|
||||
<!-- 24小时分布 -->
|
||||
<SectionCard :title="t('hourlyTitle')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<EChartBar :data="hourlyChartData" :height="256" />
|
||||
|
||||
<div class="mt-6 grid grid-cols-4 gap-2">
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('lateNight') }}</div>
|
||||
<div class="mt-1 text-base font-semibold text-gray-900 dark:text-white">{{ lateNightRatio }}%</div>
|
||||
<div class="mx-auto mt-1 h-1 w-full max-w-12 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div class="h-full rounded-full bg-pink-300 transition-all" :style="{ width: `${lateNightRatio}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('morning') }}</div>
|
||||
<div class="mt-1 text-base font-semibold text-gray-900 dark:text-white">{{ morningRatio }}%</div>
|
||||
<div class="mx-auto mt-1 h-1 w-full max-w-12 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div class="h-full rounded-full bg-pink-400 transition-all" :style="{ width: `${morningRatio}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('afternoon') }}</div>
|
||||
<div class="mt-1 text-base font-semibold text-gray-900 dark:text-white">{{ afternoonRatio }}%</div>
|
||||
<div class="mx-auto mt-1 h-1 w-full max-w-12 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div class="h-full rounded-full bg-pink-500 transition-all" :style="{ width: `${afternoonRatio}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('evening') }}</div>
|
||||
<div class="mt-1 text-base font-semibold text-gray-900 dark:text-white">{{ eveningRatio }}%</div>
|
||||
<div class="mx-auto mt-1 h-1 w-full max-w-12 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div class="h-full rounded-full bg-pink-600 transition-all" :style="{ width: `${eveningRatio}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<!-- 星期分布 -->
|
||||
<SectionCard :title="t('weekdayTitle')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<div v-if="isLoadingWeekday" class="flex h-64 items-center justify-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-6 w-6 animate-spin text-pink-500" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<EChartBar :data="weekdayChartData" :height="256" />
|
||||
|
||||
<div class="mt-6 grid grid-cols-2 gap-4">
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('weekdays') }}</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ weekdayVsWeekend.weekday }}%
|
||||
</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-24 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div
|
||||
class="h-full rounded-full bg-pink-500 transition-all"
|
||||
:style="{ width: `${weekdayVsWeekend.weekday}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('weekend') }}</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ weekdayVsWeekend.weekend }}%
|
||||
</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-24 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div
|
||||
class="h-full rounded-full bg-blue-500 transition-all"
|
||||
:style="{ width: `${weekdayVsWeekend.weekend}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<!-- 月份活跃分布 -->
|
||||
<SectionCard :title="t('monthlyTitle')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<div v-if="isLoadingMonthly" class="flex h-64 items-center justify-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-6 w-6 animate-spin text-pink-500" />
|
||||
</div>
|
||||
<EChartBar v-else :data="monthlyChartData" :height="256" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"hourlyTitle": "24小时活跃分布",
|
||||
"lateNight": "凌晨",
|
||||
"morning": "上午",
|
||||
"afternoon": "下午",
|
||||
"evening": "晚上",
|
||||
"weekdayTitle": "星期活跃分布",
|
||||
"weekdays": "工作日",
|
||||
"weekend": "周末",
|
||||
"monthlyTitle": "月份活跃分布"
|
||||
},
|
||||
"en-US": {
|
||||
"hourlyTitle": "24-Hour Activity",
|
||||
"lateNight": "Late Night",
|
||||
"morning": "Morning",
|
||||
"afternoon": "Afternoon",
|
||||
"evening": "Evening",
|
||||
"weekdayTitle": "Weekly Activity",
|
||||
"weekdays": "Weekdays",
|
||||
"weekend": "Weekend",
|
||||
"monthlyTitle": "Monthly Activity"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
+77
-1
@@ -66,6 +66,36 @@ const typeChartData = computed<EChartPieData>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 消息类型摘要数据(用于右侧列表展示)
|
||||
const typeSummary = computed(() => {
|
||||
const total = messageTypes.value.reduce((sum, t) => sum + t.count, 0)
|
||||
const sorted = [...messageTypes.value].sort((a, b) => b.count - a.count)
|
||||
|
||||
return sorted.map((item) => ({
|
||||
name: getMessageTypeName(item.type),
|
||||
count: item.count,
|
||||
percentage: total > 0 ? Math.round((item.count / total) * 100) : 0,
|
||||
}))
|
||||
})
|
||||
|
||||
// 类型颜色(与 EChartPie 保持一致)
|
||||
const typeColors = [
|
||||
'#6366f1', // indigo
|
||||
'#8b5cf6', // violet
|
||||
'#ec4899', // pink
|
||||
'#f43f5e', // rose
|
||||
'#f97316', // orange
|
||||
'#eab308', // yellow
|
||||
'#22c55e', // green
|
||||
'#14b8a6', // teal
|
||||
'#06b6d4', // cyan
|
||||
'#3b82f6', // blue
|
||||
]
|
||||
|
||||
function getTypeColor(index: number): string {
|
||||
return typeColors[index % typeColors.length]
|
||||
}
|
||||
|
||||
// 小时分布图表数据
|
||||
const hourlyChartData = computed<EChartBarData>(() => {
|
||||
// 补全 24 小时数据
|
||||
@@ -198,7 +228,53 @@ watch(
|
||||
<!-- 消息类型分布 -->
|
||||
<SectionCard :title="t('typeDistribution')" :show-divider="false">
|
||||
<div class="p-5">
|
||||
<EChartPie v-if="typeChartData.values.length > 0" :data="typeChartData" :height="280" />
|
||||
<div v-if="typeChartData.values.length > 0" class="flex flex-col gap-6 lg:flex-row lg:items-center">
|
||||
<!-- 左侧饼图 -->
|
||||
<div class="lg:w-1/2">
|
||||
<EChartPie :data="typeChartData" :height="280" :show-legend="false" />
|
||||
</div>
|
||||
<!-- 右侧摘要列表 -->
|
||||
<div class="lg:w-1/2">
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(item, index) in typeSummary"
|
||||
:key="index"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<!-- 颜色标记 -->
|
||||
<div
|
||||
class="h-3 w-3 shrink-0 rounded-full"
|
||||
:style="{ backgroundColor: getTypeColor(index) }"
|
||||
/>
|
||||
<!-- 类型名称 -->
|
||||
<div class="min-w-20 shrink-0 text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="flex-1">
|
||||
<div class="h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div
|
||||
class="h-full rounded-full transition-all"
|
||||
:style="{
|
||||
width: `${item.percentage}%`,
|
||||
backgroundColor: getTypeColor(index),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 数量和占比 -->
|
||||
<div class="shrink-0 text-right">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ item.count.toLocaleString() }}
|
||||
</span>
|
||||
<span class="ml-1 text-xs text-gray-400">
|
||||
({{ item.percentage }}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex h-48 items-center justify-center text-gray-400">
|
||||
{{ t('noData') }}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-64 items-center justify-center">
|
||||
<div class="text-center">
|
||||
<UIcon name="i-heroicons-user-circle" class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600" />
|
||||
<p class="mt-3 text-sm text-gray-400">{{ t('comingSoon') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"comingSoon": "对话画像功能开发中..."
|
||||
},
|
||||
"en-US": {
|
||||
"comingSoon": "Chat portrait coming soon..."
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-64 items-center justify-center">
|
||||
<div class="text-center">
|
||||
<UIcon name="i-heroicons-cloud" class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600" />
|
||||
<p class="mt-3 text-sm text-gray-400">{{ t('comingSoon') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"comingSoon": "词云功能开发中..."
|
||||
},
|
||||
"en-US": {
|
||||
"comingSoon": "Word cloud coming soon..."
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// 视图组件统一导出
|
||||
export { default as MessageView } from './MessageView.vue'
|
||||
export { default as WordcloudView } from './WordcloudView.vue'
|
||||
export { default as PortraitView } from './PortraitView.vue'
|
||||
|
||||
@@ -3,13 +3,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import { getMessageTypeName } from '@/types/base'
|
||||
import type {
|
||||
MemberActivity,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
WeekdayActivity,
|
||||
MonthlyActivity,
|
||||
} from '@/types/analysis'
|
||||
import type { MemberActivity, HourlyActivity, DailyActivity, WeekdayActivity } from '@/types/analysis'
|
||||
import { EChartPie } from '@/components/charts'
|
||||
import type { EChartPieData } from '@/components/charts'
|
||||
import { SectionCard } from '@/components/UI'
|
||||
@@ -17,7 +11,6 @@ import { useOverviewStatistics } from '@/composables/analysis/useOverviewStatist
|
||||
import { useDailyTrend } from '@/composables/analysis/useDailyTrend'
|
||||
import OverviewStatCards from '@/components/analysis/Overview/OverviewStatCards.vue'
|
||||
import OverviewIdentityCard from '@/components/analysis/Overview/OverviewIdentityCard.vue'
|
||||
import ActivityTimeDistribution from '@/components/analysis/Overview/ActivityTimeDistribution.vue'
|
||||
import DailyTrendCard from '@/components/analysis/Overview/DailyTrendCard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -37,9 +30,8 @@ const props = defineProps<{
|
||||
timeFilter?: { startTs?: number; endTs?: number }
|
||||
}>()
|
||||
|
||||
// 星期活跃度数据
|
||||
// 星期活跃度数据(用于统计信息计算)
|
||||
const weekdayActivity = ref<WeekdayActivity[]>([])
|
||||
const isLoadingWeekday = ref(false)
|
||||
|
||||
// 使用 Composables
|
||||
const {
|
||||
@@ -89,33 +81,13 @@ const memberChartData = computed<EChartPieData>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 月份活跃度数据
|
||||
const monthlyActivity = ref<MonthlyActivity[]>([])
|
||||
const isLoadingMonthly = ref(false)
|
||||
|
||||
// 加载星期活跃度数据
|
||||
// 加载星期活跃度数据(用于统计信息计算)
|
||||
async function loadWeekdayActivity() {
|
||||
if (!props.session.id) return
|
||||
isLoadingWeekday.value = true
|
||||
try {
|
||||
weekdayActivity.value = await window.chatApi.getWeekdayActivity(props.session.id, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载星期活跃度失败:', error)
|
||||
} finally {
|
||||
isLoadingWeekday.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载月份活跃度数据
|
||||
async function loadMonthlyActivity() {
|
||||
if (!props.session.id) return
|
||||
isLoadingMonthly.value = true
|
||||
try {
|
||||
monthlyActivity.value = await window.chatApi.getMonthlyActivity(props.session.id, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载月份活跃度失败:', error)
|
||||
} finally {
|
||||
isLoadingMonthly.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +96,6 @@ watch(
|
||||
() => [props.session.id, props.timeFilter],
|
||||
() => {
|
||||
loadWeekdayActivity()
|
||||
loadMonthlyActivity()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
@@ -173,17 +144,6 @@ watch(
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
<!-- 时间分布图表 -->
|
||||
<ActivityTimeDistribution
|
||||
:hourly-activity="hourlyActivity"
|
||||
:weekday-activity="weekdayActivity"
|
||||
:monthly-activity="monthlyActivity"
|
||||
:is-loading-weekday="isLoadingWeekday"
|
||||
:is-loading-monthly="isLoadingMonthly"
|
||||
:weekday-names="weekdayNames"
|
||||
:weekday-vs-weekend="weekdayVsWeekend"
|
||||
/>
|
||||
|
||||
<!-- 每日消息趋势 -->
|
||||
<DailyTrendCard :daily-activity="dailyActivity" :daily-chart-data="dailyChartData" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SubTabs } from '@/components/UI'
|
||||
import { MessageView, WordcloudView, PortraitView } from '@/components/view'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
// 子 Tab 配置
|
||||
const subTabs = computed(() => [
|
||||
{ id: 'message', label: t('message'), icon: 'i-heroicons-chat-bubble-left-right' },
|
||||
{ id: 'wordcloud', label: t('wordcloud'), icon: 'i-heroicons-cloud' },
|
||||
{ id: 'portrait', label: t('portrait'), icon: 'i-heroicons-user-circle' },
|
||||
])
|
||||
|
||||
const activeSubTab = ref('message')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- 子 Tab 导航 -->
|
||||
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="groupViewTab" />
|
||||
|
||||
<!-- 子 Tab 内容 -->
|
||||
<div class="flex-1 min-h-0 overflow-auto">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<!-- 消息 -->
|
||||
<MessageView
|
||||
v-if="activeSubTab === 'message'"
|
||||
:session-id="props.sessionId"
|
||||
:time-filter="props.timeFilter"
|
||||
/>
|
||||
|
||||
<!-- 词云 -->
|
||||
<WordcloudView
|
||||
v-else-if="activeSubTab === 'wordcloud'"
|
||||
:session-id="props.sessionId"
|
||||
:time-filter="props.timeFilter"
|
||||
/>
|
||||
|
||||
<!-- 对话画像 -->
|
||||
<PortraitView
|
||||
v-else-if="activeSubTab === 'portrait'"
|
||||
:session-id="props.sessionId"
|
||||
:time-filter="props.timeFilter"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"message": "消息",
|
||||
"wordcloud": "词云",
|
||||
"portrait": "对话画像"
|
||||
},
|
||||
"en-US": {
|
||||
"message": "Messages",
|
||||
"wordcloud": "Word Cloud",
|
||||
"portrait": "Chat Portrait"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@@ -10,6 +10,7 @@ import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||
import UITabs from '@/components/UI/Tabs.vue'
|
||||
import AITab from '@/components/analysis/AITab.vue'
|
||||
import OverviewTab from './components/OverviewTab.vue'
|
||||
import ViewTab from './components/ViewTab.vue'
|
||||
import RankingTab from './components/RankingTab.vue'
|
||||
import QuotesTab from './components/QuotesTab.vue'
|
||||
import MemberTab from './components/MemberTab.vue'
|
||||
@@ -57,6 +58,7 @@ const isInitialLoad = ref(true) // 用于跳过初始加载时的 watch 触发
|
||||
// Tab 配置(带语言限制)
|
||||
const allTabs = [
|
||||
{ id: 'overview', labelKey: 'analysis.tabs.overview', icon: 'i-heroicons-chart-pie' },
|
||||
{ id: 'view', labelKey: 'analysis.tabs.view', icon: 'i-heroicons-presentation-chart-bar' },
|
||||
{ id: 'ranking', labelKey: 'analysis.tabs.ranking', icon: 'i-heroicons-trophy', feature: 'groupRanking' },
|
||||
{ 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' },
|
||||
@@ -354,6 +356,12 @@ onMounted(() => {
|
||||
:filtered-member-count="filteredMemberCount"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<ViewTab
|
||||
v-else-if="activeTab === 'view'"
|
||||
:key="'view-' + selectedYear"
|
||||
:session-id="currentSessionId!"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<RankingTab
|
||||
v-else-if="activeTab === 'ranking'"
|
||||
:key="'ranking-' + selectedYear"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import { getMessageTypeName } from '@/types/base'
|
||||
import type { MemberActivity, HourlyActivity, DailyActivity, WeekdayActivity, MonthlyActivity } from '@/types/analysis'
|
||||
import type { MemberActivity, HourlyActivity, DailyActivity, WeekdayActivity } from '@/types/analysis'
|
||||
import { EChartPie } from '@/components/charts'
|
||||
import type { EChartPieData } from '@/components/charts'
|
||||
import { SectionCard } from '@/components/UI'
|
||||
@@ -11,7 +11,6 @@ import { useOverviewStatistics } from '@/composables/analysis/useOverviewStatist
|
||||
import { useDailyTrend } from '@/composables/analysis/useDailyTrend'
|
||||
import OverviewStatCards from '@/components/analysis/Overview/OverviewStatCards.vue'
|
||||
import OverviewIdentityCard from '@/components/analysis/Overview/OverviewIdentityCard.vue'
|
||||
import ActivityTimeDistribution from '@/components/analysis/Overview/ActivityTimeDistribution.vue'
|
||||
import DailyTrendCard from '@/components/analysis/Overview/DailyTrendCard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -29,9 +28,8 @@ const props = defineProps<{
|
||||
timeFilter?: { startTs?: number; endTs?: number }
|
||||
}>()
|
||||
|
||||
// 星期活跃度数据
|
||||
// 星期活跃度数据(用于统计信息计算)
|
||||
const weekdayActivity = ref<WeekdayActivity[]>([])
|
||||
const isLoadingWeekday = ref(false)
|
||||
|
||||
// 使用 Composables
|
||||
const {
|
||||
@@ -99,33 +97,13 @@ const comparisonChartData = computed<EChartPieData>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 月份活跃度数据
|
||||
const monthlyActivity = ref<MonthlyActivity[]>([])
|
||||
const isLoadingMonthly = ref(false)
|
||||
|
||||
// 加载星期活跃度数据
|
||||
// 加载星期活跃度数据(用于统计信息计算)
|
||||
async function loadWeekdayActivity() {
|
||||
if (!props.session.id) return
|
||||
isLoadingWeekday.value = true
|
||||
try {
|
||||
weekdayActivity.value = await window.chatApi.getWeekdayActivity(props.session.id, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载星期活跃度失败:', error)
|
||||
} finally {
|
||||
isLoadingWeekday.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载月份活跃度数据
|
||||
async function loadMonthlyActivity() {
|
||||
if (!props.session.id) return
|
||||
isLoadingMonthly.value = true
|
||||
try {
|
||||
monthlyActivity.value = await window.chatApi.getMonthlyActivity(props.session.id, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载月份活跃度失败:', error)
|
||||
} finally {
|
||||
isLoadingMonthly.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +112,6 @@ watch(
|
||||
() => [props.session.id, props.timeFilter],
|
||||
() => {
|
||||
loadWeekdayActivity()
|
||||
loadMonthlyActivity()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
@@ -264,17 +241,6 @@ watch(
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
<!-- 时间分布图表 -->
|
||||
<ActivityTimeDistribution
|
||||
:hourly-activity="hourlyActivity"
|
||||
:weekday-activity="weekdayActivity"
|
||||
:monthly-activity="monthlyActivity"
|
||||
:is-loading-weekday="isLoadingWeekday"
|
||||
:is-loading-monthly="isLoadingMonthly"
|
||||
:weekday-names="weekdayNames"
|
||||
:weekday-vs-weekend="weekdayVsWeekend"
|
||||
/>
|
||||
|
||||
<!-- 每日消息趋势 -->
|
||||
<DailyTrendCard :daily-activity="dailyActivity" :daily-chart-data="dailyChartData" />
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SubTabs } from '@/components/UI'
|
||||
import MessageView from './view/MessageView.vue'
|
||||
import WordcloudView from './view/WordcloudView.vue'
|
||||
import PortraitView from './view/PortraitView.vue'
|
||||
import { MessageView, WordcloudView, PortraitView } from '@/components/view'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -88,4 +86,3 @@ const activeSubTab = ref('message')
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
// Props
|
||||
defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-content flex h-full items-center justify-center p-6">
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center rounded-xl border-2 border-dashed border-gray-300 bg-gray-50 dark:border-gray-700 dark:bg-gray-900/50"
|
||||
>
|
||||
<div class="text-center">
|
||||
<UIcon name="i-heroicons-user-circle" class="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p class="mt-3 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('title') }}
|
||||
</p>
|
||||
<p class="mt-1 max-w-md px-4 text-sm text-gray-500">
|
||||
{{ t('description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"title": "对话画像",
|
||||
"description": "双方发言特征对比(消息长度、表情使用、消息类型等)"
|
||||
},
|
||||
"en-US": {
|
||||
"title": "Chat Portrait",
|
||||
"description": "Comparison of chat characteristics (message length, emoji usage, message types, etc.)"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
// Props
|
||||
defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-content flex h-full items-center justify-center p-6">
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center rounded-xl border-2 border-dashed border-gray-300 bg-gray-50 dark:border-gray-700 dark:bg-gray-900/50"
|
||||
>
|
||||
<div class="text-center">
|
||||
<UIcon name="i-heroicons-cloud" class="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p class="mt-3 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('title') }}
|
||||
</p>
|
||||
<p class="mt-1 max-w-md px-4 text-sm text-gray-500">
|
||||
{{ t('description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"title": "词云视图",
|
||||
"description": "双方高频词汇对比可视化"
|
||||
},
|
||||
"en-US": {
|
||||
"title": "Word Cloud View",
|
||||
"description": "Visual comparison of frequently used words between both parties"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
Reference in New Issue
Block a user