fix(account): persist profile actions through backend

This commit is contained in:
zetaloop
2026-04-25 14:13:55 +08:00
parent d7cc6b0141
commit e3572bf86b
2 changed files with 108 additions and 30 deletions
+64 -24
View File
@@ -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"> JPGPNG </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">
+44 -6
View File
@@ -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,11 +83,39 @@ export function Header() {
) )
const handleRoleSwitch = (role: UserRole) => { const handleRoleSwitch = (role: UserRole) => {
switchRole(role) if (role === currentRole) {
if (pathname.startsWith("/dashboard") && !canAccessDashboard(role, pathname)) { setMobileOpen(false)
router.push(role === "consumer" ? "/" : "/dashboard") return
} }
setMobileOpen(false)
void (async () => {
try {
await switchCurrentRole(role)
const updated = await getCurrentUserForLogin()
login(updated, updated.verifiedRoles ?? [updated.role])
if (pathname.startsWith("/dashboard") && !canAccessDashboard(role, pathname)) {
router.push(role === "consumer" ? "/" : "/dashboard")
}
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
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(
@@ -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>