This commit is contained in:
Julian Freeman
2026-03-22 20:29:54 -04:00
parent 0af5f1c6a9
commit a504fb36e0

View File

@@ -19,7 +19,7 @@ const isSetupComplete = ref(false);
const savePath = ref("");
const dbPath = ref("");
const isPaused = ref(false);
const currentDate = ref(new Date().toISOString().split("T")[0]);
const currentDate = ref(new Date().toLocaleDateString('sv'));
const timelineImages = ref<TimelineItem[]>([]);
const selectedImage = ref<TimelineItem | null>(null);
const lockedImage = ref<TimelineItem | null>(null);
@@ -101,7 +101,7 @@ const openStartTimePicker = async () => {
isEndTimeOpen.value = false;
if (isStartTimeOpen.value) {
await nextTick();
const activeItems = document.querySelectorAll('.z-\\[120\\] .bg-\\[\\#007AFF\\].text-white');
const activeItems = document.querySelectorAll('.z-120 .bg-\\[\\#007AFF\\].text-white');
activeItems.forEach(el => el.scrollIntoView({ block: 'center' }));
}
};
@@ -111,7 +111,7 @@ const openEndTimePicker = async () => {
isStartTimeOpen.value = false;
if (isEndTimeOpen.value) {
await nextTick();
const activeItems = document.querySelectorAll('.z-\\[120\\] .bg-\\[\\#007AFF\\].text-white');
const activeItems = document.querySelectorAll('.z-120 .bg-\\[\\#007AFF\\].text-white');
activeItems.forEach(el => el.scrollIntoView({ block: 'center' }));
}
};
@@ -439,9 +439,9 @@ const handleExport = async () => {
<div class="h-screen w-screen flex flex-col overflow-hidden text-[#1D1D1F] bg-[#FBFBFD]">
<div v-if="!isSetupComplete" class="flex-1 flex items-center justify-center p-10">
<div class="bg-white p-12 rounded-[32px] shadow-2xl max-w-lg text-center border border-[#E5E5E7]">
<div class="bg-white p-12 rounded-4xl shadow-2xl max-w-lg text-center border border-[#E5E5E7]">
<Settings :size="40" class="text-[#007AFF] mx-auto mb-8" />
<h1 class="text-3xl font-bold mb-4">初始化 Chrono Snap</h1>
<h1 class="text-3xl font-bold mb-4">初始化设置</h1>
<div class="space-y-6 text-left mb-10">
<div class="space-y-2">
<label class="text-xs font-bold text-[#86868B]">截图保存目录</label>
@@ -451,9 +451,9 @@ const handleExport = async () => {
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-bold text-[#86868B]">SQLite 数据库文件</label>
<label class="text-xs font-bold text-[#86868B]">数据库文件</label>
<div class="flex gap-2">
<input type="text" readonly :value="dbPath" placeholder="请选择或创建 .db 文件..." class="flex-1 bg-[#F2F2F7] rounded-xl px-4 py-2.5 text-sm outline-none" />
<input type="text" readonly :value="dbPath" placeholder="请选择或创建数据库文件..." class="flex-1 bg-[#F2F2F7] rounded-xl px-4 py-2.5 text-sm outline-none" />
<div class="flex gap-1">
<button @click="selectDBFile" class="bg-white border border-[#E5E5E7] p-2.5 rounded-xl hover:border-[#007AFF]"><FolderOpen :size="18" /></button>
<button @click="createDBFile" class="bg-white border border-[#E5E5E7] p-2.5 rounded-xl hover:border-[#007AFF]"><Plus :size="18" /></button>
@@ -468,13 +468,13 @@ const handleExport = async () => {
<div v-else class="flex flex-1 overflow-hidden">
<div class="w-80 bg-[#F8FAFD] border-r border-[#E5E5E7] flex flex-col select-none relative">
<div class="p-6 bg-[#F8FAFD]/80 backdrop-blur-md sticky top-0 z-50 border-b border-[#E5E5E7]/50">
<div class="flex items-center justify-between mb-4"><h2 class="text-lg font-bold">历史活动</h2><button @click="loadTimeline(true); loadEvents()" class="p-2 hover:bg-white rounded-xl text-[#86868B]"><RefreshCw :size="18" /></button></div>
<div class="flex items-center justify-between mb-4"><h2 class="text-lg font-bold">瞬影 - 时间记录</h2><button @click="loadTimeline(true); loadEvents()" class="p-2 hover:bg-white rounded-xl text-[#86868B]"><RefreshCw :size="18" /></button></div>
<div ref="calendarRef" class="relative group">
<button @click="isCalendarOpen = !isCalendarOpen" class="w-full bg-white border border-[#E5E5E7] rounded-xl pl-11 pr-4 py-2.5 text-sm font-bold text-left flex items-center hover:bg-[#F2F2F7] transition-all">
<Calendar :size="16" class="absolute left-4 text-[#86868B]" />
{{ currentDate }}
</button>
<div v-if="isCalendarOpen" class="absolute top-full left-0 right-0 mt-2 bg-white rounded-[24px] shadow-2xl border border-[#E5E5E7] z-[100] p-5 animate-in fade-in zoom-in-95 duration-200">
<div v-if="isCalendarOpen" class="absolute top-full left-0 right-0 mt-2 bg-white rounded-3xl shadow-2xl border border-[#E5E5E7] z-100 p-5 animate-in fade-in zoom-in-95 duration-200">
<div class="flex items-center justify-between mb-4">
<button @click="calendarMonth = new Date(calendarMonth.getFullYear(), calendarMonth.getMonth()-1, 1)" class="p-2 hover:bg-[#F2F2F7] rounded-xl"><ChevronLeft :size="18"/></button>
<span class="text-sm font-black">{{ calendarMonth.getFullYear() }} {{ calendarMonth.getMonth()+1 }}</span>
@@ -524,9 +524,9 @@ const handleExport = async () => {
</div>
<!-- Modals -->
<div v-if="isEventModalOpen" class="fixed inset-0 z-[110] bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="isEventModalOpen = false">
<div v-if="isEventModalOpen" class="fixed inset-0 z-110 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="isEventModalOpen = false">
<div class="bg-white rounded-[40px] shadow-2xl w-full max-w-lg overflow-hidden flex flex-col">
<div class="p-8 border-b flex justify-between items-center"><h2 class="text-2xl font-bold">{{ editingEvent.id ? '编辑事件' : '新增记录' }}</h2><button @click="isEventModalOpen = false; isStartTimeOpen = false; isEndTimeOpen = false"><X :size="24" /></button></div>
<div class="p-8 border-b flex justify-between items-center"><h2 class="text-2xl font-bold">{{ editingEvent.id ? '编辑记录' : '新增记录' }}</h2><button @click="isEventModalOpen = false; isStartTimeOpen = false; isEndTimeOpen = false"><X :size="24" /></button></div>
<div class="p-10 space-y-8">
<div class="flex gap-4">
<!-- Start Time Custom Picker -->
@@ -537,9 +537,9 @@ const handleExport = async () => {
<!-- <Clock :size="14" class="text-[#86868B] block" /> -->
<span class="text-sm font-bold leading-none">{{ startTimeInput }}</span>
</button>
<div v-if="isStartTimeOpen" class="absolute top-full left-0 right-0 mt-2 bg-white rounded-3xl shadow-2xl border border-[#E5E5E7] z-[120] p-4 animate-in fade-in slide-in-from-top-2 flex gap-4 h-64">
<div v-if="isStartTimeOpen" class="absolute top-full left-0 right-0 mt-2 bg-white rounded-3xl shadow-2xl border border-[#E5E5E7] z-120 p-4 animate-in fade-in slide-in-from-top-2 flex gap-4 h-64">
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1">
<div class="text-[9px] font-bold text-sec mb-2 sticky top-0 bg-white py-1 text-center border-b border-[#E5E5E7]/30"></div>
<div class="text-[12px] font-bold text-sec mb-2 sticky top-0 bg-white py-1 text-center border-b border-[#E5E5E7]/30"></div>
<button v-for="h in 24" :key="h"
@click="editingEvent.start_minute = logicalMinutesFromTime(`${String((h-1+3)%24).padStart(2,'0')}:${String(editingEvent.start_minute % 60).padStart(2,'0')}`)"
class="py-2 text-[11px] rounded-lg transition-colors w-full text-center flex items-center justify-center"
@@ -548,7 +548,7 @@ const handleExport = async () => {
</div>
<div class="w-px bg-[#E5E5E7] my-2"></div>
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1">
<div class="text-[9px] font-bold text-sec mb-2 sticky top-0 bg-white py-1 text-center border-b border-[#E5E5E7]/30"></div>
<div class="text-[12px] font-bold text-sec mb-2 sticky top-0 bg-white py-1 text-center border-b border-[#E5E5E7]/30"></div>
<button v-for="m in 60" :key="m"
@click="editingEvent.start_minute = Math.floor(editingEvent.start_minute / 60) * 60 + (m-1)"
class="py-2 text-[11px] rounded-lg transition-colors w-full text-center flex items-center justify-center"
@@ -565,9 +565,9 @@ const handleExport = async () => {
<!-- <Clock :size="14" class="text-[#86868B] block" /> -->
<span class="text-sm font-bold leading-none">{{ endTimeInput }}</span>
</button>
<div v-if="isEndTimeOpen" class="absolute top-full left-0 right-0 mt-2 bg-white rounded-3xl shadow-2xl border border-[#E5E5E7] z-[120] p-4 animate-in fade-in slide-in-from-top-2 flex gap-4 h-64">
<div v-if="isEndTimeOpen" class="absolute top-full left-0 right-0 mt-2 bg-white rounded-3xl shadow-2xl border border-[#E5E5E7] z-120 p-4 animate-in fade-in slide-in-from-top-2 flex gap-4 h-64">
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1 text-center">
<div class="text-[9px] font-bold text-sec mb-2 sticky top-0 bg-white py-1"></div>
<div class="text-[12px] font-bold text-sec mb-2 sticky top-0 bg-white py-1"></div>
<button v-for="h in 24" :key="h"
@click="editingEvent.end_minute = logicalMinutesFromTime(`${String((h-1+3)%24).padStart(2,'0')}:${String(editingEvent.end_minute % 60).padStart(2,'0')}`)"
class="py-2 text-xs rounded-lg transition-colors w-full text-center"
@@ -576,7 +576,7 @@ const handleExport = async () => {
</div>
<div class="w-px bg-[#E5E5E7] my-2"></div>
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1 text-center">
<div class="text-[9px] font-bold text-sec mb-2 sticky top-0 bg-white py-1"></div>
<div class="text-[12px] font-bold text-sec mb-2 sticky top-0 bg-white py-1"></div>
<button v-for="m in 60" :key="m"
@click="editingEvent.end_minute = Math.floor(editingEvent.end_minute / 60) * 60 + (m-1)"
class="py-2 text-xs rounded-lg transition-colors w-full text-center"
@@ -588,16 +588,16 @@ const handleExport = async () => {
</div>
<div class="space-y-4">
<div class="space-y-2"><label class="text-[10px] font-bold text-[#86868B]">主标签</label>
<div class="grid grid-cols-4 gap-2"><button v-for="tag in mainTags" :key="tag.id" @click="editingEvent.main_tag_id = tag.id; editingEvent.sub_tag_id = null" class="px-2 py-2 rounded-xl text-[10px] font-bold border-2" :style="{ backgroundColor: editingEvent.main_tag_id === tag.id ? tag.color : 'transparent', borderColor: tag.color, color: editingEvent.main_tag_id === tag.id ? 'white' : tag.color }">{{ tag.name }}</button></div>
<div class="grid grid-cols-4 gap-2"><button v-for="tag in mainTags" :key="tag.id" @click="editingEvent.main_tag_id = tag.id; editingEvent.sub_tag_id = null" class="px-2 py-2 rounded-xl text-[12px] font-bold border-2" :style="{ backgroundColor: editingEvent.main_tag_id === tag.id ? tag.color : 'transparent', borderColor: tag.color, color: editingEvent.main_tag_id === tag.id ? 'white' : tag.color }">{{ tag.name }}</button></div>
</div>
<div v-if="editingEvent.main_tag_id && getSubTags(editingEvent.main_tag_id).length" class="space-y-2"><label class="text-[10px] font-bold text-[#86868B]">副标签</label>
<div class="flex flex-wrap gap-2"><button v-for="sub in getSubTags(editingEvent.main_tag_id)" :key="sub.id" @click="editingEvent.sub_tag_id = sub.id" class="px-3 py-1.5 rounded-lg text-[10px] font-medium" :class="editingEvent.sub_tag_id === sub.id ? 'bg-[#1D1D1F] text-white' : 'bg-[#F2F2F7] text-[#86868B]'">{{ sub.name }}</button></div>
<div class="flex flex-wrap gap-2"><button v-for="sub in getSubTags(editingEvent.main_tag_id)" :key="sub.id" @click="editingEvent.sub_tag_id = sub.id" class="px-3 py-1.5 rounded-lg text-[12px] font-medium" :class="editingEvent.sub_tag_id === sub.id ? 'bg-[#1D1D1F] text-white' : 'bg-[#F2F2F7] text-[#86868B]'">{{ sub.name }}</button></div>
</div>
<textarea
v-model="editingEvent.content"
placeholder="记录具体内容..."
@keydown.ctrl.enter="saveEvent"
class="w-full bg-[#F2F2F7] rounded-2xl p-4 text-sm min-h-[100px] outline-none border border-transparent focus:bg-white focus:border-[#E5E5E7] transition-all"
class="w-full bg-[#F2F2F7] rounded-2xl p-4 text-sm min-h-25 outline-none border border-transparent focus:bg-white focus:border-[#E5E5E7] transition-all"
></textarea>
</div>
<div class="flex gap-4 pt-4">
@@ -608,7 +608,7 @@ const handleExport = async () => {
</div>
</div>
<div v-if="isTagManagerOpen" class="fixed inset-0 z-[100] bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="isTagManagerOpen = false">
<div v-if="isTagManagerOpen" class="fixed inset-0 z-100 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="isTagManagerOpen = false">
<div class="bg-white 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="isTagManagerOpen = false"><X :size="24" /></button></div>
<div class="flex-1 overflow-hidden p-10 flex gap-10">
@@ -630,7 +630,7 @@ const handleExport = async () => {
<span>{{ getTagName(newTagParent) }}</span>
<ChevronDown :size="14" class="text-[#86868B] transition-transform" :class="{ 'rotate-180': isTagSelectOpen }" />
</button>
<div v-if="isTagSelectOpen" class="absolute top-full left-0 right-0 mt-2 bg-white rounded-2xl shadow-xl border border-[#E5E5E7] z-[60] overflow-hidden py-2 animate-in fade-in zoom-in-95 duration-200">
<div v-if="isTagSelectOpen" class="absolute top-full left-0 right-0 mt-2 bg-white rounded-2xl shadow-xl border border-[#E5E5E7] 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-[#F2F2F7] 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-[#F2F2F7] cursor-pointer" :class="{ 'text-[#007AFF] font-bold': newTagParent === t.id }">{{ t.name }}</div>
</div>
@@ -645,7 +645,7 @@ const handleExport = async () => {
</div>
</div>
<div v-if="isExportModalOpen" class="fixed inset-0 z-[100] bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="isExportModalOpen = false">
<div v-if="isExportModalOpen" class="fixed inset-0 z-100 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="isExportModalOpen = false">
<div class="bg-white 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="isExportModalOpen = false"><X :size="24" /></button></div>
<div class="p-10 space-y-8">
@@ -658,7 +658,7 @@ const handleExport = async () => {
<Calendar :size="16" class="absolute left-4 text-[#86868B]" />
{{ exportStartDate }}
</button>
<div v-if="isExportStartCalendarOpen" class="absolute top-full left-0 mt-2 bg-white rounded-[24px] shadow-2xl border border-[#E5E5E7] z-[120] p-5 animate-in fade-in zoom-in-95 duration-200 w-72">
<div v-if="isExportStartCalendarOpen" class="absolute top-full left-0 mt-2 bg-white rounded-3xl shadow-2xl border border-[#E5E5E7] 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-[#F2F2F7] rounded-xl"><ChevronLeft :size="18"/></button>
<span class="text-sm font-black">{{ exportStartMonth.getFullYear() }} {{ exportStartMonth.getMonth()+1 }}</span>
@@ -678,7 +678,7 @@ const handleExport = async () => {
<Calendar :size="16" class="absolute left-4 text-[#86868B]" />
{{ exportEndDate }}
</button>
<div v-if="isExportEndCalendarOpen" class="absolute top-full right-0 mt-2 bg-white rounded-[24px] shadow-2xl border border-[#E5E5E7] z-[120] p-5 animate-in fade-in zoom-in-95 duration-200 w-72">
<div v-if="isExportEndCalendarOpen" class="absolute top-full right-0 mt-2 bg-white rounded-3xl shadow-2xl border border-[#E5E5E7] 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-[#F2F2F7] rounded-xl"><ChevronLeft :size="18"/></button>
<span class="text-sm font-black">{{ exportEndMonth.getFullYear() }} {{ exportEndMonth.getMonth()+1 }}</span>
@@ -700,22 +700,22 @@ const handleExport = async () => {
</div>
</div>
<div v-if="isSettingsOpen" class="fixed inset-0 z-[100] bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="isSettingsOpen = false">
<div v-if="isSettingsOpen" class="fixed inset-0 z-100 bg-black/40 backdrop-blur-sm flex items-center justify-center p-6" @click.self="isSettingsOpen = false">
<div class="bg-white rounded-[40px] shadow-2xl w-full max-w-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="isSettingsOpen = false"><X :size="24" /></button></div>
<div class="p-10 space-y-8">
<div class="space-y-3"><label class="text-xs font-bold text-[#86868B]">截图保存位置</label><div class="flex gap-2"><input type="text" readonly :value="savePath" class="flex-1 bg-[#F2F2F7] rounded-xl px-4 py-2 text-sm outline-none" /><button @click="selectFolder(); updateSettings()" class="bg-white 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-[#86868B]">SQLite 数据库文件</label><div class="flex gap-2"><input type="text" readonly :value="dbPath" class="flex-1 bg-[#F2F2F7] rounded-xl px-4 py-2 text-sm outline-none" /><div class="flex gap-1"><button @click="selectDBFile().then(updateSettings)" title="打开现有" class="bg-white border p-2 rounded-xl hover:border-[#007AFF] transition-all"><FolderOpen :size="18" /></button><button @click="createDBFile().then(updateSettings)" title="新建" class="bg-white border p-2 rounded-xl hover:border-[#007AFF] transition-all"><Plus :size="18" /></button></div></div></div>
<div class="flex items-center justify-between p-4 bg-[#F2F2F7] rounded-3xl"><div class="space-y-0.5"><div class="font-bold">多屏合并</div><div class="text-[10px] text-[#86868B]">拼接所有屏幕</div></div><button @click="mergeScreens = !mergeScreens; updateSettings()" class="w-12 h-6 rounded-full relative transition-all" :class="mergeScreens ? 'bg-[#34C759]' : 'bg-[#E5E5E7]'"><div class="absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-all" :style="{ transform: mergeScreens ? 'translateX(24px)' : 'translateX(0)' }"></div></button></div>
<div class="space-y-3"><label class="text-xs font-bold text-[#86868B]">数据库文件</label><div class="flex gap-2"><input type="text" readonly :value="dbPath" class="flex-1 bg-[#F2F2F7] rounded-xl px-4 py-2 text-sm outline-none" /><div class="flex gap-1"><button @click="selectDBFile().then(updateSettings)" title="打开现有" class="bg-white border p-2 rounded-xl hover:border-[#007AFF] transition-all"><FolderOpen :size="18" /></button><button @click="createDBFile().then(updateSettings)" title="新建" class="bg-white border p-2 rounded-xl hover:border-[#007AFF] transition-all"><Plus :size="18" /></button></div></div></div>
<div class="flex items-center justify-between p-4 bg-[#F2F2F7] rounded-3xl"><div class="space-y-0.5"><div class="font-bold">多屏合并</div></div><button @click="mergeScreens = !mergeScreens; updateSettings()" class="w-12 h-6 rounded-full relative transition-all" :class="mergeScreens ? 'bg-[#34C759]' : 'bg-[#E5E5E7]'"><div class="absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-all" :style="{ transform: mergeScreens ? 'translateX(24px)' : 'translateX(0)' }"></div></button></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-[#E5E5E7] 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-[#E5E5E7] accent-[#007AFF]" /></div>
</div>
</div>
</div>
<div v-if="isFullscreen && previewSrc" class="fixed inset-0 z-[200] bg-black/95 flex items-center justify-center p-6 backdrop-blur-xl"><button @click="isFullscreen = false" class="absolute top-10 right-10 w-12 h-12 bg-white/10 rounded-full flex items-center justify-center"><X :size="32" class="text-white" /></button><img :src="previewSrc" class="max-w-full max-h-full object-contain shadow-2xl" /></div>
<div v-if="isFullscreen && previewSrc" class="fixed inset-0 z-200 bg-black/95 flex items-center justify-center p-6 backdrop-blur-xl"><button @click="isFullscreen = false" class="absolute top-10 right-10 w-12 h-12 bg-white/10 rounded-full flex items-center justify-center"><X :size="32" class="text-white" /></button><img :src="previewSrc" class="max-w-full max-h-full object-contain shadow-2xl" /></div>
<div v-if="toast.visible" class="fixed bottom-10 left-1/2 -translate-x-1/2 z-[300] animate-in fade-in slide-in-from-bottom-4 duration-300">
<div v-if="toast.visible" class="fixed bottom-10 left-1/2 -translate-x-1/2 z-300 animate-in fade-in slide-in-from-bottom-4 duration-300">
<div class="px-6 py-3 rounded-2xl shadow-2xl backdrop-blur-md flex items-center gap-3 border border-white/20" :class="toast.type === 'error' ? 'bg-[#FF3B30] text-white' : 'bg-white/90 text-[#1D1D1F]'">
<div v-if="toast.type === 'error'" class="w-5 h-5 rounded-full border-2 border-white flex items-center justify-center text-[12px] font-black">!</div>
<div v-else class="w-5 h-5 rounded-full bg-[#34C759] flex items-center justify-center text-white text-[10px]"></div>