diff --git a/src/App.vue b/src/App.vue index 293e940..9b841a6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,7 +4,7 @@ import { load } from "@tauri-apps/plugin-store"; import { open, save } from "@tauri-apps/plugin-dialog"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import { Tag as TagIcon, FolderOpen, Settings, Play, Pause, Maximize2, X, RefreshCw, Plus, Trash2, ChevronDown, ChevronLeft, ChevronRight, Calendar, Download, SquarePlus } from "lucide-vue-next"; +import { Tag as TagIcon, FolderOpen, Settings, Play, Pause, Maximize2, X, RefreshCw, Plus, Trash2, ChevronDown, ChevronLeft, ChevronRight, Calendar, Download, SquarePlus, BarChart2, Image as ImageIcon } from "lucide-vue-next"; // --- Types --- interface Tag { id: number; name: string; parent_id: number | null; color: string; } @@ -46,6 +46,94 @@ const captureInterval = ref(60); const timelineZoom = ref(1.5); const theme = ref("system"); +const viewMode = ref<'preview' | 'dashboard'>('preview'); +const dashboardRange = ref<'today' | '7days' | '30days'>('today'); +const dashboardStartDate = ref(currentDate.value); +const dashboardEndDate = ref(currentDate.value); +const dashboardEvents = ref([]); +const dailyAverageMode = ref<'natural' | 'recorded'>('natural'); + +const loadDashboardEvents = async () => { + dashboardEvents.value = await invoke("get_events_range", { startDate: dashboardStartDate.value, endDate: dashboardEndDate.value }); +}; + +watch([dashboardRange, currentDate], async () => { + const end = new Date(currentDate.value); + let start = new Date(currentDate.value); + if (dashboardRange.value === '7days') start.setDate(end.getDate() - 6); + else if (dashboardRange.value === '30days') start.setDate(end.getDate() - 29); + dashboardStartDate.value = start.toLocaleDateString('sv'); + dashboardEndDate.value = end.toLocaleDateString('sv'); + if (viewMode.value === 'dashboard') await loadDashboardEvents(); +}); + +watch(viewMode, async (mode) => { + if (mode === 'dashboard') { + const end = new Date(currentDate.value); + let start = new Date(currentDate.value); + if (dashboardRange.value === '7days') start.setDate(end.getDate() - 6); + else if (dashboardRange.value === '30days') start.setDate(end.getDate() - 29); + dashboardStartDate.value = start.toLocaleDateString('sv'); + dashboardEndDate.value = end.toLocaleDateString('sv'); + await loadDashboardEvents(); + } +}); + +const dashboardStats = computed(() => { + let totalMinutes = 0; + const mainTagMap = new Map }>(); + const uniqueDays = new Set(); + + dashboardEvents.value.forEach(ev => { + let diff = ev.end_minute - ev.start_minute; + if (diff < 0) diff += 1440; + totalMinutes += diff; + uniqueDays.add(ev.date); + + if (!mainTagMap.has(ev.main_tag_id)) { + mainTagMap.set(ev.main_tag_id, { total: 0, subTags: new Map() }); + } + const mainStat = mainTagMap.get(ev.main_tag_id)!; + mainStat.total += diff; + + if (ev.sub_tag_id) { + const subTotal = mainStat.subTags.get(ev.sub_tag_id) || 0; + mainStat.subTags.set(ev.sub_tag_id, subTotal + diff); + } + }); + + const start = new Date(dashboardStartDate.value); + const end = new Date(dashboardEndDate.value); + const naturalDays = Math.max(1, Math.floor((end.getTime() - start.getTime()) / 86400000) + 1); + const recordedDays = Math.max(1, uniqueDays.size); + const daysCount = dailyAverageMode.value === 'natural' ? naturalDays : recordedDays; + + const mainTagsList = Array.from(mainTagMap.entries()).map(([id, stat]) => { + const subTagsList = Array.from(stat.subTags.entries()).map(([subId, subTotal]) => ({ + id: subId, + total: subTotal, + percentage: stat.total > 0 ? (subTotal / stat.total) * 100 : 0 + })).sort((a, b) => b.total - a.total); + + return { + id, + total: stat.total, + dailyAverage: stat.total / daysCount, + percentage: totalMinutes > 0 ? (stat.total / totalMinutes) * 100 : 0, + subTags: subTagsList + }; + }).sort((a, b) => b.total - a.total); + + return { totalMinutes, dailyAverage: totalMinutes / daysCount, mainTags: mainTagsList, naturalDays, recordedDays, daysCount }; +}); + +const formatMinutes = (mins: number) => { + const h = Math.floor(mins / 60); + const m = Math.floor(mins % 60); + if (h === 0) return `${m}m`; + return `${h}h ${m}m`; +}; + const applyTheme = (val: string) => { if (val === 'dark') { document.documentElement.classList.add('dark'); @@ -251,7 +339,10 @@ const completeSetup = async () => { }; const loadTags = async () => { tags.value = await invoke("get_tags"); }; -const loadEvents = async () => { dayEvents.value = await invoke("get_events", { date: currentDate.value }); }; +const loadEvents = async () => { + dayEvents.value = await invoke("get_events", { date: currentDate.value }); + if (viewMode.value === 'dashboard') await loadDashboardEvents(); +}; const handleAddTag = async (name: string, parentId: number | null, color: string) => { try { @@ -363,6 +454,7 @@ const handleTimelineMouseMove = (e: MouseEvent) => { if (min >= 0 && min < 1440) { hoveredTime.value = logicalMinutesToTime(min); if (isDragging.value) dragEndMin.value = min; + if (viewMode.value === 'dashboard') return; const closest = timelineImages.value.reduce((p, c) => { const pd = Math.abs(timeToLogicalMinutes(p.time, p.isNextDay) - min); const cd = Math.abs(timeToLogicalMinutes(c.time, c.isNextDay) - min); @@ -386,13 +478,18 @@ const handleTimelineMouseUp = () => { const cd = Math.abs(timeToLogicalMinutes(c.time, c.isNextDay) - start); return cd < pd ? c : p; }, timelineImages.value[0]); - if (closest) { lockedImage.value = closest; updatePreview(closest); } + if (closest) { + lockedImage.value = closest; + viewMode.value = 'preview'; + updatePreview(closest); + } } } }; const handleTimelineMouseLeave = () => { hoveredTime.value = null; + if (viewMode.value === 'dashboard') return; if (!isDragging.value && lockedImage.value) updatePreview(lockedImage.value); }; @@ -636,13 +733,106 @@ const handleExport = async () => {
-
{{ selectedImage.time }}{{ lockedImage?.path === selectedImage.path ? '已定格' : '预览中' }}
-
未选中活动
- +
+ + 未选中活动 +
+
+ 时间分析 +
+ +
+ +
+ + +
+
-
- -

在时间轴上滑动或拖拽以记录

+ +
+
+ +

在时间轴上滑动或拖拽以记录

+
+ +
+
+
+
+ + + +
+
+ + +
+
+ +
+
+
总记录时长
+
{{ formatMinutes(dashboardStats.totalMinutes) }}
+
+
+
日均时长
+
{{ formatMinutes(dashboardStats.dailyAverage) }}
+
+
+ +
+

占比总览

+
+
+
+
+
+
+ {{ getTagName(tag.id) }} {{ tag.percentage.toFixed(0) }}% +
+
+
+ +
+

时间分布明细

+
+
+
+
+ {{ getTagName(tag.id) }} +
+
+
{{ formatMinutes(tag.total) }}
+
+
+
+ 占比 {{ tag.percentage.toFixed(1) }}% + 日均 {{ formatMinutes(tag.dailyAverage) }} +
+ +
+
+ {{ getTagName(sub.id) }} +
+
+
+
+ {{ formatMinutes(sub.total) }} +
+
+
+
+ +
+ +
该时间范围内没有记录数据
+
+
+
+
@@ -658,7 +848,6 @@ const handleExport = async () => {
@@ -690,7 +879,6 @@ const handleExport = async () => {
@@ -841,7 +1029,7 @@ const handleExport = async () => {
-
+

设置