diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 094120a..6c50eda 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -103,12 +103,32 @@ pub fn get_events(path: &str, date: &str) -> anyhow::Result> { content: row.get(6)?, }) })?; - - let mut results = Vec::new(); + let mut events = Vec::new(); for event in event_iter { - results.push(event?); + events.push(event?); } - Ok(results) + Ok(events) +} + +pub fn get_events_range(path: &str, start_date: &str, end_date: &str) -> anyhow::Result> { + let conn = Connection::open(path)?; + let mut stmt = conn.prepare("SELECT id, date, start_minute, end_minute, main_tag_id, sub_tag_id, content FROM events WHERE date >= ?1 AND date <= ?2 ORDER BY date, start_minute")?; + let event_iter = stmt.query_map(params![start_date, end_date], |row| { + Ok(Event { + id: row.get(0)?, + date: row.get(1)?, + start_minute: row.get(2)?, + end_minute: row.get(3)?, + main_tag_id: row.get(4)?, + sub_tag_id: row.get(5)?, + content: row.get(6)?, + }) + })?; + let mut events = Vec::new(); + for event in event_iter { + events.push(event?); + } + Ok(events) } pub fn save_event(path: &str, event: Event) -> anyhow::Result { diff --git a/src-tauri/src/engine.rs b/src-tauri/src/engine.rs index ba7a969..2bd8609 100644 --- a/src-tauri/src/engine.rs +++ b/src-tauri/src/engine.rs @@ -51,6 +51,13 @@ pub fn get_events(state: tauri::State<'_, AppState>, date: String) -> Result, start_date: String, end_date: String) -> Result, String> { + let path = state.db_path.lock().unwrap(); + let path = path.as_ref().ok_or("Database path not set")?; + crate::db::get_events_range(path, &start_date, &end_date).map_err(|e| e.to_string()) +} + #[tauri::command] pub fn save_event(state: tauri::State<'_, AppState>, event: crate::db::Event) -> Result { let path = state.db_path.lock().unwrap(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 65aa1fe..76b869d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -28,6 +28,7 @@ pub fn run() { engine::add_tag, engine::delete_tag, engine::get_events, + engine::get_events_range, engine::save_event, engine::delete_event ]) diff --git a/src/App.vue b/src/App.vue index 9e36ec8..f5a5f77 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,9 +2,10 @@ import { ref, onMounted, onUnmounted, computed, nextTick } from "vue"; import { load } from "@tauri-apps/plugin-store"; import { open, save } from "@tauri-apps/plugin-dialog"; +import { writeTextFile } from "@tauri-apps/plugin-fs"; 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 } from "lucide-vue-next"; +import { Tag as TagIcon, FolderOpen, Settings, Play, Pause, Maximize2, X, RefreshCw, Plus, Trash2, ChevronDown, ChevronLeft, ChevronRight, Calendar, Download } from "lucide-vue-next"; // --- Types --- interface Tag { id: number; name: string; parent_id: number | null; color: string; } @@ -344,6 +345,64 @@ const resetTagForm = () => { newTagName.value = ""; newTagParent.value = null; n // --- Custom Select State --- const isTagSelectOpen = ref(false); + +// --- Export State --- +const isExportModalOpen = ref(false); +const exportStartDate = ref(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toLocaleDateString('sv')); +const exportEndDate = ref(currentDate.value); +const exportSelectedTags = ref([]); + +const toggleExportTag = (tagId: number) => { + if (exportSelectedTags.value.includes(tagId)) { + exportSelectedTags.value = exportSelectedTags.value.filter(id => id !== tagId); + } else { + exportSelectedTags.value.push(tagId); + } +}; + +const selectAllTags = () => { + exportSelectedTags.value = tags.value.map(t => t.id); +}; + +const openExportModal = () => { + exportEndDate.value = currentDate.value; + selectAllTags(); + isExportModalOpen.value = true; +}; + +const handleExport = async () => { + try { + const events: DBEvent[] = await invoke("get_events_range", { startDate: exportStartDate.value, endDate: exportEndDate.value }); + const filteredEvents = events.filter(e => exportSelectedTags.value.includes(e.main_tag_id) || (e.sub_tag_id && exportSelectedTags.value.includes(e.sub_tag_id))); + + if (filteredEvents.length === 0) { + showToast("所选范围内没有找到匹配的记录", "error"); + return; + } + + const savePath = await save({ + filters: [{ name: "CSV 逗号分隔值文件", extensions: ["csv"] }], + defaultPath: `ChronoSnap_Export_${exportStartDate.value}_${exportEndDate.value}.csv` + }); + + if (savePath) { + let csvContent = "日期,开始时间,结束时间,主标签,副标签,事件内容\n"; + for (const e of filteredEvents) { + const start = logicalMinutesToTime(e.start_minute); + const end = logicalMinutesToTime(e.end_minute); + const mainTag = getTagName(e.main_tag_id); + const subTag = getTagName(e.sub_tag_id); + const content = `"${e.content.replace(/"/g, '""')}"`; + csvContent += `${e.date},${start},${end},${mainTag},${subTag},${content}\n`; + } + await writeTextFile(savePath, "\uFEFF" + csvContent); + isExportModalOpen.value = false; + showToast("导出成功"); + } + } catch (e) { + showToast("导出失败: " + e, "error"); + } +};