Files
2026-04-26 01:53:15 +08:00

297 lines
10 KiB
TypeScript

"use client"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
import { getCurrentUserForLogin, switchCurrentRole, updateCurrentUser, uploadFile } from "@/lib/api"
import { toApiError } from "@/lib/errors"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import type { UserRole } from "@/lib/types"
import type { ThemePreference } from "@/store/auth"
import { useAuthStore } from "@/store/auth"
import { Camera } from "lucide-react"
import { useTheme } from "next-themes"
import Link from "next/link"
import { useRef, useState } from "react"
export default function SettingsPage() {
const { currentRole, verifiedRoles, user, login, notificationPrefs, setNotificationPref } =
useAuthStore()
const [nickname, setNickname] = useState(user?.nickname ?? "")
const [bio, setBio] = useState(user?.bio ?? "")
const [avatar, setAvatar] = useState(user?.avatar ?? "")
const [uploadingAvatar, setUploadingAvatar] = useState(false)
const fileRef = useRef<HTMLInputElement>(null)
const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role)
const { theme, setTheme } = useTheme()
const setThemePreference = useAuthStore((s) => s.setThemePreference)
function handleThemeChange(value: string) {
const pref = value as ThemePreference
setTheme(pref)
setThemePreference(pref)
}
async function handleAvatarFile(file: File, resetInput: () => void) {
setUploadingAvatar(true)
try {
const url = await uploadFile(file, "avatar")
setAvatar(url)
notifySuccess("头像已上传")
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
setUploadingAvatar(false)
resetInput()
}
}
async function handleProfileSave() {
if (!user) {
notifyInfo("请先登录")
return
}
try {
const updated = await updateCurrentUser({
nickname: nickname.trim() || user.nickname,
bio: bio.trim(),
avatar,
})
login(updated, updated.verifiedRoles ?? [updated.role])
notifySuccess("资料已保存")
} catch (error) {
notifyInfo(toApiError(error).msg)
}
}
async function handleRoleChange(value: string) {
const role = value as UserRole
if (!isRoleVerified(role) || role === currentRole) return
try {
await switchCurrentRole(role)
const updated = await getCurrentUserForLogin()
login(updated, updated.verifiedRoles ?? [updated.role])
notifySuccess("身份已切换")
} catch (error) {
notifyInfo(toApiError(error).msg)
}
}
return (
<div className="mx-auto max-w-2xl space-y-6">
<h1 className="text-2xl font-bold"></h1>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="flex items-center gap-4">
<div className="relative">
<Avatar className="h-20 w-20">
<AvatarImage src={avatar} />
<AvatarFallback className="text-lg">{user?.nickname?.[0] ?? "?"}</AvatarFallback>
</Avatar>
<input
ref={fileRef}
type="file"
accept="image/*"
className="hidden"
onChange={(event) => {
const target = event.currentTarget
const file = target.files?.[0]
if (!file) return
void handleAvatarFile(file, () => {
target.value = ""
})
}}
/>
<button
type="button"
disabled={uploadingAvatar}
className="absolute bottom-0 right-0 h-7 w-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center"
onClick={() => fileRef.current?.click()}
>
<Camera className="h-3.5 w-3.5" />
</button>
</div>
<p className="text-sm text-muted-foreground">
{uploadingAvatar ? "头像上传中..." : "点击上传头像,支持 JPG、PNG 格式"}
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="nickname"></Label>
<Input
id="nickname"
value={nickname}
onChange={(event) => setNickname(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="bio"></Label>
<Textarea
id="bio"
value={bio}
onChange={(event) => setBio(event.target.value)}
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input id="phone" defaultValue={user?.phone ?? ""} disabled />
<p className="text-xs text-muted-foreground"></p>
</div>
<Button
onClick={() => {
void handleProfileSave()
}}
>
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">,</p>
<RadioGroup
value={currentRole}
onValueChange={(v) => {
void handleRoleChange(v)
}}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<RadioGroupItem value="consumer" id="role-consumer" />
<Label htmlFor="role-consumer"></Label>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<RadioGroupItem
value="player"
id="role-player"
disabled={!isRoleVerified("player")}
/>
<Label
htmlFor="role-player"
className={!isRoleVerified("player") ? "text-muted-foreground" : ""}
>
</Label>
</div>
{!isRoleVerified("player") && (
<Link href="/verify" className="text-sm text-primary hover:underline">
</Link>
)}
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<RadioGroupItem value="owner" id="role-owner" disabled={!isRoleVerified("owner")} />
<Label
htmlFor="role-owner"
className={!isRoleVerified("owner") ? "text-muted-foreground" : ""}
>
</Label>
</div>
{!isRoleVerified("owner") && (
<Link href="/verify" className="text-sm text-primary hover:underline">
</Link>
)}
</div>
</RadioGroup>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<Label></Label>
<Select value={theme} onValueChange={handleThemeChange}>
<SelectTrigger className="w-32">
<SelectValue placeholder="选择主题" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light"></SelectItem>
<SelectItem value="dark"></SelectItem>
<SelectItem value="system"></SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground"></p>
</div>
<Switch
checked={notificationPrefs.order}
onCheckedChange={(checked) => setNotificationPref("order", checked)}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground"></p>
</div>
<Switch
checked={notificationPrefs.community}
onCheckedChange={(checked) => setNotificationPref("community", checked)}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground"></p>
</div>
<Switch
checked={notificationPrefs.system}
onCheckedChange={(checked) => setNotificationPref("system", checked)}
/>
</div>
</CardContent>
</Card>
</div>
)
}