"use client"; import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { toast } from "sonner"; import { Trash2, Upload, Copy, ExternalLink } from "lucide-react"; type SiteSettings = { grayscale: boolean; shutdownBannerEnabled: boolean; shutdownTitle: string; shutdownTitleEn: string; shutdownSubtitle: string; shutdownSubtitleEn: string; shutdownAt: string | null; bgmUrl: string; bgmAutoplay: boolean; bgmVolume: number; }; function toDatetimeLocal(iso: string | null): string { if (!iso) return ""; const d = new Date(iso); if (isNaN(d.getTime())) return ""; const pad = (n: number) => n.toString().padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad( d.getHours() )}:${pad(d.getMinutes())}`; } export default function SettingsPage() { const [loading, setLoading] = useState(false); const [launcherUrl, setLauncherUrl] = useState(""); useEffect(() => { if (typeof window !== "undefined") { setLauncherUrl(`${window.location.origin}/download/launcher`); } }, []); async function handleCopyLauncherUrl() { try { await navigator.clipboard.writeText(launcherUrl); toast.success("已复制到剪贴板"); } catch { toast.error("复制失败"); } } const [siteLoading, setSiteLoading] = useState(true); const [siteSaving, setSiteSaving] = useState(false); const [grayscale, setGrayscale] = useState(false); const [bannerEnabled, setBannerEnabled] = useState(false); const [title, setTitle] = useState(""); const [titleEn, setTitleEn] = useState(""); const [subtitle, setSubtitle] = useState(""); const [subtitleEn, setSubtitleEn] = useState(""); const [shutdownAtLocal, setShutdownAtLocal] = useState(""); const [bgmUrl, setBgmUrl] = useState(""); const [bgmAutoplay, setBgmAutoplay] = useState(false); const [bgmVolume, setBgmVolume] = useState(50); const [bgmUploading, setBgmUploading] = useState(false); const bgmFileRef = useRef(null); useEffect(() => { (async () => { try { const res = await fetch("/api/admin/site-settings"); if (!res.ok) throw new Error(); const data: SiteSettings = await res.json(); setGrayscale(data.grayscale); setBannerEnabled(data.shutdownBannerEnabled); setTitle(data.shutdownTitle); setTitleEn(data.shutdownTitleEn ?? ""); setSubtitle(data.shutdownSubtitle); setSubtitleEn(data.shutdownSubtitleEn ?? ""); setShutdownAtLocal(toDatetimeLocal(data.shutdownAt)); setBgmUrl(data.bgmUrl || ""); setBgmAutoplay(!!data.bgmAutoplay); setBgmVolume( typeof data.bgmVolume === "number" ? data.bgmVolume : 50 ); } catch { toast.error("加载站点设置失败"); } finally { setSiteLoading(false); } })(); }, []); async function handleSiteSave(e: React.FormEvent) { e.preventDefault(); setSiteSaving(true); const payload = { grayscale, shutdownBannerEnabled: bannerEnabled, shutdownTitle: title, shutdownTitleEn: titleEn, shutdownSubtitle: subtitle, shutdownSubtitleEn: subtitleEn, shutdownAt: shutdownAtLocal ? new Date(shutdownAtLocal).toISOString() : null, bgmUrl, bgmAutoplay, bgmVolume, }; const res = await fetch("/api/admin/site-settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (res.ok) { toast.success("站点设置已保存"); } else { const data = await res.json().catch(() => ({})); toast.error(data?.error || "保存失败"); } setSiteSaving(false); } async function handleBgmUpload(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; if (!file.type.startsWith("audio/")) { toast.error("请选择音频文件"); return; } setBgmUploading(true); try { const form = new FormData(); form.append("file", file); const res = await fetch("/api/upload", { method: "POST", body: form }); if (!res.ok) throw new Error(); const data = await res.json(); setBgmUrl(data.filePath as string); toast.success("音频上传成功,请记得保存"); } catch { toast.error("音频上传失败"); } finally { setBgmUploading(false); if (bgmFileRef.current) bgmFileRef.current.value = ""; } } async function handlePasswordSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); const formData = new FormData(e.currentTarget); const newPassword = formData.get("newPassword") as string; const confirmPassword = formData.get("confirmPassword") as string; if (newPassword !== confirmPassword) { toast.error("两次输入的新密码不一致"); setLoading(false); return; } const res = await fetch("/api/admin/change-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ currentPassword: formData.get("currentPassword"), newPassword, }), }); const data = await res.json(); if (res.ok) { toast.success("密码修改成功"); (e.target as HTMLFormElement).reset(); } else { toast.error(data.error || "修改失败"); } setLoading(false); } return (

系统设置

站点外观

启用后前台整站将置为黑白灰,适用于重大纪念或肃穆场合。

在首页顶部显示倒计时横幅,整体为肃穆风格。