Files
chrono-snap/src/components/modals/ReminderManager.vue
Julian Freeman 10583fd882 improve
2026-03-26 19:23:22 -04:00

199 lines
12 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
import { X, Plus, Trash2, Check, Clock, Calendar, Edit2 } from "lucide-vue-next";
import { invoke } from "@tauri-apps/api/core";
import { reminders, loadReminders, showToast, currentDate, logicalMinutesToTime, logicalMinutesFromTime, currentLogicalMinute, getLogicDateStr, TIME_OFFSET_MINUTES } from "../../store";
import { Reminder } from "../../types";
const emit = defineEmits(["close"]);
const getInitialMinute = () => {
const now = new Date();
const later = new Date(now.getTime() + 10 * 60000);
const h = later.getHours();
const m = later.getMinutes();
return logicalMinutesFromTime(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
};
const newContent = ref("");
const newMinute = ref(getInitialMinute());
const editingId = ref<number | null>(null);
const isTimePickerOpen = ref(false);
const timePickerRef = ref<HTMLElement | null>(null);
const openTimePicker = async () => {
isTimePickerOpen.value = !isTimePickerOpen.value;
if (isTimePickerOpen.value) {
await nextTick();
const activeItems = timePickerRef.value?.querySelectorAll('.bg-\\[\\#007AFF\\].text-white');
activeItems?.forEach(el => el.scrollIntoView({ block: 'center' }));
}
};
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (isTimePickerOpen.value && timePickerRef.value && !timePickerRef.value.contains(target)) {
isTimePickerOpen.value = false;
}
};
onMounted(() => window.addEventListener('mousedown', handleClickOutside));
onUnmounted(() => window.removeEventListener('mousedown', handleClickOutside));
const addReminder = async () => {
if (!newContent.value.trim()) {
showToast("请输入提醒内容", "error");
return;
}
const isEditing = editingId.value !== null;
try {
await invoke("save_reminder", {
reminder: { id: editingId.value || 0, date: currentDate.value, minute: newMinute.value, content: newContent.value, is_completed: false }
});
newContent.value = "";
newMinute.value = getInitialMinute();
editingId.value = null;
await loadReminders();
showToast(isEditing ? "提醒已更新" : "提醒已添加");
} catch (e) {
showToast("保存失败: " + e, "error");
}
};
const editReminder = (r: Reminder) => {
editingId.value = r.id;
newContent.value = r.content;
newMinute.value = r.minute;
};
const cancelEdit = () => {
editingId.value = null;
newContent.value = "";
newMinute.value = getInitialMinute();
};
const toggleStatus = async (r: Reminder) => {
try {
await invoke("toggle_reminder", { id: r.id, isCompleted: !r.is_completed });
await loadReminders();
} catch (e) {}
};
const deleteItem = async (id: number) => {
try {
await invoke("delete_reminder", { id });
await loadReminders();
showToast("提醒已删除");
} catch (e) {}
};
const safeReminders = computed(() => Array.isArray(reminders.value) ? reminders.value : []);
const isPast = computed(() => currentDate.value < getLogicDateStr());
const isToday = computed(() => currentDate.value === getLogicDateStr());
const overdueReminders = computed(() => safeReminders.value.filter(r =>
!r.is_completed && (isPast.value || (isToday.value && r.minute < currentLogicalMinute.value))
));
const upcomingReminders = computed(() => safeReminders.value.filter(r =>
!r.is_completed && (!isPast.value && (!isToday.value || r.minute >= currentLogicalMinute.value))
));
const completedReminders = computed(() => safeReminders.value.filter(r => r.is_completed));
</script>
<template>
<div class="fixed inset-0 z-110 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-md overflow-hidden flex flex-col h-[80vh] max-h-175">
<div class="p-8 border-b border-border-main/50 flex justify-between items-center bg-bg-card/80 backdrop-blur-md sticky top-0 z-10">
<h2 class="text-2xl font-bold flex items-center gap-3"><Clock :size="24" class="text-[#007AFF]"/> 提醒事项</h2>
<button @click="emit('close')" class="p-2 hover:bg-bg-input rounded-xl text-text-sec"><X :size="24" /></button>
</div>
<div class="p-6 border-b border-border-main/50 bg-bg-input/30 relative">
<form @submit.prevent="addReminder" class="flex items-center gap-3">
<div ref="timePickerRef" class="relative">
<button type="button" @click="openTimePicker" class="bg-bg-card border border-border-main rounded-xl px-4 py-3 text-sm font-bold flex items-center justify-center hover:bg-bg-hover transition-all focus:border-[#007AFF]/50 w-24 outline-none" :class="{ 'ring-2 ring-[#007AFF]/50 border-transparent': editingId }">
{{ logicalMinutesToTime(newMinute) }}
</button>
<div v-if="isTimePickerOpen" class="absolute top-full left-0 mt-2 bg-bg-card rounded-3xl shadow-2xl border border-border-main z-120 p-4 animate-in fade-in slide-in-from-top-2 flex gap-4 h-64 w-48">
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1 text-center">
<button type="button" v-for="h in 24" :key="h" @click="newMinute = logicalMinutesFromTime(`${String((h-1+3)%24).padStart(2,'0')}:${String((newMinute + TIME_OFFSET_MINUTES) % 60).padStart(2,'0')}`)" class="py-2 text-xs rounded-lg transition-colors w-full text-center" :class="Math.floor((newMinute + TIME_OFFSET_MINUTES) / 60) % 24 === (h-1+3)%24 ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">
{{ String((h-1+3)%24).padStart(2,'0') }}
</button>
</div>
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1 text-center">
<button type="button" v-for="m in 60" :key="m" @click="newMinute = Math.floor(newMinute / 60) * 60 + (m-1); isTimePickerOpen = false" class="py-2 text-xs rounded-lg transition-colors w-full text-center" :class="newMinute % 60 === (m-1) ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">
{{ String(m-1).padStart(2,'0') }}
</button>
</div>
</div>
</div>
<div class="flex-1 relative">
<input type="text" v-model="newContent" :placeholder="editingId ? '编辑提醒...' : '添加新提醒...'" class="w-full bg-bg-card border border-border-main rounded-xl px-4 py-3 text-sm font-bold outline-none focus:border-[#007AFF]/50 transition-all pr-10" :class="{ 'ring-2 ring-[#007AFF]/50 border-transparent': editingId }" />
<button v-if="editingId" type="button" @click="cancelEdit" class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-text-sec hover:bg-bg-input rounded-lg"><X :size="16" /></button>
</div>
<button type="submit" class="bg-[#007AFF] text-white p-3 rounded-xl hover:brightness-110 transition-all shadow-md shadow-[#007AFF]/20"><Check v-if="editingId" :size="20" /><Plus v-else :size="20" /></button>
</form>
</div>
<div class="flex-1 overflow-y-auto p-6 space-y-6 no-scrollbar">
<!-- 已超时未办 -->
<div v-if="overdueReminders && overdueReminders.length > 0">
<h3 class="text-xs font-bold text-[#FF3B30] mb-3 uppercase tracking-wider">已超时未办</h3>
<div class="space-y-2 mb-6">
<div v-for="r in overdueReminders" :key="r.id" class="flex items-center gap-3 bg-[#FF3B30]/10 p-3 rounded-2xl border border-[#FF3B30]/30 group transition-all hover:bg-[#FF3B30]/20 hover:shadow-sm" :class="{ 'ring-2 ring-[#FF3B30]/50': editingId === r.id }">
<button @click="toggleStatus(r)" class="w-6 h-6 rounded-full border-2 border-[#FF3B30]/50 flex items-center justify-center text-transparent hover:border-[#FF3B30] transition-all"><Check :size="14" class="opacity-0 group-hover:opacity-50 group-hover:text-[#FF3B30]"/></button>
<div class="font-bold text-[#FF3B30] text-sm w-12">{{ logicalMinutesToTime(r.minute) }}</div>
<div class="flex-1 font-medium text-sm text-[#FF3B30] truncate" :class="{ 'opacity-50': editingId === r.id }">{{ r.content }}</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all">
<button @click="editReminder(r)" class="p-2 text-text-sec hover:text-[#007AFF] rounded-lg hover:bg-[#007AFF]/10"><Edit2 :size="16" /></button>
<button @click="deleteItem(r.id)" class="p-2 text-text-sec hover:text-[#FF3B30] rounded-lg hover:bg-[#FF3B30]/20"><Trash2 :size="16" /></button>
</div>
</div>
</div>
</div>
<!-- 即将到来 -->
<div v-if="upcomingReminders && upcomingReminders.length > 0">
<h3 class="text-xs font-bold text-text-sec mb-3 uppercase tracking-wider">即将到来</h3>
<div class="space-y-2 mb-6">
<div v-for="r in upcomingReminders" :key="r.id" class="flex items-center gap-3 bg-bg-input/50 p-3 rounded-2xl border border-border-main/30 group transition-all hover:bg-bg-input hover:shadow-sm" :class="{ 'ring-2 ring-[#007AFF]/50 border-transparent': editingId === r.id }">
<button @click="toggleStatus(r)" class="w-6 h-6 rounded-full border-2 border-border-main flex items-center justify-center text-transparent hover:border-[#007AFF] transition-all"><Check :size="14" class="opacity-0 group-hover:opacity-30 group-hover:text-[#007AFF]"/></button>
<div class="font-bold text-[#007AFF] text-sm w-12">{{ logicalMinutesToTime(r.minute) }}</div>
<div class="flex-1 font-medium text-sm text-text-main truncate" :class="{ 'opacity-50': editingId === r.id }">{{ r.content }}</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all">
<button @click="editReminder(r)" class="p-2 text-text-sec hover:text-[#007AFF] rounded-lg hover:bg-[#007AFF]/10"><Edit2 :size="16" /></button>
<button @click="deleteItem(r.id)" class="p-2 text-text-sec hover:text-[#FF3B30] rounded-lg hover:bg-[#FF3B30]/10"><Trash2 :size="16" /></button>
</div>
</div>
</div>
</div>
<!-- 已完成 -->
<div v-if="completedReminders && completedReminders.length > 0">
<h3 class="text-xs font-bold text-text-sec mb-3 uppercase tracking-wider">已完成</h3>
<div class="space-y-2 opacity-60">
<div v-for="r in completedReminders" :key="r.id" class="flex items-center gap-3 bg-bg-input/30 p-3 rounded-2xl border border-transparent group transition-all" :class="{ 'ring-2 ring-border-main': editingId === r.id }">
<button @click="toggleStatus(r)" class="w-6 h-6 rounded-full bg-[#007AFF] flex items-center justify-center text-white"><Check :size="14" /></button>
<div class="font-bold text-text-sec text-sm w-12 line-through">{{ logicalMinutesToTime(r.minute) }}</div>
<div class="flex-1 font-medium text-sm text-text-sec line-through truncate" :class="{ 'opacity-50': editingId === r.id }">{{ r.content }}</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all">
<button @click="editReminder(r)" class="p-2 text-text-sec hover:text-[#007AFF] rounded-lg hover:bg-[#007AFF]/10"><Edit2 :size="16" /></button>
<button @click="deleteItem(r.id)" class="p-2 text-text-sec hover:text-[#FF3B30] rounded-lg hover:bg-[#FF3B30]/10"><Trash2 :size="16" /></button>
</div>
</div>
</div>
</div>
<!-- 无提醒事项 -->
<div v-if="(!overdueReminders || overdueReminders.length === 0) && (!upcomingReminders || upcomingReminders.length === 0) && (!completedReminders || completedReminders.length === 0)" class="flex flex-col items-center justify-center h-full text-text-sec opacity-40 mt-10">
<Calendar :size="48" class="mb-4" />
<p class="font-bold">今天没有提醒事项</p>
</div>
</div>
</div>
</div>
</template>