mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-26 00:20:26 +08:00
feat: 重构总览模块卡片
This commit is contained in:
@@ -203,85 +203,82 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ThemeCard variant="elevated" decorative class="p-8">
|
||||
<div class="relative">
|
||||
<div class="flex flex-col">
|
||||
<!-- 上方:身份信息 + 统计数据 -->
|
||||
<div class="pb-2">
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-100">
|
||||
{{ session.name }}
|
||||
</h2>
|
||||
<ThemeCard variant="elevated" decorative class="flex flex-col">
|
||||
<!-- 身份信息 + 基础统计 -->
|
||||
<div class="relative z-10 px-6 pt-8 pb-4 sm:px-8">
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-100">
|
||||
{{ session.name }}
|
||||
</h2>
|
||||
|
||||
<div class="mt-4 flex items-start gap-6 sm:gap-24">
|
||||
<div class="min-w-0 flex flex-col gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-6 w-6 shrink-0 items-center justify-center">
|
||||
<UIcon v-if="session.type === 'group'" name="i-heroicons-user-group" class="h-4 w-4 opacity-70" />
|
||||
<UIcon v-else name="i-heroicons-user" class="h-4 w-4 opacity-70" />
|
||||
</div>
|
||||
<span class="whitespace-nowrap">
|
||||
{{ session.platform.toUpperCase() }}
|
||||
·
|
||||
{{
|
||||
session.type === 'private'
|
||||
? t('analysis.overview.identity.privateChat')
|
||||
: t('analysis.overview.identity.groupChat')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="fullTimeRangeText" class="flex items-center gap-2">
|
||||
<div class="flex h-6 w-6 shrink-0 items-center justify-center">
|
||||
<UIcon name="i-heroicons-calendar" class="h-4 w-4 opacity-70" />
|
||||
</div>
|
||||
<span class="font-mono text-xs opacity-90 whitespace-nowrap">{{ fullTimeRangeText }}</span>
|
||||
</div>
|
||||
<div class="mt-4 flex items-start gap-6 sm:gap-24">
|
||||
<div class="min-w-0 flex flex-col gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-6 w-6 shrink-0 items-center justify-center">
|
||||
<UIcon v-if="session.type === 'group'" name="i-heroicons-user-group" class="h-4 w-4 opacity-70" />
|
||||
<UIcon v-else name="i-heroicons-user" class="h-4 w-4 opacity-70" />
|
||||
</div>
|
||||
<span class="whitespace-nowrap">
|
||||
{{ session.platform.toUpperCase() }}
|
||||
·
|
||||
{{
|
||||
session.type === 'private'
|
||||
? t('analysis.overview.identity.privateChat')
|
||||
: t('analysis.overview.identity.groupChat')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 紧凑统计数据 -->
|
||||
<div class="flex shrink-0 gap-6">
|
||||
<div class="flex flex-col gap-1 text-center">
|
||||
<span class="text-2xl font-black font-mono tracking-tight text-gray-900 dark:text-white">
|
||||
{{ session.messageCount.toLocaleString() }}
|
||||
</span>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('analysis.overview.identity.totalMessages') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 text-center">
|
||||
<span class="text-2xl font-black font-mono tracking-tight text-gray-900 dark:text-white">
|
||||
{{ totalDurationDays.toLocaleString() }}
|
||||
</span>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('analysis.overview.identity.durationDays') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 text-center">
|
||||
<span class="text-2xl font-black font-mono tracking-tight text-gray-900 dark:text-white">
|
||||
{{ totalDailyAvgMessages.toLocaleString() }}
|
||||
</span>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('analysis.overview.identity.dailyAvgMessages') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="fullTimeRangeText" class="flex items-center gap-2">
|
||||
<div class="flex h-6 w-6 shrink-0 items-center justify-center">
|
||||
<UIcon name="i-heroicons-calendar" class="h-4 w-4 opacity-70" />
|
||||
</div>
|
||||
<span class="font-mono text-xs opacity-90 whitespace-nowrap">{{ fullTimeRangeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热力图区域 -->
|
||||
<div class="pt-2">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest text-gray-400 dark:text-gray-500">
|
||||
Activity Heatmap
|
||||
<div class="flex shrink-0 gap-6">
|
||||
<div class="flex flex-col gap-1 text-center">
|
||||
<span class="text-2xl font-black font-mono tracking-tight text-gray-900 dark:text-white">
|
||||
{{ session.messageCount.toLocaleString() }}
|
||||
</span>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('analysis.overview.identity.totalMessages') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto overflow-y-hidden scrollbar-hide py-1">
|
||||
<div ref="chartRef" class="h-[140px] min-w-[700px] lg:w-full" />
|
||||
|
||||
<div class="flex flex-col gap-1 text-center">
|
||||
<span class="text-2xl font-black font-mono tracking-tight text-gray-900 dark:text-white">
|
||||
{{ totalDurationDays.toLocaleString() }}
|
||||
</span>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('analysis.overview.identity.durationDays') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 text-center">
|
||||
<span class="text-2xl font-black font-mono tracking-tight text-gray-900 dark:text-white">
|
||||
{{ totalDailyAvgMessages.toLocaleString() }}
|
||||
</span>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('analysis.overview.identity.dailyAvgMessages') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热力图区域 -->
|
||||
<div class="relative z-10 px-6 pb-2 sm:px-8">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest text-gray-400 dark:text-gray-500">
|
||||
Activity Heatmap
|
||||
</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto overflow-y-hidden scrollbar-hide py-1">
|
||||
<div ref="chartRef" class="h-[140px] min-w-[700px] lg:w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</ThemeCard>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { StatCard } from '@/components/UI'
|
||||
import type { WeekdayActivity, DailyActivity, HourlyActivity } from '@/types/analysis'
|
||||
@@ -6,25 +7,126 @@ import dayjs from 'dayjs'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
dailyAvgMessages: number
|
||||
durationDays: number
|
||||
imageCount: number
|
||||
peakHour: HourlyActivity | null
|
||||
peakWeekday: WeekdayActivity | null
|
||||
weekdayNames: string[]
|
||||
weekdayVsWeekend: { weekday: number; weekend: number }
|
||||
peakDay: DailyActivity | null
|
||||
activeDays: number
|
||||
totalDays: number
|
||||
activeRate: number
|
||||
maxConsecutiveDays: number
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
dailyAvgMessages: number
|
||||
durationDays: number
|
||||
imageCount: number
|
||||
peakHour: HourlyActivity | null
|
||||
peakWeekday: WeekdayActivity | null
|
||||
weekdayNames: string[]
|
||||
weekdayVsWeekend: { weekday: number; weekend: number }
|
||||
peakDay: DailyActivity | null
|
||||
activeDays: number
|
||||
totalDays: number
|
||||
activeRate: number
|
||||
maxConsecutiveDays: number
|
||||
/** 扁平模式:无边框/阴影,适合嵌入在父级 ThemeCard 内 */
|
||||
flat?: boolean
|
||||
}>(),
|
||||
{ flat: false }
|
||||
)
|
||||
|
||||
interface FlatStatItem {
|
||||
icon: string
|
||||
label: string
|
||||
value: string
|
||||
subtext: string
|
||||
colorClass: string
|
||||
}
|
||||
|
||||
const flatItems = computed<FlatStatItem[]>(() => [
|
||||
{
|
||||
icon: 'i-heroicons-chat-bubble-left-right',
|
||||
label: t('analysis.overview.statCards.dailyAvgMessages'),
|
||||
value: t('analysis.overview.statCards.messagesCount', { count: props.dailyAvgMessages }),
|
||||
subtext: t('analysis.overview.statCards.daysCount', { count: props.durationDays }),
|
||||
colorClass: 'text-blue-600 dark:text-blue-400',
|
||||
},
|
||||
{
|
||||
icon: 'i-heroicons-photo',
|
||||
label: t('analysis.overview.statCards.imageMessages'),
|
||||
value: t('analysis.overview.statCards.imagesCount', { count: props.imageCount }),
|
||||
subtext: `${t('analysis.overview.statCards.peakHour')} ${props.peakHour?.hour || 0}:00`,
|
||||
colorClass: 'text-pink-600 dark:text-pink-400',
|
||||
},
|
||||
{
|
||||
icon: 'i-heroicons-calendar-days',
|
||||
label: t('analysis.overview.statCards.mostActiveWeekday'),
|
||||
value: props.peakWeekday ? props.weekdayNames[props.peakWeekday.weekday - 1] : '-',
|
||||
subtext: t('analysis.overview.statCards.messagesOnDay', { count: props.peakWeekday?.messageCount ?? 0 }),
|
||||
colorClass: 'text-amber-600 dark:text-amber-400',
|
||||
},
|
||||
{
|
||||
icon: 'i-heroicons-sun',
|
||||
label: t('analysis.overview.statCards.weekendActivity'),
|
||||
value: `${props.weekdayVsWeekend.weekend}%`,
|
||||
subtext: t('analysis.overview.statCards.weekendRatio'),
|
||||
colorClass: 'text-green-600 dark:text-green-400',
|
||||
},
|
||||
{
|
||||
icon: 'i-heroicons-fire',
|
||||
label: t('analysis.overview.statCards.mostActiveDate'),
|
||||
value: props.peakDay ? dayjs(props.peakDay.date).format('MM/DD') : '-',
|
||||
subtext: t('analysis.overview.statCards.messagesOnDay', { count: props.peakDay?.messageCount ?? 0 }),
|
||||
colorClass: 'text-red-600 dark:text-red-400',
|
||||
},
|
||||
{
|
||||
icon: 'i-heroicons-calendar',
|
||||
label: t('analysis.overview.statCards.activeDays'),
|
||||
value: `${props.activeDays}`,
|
||||
subtext: t('analysis.overview.statCards.slashDays', { count: props.totalDays }),
|
||||
colorClass: 'text-blue-600 dark:text-blue-400',
|
||||
},
|
||||
{
|
||||
icon: 'i-heroicons-bolt',
|
||||
label: t('analysis.overview.statCards.consecutiveStreak'),
|
||||
value: t('analysis.overview.statCards.daysStreak', { count: props.maxConsecutiveDays }),
|
||||
subtext: t('analysis.overview.statCards.longestStreak'),
|
||||
colorClass: 'text-amber-600 dark:text-amber-400',
|
||||
},
|
||||
{
|
||||
icon: 'i-heroicons-chart-bar',
|
||||
label: t('analysis.overview.statCards.activityRate'),
|
||||
value: `${props.activeRate}%`,
|
||||
subtext: t('analysis.overview.statCards.activeDaysRatio'),
|
||||
colorClass: 'text-gray-900 dark:text-white',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- 日均消息 -->
|
||||
<!-- flat 模式:嵌入父级 ThemeCard 内的紧凑子卡片 -->
|
||||
<div v-if="flat" class="relative z-10 px-6 pb-6 pt-2 sm:px-8">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest text-gray-400 dark:text-gray-500">
|
||||
Key Metrics
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<div
|
||||
v-for="item in flatItems"
|
||||
:key="item.icon + item.label"
|
||||
class="flex items-start gap-2 rounded-lg bg-white/60 p-2.5 ring-1 ring-gray-900/5 dark:bg-white/5 dark:ring-white/10"
|
||||
>
|
||||
<UIcon :name="item.icon" class="mt-0.5 h-3.5 w-3.5 shrink-0" :class="item.colorClass" />
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-mono text-sm font-black leading-tight tabular-nums" :class="item.colorClass">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
<div class="mt-0.5 truncate text-[10px] font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div class="mt-0.5 truncate text-[9px] text-gray-400 dark:text-gray-500">
|
||||
{{ item.subtext }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标准模式:独立的 StatCard 组件 -->
|
||||
<div v-else class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard
|
||||
:label="t('analysis.overview.statCards.dailyAvgMessages')"
|
||||
:value="t('analysis.overview.statCards.messagesCount', { count: dailyAvgMessages })"
|
||||
@@ -38,7 +140,6 @@ defineProps<{
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<!-- 图片/表情 -->
|
||||
<StatCard
|
||||
:label="t('analysis.overview.statCards.imageMessages')"
|
||||
:value="t('analysis.overview.statCards.imagesCount', { count: imageCount })"
|
||||
@@ -51,7 +152,6 @@ defineProps<{
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<!-- 最活跃星期 -->
|
||||
<StatCard
|
||||
:label="t('analysis.overview.statCards.mostActiveWeekday')"
|
||||
:value="peakWeekday ? weekdayNames[peakWeekday.weekday - 1] : '-'"
|
||||
@@ -65,7 +165,6 @@ defineProps<{
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<!-- 周末活跃度 -->
|
||||
<StatCard
|
||||
:label="t('analysis.overview.statCards.weekendActivity')"
|
||||
:value="`${weekdayVsWeekend.weekend}%`"
|
||||
@@ -77,7 +176,6 @@ defineProps<{
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<!-- 最活跃日期 -->
|
||||
<StatCard
|
||||
:label="t('analysis.overview.statCards.mostActiveDate')"
|
||||
:value="peakDay ? dayjs(peakDay.date).format('MM/DD') : '-'"
|
||||
@@ -91,7 +189,6 @@ defineProps<{
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<!-- 活跃天数 -->
|
||||
<StatCard
|
||||
:label="t('analysis.overview.statCards.activeDays')"
|
||||
:value="`${activeDays}`"
|
||||
@@ -105,7 +202,6 @@ defineProps<{
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<!-- 连续打卡 -->
|
||||
<StatCard
|
||||
:label="t('analysis.overview.statCards.consecutiveStreak')"
|
||||
:value="t('analysis.overview.statCards.daysStreak', { count: maxConsecutiveDays })"
|
||||
@@ -117,7 +213,6 @@ defineProps<{
|
||||
</template>
|
||||
</StatCard>
|
||||
|
||||
<!-- 活跃率 -->
|
||||
<StatCard
|
||||
:label="t('analysis.overview.statCards.activityRate')"
|
||||
:value="`${activeRate}%`"
|
||||
|
||||
@@ -103,30 +103,30 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="main-content mx-auto max-w-[920px] space-y-6 p-6">
|
||||
<!-- 群聊身份卡 -->
|
||||
<!-- 群聊身份卡 + 关键指标 -->
|
||||
<OverviewIdentityCard
|
||||
:session="session"
|
||||
:daily-activity="dailyActivity"
|
||||
:total-duration-days="totalDurationDays"
|
||||
:total-daily-avg-messages="totalDailyAvgMessages"
|
||||
:time-range="timeRange"
|
||||
/>
|
||||
|
||||
<!-- 关键指标卡片 -->
|
||||
<OverviewStatCards
|
||||
:daily-avg-messages="dailyAvgMessages"
|
||||
:duration-days="durationDays"
|
||||
:image-count="imageCount"
|
||||
:peak-hour="peakHour"
|
||||
:peak-weekday="peakWeekday"
|
||||
:weekday-names="weekdayNames"
|
||||
:weekday-vs-weekend="weekdayVsWeekend"
|
||||
:peak-day="peakDay"
|
||||
:active-days="activeDays"
|
||||
:total-days="totalDays"
|
||||
:active-rate="activeRate"
|
||||
:max-consecutive-days="maxConsecutiveDays"
|
||||
/>
|
||||
>
|
||||
<OverviewStatCards
|
||||
flat
|
||||
:daily-avg-messages="dailyAvgMessages"
|
||||
:duration-days="durationDays"
|
||||
:image-count="imageCount"
|
||||
:peak-hour="peakHour"
|
||||
:peak-weekday="peakWeekday"
|
||||
:weekday-names="weekdayNames"
|
||||
:weekday-vs-weekend="weekdayVsWeekend"
|
||||
:peak-day="peakDay"
|
||||
:active-days="activeDays"
|
||||
:total-days="totalDays"
|
||||
:active-rate="activeRate"
|
||||
:max-consecutive-days="maxConsecutiveDays"
|
||||
/>
|
||||
</OverviewIdentityCard>
|
||||
|
||||
<!-- 图表区域:消息类型 & 成员分布 -->
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
|
||||
@@ -119,30 +119,30 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="main-content mx-auto max-w-[920px] space-y-6 p-6">
|
||||
<!-- 私聊身份卡 -->
|
||||
<!-- 私聊身份卡 + 关键指标 -->
|
||||
<OverviewIdentityCard
|
||||
:session="session"
|
||||
:daily-activity="dailyActivity"
|
||||
:total-duration-days="totalDurationDays"
|
||||
:total-daily-avg-messages="totalDailyAvgMessages"
|
||||
:time-range="timeRange"
|
||||
/>
|
||||
|
||||
<!-- 关键指标卡片 -->
|
||||
<OverviewStatCards
|
||||
:daily-avg-messages="dailyAvgMessages"
|
||||
:duration-days="durationDays"
|
||||
:image-count="imageCount"
|
||||
:peak-hour="peakHour"
|
||||
:peak-weekday="peakWeekday"
|
||||
:weekday-names="weekdayNames"
|
||||
:weekday-vs-weekend="weekdayVsWeekend"
|
||||
:peak-day="peakDay"
|
||||
:active-days="activeDays"
|
||||
:total-days="totalDays"
|
||||
:active-rate="activeRate"
|
||||
:max-consecutive-days="maxConsecutiveDays"
|
||||
/>
|
||||
>
|
||||
<OverviewStatCards
|
||||
flat
|
||||
:daily-avg-messages="dailyAvgMessages"
|
||||
:duration-days="durationDays"
|
||||
:image-count="imageCount"
|
||||
:peak-hour="peakHour"
|
||||
:peak-weekday="peakWeekday"
|
||||
:weekday-names="weekdayNames"
|
||||
:weekday-vs-weekend="weekdayVsWeekend"
|
||||
:peak-day="peakDay"
|
||||
:active-days="activeDays"
|
||||
:total-days="totalDays"
|
||||
:active-rate="activeRate"
|
||||
:max-consecutive-days="maxConsecutiveDays"
|
||||
/>
|
||||
</OverviewIdentityCard>
|
||||
|
||||
<!-- 图表区域:消息类型 & 双方占比 -->
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
|
||||
@@ -19,11 +19,11 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const subTabs = computed(() => [
|
||||
{ id: 'message', label: t('analysis.subTabs.view.message'), icon: 'i-heroicons-chat-bubble-left-right' },
|
||||
{ id: 'relationship', label: t('analysis.subTabs.view.relationship'), icon: 'i-heroicons-heart' },
|
||||
{ id: 'message', label: t('analysis.subTabs.view.message'), icon: 'i-heroicons-chat-bubble-left-right' },
|
||||
])
|
||||
|
||||
const activeSubTab = ref('message')
|
||||
const activeSubTab = ref('relationship')
|
||||
|
||||
// 成员筛选(仅用于消息视图)
|
||||
const selectedMemberId = ref<number | null>(null)
|
||||
|
||||
Reference in New Issue
Block a user