fix(account): persist profile actions through backend
This commit is contained in:
@@ -16,7 +16,9 @@ import {
|
|||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { notifySuccess } from "@/lib/toast"
|
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 { UserRole } from "@/lib/types"
|
||||||
import type { ThemePreference } from "@/store/auth"
|
import type { ThemePreference } from "@/store/auth"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
@@ -26,18 +28,12 @@ import Link from "next/link"
|
|||||||
import { useRef, useState } from "react"
|
import { useRef, useState } from "react"
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const {
|
const { currentRole, verifiedRoles, user, login, notificationPrefs, setNotificationPref } =
|
||||||
currentRole,
|
useAuthStore()
|
||||||
verifiedRoles,
|
|
||||||
switchRole,
|
|
||||||
user,
|
|
||||||
updateProfile,
|
|
||||||
notificationPrefs,
|
|
||||||
setNotificationPref,
|
|
||||||
} = useAuthStore()
|
|
||||||
const [nickname, setNickname] = useState(user?.nickname ?? "")
|
const [nickname, setNickname] = useState(user?.nickname ?? "")
|
||||||
const [bio, setBio] = useState(user?.bio ?? "")
|
const [bio, setBio] = useState(user?.bio ?? "")
|
||||||
const [avatar, setAvatar] = useState(user?.avatar ?? "")
|
const [avatar, setAvatar] = useState(user?.avatar ?? "")
|
||||||
|
const [uploadingAvatar, setUploadingAvatar] = useState(false)
|
||||||
const fileRef = useRef<HTMLInputElement>(null)
|
const fileRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role)
|
const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role)
|
||||||
@@ -50,6 +46,53 @@ export default function SettingsPage() {
|
|||||||
setThemePreference(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 (
|
return (
|
||||||
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
|
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
|
||||||
<h1 className="text-2xl font-bold">个人设置</h1>
|
<h1 className="text-2xl font-bold">个人设置</h1>
|
||||||
@@ -70,21 +113,26 @@ export default function SettingsPage() {
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const file = event.target.files?.[0]
|
const target = event.currentTarget
|
||||||
|
const file = target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
setAvatar(URL.createObjectURL(file))
|
void handleAvatarFile(file, () => {
|
||||||
event.target.value = ""
|
target.value = ""
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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()}
|
onClick={() => fileRef.current?.click()}
|
||||||
>
|
>
|
||||||
<Camera className="h-3.5 w-3.5" />
|
<Camera className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">点击更换头像,支持 JPG、PNG 格式</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{uploadingAvatar ? "头像上传中..." : "点击上传头像,支持 JPG、PNG 格式"}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -117,12 +165,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateProfile({
|
void handleProfileSave()
|
||||||
nickname: nickname.trim() || user?.nickname,
|
|
||||||
bio: bio.trim(),
|
|
||||||
avatar,
|
|
||||||
})
|
|
||||||
notifySuccess("资料已保存")
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
保存修改
|
保存修改
|
||||||
@@ -139,10 +182,7 @@ export default function SettingsPage() {
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={currentRole}
|
value={currentRole}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
const role = v as UserRole
|
void handleRoleChange(v)
|
||||||
if (isRoleVerified(role)) {
|
|
||||||
switchRole(role)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
+41
-3
@@ -13,6 +13,9 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
||||||
|
import { getCurrentUserForLogin, logout as logoutRequest, switchCurrentRole } from "@/lib/api"
|
||||||
|
import { toApiError } from "@/lib/errors"
|
||||||
|
import { notifyInfo } from "@/lib/toast"
|
||||||
import type { UserRole } from "@/lib/types"
|
import type { UserRole } from "@/lib/types"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
@@ -46,7 +49,14 @@ export function Header() {
|
|||||||
const [mobileOpen, setMobileOpen] = useState(false)
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isAuthenticated, currentRole, verifiedRoles, switchRole, logout, user } = useAuthStore()
|
const {
|
||||||
|
isAuthenticated,
|
||||||
|
currentRole,
|
||||||
|
verifiedRoles,
|
||||||
|
login,
|
||||||
|
logout: clearAuth,
|
||||||
|
user,
|
||||||
|
} = useAuthStore()
|
||||||
const ownerShop = useShopStore((state) =>
|
const ownerShop = useShopStore((state) =>
|
||||||
user ? state.shops.find((shop) => shop.owner.id === user.id) : undefined,
|
user ? state.shops.find((shop) => shop.owner.id === user.id) : undefined,
|
||||||
)
|
)
|
||||||
@@ -73,12 +83,40 @@ export function Header() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleRoleSwitch = (role: UserRole) => {
|
const handleRoleSwitch = (role: UserRole) => {
|
||||||
switchRole(role)
|
if (role === currentRole) {
|
||||||
|
setMobileOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await switchCurrentRole(role)
|
||||||
|
const updated = await getCurrentUserForLogin()
|
||||||
|
login(updated, updated.verifiedRoles ?? [updated.role])
|
||||||
if (pathname.startsWith("/dashboard") && !canAccessDashboard(role, pathname)) {
|
if (pathname.startsWith("/dashboard") && !canAccessDashboard(role, pathname)) {
|
||||||
router.push(role === "consumer" ? "/" : "/dashboard")
|
router.push(role === "consumer" ? "/" : "/dashboard")
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifyInfo(toApiError(error).msg)
|
||||||
|
} finally {
|
||||||
setMobileOpen(false)
|
setMobileOpen(false)
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await logoutRequest()
|
||||||
|
} catch (error) {
|
||||||
|
notifyInfo(toApiError(error).msg)
|
||||||
|
} finally {
|
||||||
|
clearAuth()
|
||||||
|
setMobileOpen(false)
|
||||||
|
router.push("/")
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
const unreadCount = useNotificationStore(
|
const unreadCount = useNotificationStore(
|
||||||
(state) => state.notifications.filter((notification) => !notification.read).length,
|
(state) => state.notifications.filter((notification) => !notification.read).length,
|
||||||
@@ -226,7 +264,7 @@ export function Header() {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={logout}>
|
<DropdownMenuItem onClick={handleLogout}>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
退出登录
|
退出登录
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
Reference in New Issue
Block a user