mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-18 04:20:57 +08:00
feat: 重构TimeTab
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
||||
MemberActivity,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
WeekdayActivity,
|
||||
MessageType,
|
||||
RepeatAnalysis,
|
||||
RepeatStatItem,
|
||||
@@ -520,6 +521,54 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取星期活跃度分布
|
||||
* 返回周一到周日的消息统计
|
||||
*/
|
||||
export function getWeekdayActivity(sessionId: string, filter?: TimeFilter): WeekdayActivity[] {
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return []
|
||||
|
||||
try {
|
||||
const { clause, params } = buildTimeFilter(filter)
|
||||
const clauseWithSystem = buildSystemMessageFilter(clause)
|
||||
|
||||
// SQLite strftime('%w') 返回 0-6,0=周日
|
||||
// 我们需要转换为 1-7,1=周一,7=周日
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
CASE
|
||||
WHEN CAST(strftime('%w', msg.ts, 'unixepoch', 'localtime') AS INTEGER) = 0 THEN 7
|
||||
ELSE CAST(strftime('%w', msg.ts, 'unixepoch', 'localtime') AS INTEGER)
|
||||
END as weekday,
|
||||
COUNT(*) as messageCount
|
||||
FROM message msg
|
||||
JOIN member m ON msg.sender_id = m.id
|
||||
${clauseWithSystem}
|
||||
GROUP BY weekday
|
||||
ORDER BY weekday
|
||||
`
|
||||
)
|
||||
.all(...params) as Array<{ weekday: number; messageCount: number }>
|
||||
|
||||
// 补全所有星期(1-7)
|
||||
const result: WeekdayActivity[] = []
|
||||
for (let w = 1; w <= 7; w++) {
|
||||
const found = rows.find((r) => r.weekday === w)
|
||||
result.push({
|
||||
weekday: w,
|
||||
messageCount: found ? found.messageCount : 0,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取口头禅分析数据
|
||||
* 统计每个成员最常说的内容(前5个)
|
||||
|
||||
@@ -12,6 +12,7 @@ export {
|
||||
getMemberActivity,
|
||||
getHourlyActivity,
|
||||
getDailyActivity,
|
||||
getWeekdayActivity,
|
||||
getMessageTypeDistribution,
|
||||
getTimeRange,
|
||||
getMemberNameHistory,
|
||||
|
||||
@@ -315,6 +315,21 @@ const mainIpcMain = (win: BrowserWindow) => {
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取星期活跃度分布
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'chat:getWeekdayActivity',
|
||||
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
|
||||
try {
|
||||
return database.getWeekdayActivity(sessionId, filter)
|
||||
} catch (error) {
|
||||
console.error('获取星期活跃度失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取消息类型分布
|
||||
*/
|
||||
|
||||
Vendored
+2
@@ -5,6 +5,7 @@ import type {
|
||||
MemberNameHistory,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
WeekdayActivity,
|
||||
MessageType,
|
||||
ImportProgress,
|
||||
RepeatAnalysis,
|
||||
@@ -27,6 +28,7 @@ interface ChatApi {
|
||||
getMemberNameHistory: (sessionId: string, memberId: number) => Promise<MemberNameHistory[]>
|
||||
getHourlyActivity: (sessionId: string, filter?: TimeFilter) => Promise<HourlyActivity[]>
|
||||
getDailyActivity: (sessionId: string, filter?: TimeFilter) => Promise<DailyActivity[]>
|
||||
getWeekdayActivity: (sessionId: string, filter?: TimeFilter) => Promise<WeekdayActivity[]>
|
||||
getMessageTypeDistribution: (
|
||||
sessionId: string,
|
||||
filter?: TimeFilter
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
MemberNameHistory,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
WeekdayActivity,
|
||||
MessageType,
|
||||
ImportProgress,
|
||||
RepeatAnalysis,
|
||||
@@ -111,6 +112,13 @@ const chatApi = {
|
||||
return ipcRenderer.invoke('chat:getDailyActivity', sessionId, filter)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取星期活跃度分布
|
||||
*/
|
||||
getWeekdayActivity: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise<WeekdayActivity[]> => {
|
||||
return ipcRenderer.invoke('chat:getWeekdayActivity', sessionId, filter)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取消息类型分布
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { AnalysisSession, MemberActivity, HourlyActivity, DailyActivity, Me
|
||||
import UITabs from '@/components/UI/Tabs.vue'
|
||||
import OverviewTab from './analysis/OverviewTab.vue'
|
||||
import MembersTab from './analysis/MembersTab.vue'
|
||||
import TimeTab from './analysis/TimeTab.vue'
|
||||
import TimelineTab from './analysis/TimelineTab.vue'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
@@ -28,7 +29,8 @@ const selectedYear = ref<number>(0) // 0 表示全部
|
||||
const tabs = [
|
||||
{ id: 'overview', label: '总览', icon: 'i-heroicons-chart-pie' },
|
||||
{ id: 'members', label: '成员', icon: 'i-heroicons-user-group' },
|
||||
{ id: 'timeline', label: '时间', icon: 'i-heroicons-chart-bar' },
|
||||
{ id: 'time', label: '规律', icon: 'i-heroicons-clock' },
|
||||
{ id: 'timeline', label: '趋势', icon: 'i-heroicons-chart-bar' },
|
||||
]
|
||||
|
||||
const activeTab = ref('overview')
|
||||
@@ -241,12 +243,13 @@ onMounted(loadData)
|
||||
:member-activity="memberActivity"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<TimelineTab
|
||||
v-else-if="activeTab === 'timeline'"
|
||||
:daily-activity="dailyActivity"
|
||||
<TimeTab
|
||||
v-else-if="activeTab === 'time'"
|
||||
:session-id="currentSessionId!"
|
||||
:hourly-activity="hourlyActivity"
|
||||
:time-range="timeRange"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<TimelineTab v-else-if="activeTab === 'timeline'" :daily-activity="dailyActivity" :time-range="timeRange" />
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { HourlyActivity, WeekdayActivity } from '@/types/chat'
|
||||
import { BarChart } from '@/components/charts'
|
||||
import type { BarChartData } from '@/components/charts'
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
hourlyActivity: HourlyActivity[]
|
||||
timeFilter?: { startTs?: number; endTs?: number }
|
||||
}>()
|
||||
|
||||
// 星期活跃度数据
|
||||
const weekdayActivity = ref<WeekdayActivity[]>([])
|
||||
const isLoadingWeekday = ref(false)
|
||||
|
||||
// 星期名称映射(周一开始)
|
||||
const weekdayNames = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
|
||||
// 加载星期活跃度数据
|
||||
async function loadWeekdayActivity() {
|
||||
if (!props.sessionId) return
|
||||
isLoadingWeekday.value = true
|
||||
try {
|
||||
weekdayActivity.value = await window.chatApi.getWeekdayActivity(props.sessionId, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载星期活跃度失败:', error)
|
||||
} finally {
|
||||
isLoadingWeekday.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 sessionId 和 timeFilter 变化
|
||||
watch(
|
||||
() => [props.sessionId, props.timeFilter],
|
||||
() => {
|
||||
loadWeekdayActivity()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// 24小时分布图数据
|
||||
const hourlyChartData = computed<BarChartData>(() => {
|
||||
return {
|
||||
labels: props.hourlyActivity.map((h) => `${h.hour}:00`),
|
||||
values: props.hourlyActivity.map((h) => h.messageCount),
|
||||
}
|
||||
})
|
||||
|
||||
// 星期分布图数据
|
||||
const weekdayChartData = computed<BarChartData>(() => {
|
||||
return {
|
||||
labels: weekdayActivity.value.map((w) => weekdayNames[w.weekday - 1]),
|
||||
values: weekdayActivity.value.map((w) => w.messageCount),
|
||||
}
|
||||
})
|
||||
|
||||
// 分析指标
|
||||
const peakHour = computed(() => {
|
||||
if (!props.hourlyActivity.length) return null
|
||||
return props.hourlyActivity.reduce((max, h) => (h.messageCount > max.messageCount ? h : max), props.hourlyActivity[0])
|
||||
})
|
||||
|
||||
const peakWeekday = computed(() => {
|
||||
if (!weekdayActivity.value.length) return null
|
||||
return weekdayActivity.value.reduce((max, w) => (w.messageCount > max.messageCount ? w : max), weekdayActivity.value[0])
|
||||
})
|
||||
|
||||
const lateNightRatio = computed(() => {
|
||||
// 深夜定义为 0-6 点
|
||||
const lateNight = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 0 && h.hour < 6)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
const total = props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return total > 0 ? Math.round((lateNight / total) * 100) : 0
|
||||
})
|
||||
|
||||
const morningRatio = computed(() => {
|
||||
// 早间定义为 6-12 点
|
||||
const morning = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 6 && h.hour < 12)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
const total = props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return total > 0 ? Math.round((morning / total) * 100) : 0
|
||||
})
|
||||
|
||||
const afternoonRatio = computed(() => {
|
||||
// 下午定义为 12-18 点
|
||||
const afternoon = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 12 && h.hour < 18)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
const total = props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return total > 0 ? Math.round((afternoon / total) * 100) : 0
|
||||
})
|
||||
|
||||
const eveningRatio = computed(() => {
|
||||
// 晚间定义为 18-24 点
|
||||
const evening = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 18 && h.hour < 24)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
const total = props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return total > 0 ? Math.round((evening / total) * 100) : 0
|
||||
})
|
||||
|
||||
// 工作日 vs 周末
|
||||
const weekdayVsWeekend = computed(() => {
|
||||
if (!weekdayActivity.value.length) return { weekday: 0, weekend: 0 }
|
||||
const weekdaySum = weekdayActivity.value
|
||||
.filter((w) => w.weekday >= 1 && w.weekday <= 5)
|
||||
.reduce((sum, w) => sum + w.messageCount, 0)
|
||||
const weekendSum = weekdayActivity.value
|
||||
.filter((w) => w.weekday >= 6 && w.weekday <= 7)
|
||||
.reduce((sum, w) => sum + w.messageCount, 0)
|
||||
const total = weekdaySum + weekendSum
|
||||
return {
|
||||
weekday: total > 0 ? Math.round((weekdaySum / total) * 100) : 0,
|
||||
weekend: total > 0 ? Math.round((weekendSum / total) * 100) : 0,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 标题 -->
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">时间规律分析</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">发现群聊的周期性活跃规律</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">最活跃时段</p>
|
||||
<p class="mt-1 text-2xl font-bold text-pink-600 dark:text-pink-400">{{ peakHour?.hour ?? 0 }}:00</p>
|
||||
<p class="mt-1 text-xs text-gray-400">{{ peakHour?.messageCount ?? 0 }} 条消息</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">最活跃星期</p>
|
||||
<p class="mt-1 text-2xl font-bold text-pink-600 dark:text-pink-400">
|
||||
{{ peakWeekday ? weekdayNames[peakWeekday.weekday - 1] : '-' }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-400">{{ peakWeekday?.messageCount ?? 0 }} 条消息</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">夜猫子指数</p>
|
||||
<p class="mt-1 text-2xl font-bold text-amber-600 dark:text-amber-400">{{ lateNightRatio }}%</p>
|
||||
<p class="mt-1 text-xs text-gray-400">深夜活跃占比</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">周末活跃度</p>
|
||||
<p class="mt-1 text-2xl font-bold text-blue-600 dark:text-blue-400">{{ weekdayVsWeekend.weekend }}%</p>
|
||||
<p class="mt-1 text-xs text-gray-400">周末消息占比</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 星期分布 -->
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<h3 class="mb-4 font-semibold text-gray-900 dark:text-white">星期活跃分布</h3>
|
||||
<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>
|
||||
<BarChart v-else :data="weekdayChartData" :height="256" />
|
||||
|
||||
<!-- 工作日 vs 周末 -->
|
||||
<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">工作日(周一至周五)</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">周末(周六、周日)</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>
|
||||
</div>
|
||||
|
||||
<!-- 24小时分布 -->
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<h3 class="mb-4 font-semibold text-gray-900 dark:text-white">24小时活跃分布</h3>
|
||||
<BarChart
|
||||
:data="hourlyChartData"
|
||||
:height="256"
|
||||
:x-label-filter="(_, index) => (index % 3 === 0 ? `${index}:00` : '')"
|
||||
/>
|
||||
|
||||
<!-- 时段分析 -->
|
||||
<div class="mt-6 grid grid-cols-4 gap-4">
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">凌晨 0-6点</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ lateNightRatio }}%</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-16 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">上午 6-12点</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ morningRatio }}%</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-16 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">下午 12-18点</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ afternoonRatio }}%</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-16 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">晚上 18-24点</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ eveningRatio }}%</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-16 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { HourlyActivity, DailyActivity } from '@/types/chat'
|
||||
import type { DailyActivity } from '@/types/chat'
|
||||
import dayjs from 'dayjs'
|
||||
import { LineChart, BarChart } from '@/components/charts'
|
||||
import type { LineChartData, BarChartData } from '@/components/charts'
|
||||
import { LineChart } from '@/components/charts'
|
||||
import type { LineChartData } from '@/components/charts'
|
||||
|
||||
const props = defineProps<{
|
||||
dailyActivity: DailyActivity[]
|
||||
hourlyActivity: HourlyActivity[]
|
||||
timeRange: { start: number; end: number } | null
|
||||
}>()
|
||||
|
||||
@@ -29,56 +28,6 @@ const dailyChartData = computed<LineChartData>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 24小时分布图数据
|
||||
const hourlyChartData = computed<BarChartData>(() => {
|
||||
return {
|
||||
labels: props.hourlyActivity.map((h) => `${h.hour}:00`),
|
||||
values: props.hourlyActivity.map((h) => h.messageCount),
|
||||
}
|
||||
})
|
||||
|
||||
// 分析指标
|
||||
const peakHour = computed(() => {
|
||||
if (!props.hourlyActivity.length) return null
|
||||
return props.hourlyActivity.reduce((max, h) => (h.messageCount > max.messageCount ? h : max), props.hourlyActivity[0])
|
||||
})
|
||||
|
||||
const lateNightRatio = computed(() => {
|
||||
// 深夜定义为 0-6 点
|
||||
const lateNight = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 0 && h.hour < 6)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
const total = props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return total > 0 ? Math.round((lateNight / total) * 100) : 0
|
||||
})
|
||||
|
||||
const morningRatio = computed(() => {
|
||||
// 早间定义为 6-12 点
|
||||
const morning = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 6 && h.hour < 12)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
const total = props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return total > 0 ? Math.round((morning / total) * 100) : 0
|
||||
})
|
||||
|
||||
const afternoonRatio = computed(() => {
|
||||
// 下午定义为 12-18 点
|
||||
const afternoon = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 12 && h.hour < 18)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
const total = props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return total > 0 ? Math.round((afternoon / total) * 100) : 0
|
||||
})
|
||||
|
||||
const eveningRatio = computed(() => {
|
||||
// 晚间定义为 18-24 点
|
||||
const evening = props.hourlyActivity
|
||||
.filter((h) => h.hour >= 18 && h.hour < 24)
|
||||
.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
const total = props.hourlyActivity.reduce((sum, h) => sum + h.messageCount, 0)
|
||||
return total > 0 ? Math.round((evening / total) * 100) : 0
|
||||
})
|
||||
|
||||
// 最活跃的一天
|
||||
const peakDay = computed(() => {
|
||||
if (!props.dailyActivity.length) return null
|
||||
@@ -91,30 +40,37 @@ const avgDailyMessages = computed(() => {
|
||||
const total = props.dailyActivity.reduce((sum, d) => sum + d.messageCount, 0)
|
||||
return Math.round(total / props.dailyActivity.length)
|
||||
})
|
||||
|
||||
// 活跃天数
|
||||
const activeDays = computed(() => {
|
||||
return props.dailyActivity.filter((d) => d.messageCount > 0).length
|
||||
})
|
||||
|
||||
// 总天数(从第一条到最后一条消息)
|
||||
const totalDays = computed(() => {
|
||||
if (!props.timeRange) return 0
|
||||
const start = dayjs.unix(props.timeRange.start)
|
||||
const end = dayjs.unix(props.timeRange.end)
|
||||
return end.diff(start, 'day') + 1
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 标题 -->
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">时间维度分析</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">探索群聊的活跃规律</p>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">时间轴分析</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">追踪群聊的活跃趋势变化</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">最活跃时段</p>
|
||||
<p class="mt-1 text-2xl font-bold text-pink-600 dark:text-pink-400">{{ peakHour?.hour || 0 }}:00</p>
|
||||
<p class="mt-1 text-xs text-gray-400">{{ peakHour?.messageCount || 0 }} 条消息</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">最活跃日期</p>
|
||||
<p class="mt-1 text-2xl font-bold text-pink-600 dark:text-pink-400">
|
||||
{{ peakDay ? dayjs(peakDay.date).format('MM/DD') : '-' }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-400">{{ peakDay?.messageCount || 0 }} 条消息</p>
|
||||
<p class="mt-1 text-xs text-gray-400">{{ peakDay?.messageCount ?? 0 }} 条消息</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
@@ -126,9 +82,19 @@ const avgDailyMessages = computed(() => {
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">夜猫子指数</p>
|
||||
<p class="mt-1 text-2xl font-bold text-amber-600 dark:text-amber-400">{{ lateNightRatio }}%</p>
|
||||
<p class="mt-1 text-xs text-gray-400">深夜活跃占比</p>
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">活跃天数</p>
|
||||
<p class="mt-1 text-2xl font-bold text-pink-600 dark:text-pink-400">
|
||||
{{ activeDays }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-400">/ {{ totalDays }} 天</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">活跃率</p>
|
||||
<p class="mt-1 text-2xl font-bold text-pink-600 dark:text-pink-400">
|
||||
{{ totalDays > 0 ? Math.round((activeDays / totalDays) * 100) : 0 }}%
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-400">有消息的天数占比</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,47 +103,5 @@ const avgDailyMessages = computed(() => {
|
||||
<h3 class="mb-4 font-semibold text-gray-900 dark:text-white">每日消息趋势</h3>
|
||||
<LineChart :data="dailyChartData" :height="288" />
|
||||
</div>
|
||||
|
||||
<!-- 24小时分布 -->
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<h3 class="mb-4 font-semibold text-gray-900 dark:text-white">24小时活跃分布</h3>
|
||||
<BarChart
|
||||
:data="hourlyChartData"
|
||||
:height="256"
|
||||
:x-label-filter="(_, index) => (index % 3 === 0 ? `${index}:00` : '')"
|
||||
/>
|
||||
|
||||
<!-- 时段分析 -->
|
||||
<div class="mt-6 grid grid-cols-4 gap-4">
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">凌晨 0-6点</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ lateNightRatio }}%</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-16 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">上午 6-12点</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ morningRatio }}%</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-16 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">下午 12-18点</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ afternoonRatio }}%</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-16 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">晚上 18-24点</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ eveningRatio }}%</div>
|
||||
<div class="mx-auto mt-2 h-1 w-full max-w-16 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -46,7 +46,7 @@ const formattedCount = computed(() => props.countTemplate.replace('{count}', Str
|
||||
<slot name="headerRight" />
|
||||
|
||||
<!-- 完整列表弹窗 -->
|
||||
<UModal v-model:open="isOpen" :ui="{ width: 'max-w-3xl' }">
|
||||
<UModal v-model:open="isOpen" :ui="{ content: 'md:w-full max-w-4xl' }">
|
||||
<UButton v-if="showViewAll" icon="i-heroicons-list-bullet" color="gray" variant="ghost">查看完整列表</UButton>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -44,7 +44,7 @@ const showViewAll = computed(() => {
|
||||
</div>
|
||||
|
||||
<!-- 完整排行榜 Dialog -->
|
||||
<UModal v-model:open="isOpen" :ui="{ width: 'max-w-3xl' }">
|
||||
<UModal v-model:open="isOpen" :ui="{ content: 'md:w-full max-w-3xl' }">
|
||||
<UButton v-if="showViewAll" icon="i-heroicons-list-bullet" color="gray" variant="ghost">查看完整排行</UButton>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -133,6 +133,14 @@ export interface DailyActivity {
|
||||
messageCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 星期活跃度统计
|
||||
*/
|
||||
export interface WeekdayActivity {
|
||||
weekday: number // 1-7,1=周一,7=周日
|
||||
messageCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析会话信息(用于会话列表展示)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user