297 lines
10 KiB
TypeScript
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>
|
|
)
|
|
}
|