feat(auth): add Zustand store for authentication management

- Implemented a Zustand store to manage authentication state.
- Added user information handling, including user roles and organization switching.
- Integrated toast notifications for user feedback on actions.
- Included initialization logic to check for local user and token.
This commit is contained in:
2026-02-22 01:18:28 +08:00
parent 8349e1ca4c
commit ce38fd6dad
4 changed files with 2913 additions and 7 deletions

View File

@@ -18,10 +18,10 @@
"license": "MIT",
"dependencies": {
"@base-ui/react": "^1.2.0",
"@kevisual/api": "^0.0.52",
"@kevisual/api": "^0.0.59",
"@kevisual/context": "^0.0.8",
"@kevisual/router": "0.0.83",
"@tanstack/react-router": "^1.161.3",
"@tanstack/react-router": "^1.161.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -33,7 +33,7 @@
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.71.1",
"react-hook-form": "^7.71.2",
"sonner": "^2.0.7",
"zustand": "^5.0.11"
},
@@ -44,8 +44,8 @@
"@kevisual/query": "0.0.49",
"@kevisual/types": "^0.0.12",
"@tailwindcss/vite": "^4.2.0",
"@tanstack/react-router-devtools": "^1.161.3",
"@tanstack/router-plugin": "^1.161.3",
"@tanstack/react-router-devtools": "^1.161.4",
"@tanstack/router-plugin": "^1.161.4",
"@types/node": "^25.3.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",

2804
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,44 @@
import { useEffect } from "react"
import { useLayoutStore } from "./store"
import { useShallow } from "zustand/shallow"
import { LogIn, LockKeyhole } from "lucide-react"
type Props = {
children: React.ReactNode
children?: React.ReactNode,
mustLogin?: boolean,
}
export const AuthProvider = ({ children }: Props) => {
export const AuthProvider = ({ children, mustLogin }: Props) => {
const store = useLayoutStore(useShallow(state => ({
init: state.init,
me: state.me,
})));
useEffect(() => {
store.init()
}, [])
const loginUrl = '/root/login?redirect=' + encodeURIComponent(window.location.href);
if (mustLogin && !store.me) {
return (
<div className="w-full h-full min-h-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-6 p-10 rounded-2xl border border-border bg-card shadow-lg max-w-sm w-full mx-4">
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-muted">
<LockKeyhole className="w-8 h-8 text-muted-foreground" />
</div>
<div className="flex flex-col items-center gap-2 text-center">
<h2 className="text-xl font-semibold text-foreground"></h2>
<p className="text-sm text-muted-foreground">访</p>
</div>
<a
href={loginUrl}
className="inline-flex items-center justify-center gap-2 w-full px-6 py-2.5 rounded-lg bg-foreground text-background text-sm font-medium transition-opacity hover:opacity-80 active:opacity-70"
>
<LogIn className="w-4 h-4" />
</a>
</div>
</div>
)
}
return <>
{children}
</>

65
src/pages/auth/store.ts Normal file
View File

@@ -0,0 +1,65 @@
import { queryLogin } from '@/modules/query';
import { create } from 'zustand';
import { toast } from 'sonner';
type UserInfo = {
id?: string;
username?: string;
nickname?: string | null;
needChangePassword?: boolean;
description?: string | null;
type?: 'user' | 'org';
orgs?: string[];
avatar?: string;
};
export type LayoutStore = {
open: boolean;
setOpen: (open: boolean) => void;
openUser: boolean;
setOpenUser: (openUser: boolean) => void;
me?: UserInfo;
setMe: (me: UserInfo) => void;
getMe: () => Promise<void>;
switchOrg: (username?: string) => Promise<void>;
isAdmin: boolean;
setIsAdmin: (isAdmin: boolean) => void
init: () => Promise<void>;
};
export const useLayoutStore = create<LayoutStore>((set, get) => ({
open: false,
setOpen: (open) => set({ open }),
openUser: false,
setOpenUser: (openUser) => set({ openUser }),
me: undefined,
setMe: (me) => set({ me }),
getMe: async () => {
const res = await queryLogin.getMe();
if (res.code === 200) {
set({ me: res.data });
set({ isAdmin: res.data.orgs?.includes?.('admin') || false });
}
},
switchOrg: async (username?: string) => {
const res = await queryLogin.switchUser(username || '');
if (res.code === 200) {
toast.success('切换成功');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
toast.error(res.message || '请求失败');
}
},
isAdmin: false,
setIsAdmin: (isAdmin) => set({ isAdmin }),
init: async () => {
const token = await queryLogin.getToken()
if (token) {
const user = await queryLogin.checkLocalUser() as UserInfo;
if (user) {
set({ me: user });
set({ isAdmin: user.orgs?.includes?.('admin') || false });
}
}
}
}));