import { useAuth } from "@/_core/hooks/useAuth"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { toast } from "sonner"; import { useState, useEffect, useCallback, useMemo } from "react"; import { Bell, BellRing, Plus, Trash2, Clock, Calendar, CheckCircle2, XCircle, Settings, BellOff, Volume2 } from "lucide-react"; const DAY_NAMES = ["日", "一", "二", "三", "四", "五", "六"]; const REMINDER_TYPES = [ { value: "training", label: "训练提醒", icon: }, { value: "checkin", label: "打卡提醒", icon: }, { value: "analysis", label: "分析提醒", icon: }, ]; export default function Reminders() { const { user } = useAuth(); const [showCreate, setShowCreate] = useState(false); const [newReminder, setNewReminder] = useState({ reminderType: "training", title: "", message: "", timeOfDay: "08:00", daysOfWeek: [1, 2, 3, 4, 5] as number[], }); const utils = trpc.useUtils(); const { data: reminders, isLoading } = trpc.reminder.list.useQuery(undefined, { enabled: !!user }); const { data: notifications } = trpc.notification.list.useQuery(undefined, { enabled: !!user }); const { data: unreadCount } = trpc.notification.unreadCount.useQuery(undefined, { enabled: !!user }); const createReminder = trpc.reminder.create.useMutation({ onSuccess: () => { toast.success("提醒已创建"); setShowCreate(false); setNewReminder({ reminderType: "training", title: "", message: "", timeOfDay: "08:00", daysOfWeek: [1, 2, 3, 4, 5] }); utils.reminder.list.invalidate(); }, }); const deleteReminder = trpc.reminder.delete.useMutation({ onSuccess: () => { toast.success("提醒已删除"); utils.reminder.list.invalidate(); }, }); const toggleReminder = trpc.reminder.toggle.useMutation({ onSuccess: () => { utils.reminder.list.invalidate(); }, }); const markAllRead = trpc.notification.markAllRead.useMutation({ onSuccess: () => { utils.notification.list.invalidate(); utils.notification.unreadCount.invalidate(); toast.success("全部已读"); }, }); const markRead = trpc.notification.markRead.useMutation({ onSuccess: () => { utils.notification.list.invalidate(); utils.notification.unreadCount.invalidate(); }, }); const toggleDay = useCallback((day: number) => { setNewReminder(prev => ({ ...prev, daysOfWeek: prev.daysOfWeek.includes(day) ? prev.daysOfWeek.filter(d => d !== day) : [...prev.daysOfWeek, day].sort(), })); }, []); const handleCreate = () => { if (!newReminder.title.trim()) { toast.error("请输入提醒标题"); return; } if (newReminder.daysOfWeek.length === 0) { toast.error("请至少选择一天"); return; } createReminder.mutate(newReminder); }; // Browser notification permission const [notifPermission, setNotifPermission] = useState("default"); useEffect(() => { if ("Notification" in window) { setNotifPermission(Notification.permission); } }, []); const requestPermission = async () => { if ("Notification" in window) { const perm = await Notification.requestPermission(); setNotifPermission(perm); if (perm === "granted") { toast.success("通知权限已开启"); new Notification("Tennis Training Hub", { body: "训练提醒已开启!" }); } } }; // Check reminders and trigger browser notifications useEffect(() => { if (!reminders || notifPermission !== "granted") return; const checkInterval = setInterval(() => { const now = new Date(); const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; const currentDay = now.getDay(); reminders.forEach((r: any) => { if (r.isActive && r.timeOfDay === currentTime) { const days = typeof r.daysOfWeek === "string" ? JSON.parse(r.daysOfWeek) : r.daysOfWeek; if (Array.isArray(days) && days.includes(currentDay)) { new Notification(r.title, { body: r.message || "该训练了!", icon: "/favicon.ico", }); } } }); }, 60000); // Check every minute return () => clearInterval(checkInterval); }, [reminders, notifPermission]); const activeReminders = useMemo(() => reminders?.filter((r: any) => r.isActive) || [], [reminders]); const inactiveReminders = useMemo(() => reminders?.filter((r: any) => !r.isActive) || [], [reminders]); if (isLoading) { return ( ); } return ( {/* Header */} 训练提醒 设置定时提醒,保持训练节奏 {notifPermission !== "granted" && ( 开启通知 )} 新建提醒 创建训练提醒 提醒类型 setNewReminder(p => ({ ...p, reminderType: v }))}> {REMINDER_TYPES.map(t => ( {t.icon} {t.label} ))} 标题 setNewReminder(p => ({ ...p, title: e.target.value }))} /> 提醒内容(可选) setNewReminder(p => ({ ...p, message: e.target.value }))} rows={2} /> 提醒时间 setNewReminder(p => ({ ...p, timeOfDay: e.target.value }))} /> 重复日期 {DAY_NAMES.map((name, i) => ( toggleDay(i)} className={`w-9 h-9 rounded-full text-sm font-medium transition-colors ${ newReminder.daysOfWeek.includes(i) ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-muted/80" }`} > {name} ))} setNewReminder(p => ({ ...p, daysOfWeek: [1, 2, 3, 4, 5] }))}> 工作日 setNewReminder(p => ({ ...p, daysOfWeek: [0, 6] }))}> 周末 setNewReminder(p => ({ ...p, daysOfWeek: [0, 1, 2, 3, 4, 5, 6] }))}> 每天 setShowCreate(false)}>取消 {createReminder.isPending ? "创建中..." : "创建"} {/* Notification Permission Banner */} {notifPermission === "default" && ( 开启浏览器通知 允许通知后,到达设定时间时会收到提醒 允许通知 )} {notifPermission === "denied" && ( 通知已被禁用 请在浏览器设置中手动开启本站通知权限 )} {/* Active Reminders */} 活跃提醒 ({activeReminders.length}) {activeReminders.length === 0 ? ( 暂无活跃提醒,点击"新建提醒"开始 ) : ( {activeReminders.map((reminder: any) => { const days = typeof reminder.daysOfWeek === "string" ? JSON.parse(reminder.daysOfWeek) : reminder.daysOfWeek; const type = REMINDER_TYPES.find(t => t.value === reminder.reminderType); return ( {type?.icon} {reminder.title} {reminder.message && ( {reminder.message} )} {reminder.timeOfDay} {DAY_NAMES.map((name, i) => ( {name} ))} toggleReminder.mutate({ reminderId: reminder.id, isActive: checked ? 1 : 0 })} /> deleteReminder.mutate({ reminderId: reminder.id })} > ); })} )} {/* Inactive Reminders */} {inactiveReminders.length > 0 && ( 已暂停 ({inactiveReminders.length}) {inactiveReminders.map((reminder: any) => { const days = typeof reminder.daysOfWeek === "string" ? JSON.parse(reminder.daysOfWeek) : reminder.daysOfWeek; return ( {reminder.title} {reminder.timeOfDay} toggleReminder.mutate({ reminderId: reminder.id, isActive: checked ? 1 : 0 })} /> deleteReminder.mutate({ reminderId: reminder.id })} > ); })} )} {/* Notification History */} 通知记录 {(unreadCount as number) > 0 && ( {unreadCount as number} )} {(unreadCount as number) > 0 && ( markAllRead.mutate()}> 全部已读 )} {(!notifications || notifications.length === 0) ? ( 暂无通知记录 ) : ( {notifications.map((notif: any) => ( !notif.isRead && markRead.mutate({ notificationId: notif.id })} > {notif.isRead ? ( ) : ( )} {notif.title} {notif.message && {notif.message}} {new Date(notif.createdAt).toLocaleString("zh-CN", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })} ))} )} ); }
设置定时提醒,保持训练节奏
开启浏览器通知
允许通知后,到达设定时间时会收到提醒
通知已被禁用
请在浏览器设置中手动开启本站通知权限
暂无活跃提醒,点击"新建提醒"开始
{reminder.message}
暂无通知记录
{notif.title}
{notif.message}