feat: 重构总览模块卡片

This commit is contained in:
digua
2026-04-11 00:26:35 +08:00
committed by digua
parent f2ee63ccdd
commit 11496f5b0c
5 changed files with 220 additions and 128 deletions
@@ -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}%`"
+18 -18
View File
@@ -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)