!
diff --git a/src/components/Setup.vue b/src/components/Setup.vue
new file mode 100644
index 0000000..835122e
--- /dev/null
+++ b/src/components/Setup.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
diff --git a/src/components/modals/ExportModal.vue b/src/components/modals/ExportModal.vue
new file mode 100644
index 0000000..f1f22ba
--- /dev/null
+++ b/src/components/modals/ExportModal.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
导出记录
+
+
+
+
+
+
+
+
+
+
+ {{ exportStartMonth.getFullYear() }}年 {{ exportStartMonth.getMonth()+1 }}月
+
+
+
+
+
+
+
+
+
+
+
至
+
+
+
+
+
+ {{ exportEndMonth.getFullYear() }}年 {{ exportEndMonth.getMonth()+1 }}月
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/modals/SettingsModal.vue b/src/components/modals/SettingsModal.vue
new file mode 100644
index 0000000..6a51e9a
--- /dev/null
+++ b/src/components/modals/SettingsModal.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/modals/TagManager.vue b/src/components/modals/TagManager.vue
new file mode 100644
index 0000000..b6f0dcf
--- /dev/null
+++ b/src/components/modals/TagManager.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
diff --git a/src/components/views/Dashboard.vue b/src/components/views/Dashboard.vue
new file mode 100644
index 0000000..9793424
--- /dev/null
+++ b/src/components/views/Dashboard.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
总记录时长
+
{{ 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) }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/views/Preview.vue b/src/components/views/Preview.vue
new file mode 100644
index 0000000..5723952
--- /dev/null
+++ b/src/components/views/Preview.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/src/store/dashboardStore.ts b/src/store/dashboardStore.ts
new file mode 100644
index 0000000..6d2634d
--- /dev/null
+++ b/src/store/dashboardStore.ts
@@ -0,0 +1,75 @@
+import { ref, computed, watch } from "vue";
+import { invoke } from "@tauri-apps/api/core";
+import { DBEvent } from "../types";
+import { currentDate, getTagColor, getTagName, formatMinutes } from "./index";
+
+// Re-export for easier access in Dashboard component
+export { getTagColor, getTagName, formatMinutes };
+
+export const dashboardRange = ref<'today' | '7days' | '30days'>('today');
+export const dashboardStartDate = ref(currentDate.value);
+export const dashboardEndDate = ref(currentDate.value);
+export const dashboardEvents = ref
([]);
+export const dailyAverageMode = ref<'natural' | 'recorded'>('natural');
+
+export const loadDashboardEvents = async () => {
+ dashboardEvents.value = await invoke("get_events_range", { startDate: dashboardStartDate.value, endDate: dashboardEndDate.value });
+};
+
+watch([dashboardRange, currentDate], () => {
+ 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');
+ loadDashboardEvents();
+}, { immediate: true });
+
+export 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 };
+});
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..730b9b1
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,75 @@
+import { ref, computed } from "vue";
+import { invoke } from "@tauri-apps/api/core";
+import { Tag, DBEvent, TimelineItem, Toast } from "../types";
+
+export const TIME_OFFSET_MINUTES = 180;
+export const TOTAL_MINUTES = 1440;
+
+// --- Config State ---
+export const isSetupComplete = ref(false);
+export const savePath = ref("");
+export const dbPath = ref("");
+export const retainDays = ref(30);
+export const captureInterval = ref(60);
+export const timelineZoom = ref(1.5);
+export const theme = ref("system");
+
+// --- Global UI State ---
+export const isPaused = ref(false);
+export const currentDate = ref(new Date(Date.now() - TIME_OFFSET_MINUTES * 60000).toLocaleDateString('sv'));
+export const viewMode = ref<'preview' | 'dashboard'>('preview');
+export const isFullscreen = ref(false);
+export const previewSrc = ref("");
+export const selectedImage = ref(null);
+export const lockedImage = ref(null);
+
+// --- Data State ---
+export const tags = ref([]);
+export const dayEvents = ref([]);
+export const timelineImages = ref([]);
+
+// --- Global Toast ---
+export const toast = ref({ message: "", type: "success", visible: false });
+export const showToast = (message: string, type: "success" | "error" = "success") => {
+ toast.value = { message, type, visible: true };
+ setTimeout(() => { toast.value.visible = false; }, 3000);
+};
+
+// --- Computed Helpers ---
+export const mainTags = computed(() => tags.value.filter(t => t.parent_id === null));
+export const getSubTags = (parentId: number) => tags.value.filter(t => t.parent_id === parentId);
+export const getTagColor = (tagId: number) => tags.value.find(t => t.id === tagId)?.color || "#007AFF";
+export const getTagName = (tagId: number | null) => tags.value.find(t => t.id === tagId)?.name || "-- 无 --";
+
+// --- Time Helper Functions ---
+export const logicalMinutesToTime = (min: number) => {
+ let t = (min + TIME_OFFSET_MINUTES) % 1440;
+ const h = Math.floor(t / 60); const m = Math.floor(t % 60);
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
+};
+
+export const logicalMinutesFromTime = (timeStr: string) => {
+ const [h, m] = timeStr.split(":").map(Number);
+ let total = h * 60 + m;
+ if (h < 3) total += 1440;
+ return total - TIME_OFFSET_MINUTES;
+};
+
+export const formatDuration = (start: number, end: number) => {
+ let diff = end - start;
+ if (diff < 0) diff += 1440;
+ const h = Math.floor(diff / 60);
+ const m = diff % 60;
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
+};
+
+export 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`;
+};
+
+// --- Shared Actions ---
+export const loadTags = async () => { tags.value = await invoke("get_tags"); };
+export const loadEvents = async () => { dayEvents.value = await invoke("get_events", { date: currentDate.value }); };
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..c783646
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,28 @@
+export interface Tag {
+ id: number;
+ name: string;
+ parent_id: number | null;
+ color: string;
+}
+
+export interface DBEvent {
+ id: number;
+ date: string;
+ start_minute: number;
+ end_minute: number;
+ main_tag_id: number;
+ sub_tag_id: number | null;
+ content: string;
+}
+
+export interface TimelineItem {
+ time: string;
+ path: string;
+ isNextDay?: boolean;
+}
+
+export interface Toast {
+ message: string;
+ type: "success" | "error";
+ visible: boolean;
+}