first commit
This commit is contained in:
204
frontend/src/components/SettingsPanel.tsx
Normal file
204
frontend/src/components/SettingsPanel.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Avatar from "@/components/ui/avatar";
|
||||
import { api } from "@/lib/api";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { useUser } from "@/lib/user-context";
|
||||
|
||||
type Me = {
|
||||
username?: string;
|
||||
avatar?: string | null;
|
||||
timezone: string;
|
||||
barkUrl?: string | null;
|
||||
inappEnabled: boolean;
|
||||
barkEnabled: boolean;
|
||||
};
|
||||
|
||||
const SettingsPanel = () => {
|
||||
const t = useTranslation();
|
||||
const { refreshUser } = useUser();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [form, setForm] = useState<Me>({
|
||||
timezone: "Asia/Shanghai",
|
||||
barkUrl: "",
|
||||
avatar: null,
|
||||
inappEnabled: true,
|
||||
barkEnabled: false,
|
||||
});
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.getMe()
|
||||
.then((data) => setForm(data as Me))
|
||||
.catch(() => null);
|
||||
}, []);
|
||||
|
||||
const save = async () => {
|
||||
await api.updateSettings({
|
||||
avatar: form.avatar || null,
|
||||
timezone: form.timezone,
|
||||
barkUrl: form.barkUrl || null,
|
||||
inappEnabled: form.inappEnabled,
|
||||
barkEnabled: form.barkEnabled,
|
||||
});
|
||||
await refreshUser();
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 1500);
|
||||
};
|
||||
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// 验证文件类型
|
||||
const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
alert("不支持的文件格式,请上传 jpg、png、gif 或 webp 格式的图片");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件大小 (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert("文件大小不能超过 5MB");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
const result = await api.uploadAvatar(file);
|
||||
setForm({ ...form, avatar: result.avatarUrl });
|
||||
await refreshUser();
|
||||
} catch (error) {
|
||||
console.error("上传头像失败:", error);
|
||||
alert("上传头像失败,请重试");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAvatar = () => {
|
||||
setForm({ ...form, avatar: null });
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-white">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("settings")}</CardTitle>
|
||||
<CardDescription>{t("settingsDesc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Avatar Section */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t("avatar")}</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar username={form.username} src={form.avatar} size="lg" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
id="avatar-upload"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? t("uploading") : t("uploadAvatar")}
|
||||
</Button>
|
||||
{form.avatar && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRemoveAvatar}
|
||||
>
|
||||
{t("removeAvatar")}
|
||||
</Button>
|
||||
)}
|
||||
<p className="text-xs text-slate-500">{t("avatarHint")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="barkUrl">{t("barkPushUrl")}</Label>
|
||||
<Input
|
||||
id="barkUrl"
|
||||
value={form.barkUrl ?? ""}
|
||||
onChange={(event) => setForm({ ...form, barkUrl: event.target.value })}
|
||||
placeholder="https://your.bark.server"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timezone">{t("timezone")}</Label>
|
||||
<Input
|
||||
id="timezone"
|
||||
value={form.timezone}
|
||||
onChange={(event) => setForm({ ...form, timezone: event.target.value })}
|
||||
placeholder="Asia/Shanghai"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("notificationChannels")}</Label>
|
||||
<div className="rounded-lg bg-slate-50/80">
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-800">{t("webNotifications")}</div>
|
||||
<div className="text-xs text-slate-500">{t("webNotificationsDesc")}</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="inapp"
|
||||
checked={form.inappEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm({ ...form, inappEnabled: checked === true })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4 h-px bg-slate-200/50" />
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-800">{t("barkAlerts")}</div>
|
||||
<div className="text-xs text-slate-500">{t("barkAlertsDesc")}</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="bark"
|
||||
checked={form.barkEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm({ ...form, barkEnabled: checked === true })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" onClick={save}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
{saved && <div className="text-sm text-primary">{t("saved")}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPanel;
|
||||
Reference in New Issue
Block a user