refactor
This commit is contained in:
126
src/components/modals/ExportModal.vue
Normal file
126
src/components/modals/ExportModal.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { X, Calendar, ChevronLeft, ChevronRight } from "lucide-vue-next";
|
||||
import { currentDate, showToast, getTagName, logicalMinutesToTime } from "../../store";
|
||||
import { DBEvent } from "../../types";
|
||||
|
||||
defineEmits(['close']);
|
||||
|
||||
const exportStartDate = ref(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toLocaleDateString('sv'));
|
||||
const exportEndDate = ref(currentDate.value);
|
||||
const exportStartMonth = ref(new Date(exportStartDate.value));
|
||||
const exportEndMonth = ref(new Date(exportEndDate.value));
|
||||
const isExportStartCalendarOpen = ref(false);
|
||||
const isExportEndCalendarOpen = ref(false);
|
||||
|
||||
const exportStartCalendarDays = computed(() => {
|
||||
const y = exportStartMonth.value.getFullYear();
|
||||
const m = exportStartMonth.value.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 exportEndCalendarDays = computed(() => {
|
||||
const y = exportEndMonth.value.getFullYear();
|
||||
const m = exportEndMonth.value.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 handleExport = async () => {
|
||||
try {
|
||||
const events: DBEvent[] = await invoke("get_events_range", { startDate: exportStartDate.value, endDate: exportEndDate.value });
|
||||
if (events.length === 0) {
|
||||
showToast("所选范围内没有找到记录", "error");
|
||||
return;
|
||||
}
|
||||
const savePath = await save({
|
||||
filters: [{ name: "JSON 文件", extensions: ["json"] }],
|
||||
defaultPath: `ChronoSnap_Export_${exportStartDate.value}_${exportEndDate.value}.json`
|
||||
});
|
||||
if (savePath) {
|
||||
const exportData = events.map(e => ({
|
||||
date: e.date,
|
||||
start_time: logicalMinutesToTime(e.start_minute),
|
||||
end_time: logicalMinutesToTime(e.end_minute),
|
||||
main_tag: getTagName(e.main_tag_id),
|
||||
sub_tag: getTagName(e.sub_tag_id),
|
||||
content: e.content
|
||||
}));
|
||||
await invoke("write_file", { path: savePath, content: JSON.stringify(exportData, null, 2) });
|
||||
showToast("导出成功");
|
||||
}
|
||||
} catch (e) {
|
||||
showToast("导出失败: " + e, "error");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 z-100 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="$emit('close')">
|
||||
<div class="bg-bg-card rounded-[40px] shadow-2xl w-full max-w-2xl overflow-visible flex flex-col animate-in fade-in zoom-in duration-200">
|
||||
<div class="p-8 border-b flex justify-between items-center"><h2 class="text-2xl font-bold">导出记录</h2><button @click="$emit('close')"><X :size="24" /></button></div>
|
||||
<div class="p-10 space-y-8">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-bold text-text-sec">日期范围</label>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="relative flex-1">
|
||||
<button @click="isExportStartCalendarOpen = !isExportStartCalendarOpen; isExportEndCalendarOpen = false" class="w-full bg-bg-input rounded-xl pl-10 pr-4 py-3 text-sm outline-none border border-transparent focus:border-[#007AFF] transition-all text-left flex items-center">
|
||||
<Calendar :size="16" class="absolute left-4 text-text-sec" />
|
||||
{{ exportStartDate }}
|
||||
</button>
|
||||
<div v-if="isExportStartCalendarOpen" class="absolute top-full left-0 mt-2 bg-bg-card rounded-3xl shadow-2xl border border-border-main z-120 p-5 animate-in fade-in zoom-in-95 duration-200 w-72">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<button @click="exportStartMonth = new Date(exportStartMonth.getFullYear(), exportStartMonth.getMonth()-1, 1)" class="p-2 hover:bg-bg-input rounded-xl"><ChevronLeft :size="18"/></button>
|
||||
<span class="text-sm font-black">{{ exportStartMonth.getFullYear() }}年 {{ exportStartMonth.getMonth()+1 }}月</span>
|
||||
<button @click="exportStartMonth = new Date(exportStartMonth.getFullYear(), exportStartMonth.getMonth()+1, 1)" class="p-2 hover:bg-bg-input rounded-xl"><ChevronRight :size="18"/></button>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-1 text-center mb-2"><div v-for="d in ['一','二','三','四','五','六','日']" :key="d" class="text-[10px] font-bold text-text-sec">{{d}}</div></div>
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<div v-for="(date, i) in exportStartCalendarDays" :key="i" class="aspect-square flex items-center justify-center">
|
||||
<button v-if="date" @click="exportStartDate = date.toLocaleDateString('sv'); isExportStartCalendarOpen = false" class="w-8 h-8 rounded-full text-xs font-medium transition-all" :class="date.toLocaleDateString('sv') === exportStartDate ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ date.getDate() }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-text-sec font-black text-xs">至</span>
|
||||
<div class="relative flex-1">
|
||||
<button @click="isExportEndCalendarOpen = !isExportEndCalendarOpen; isExportStartCalendarOpen = false" class="w-full bg-bg-input rounded-xl pl-10 pr-4 py-3 text-sm outline-none border border-transparent focus:border-[#007AFF] transition-all text-left flex items-center">
|
||||
<Calendar :size="16" class="absolute left-4 text-text-sec" />
|
||||
{{ exportEndDate }}
|
||||
</button>
|
||||
<div v-if="isExportEndCalendarOpen" class="absolute top-full right-0 mt-2 bg-bg-card rounded-3xl shadow-2xl border border-border-main z-120 p-5 animate-in fade-in zoom-in-95 duration-200 w-72">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<button @click="exportEndMonth = new Date(exportEndMonth.getFullYear(), exportEndMonth.getMonth()-1, 1)" class="p-2 hover:bg-bg-input rounded-xl"><ChevronLeft :size="18"/></button>
|
||||
<span class="text-sm font-black">{{ exportEndMonth.getFullYear() }}年 {{ exportEndMonth.getMonth()+1 }}月</span>
|
||||
<button @click="exportEndMonth = new Date(exportEndMonth.getFullYear(), exportEndMonth.getMonth()+1, 1)" class="p-2 hover:bg-bg-input rounded-xl"><ChevronRight :size="18"/></button>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-1 text-center mb-2"><div v-for="d in ['一','二','三','四','五','六','日']" :key="d" class="text-[10px] font-bold text-text-sec">{{d}}</div></div>
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<div v-for="(date, i) in exportEndCalendarDays" :key="i" class="aspect-square flex items-center justify-center">
|
||||
<button v-if="date" @click="exportEndDate = date.toLocaleDateString('sv'); isExportEndCalendarOpen = false" class="w-8 h-8 rounded-full text-xs font-medium transition-all" :class="date.toLocaleDateString('sv') === exportEndDate ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ date.getDate() }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="handleExport" :disabled="!exportStartDate || !exportEndDate" class="w-full bg-[#007AFF] text-white py-4 rounded-2xl font-bold shadow-lg shadow-[#007AFF]/20 active:scale-95 transition-all disabled:opacity-50 disabled:active:scale-100">导出为 JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
66
src/components/modals/SettingsModal.vue
Normal file
66
src/components/modals/SettingsModal.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { open, save } from "@tauri-apps/plugin-dialog";
|
||||
import { X, FolderOpen, Plus } from "lucide-vue-next";
|
||||
import { savePath, dbPath, theme, captureInterval, retainDays } from "../../store";
|
||||
import { load as loadStore } from "@tauri-apps/plugin-store";
|
||||
|
||||
defineEmits(['close']);
|
||||
|
||||
const updateSettings = async () => {
|
||||
const store = await loadStore("config.json");
|
||||
await store.set("savePath", savePath.value);
|
||||
await store.set("dbPath", dbPath.value);
|
||||
await store.set("retainDays", retainDays.value);
|
||||
await store.set("captureInterval", captureInterval.value);
|
||||
await store.set("theme", theme.value);
|
||||
await store.save();
|
||||
await invoke("update_db_path", { path: dbPath.value });
|
||||
await invoke("update_interval", { seconds: captureInterval.value });
|
||||
|
||||
if (theme.value === 'dark') document.documentElement.classList.add('dark');
|
||||
else if (theme.value === 'light') document.documentElement.classList.remove('dark');
|
||||
else {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) document.documentElement.classList.add('dark');
|
||||
else document.documentElement.classList.remove('dark');
|
||||
}
|
||||
};
|
||||
|
||||
const selectFolder = async () => {
|
||||
const s = await open({ directory: true });
|
||||
if (s) { savePath.value = s as string; await updateSettings(); }
|
||||
};
|
||||
|
||||
const selectDBFile = async () => {
|
||||
const s = await open({ filters: [{ name: "SQLite", extensions: ["db"] }] });
|
||||
if (s) { dbPath.value = s as string; await updateSettings(); }
|
||||
};
|
||||
|
||||
const createDBFile = async () => {
|
||||
const s = await save({ filters: [{ name: "SQLite", extensions: ["db"] }] });
|
||||
if (s) { dbPath.value = s as string; await updateSettings(); }
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 z-100 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="$emit('close')">
|
||||
<div class="bg-bg-card rounded-[40px] shadow-2xl w-full max-md overflow-hidden flex flex-col">
|
||||
<div class="p-8 border-b flex justify-between items-center"><h2 class="text-2xl font-bold">设置</h2><button @click="$emit('close')"><X :size="24" /></button></div>
|
||||
<div class="p-10 space-y-8">
|
||||
<div class="space-y-3"><label class="text-xs font-bold text-text-sec">截图保存位置</label><div class="flex gap-2"><input type="text" readonly :value="savePath" class="flex-1 bg-bg-input rounded-xl px-4 py-2 text-sm outline-none" /><button @click="selectFolder" class="bg-bg-card border p-2 rounded-xl hover:border-[#007AFF] transition-all"><FolderOpen :size="18" /></button></div></div>
|
||||
<div class="space-y-3"><label class="text-xs font-bold text-text-sec">数据库文件</label><div class="flex gap-2"><input type="text" readonly :value="dbPath" class="flex-1 bg-bg-input rounded-xl px-4 py-2 text-sm outline-none" /><div class="flex gap-1"><button @click="selectDBFile" title="打开现有" class="bg-bg-card border p-2 rounded-xl hover:border-[#007AFF] transition-all"><FolderOpen :size="18" /></button><button @click="createDBFile" title="新建" class="bg-bg-card border p-2 rounded-xl hover:border-[#007AFF] transition-all"><Plus :size="18" /></button></div></div></div>
|
||||
|
||||
<div class="space-y-2"><div class="flex justify-between"><label class="text-xs font-bold">主题设置</label></div>
|
||||
<div class="flex bg-bg-input rounded-xl p-1 gap-1">
|
||||
<button @click="theme = 'light'; updateSettings()" class="flex-1 py-1.5 text-xs font-bold rounded-lg transition-all" :class="theme === 'light' ? 'bg-bg-card shadow-sm text-text-main' : 'text-text-sec hover:text-text-main'">浅色</button>
|
||||
<button @click="theme = 'dark'; updateSettings()" class="flex-1 py-1.5 text-xs font-bold rounded-lg transition-all" :class="theme === 'dark' ? 'bg-bg-card shadow-sm text-text-main' : 'text-text-sec hover:text-text-main'">深色</button>
|
||||
<button @click="theme = 'system'; updateSettings()" class="flex-1 py-1.5 text-xs font-bold rounded-lg transition-all" :class="theme === 'system' ? 'bg-bg-card shadow-sm text-text-main' : 'text-text-sec hover:text-text-main'">跟随系统</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2"><div class="flex justify-between"><label class="text-xs font-bold">截图间隔</label><span class="text-xs font-bold text-[#007AFF]">{{ captureInterval }}s</span></div><input type="range" v-model.number="captureInterval" min="60" max="600" step="10" @change="updateSettings" class="w-full h-1 bg-bg-hover accent-[#007AFF]" /></div>
|
||||
<div class="space-y-2"><div class="flex justify-between"><label class="text-xs font-bold">清理策略</label><span class="text-xs font-bold text-[#007AFF]">{{ retainDays }}天</span></div><input type="range" v-model.number="retainDays" min="1" max="180" @change="updateSettings" class="w-full h-1 bg-bg-hover accent-[#007AFF]" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
96
src/components/modals/TagManager.vue
Normal file
96
src/components/modals/TagManager.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { X, Trash2, ChevronRight, ChevronDown } from "lucide-vue-next";
|
||||
import { mainTags, getSubTags, getTagName, showToast, loadTags } from "../../store";
|
||||
|
||||
defineEmits(['close']);
|
||||
|
||||
const expandedMainTags = ref<number[]>([]);
|
||||
const toggleMainTag = (id: number) => {
|
||||
const index = expandedMainTags.value.indexOf(id);
|
||||
if (index > -1) expandedMainTags.value.splice(index, 1);
|
||||
else expandedMainTags.value.push(id);
|
||||
};
|
||||
|
||||
const isTagSelectOpen = ref(false);
|
||||
const newTagName = ref("");
|
||||
const newTagParent = ref<number | null>(null);
|
||||
const newTagColor = ref("#007AFF");
|
||||
|
||||
const handleAddTag = async () => {
|
||||
try {
|
||||
await invoke("add_tag", { name: newTagName.value, parentId: newTagParent.value, color: newTagColor.value });
|
||||
await loadTags();
|
||||
showToast("标签添加成功");
|
||||
newTagName.value = ""; newTagParent.value = null; newTagColor.value = "#007AFF";
|
||||
} catch (e) {
|
||||
showToast("添加失败: " + e, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTag = async (id: number) => {
|
||||
try {
|
||||
await invoke("delete_tag", { id });
|
||||
await loadTags();
|
||||
showToast("标签已删除");
|
||||
} catch (e: any) {
|
||||
if (e.toString().includes("FOREIGN KEY")) {
|
||||
showToast("该标签正在被使用,无法删除", "error");
|
||||
} else {
|
||||
showToast("删除失败: " + e, "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 z-100 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="$emit('close')">
|
||||
<div class="bg-bg-card rounded-[40px] shadow-2xl w-full max-w-2xl h-[80vh] overflow-hidden flex flex-col animate-in fade-in zoom-in duration-200">
|
||||
<div class="p-8 border-b flex justify-between items-center"><h2 class="text-2xl font-bold">标签管理</h2><button @click="$emit('close')"><X :size="24" /></button></div>
|
||||
<div class="flex-1 overflow-hidden p-10 flex gap-10">
|
||||
<div class="flex-1 overflow-y-auto space-y-2 pr-4 no-scrollbar">
|
||||
<div v-for="mt in mainTags" :key="mt.id" class="space-y-2">
|
||||
<div @click="toggleMainTag(mt.id)" class="flex items-center justify-between group p-2 hover:bg-bg-input rounded-xl cursor-pointer">
|
||||
<div class="flex items-center gap-3">
|
||||
<ChevronRight :size="14" class="text-text-sec transition-transform" :class="{ 'rotate-90': expandedMainTags.includes(mt.id) }" />
|
||||
<div class="w-4 h-4 rounded-full" :style="{ backgroundColor: mt.color }"></div>
|
||||
<span class="font-bold">{{ mt.name }}</span>
|
||||
</div>
|
||||
<button @click.stop="handleDeleteTag(mt.id)" class="text-[#FF3B30] opacity-0 group-hover:opacity-100"><Trash2 :size="16" /></button>
|
||||
</div>
|
||||
<div v-if="expandedMainTags.includes(mt.id)" class="ml-7 space-y-1 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<div v-for="st in getSubTags(mt.id)" :key="st.id" class="flex items-center justify-between group p-1.5 hover:bg-bg-input rounded-lg">
|
||||
<span class="text-sm">{{ st.name }}</span>
|
||||
<button @click="handleDeleteTag(st.id)" class="text-[#FF3B30] opacity-0 group-hover:opacity-100"><Trash2 :size="14" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-64 bg-bg-input p-6 rounded-3xl space-y-4 h-fit">
|
||||
<h3 class="font-bold text-xs text-text-sec uppercase">添加标签</h3>
|
||||
<div class="space-y-1">
|
||||
<label class="text-[10px] font-bold text-text-sec ml-1">标签名称</label>
|
||||
<input v-model="newTagName" placeholder="输入名称..." class="w-full bg-bg-card rounded-xl px-4 py-2.5 text-sm outline-none border border-transparent focus:border-[#007AFF] transition-all" />
|
||||
</div>
|
||||
<div class="space-y-1 relative">
|
||||
<label class="text-[10px] font-bold text-text-sec ml-1">父级标签</label>
|
||||
<button @click="isTagSelectOpen = !isTagSelectOpen" class="w-full bg-bg-card rounded-xl px-4 py-2.5 text-sm text-left flex justify-between items-center border border-transparent focus:border-[#007AFF] transition-all">
|
||||
<span>{{ getTagName(newTagParent) }}</span>
|
||||
<ChevronDown :size="14" class="text-text-sec transition-transform" :class="{ 'rotate-180': isTagSelectOpen }" />
|
||||
</button>
|
||||
<div v-if="isTagSelectOpen" class="absolute top-full left-0 right-0 mt-2 bg-bg-card rounded-2xl shadow-xl border border-border-main z-60 overflow-hidden py-2 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div @click="newTagParent = null; isTagSelectOpen = false" class="px-4 py-2 text-sm hover:bg-bg-input cursor-pointer" :class="{ 'text-[#007AFF] font-bold': newTagParent === null }">-- 无 --</div>
|
||||
<div v-for="t in mainTags" :key="t.id" @click="newTagParent = t.id; isTagSelectOpen = false" class="px-4 py-2 text-sm hover:bg-bg-input cursor-pointer" :class="{ 'text-[#007AFF] font-bold': newTagParent === t.id }">{{ t.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="newTagParent === null" class="space-y-1">
|
||||
<label class="text-[10px] font-bold text-text-sec ml-1">主题颜色</label>
|
||||
<div class="flex flex-wrap gap-2"><button v-for="c in ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE', '#5856D6']" :key="c" @click="newTagColor = c" class="w-5 h-5 rounded-full border-2 transition-all" :style="{ backgroundColor: c, borderColor: newTagColor === c ? '#1D1D1F' : 'transparent' }"></button></div>
|
||||
</div>
|
||||
<button @click="handleAddTag" :disabled="!newTagName" class="w-full bg-[#007AFF] text-white py-3 rounded-xl font-bold shadow-lg shadow-[#007AFF]/20 active:scale-95 transition-all">创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user