support custom stat dates
This commit is contained in:
@@ -1,26 +1,96 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BarChart2 } from "lucide-vue-next";
|
import { ref, computed } from "vue";
|
||||||
|
import { BarChart2, Calendar, ChevronLeft, ChevronRight } from "lucide-vue-next";
|
||||||
import {
|
import {
|
||||||
dashboardStats, dashboardRange, dailyAverageMode,
|
dashboardStats, dashboardRange, dailyAverageMode, dashboardStartDate, dashboardEndDate,
|
||||||
|
customStartDate, customEndDate,
|
||||||
|
isStartCalendarOpen, isEndCalendarOpen,
|
||||||
formatMinutes, getTagColor, getTagName
|
formatMinutes, getTagColor, getTagName
|
||||||
} from "../../store/dashboardStore";
|
} from "../../store/dashboardStore";
|
||||||
|
import { toISODate } from "../../store";
|
||||||
|
|
||||||
|
const startCalendarMonth = ref(new Date(customStartDate.value));
|
||||||
|
const endCalendarMonth = ref(new Date(customEndDate.value));
|
||||||
|
|
||||||
|
const getCalendarDays = (month: Date) => {
|
||||||
|
const y = month.getFullYear();
|
||||||
|
const m = month.getMonth();
|
||||||
|
const firstDay = new Date(y, m, 1).getDay();
|
||||||
|
const daysInMonth = new Date(y, m + 1, 0).getDate();
|
||||||
|
const days = [];
|
||||||
|
const padding = (firstDay + 6) % 7;
|
||||||
|
for (let i = 0; i < padding; i++) days.push(null);
|
||||||
|
for (let i = 1; i <= daysInMonth; i++) days.push(new Date(y, m, i));
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startCalendarDays = computed(() => getCalendarDays(startCalendarMonth.value));
|
||||||
|
const endCalendarDays = computed(() => getCalendarDays(endCalendarMonth.value));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="absolute inset-0 overflow-y-auto no-scrollbar p-10 bg-bg-main animate-in fade-in zoom-in-95 duration-200">
|
<div class="absolute inset-0 overflow-y-auto no-scrollbar p-10 bg-bg-main animate-in fade-in zoom-in-95 duration-200">
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="max-w-3xl mx-auto">
|
||||||
<div class="flex justify-between items-center mb-10">
|
<!-- Top Filters -->
|
||||||
<div class="flex bg-bg-input rounded-xl p-1.5 gap-1 border border-border-main/50 shadow-inner">
|
<div class="flex flex-col gap-6 mb-10">
|
||||||
<button @click="dashboardRange = 'today'" class="px-5 py-2 text-xs font-bold rounded-lg transition-all" :class="dashboardRange === 'today' ? 'bg-[#007AFF] shadow-lg text-white' : 'text-text-sec hover:text-text-main hover:bg-bg-card'">今日</button>
|
<div class="flex justify-between items-center">
|
||||||
<button @click="dashboardRange = '7days'" class="px-5 py-2 text-xs font-bold rounded-lg transition-all" :class="dashboardRange === '7days' ? 'bg-[#007AFF] shadow-lg text-white' : 'text-text-sec hover:text-text-main hover:bg-bg-card'">近 7 天</button>
|
<div class="flex bg-bg-input rounded-xl p-1.5 gap-1 border border-border-main/50 shadow-inner">
|
||||||
<button @click="dashboardRange = '30days'" class="px-5 py-2 text-xs font-bold rounded-lg transition-all" :class="dashboardRange === '30days' ? 'bg-[#007AFF] shadow-lg text-white' : 'text-text-sec hover:text-text-main hover:bg-bg-card'">近 30 天</button>
|
<button @click="dashboardRange = 'today'" class="px-5 py-2 text-xs font-bold rounded-lg transition-all" :class="dashboardRange === 'today' ? 'bg-[#007AFF] shadow-lg text-white' : 'text-text-sec hover:text-text-main hover:bg-bg-card'">今日</button>
|
||||||
</div>
|
<button @click="dashboardRange = '7days'" class="px-5 py-2 text-xs font-bold rounded-lg transition-all" :class="dashboardRange === '7days' ? 'bg-[#007AFF] shadow-lg text-white' : 'text-text-sec hover:text-text-main hover:bg-bg-card'">近 7 天</button>
|
||||||
<div v-if="dashboardRange !== 'today'" class="flex bg-bg-input rounded-xl p-1 gap-1 border border-border-main/50 shadow-inner">
|
<button @click="dashboardRange = '30days'" class="px-5 py-2 text-xs font-bold rounded-lg transition-all" :class="dashboardRange === '30days' ? 'bg-[#007AFF] shadow-lg text-white' : 'text-text-sec hover:text-text-main hover:bg-bg-card'">近 30 天</button>
|
||||||
<button @click="dailyAverageMode = 'natural'" class="px-3 py-1.5 text-[10px] font-bold rounded-lg transition-all" :class="dailyAverageMode === 'natural' ? 'bg-bg-card shadow-sm text-text-main' : 'text-text-sec hover:text-text-main'">自然天数 ({{dashboardStats.naturalDays}}天)</button>
|
<button @click="dashboardRange = 'custom'" class="px-5 py-2 text-xs font-bold rounded-lg transition-all" :class="dashboardRange === 'custom' ? 'bg-[#007AFF] shadow-lg text-white' : 'text-text-sec hover:text-text-main hover:bg-bg-card'">自定义</button>
|
||||||
<button @click="dailyAverageMode = 'recorded'" class="px-3 py-1.5 text-[10px] font-bold rounded-lg transition-all" :class="dailyAverageMode === 'recorded' ? 'bg-bg-card shadow-sm text-text-main' : 'text-text-sec hover:text-text-main'">记录天数 ({{dashboardStats.recordedDays}}天)</button>
|
</div>
|
||||||
</div>
|
<div v-if="dashboardRange !== 'today'" class="flex bg-bg-input rounded-xl p-1 gap-1 border border-border-main/50 shadow-inner">
|
||||||
|
<button @click="dailyAverageMode = 'natural'" class="px-3 py-1.5 text-[10px] font-bold rounded-lg transition-all" :class="dailyAverageMode === 'natural' ? 'bg-bg-card shadow-sm text-text-main' : 'text-text-sec hover:text-text-main'">自然天数 ({{dashboardStats.naturalDays}}天)</button>
|
||||||
|
<button @click="dailyAverageMode = 'recorded'" class="px-3 py-1.5 text-[10px] font-bold rounded-lg transition-all" :class="dailyAverageMode === 'recorded' ? 'bg-bg-card shadow-sm text-text-main' : 'text-text-sec hover:text-text-main'">记录天数 ({{dashboardStats.recordedDays}}天)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Date Pickers -->
|
||||||
|
<div v-if="dashboardRange === 'custom'" class="flex items-center gap-4 animate-in slide-in-from-top-2 duration-300">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<button @click="isStartCalendarOpen = !isStartCalendarOpen; isEndCalendarOpen = false" class="w-full bg-bg-card border border-border-main rounded-xl pl-10 pr-4 py-2.5 text-xs font-bold text-left flex items-center hover:bg-bg-input transition-all">
|
||||||
|
<Calendar :size="14" class="absolute left-3.5 text-[#007AFF]" />
|
||||||
|
<span class="text-text-sec mr-2">从</span> {{ customStartDate }}
|
||||||
|
</button>
|
||||||
|
<div v-if="isStartCalendarOpen" class="absolute top-full left-0 mt-2 bg-bg-card rounded-3xl shadow-2xl border border-border-main z-120 p-5 w-72 animate-in fade-in zoom-in-95">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<button @click="startCalendarMonth = new Date(startCalendarMonth.getFullYear(), startCalendarMonth.getMonth()-1, 1)" class="p-2 hover:bg-bg-input rounded-xl"><ChevronLeft :size="16"/></button>
|
||||||
|
<span class="text-xs font-black">{{ startCalendarMonth.getFullYear() }}年 {{ startCalendarMonth.getMonth()+1 }}月</span>
|
||||||
|
<button @click="startCalendarMonth = new Date(startCalendarMonth.getFullYear(), startCalendarMonth.getMonth()+1, 1)" class="p-2 hover:bg-bg-input rounded-xl"><ChevronRight :size="16"/></button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-1 text-center mb-2"><div v-for="d in ['一','二','三','四','五','六','日']" :key="d" class="text-[9px] font-bold text-text-sec">{{d}}</div></div>
|
||||||
|
<div class="grid grid-cols-7 gap-1">
|
||||||
|
<div v-for="(date, i) in startCalendarDays" :key="i" class="aspect-square flex items-center justify-center">
|
||||||
|
<button v-if="date" @click="customStartDate = toISODate(date); isStartCalendarOpen = false" class="w-7 h-7 rounded-full text-[10px] font-medium transition-all" :class="toISODate(date) === customStartDate ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ date.getDate() }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-text-sec font-black text-xs">至</div>
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<button @click="isEndCalendarOpen = !isEndCalendarOpen; isStartCalendarOpen = false" class="w-full bg-bg-card border border-border-main rounded-xl pl-10 pr-4 py-2.5 text-xs font-bold text-left flex items-center hover:bg-bg-input transition-all">
|
||||||
|
<Calendar :size="14" class="absolute left-3.5 text-[#007AFF]" />
|
||||||
|
<span class="text-text-sec mr-2">至</span> {{ customEndDate }}
|
||||||
|
</button>
|
||||||
|
<div v-if="isEndCalendarOpen" class="absolute top-full right-0 mt-2 bg-bg-card rounded-3xl shadow-2xl border border-border-main z-120 p-5 w-72 animate-in fade-in zoom-in-95">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<button @click="endCalendarMonth = new Date(endCalendarMonth.getFullYear(), endCalendarMonth.getMonth()-1, 1)" class="p-2 hover:bg-bg-input rounded-xl"><ChevronLeft :size="16"/></button>
|
||||||
|
<span class="text-xs font-black">{{ endCalendarMonth.getFullYear() }}年 {{ endCalendarMonth.getMonth()+1 }}月</span>
|
||||||
|
<button @click="endCalendarMonth = new Date(endCalendarMonth.getFullYear(), endCalendarMonth.getMonth()+1, 1)" class="p-2 hover:bg-bg-input rounded-xl"><ChevronRight :size="16"/></button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-1 text-center mb-2"><div v-for="d in ['一','二','三','四','五','六','日']" :key="d" class="text-[9px] font-bold text-text-sec">{{d}}</div></div>
|
||||||
|
<div class="grid grid-cols-7 gap-1">
|
||||||
|
<div v-for="(date, i) in endCalendarDays" :key="i" class="aspect-square flex items-center justify-center">
|
||||||
|
<button v-if="date" @click="customEndDate = toISODate(date); isEndCalendarOpen = false" class="w-7 h-7 rounded-full text-[10px] font-medium transition-all" :class="toISODate(date) === customEndDate ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ date.getDate() }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Overview Cards -->
|
||||||
<div class="grid grid-cols-2 gap-6 mb-10">
|
<div class="grid grid-cols-2 gap-6 mb-10">
|
||||||
<div class="bg-bg-card border border-border-main rounded-3xl p-6 shadow-sm flex flex-col justify-center">
|
<div class="bg-bg-card border border-border-main rounded-3xl p-6 shadow-sm flex flex-col justify-center">
|
||||||
<div class="text-xs font-bold text-text-sec mb-2">总记录时长</div>
|
<div class="text-xs font-bold text-text-sec mb-2">总记录时长</div>
|
||||||
@@ -32,6 +102,7 @@ import {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Visual Bar -->
|
||||||
<div class="mb-12 bg-bg-card border border-border-main p-6 rounded-3xl shadow-sm">
|
<div class="mb-12 bg-bg-card border border-border-main p-6 rounded-3xl shadow-sm">
|
||||||
<h3 class="text-sm font-bold text-text-sec mb-4 uppercase tracking-wider">占比总览</h3>
|
<h3 class="text-sm font-bold text-text-sec mb-4 uppercase tracking-wider">占比总览</h3>
|
||||||
<div class="h-6 w-full bg-bg-input rounded-full overflow-hidden flex shadow-inner mb-4 border border-border-main/30">
|
<div class="h-6 w-full bg-bg-input rounded-full overflow-hidden flex shadow-inner mb-4 border border-border-main/30">
|
||||||
@@ -45,6 +116,7 @@ import {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Detailed List -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="text-sm font-bold text-text-sec mb-4 uppercase tracking-wider">时间分布明细</h3>
|
<h3 class="text-sm font-bold text-text-sec mb-4 uppercase tracking-wider">时间分布明细</h3>
|
||||||
<div v-for="tag in dashboardStats.mainTags" :key="tag.id" class="bg-bg-card border border-border-main rounded-3xl p-5 shadow-sm transition-all hover:border-border-main/80 hover:shadow-md">
|
<div v-for="tag in dashboardStats.mainTags" :key="tag.id" class="bg-bg-card border border-border-main rounded-3xl p-5 shadow-sm transition-all hover:border-border-main/80 hover:shadow-md">
|
||||||
@@ -62,14 +134,15 @@ import {
|
|||||||
<span class="bg-bg-input/50 px-2 py-1 rounded-md border border-border-main/30 text-[#007AFF]">日均 {{ formatMinutes(tag.dailyAverage) }}</span>
|
<span class="bg-bg-input/50 px-2 py-1 rounded-md border border-border-main/30 text-[#007AFF]">日均 {{ formatMinutes(tag.dailyAverage) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub-tags -->
|
||||||
<div v-if="tag.subTags.length > 0" class="pl-7 space-y-3 pt-4 border-t border-border-main/50">
|
<div v-if="tag.subTags.length > 0" class="pl-7 space-y-3 pt-4 border-t border-border-main/50">
|
||||||
<div v-for="sub in tag.subTags" :key="sub.id" class="flex justify-between items-center group gap-4">
|
<div v-for="sub in tag.subTags" :key="sub.id" class="flex justify-between items-center group gap-4">
|
||||||
<span class="text-xs text-text-main font-bold w-16 truncate">{{ getTagName(sub.id) }}</span>
|
<span class="text-xs text-text-main font-bold w-16 truncate">{{ getTagName(sub.id) }}</span>
|
||||||
<div class="flex items-center gap-3 flex-1 justify-end">
|
<div class="flex items-center gap-3 flex-1 justify-end">
|
||||||
<div class="w-full max-w-50 h-1.5 bg-bg-input rounded-full overflow-hidden shadow-inner">
|
<div class="w-full max-w-[200px] h-1.5 bg-bg-input rounded-full overflow-hidden shadow-inner">
|
||||||
<div class="h-full rounded-full opacity-80" :style="{ width: sub.percentage + '%', backgroundColor: getTagColor(tag.id) }"></div>
|
<div class="h-full rounded-full opacity-80" :style="{ width: sub.percentage + '%', backgroundColor: getTagColor(tag.id) }"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[10px] font-bold text-text-sec whitespace-nowrap min-w-10 text-right">{{ formatMinutes(sub.total) }}</span>
|
<span class="text-[10px] font-bold text-text-sec whitespace-nowrap min-w-[40px] text-right">{{ formatMinutes(sub.total) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,9 +6,16 @@ import { currentDate, getTagColor, getTagName, formatMinutes, parseISODate, toIS
|
|||||||
// Re-export for easier access in Dashboard component
|
// Re-export for easier access in Dashboard component
|
||||||
export { getTagColor, getTagName, formatMinutes };
|
export { getTagColor, getTagName, formatMinutes };
|
||||||
|
|
||||||
export const dashboardRange = ref<'today' | '7days' | '30days'>('today');
|
export const dashboardRange = ref<'today' | '7days' | '30days' | 'custom'>('today');
|
||||||
export const dashboardStartDate = ref(currentDate.value);
|
export const dashboardStartDate = ref(currentDate.value);
|
||||||
export const dashboardEndDate = ref(currentDate.value);
|
export const dashboardEndDate = ref(currentDate.value);
|
||||||
|
|
||||||
|
// Dedicated memory for custom dates so they don't get overwritten by auto modes
|
||||||
|
export const customStartDate = ref(currentDate.value);
|
||||||
|
export const customEndDate = ref(currentDate.value);
|
||||||
|
|
||||||
|
export const isStartCalendarOpen = ref(false);
|
||||||
|
export const isEndCalendarOpen = ref(false);
|
||||||
export const dashboardEvents = ref<DBEvent[]>([]);
|
export const dashboardEvents = ref<DBEvent[]>([]);
|
||||||
export const dailyAverageMode = ref<'natural' | 'recorded'>('natural');
|
export const dailyAverageMode = ref<'natural' | 'recorded'>('natural');
|
||||||
|
|
||||||
@@ -17,6 +24,13 @@ export const loadDashboardEvents = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
watch([dashboardRange, currentDate], () => {
|
watch([dashboardRange, currentDate], () => {
|
||||||
|
if (dashboardRange.value === 'custom') {
|
||||||
|
dashboardStartDate.value = customStartDate.value;
|
||||||
|
dashboardEndDate.value = customEndDate.value;
|
||||||
|
loadDashboardEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const end = parseISODate(currentDate.value);
|
const end = parseISODate(currentDate.value);
|
||||||
let start = parseISODate(currentDate.value);
|
let start = parseISODate(currentDate.value);
|
||||||
|
|
||||||
@@ -31,6 +45,15 @@ watch([dashboardRange, currentDate], () => {
|
|||||||
loadDashboardEvents();
|
loadDashboardEvents();
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Specific trigger for custom range changes
|
||||||
|
watch([customStartDate, customEndDate], () => {
|
||||||
|
if (dashboardRange.value === 'custom') {
|
||||||
|
dashboardStartDate.value = customStartDate.value;
|
||||||
|
dashboardEndDate.value = customEndDate.value;
|
||||||
|
loadDashboardEvents();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Auto-refresh dashboard when a refresh signal is received (event added/deleted)
|
// Auto-refresh dashboard when a refresh signal is received (event added/deleted)
|
||||||
watch(refreshSignal, () => {
|
watch(refreshSignal, () => {
|
||||||
if (viewMode.value === 'dashboard') {
|
if (viewMode.value === 'dashboard') {
|
||||||
|
|||||||
Reference in New Issue
Block a user