first commit

This commit is contained in:
Michael Dong
2026-02-05 11:24:40 +08:00
commit a98e12f286
144 changed files with 26459 additions and 0 deletions

View 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;