375 lines
15 KiB
TypeScript
375 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useSession } from "next-auth/react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useState, useEffect } from "react";
|
|
import { useTheme } from "@/lib/contexts/theme-context";
|
|
import { notificationManager } from "@/lib/utils/notifications";
|
|
import Header from "@/components/Header";
|
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
|
|
|
interface UserSettings {
|
|
defaultQuality: string;
|
|
publicProfile: boolean;
|
|
allowDownload: boolean;
|
|
}
|
|
|
|
export default function SettingsPage() {
|
|
const { data: session, status } = useSession();
|
|
const router = useRouter();
|
|
const { theme, setTheme } = useTheme();
|
|
|
|
const [defaultQuality, setDefaultQuality] = useState("medium");
|
|
const [publicProfile, setPublicProfile] = useState(false);
|
|
const [allowDownload, setAllowDownload] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isExporting, setIsExporting] = useState(false);
|
|
|
|
// 获取用户设置
|
|
const fetchUserSettings = async () => {
|
|
try {
|
|
const response = await fetch("/api/user/settings");
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
if (result.success && result.data) {
|
|
const data: UserSettings = result.data;
|
|
setDefaultQuality(data.defaultQuality);
|
|
setPublicProfile(data.publicProfile);
|
|
setAllowDownload(data.allowDownload);
|
|
} else {
|
|
console.error("API 返回数据格式错误:", result);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("获取用户设置失败:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (status === "authenticated") {
|
|
fetchUserSettings();
|
|
}
|
|
}, [status]);
|
|
|
|
const handleSaveSettings = async () => {
|
|
setIsSaving(true);
|
|
try {
|
|
const response = await fetch("/api/user/settings", {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
defaultQuality,
|
|
publicProfile,
|
|
allowDownload,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
console.log("设置已保存");
|
|
notificationManager.success("设置已保存", "您的设置已成功保存");
|
|
} else {
|
|
throw new Error("保存设置失败");
|
|
}
|
|
} catch (error) {
|
|
console.error("保存设置失败:", error);
|
|
notificationManager.error("保存失败", "设置保存失败,请重试");
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleExportData = async () => {
|
|
setIsExporting(true);
|
|
try {
|
|
const response = await fetch("/api/user/export");
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `recorder-export-${Date.now()}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
notificationManager.success("导出成功", "数据已成功导出");
|
|
} else {
|
|
throw new Error("导出失败");
|
|
}
|
|
} catch (error) {
|
|
console.error("导出失败:", error);
|
|
notificationManager.error("导出失败", "数据导出失败,请重试");
|
|
} finally {
|
|
setIsExporting(false);
|
|
}
|
|
};
|
|
|
|
const handleResetSettings = async () => {
|
|
if (!confirm("确定要重置所有设置为默认值吗?")) return;
|
|
|
|
try {
|
|
const response = await fetch("/api/user/settings", {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (response.ok) {
|
|
await fetchUserSettings();
|
|
notificationManager.success("重置成功", "设置已重置为默认值");
|
|
} else {
|
|
throw new Error("重置失败");
|
|
}
|
|
} catch (error) {
|
|
console.error("重置设置失败:", error);
|
|
notificationManager.error("重置失败", "设置重置失败,请重试");
|
|
}
|
|
};
|
|
|
|
if (status === "loading" || isLoading) {
|
|
return (
|
|
<div className="flex justify-center items-center min-h-screen">
|
|
<LoadingSpinner size="lg" color="blue" text="加载中..." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === "unauthenticated") {
|
|
router.push("/login");
|
|
return null;
|
|
}
|
|
|
|
if (!session?.user) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
<Header />
|
|
|
|
<main className="container mx-auto p-4 md:p-8 max-w-4xl">
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 md:p-8">
|
|
<div className="flex items-center gap-4 mb-8">
|
|
<div className="w-12 h-12 bg-gray-500 rounded-lg flex items-center justify-center">
|
|
<svg
|
|
className="w-6 h-6 text-white"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
/>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
设置
|
|
</h1>
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
自定义你的应用体验
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-8">
|
|
{/* 录音设置 */}
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
录音设置
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
|
默认音频质量
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
选择默认的录音质量
|
|
</p>
|
|
</div>
|
|
<select
|
|
value={defaultQuality}
|
|
onChange={(e) => setDefaultQuality(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
|
>
|
|
<option value="low">低质量</option>
|
|
<option value="medium">中等质量</option>
|
|
<option value="high">高质量</option>
|
|
<option value="lossless">无损质量</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 外观设置 */}
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
外观设置
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
|
主题模式
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
选择你喜欢的主题
|
|
</p>
|
|
</div>
|
|
<select
|
|
value={theme}
|
|
onChange={(e) =>
|
|
setTheme(e.target.value as "light" | "dark" | "auto")
|
|
}
|
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
|
>
|
|
<option value="light">浅色模式</option>
|
|
<option value="dark">深色模式</option>
|
|
<option value="auto">跟随系统</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 隐私设置 */}
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
隐私设置
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
|
公开个人资料
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
允许其他用户查看你的个人资料
|
|
</p>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={publicProfile}
|
|
onChange={(e) => setPublicProfile(e.target.checked)}
|
|
className="sr-only peer"
|
|
/>
|
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
|
允许下载
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
允许其他用户下载你的录音
|
|
</p>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={allowDownload}
|
|
onChange={(e) => setAllowDownload(e.target.checked)}
|
|
className="sr-only peer"
|
|
/>
|
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 数据管理 */}
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
数据管理
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
|
导出数据
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
导出你的所有录音数据
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleExportData}
|
|
disabled={isExporting}
|
|
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white rounded-lg transition-colors flex items-center gap-2"
|
|
>
|
|
{isExporting ? (
|
|
<>
|
|
<LoadingSpinner size="sm" color="white" />
|
|
导出中...
|
|
</>
|
|
) : (
|
|
"导出"
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
|
重置设置
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
将所有设置重置为默认值
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleResetSettings}
|
|
className="px-4 py-2 bg-yellow-500 hover:bg-yellow-600 text-white rounded-lg transition-colors"
|
|
>
|
|
重置
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
|
删除账户
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
永久删除你的账户和所有录音
|
|
</p>
|
|
</div>
|
|
<button className="px-4 py-2 text-red-600 hover:text-red-700 font-medium">
|
|
删除账户
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 保存按钮 */}
|
|
<div className="flex gap-3 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
<button
|
|
onClick={handleSaveSettings}
|
|
disabled={isSaving}
|
|
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
|
>
|
|
{isSaving ? "保存中..." : "保存设置"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|