mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(insight): refine time investment rhythm
This commit is contained in:
1 parent
8e90434032
commit
ae1ed4a946
5 files changed
+135
-29
No files matched your search
@@ -60,8 +60,9 @@
|
||||
"chatTypeTitle": "Conversation types",
|
||||
"monthlyTitle": "Monthly trend",
|
||||
"monthlyDescription": "Estimated chat investment by month, in hours",
|
||||
"calendarTitle": "Investment calendar",
|
||||
"calendarDescription": "Darker days represent more estimated investment",
|
||||
"rhythmTitle": "Investment rhythm",
|
||||
"rhythmDescription": "Average daily investment by weekday",
|
||||
"topDaysTitle": "Highest-investment days",
|
||||
"rankingTitle": "Where the time went",
|
||||
"rankingDescription": "Shows the 10 private and group chats with the most invested time; overlapping time is divided evenly between conversations",
|
||||
"rankingCount": "{count} chats",
|
||||
|
||||
@@ -60,8 +60,9 @@
|
||||
"chatTypeTitle": "会話タイプ",
|
||||
"monthlyTitle": "月別推移",
|
||||
"monthlyDescription": "月ごとの推定チャット投入時間。縦軸は時間です",
|
||||
"calendarTitle": "投入カレンダー",
|
||||
"calendarDescription": "色が濃いほど、その日の推定投入時間が長いことを示します",
|
||||
"rhythmTitle": "投入のリズム",
|
||||
"rhythmDescription": "曜日別の1日平均投入時間",
|
||||
"topDaysTitle": "投入時間が長い日",
|
||||
"rankingTitle": "時間を使った会話",
|
||||
"rankingDescription": "個別チャットとグループチャットで投入時間の多い上位10件を表示します。重なる時間は会話間で均等に配分されます",
|
||||
"rankingCount": "{count} 件",
|
||||
|
||||
@@ -60,8 +60,9 @@
|
||||
"chatTypeTitle": "对话类型",
|
||||
"monthlyTitle": "月度趋势",
|
||||
"monthlyDescription": "每月预估聊天投入,纵轴单位为小时",
|
||||
"calendarTitle": "投入日历",
|
||||
"calendarDescription": "颜色越深,代表当天预估投入越多",
|
||||
"rhythmTitle": "投入节奏",
|
||||
"rhythmDescription": "比较一周中各天的平均投入",
|
||||
"topDaysTitle": "高投入日",
|
||||
"rankingTitle": "时间都去了哪里",
|
||||
"rankingDescription": "私聊与群聊分别展示投入时间最多的 10 个会话;重叠时间会在会话之间平均分配",
|
||||
"rankingCount": "{count} 个会话",
|
||||
|
||||
@@ -60,8 +60,9 @@
|
||||
"chatTypeTitle": "對話類型",
|
||||
"monthlyTitle": "月度趨勢",
|
||||
"monthlyDescription": "每月預估聊天投入,縱軸單位為小時",
|
||||
"calendarTitle": "投入日曆",
|
||||
"calendarDescription": "顏色越深,代表當天預估投入越多",
|
||||
"rhythmTitle": "投入節奏",
|
||||
"rhythmDescription": "比較一週中每天的平均投入",
|
||||
"topDaysTitle": "高投入日",
|
||||
"rankingTitle": "時間都去了哪裡",
|
||||
"rankingDescription": "私聊與群聊分別展示投入時間最多的 10 個會話;重疊時間會在會話之間平均分配",
|
||||
"rankingCount": "{count} 個會話",
|
||||
|
||||
@@ -12,10 +12,9 @@ import LoadingState from '@/components/UI/LoadingState.vue'
|
||||
import { CardDecoration, ThemeCard } from '@/components/UI'
|
||||
import { EChartLine } from '@/components/charts'
|
||||
import { PLATFORM_CAPABILITIES } from '@/utils/platform-capabilities'
|
||||
import InsightCalendarGrid from '../components/InsightCalendarGrid.vue'
|
||||
import { useInsightTimeRange, watchInsightSettingsClose } from '../insight-time-range'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
const layoutStore = useLayoutStore()
|
||||
const { showSettings } = storeToRefs(layoutStore)
|
||||
const currentYear = new Date().getFullYear()
|
||||
@@ -85,9 +84,50 @@ const monthlyChartData = computed(() => {
|
||||
values: data.map((item) => secondsToHours(item.estimatedSeconds)),
|
||||
}
|
||||
})
|
||||
const calendarData = computed(() =>
|
||||
(response.value?.dailyActivity ?? []).map((item) => ({ date: item.key, value: item.estimatedSeconds }))
|
||||
)
|
||||
const weekdayInvestmentItems = computed(() => {
|
||||
const range = response.value?.range
|
||||
if (!range) return []
|
||||
const totals = Array<number>(7).fill(0)
|
||||
const dayCounts = Array<number>(7).fill(0)
|
||||
const dailyInvestment = new Map(
|
||||
(response.value?.dailyActivity ?? []).map((item) => [item.key, item.estimatedSeconds])
|
||||
)
|
||||
const cursor = startOfLocalDay(new Date(range.startTs * 1000))
|
||||
const end = startOfLocalDay(new Date(range.endTs * 1000))
|
||||
while (cursor <= end) {
|
||||
const weekdayIndex = (cursor.getDay() + 6) % 7
|
||||
totals[weekdayIndex] += dailyInvestment.get(formatLocalDate(cursor)) ?? 0
|
||||
dayCounts[weekdayIndex]++
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
}
|
||||
const averages = totals.map((total, index) => (dayCounts[index] ? Math.round(total / dayCounts[index]) : 0))
|
||||
const maxAverage = Math.max(...averages, 1)
|
||||
const weekdayFormatter = new Intl.DateTimeFormat(locale.value, { weekday: 'short' })
|
||||
|
||||
return averages.map((seconds, index) => ({
|
||||
key: index,
|
||||
label: weekdayFormatter.format(new Date(2024, 0, 1 + index)),
|
||||
seconds,
|
||||
width: seconds > 0 ? Math.max((seconds / maxAverage) * 100, 3) : 0,
|
||||
}))
|
||||
})
|
||||
const hasWeekdayInvestment = computed(() => weekdayInvestmentItems.value.some((item) => item.seconds > 0))
|
||||
const topInvestmentDays = computed(() => {
|
||||
const weekdayFormatter = new Intl.DateTimeFormat(locale.value, { weekday: 'short' })
|
||||
const dateFormatter = new Intl.DateTimeFormat(locale.value, { month: '2-digit', day: '2-digit' })
|
||||
return [...(response.value?.dailyActivity ?? [])]
|
||||
.filter((item) => item.estimatedSeconds > 0)
|
||||
.sort((a, b) => b.estimatedSeconds - a.estimatedSeconds || a.key.localeCompare(b.key))
|
||||
.slice(0, 3)
|
||||
.map((item) => {
|
||||
const date = parseLocalDate(item.key)
|
||||
return {
|
||||
...item,
|
||||
dateLabel: dateFormatter.format(date),
|
||||
weekdayLabel: weekdayFormatter.format(date),
|
||||
}
|
||||
})
|
||||
})
|
||||
const chatTypeComparisonItems = computed(() =>
|
||||
[ChatType.GROUP, ChatType.PRIVATE]
|
||||
.map((type) => response.value?.chatTypes.find((item) => item.type === type))
|
||||
@@ -177,6 +217,19 @@ function secondsToHours(seconds: number): number {
|
||||
return Math.round((seconds / 3600) * 10) / 10
|
||||
}
|
||||
|
||||
function startOfLocalDay(date: Date): Date {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
|
||||
function formatLocalDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function parseLocalDate(value: string): Date {
|
||||
const [year, month, day] = value.split('-').map(Number)
|
||||
return new Date(year, month - 1, day)
|
||||
}
|
||||
|
||||
function chatTypeLabel(type: ChatType): string {
|
||||
return t(type === ChatType.PRIVATE ? 'insight.timeInvestment.privateChat' : 'insight.timeInvestment.groupChat')
|
||||
}
|
||||
@@ -362,24 +415,73 @@ function chatTypeLabel(type: ChatType): string {
|
||||
</section>
|
||||
</ThemeCard>
|
||||
|
||||
<div class="grid content-start gap-4 xl:col-span-4">
|
||||
<ThemeCard>
|
||||
<section class="min-w-0 p-5 sm:p-6">
|
||||
<h3
|
||||
class="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-zinc-300"
|
||||
<ThemeCard class="h-full xl:col-span-4">
|
||||
<section class="min-w-0 p-5 sm:p-6">
|
||||
<h3
|
||||
class="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-zinc-300"
|
||||
>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-pink-500 dark:bg-pink-400" />
|
||||
{{ t('insight.timeInvestment.rhythmTitle') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-[11px] text-gray-400 dark:text-zinc-500">
|
||||
{{ t('insight.timeInvestment.rhythmDescription') }}
|
||||
</p>
|
||||
|
||||
<div v-if="hasWeekdayInvestment" class="mt-5 space-y-3.5">
|
||||
<div
|
||||
v-for="item in weekdayInvestmentItems"
|
||||
:key="item.key"
|
||||
class="grid grid-cols-[2.25rem_minmax(0,1fr)_auto] items-center gap-3"
|
||||
>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-pink-500 dark:bg-pink-400" />
|
||||
{{ t('insight.timeInvestment.calendarTitle') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-[11px] text-gray-400 dark:text-zinc-500">
|
||||
{{ t('insight.timeInvestment.calendarDescription') }}
|
||||
</p>
|
||||
<div class="mt-5">
|
||||
<InsightCalendarGrid :range="response.range" :data="calendarData" :format-value="formatDuration" />
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-zinc-400">{{ item.label }}</span>
|
||||
<div class="h-1.5 overflow-hidden rounded-full bg-gray-100 dark:bg-zinc-800">
|
||||
<div
|
||||
class="h-full rounded-full bg-pink-500 transition-[width] duration-500 dark:bg-pink-400"
|
||||
:style="{ width: `${item.width}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="min-w-[4.75rem] whitespace-nowrap text-right font-mono text-[10px] font-semibold tabular-nums text-gray-600 dark:text-zinc-300"
|
||||
>
|
||||
{{ formatDuration(item.seconds) }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</ThemeCard>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="mt-5 text-xs text-gray-400">{{ t('insight.noData') }}</p>
|
||||
|
||||
<div v-if="topInvestmentDays.length" class="mt-6 border-t border-gray-200/60 pt-4 dark:border-white/5">
|
||||
<h4 class="text-xs font-semibold text-gray-700 dark:text-zinc-300">
|
||||
{{ t('insight.timeInvestment.topDaysTitle') }}
|
||||
</h4>
|
||||
<div class="mt-3 divide-y divide-gray-200/60 dark:divide-white/5">
|
||||
<div
|
||||
v-for="(item, index) in topInvestmentDays"
|
||||
:key="item.key"
|
||||
class="grid grid-cols-[1.5rem_minmax(0,1fr)_auto] items-center gap-3 py-2.5 first:pt-0 last:pb-0"
|
||||
>
|
||||
<span
|
||||
class="flex h-5 w-5 items-center justify-center rounded-full bg-pink-50 font-mono text-[9px] font-bold text-pink-600 dark:bg-pink-950/30 dark:text-pink-400"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<div class="flex min-w-0 items-baseline gap-2">
|
||||
<span class="font-mono text-xs font-semibold tabular-nums text-gray-700 dark:text-zinc-200">
|
||||
{{ item.dateLabel }}
|
||||
</span>
|
||||
<span class="truncate text-[10px] text-gray-400 dark:text-zinc-500">
|
||||
{{ item.weekdayLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="whitespace-nowrap font-mono text-[10px] font-semibold tabular-nums text-gray-600 dark:text-zinc-300"
|
||||
>
|
||||
{{ formatDuration(item.estimatedSeconds) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</ThemeCard>
|
||||
|
||||
<ThemeCard class="xl:col-span-12">
|
||||
<section class="min-w-0 p-5 sm:p-6">
|
||||
|
||||
Reference in new issue
Block a user