Commit 8e6cce3f4976bb0c698b67dabdfe66c6fffd4dd9
1 parent
bd940f2e
feat(fe-login): authSlice 登录态与 token 持久化 REQ-USR-004
Showing
3 changed files
with
90 additions
and
0 deletions
frontend/src/store/slices/authSlice.ts
0 → 100644
| 1 | +// REQ-USR-004: 全局登录态(token + user)。token 持久化到 localStorage(D6)。 | ||
| 2 | +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; | ||
| 3 | +import { TOKEN_STORAGE_KEY } from '../../api/request'; | ||
| 4 | +import type { AuthUser } from '../../api/types'; | ||
| 5 | + | ||
| 6 | +export interface AuthState { | ||
| 7 | + token: string | null; | ||
| 8 | + user: AuthUser | null; | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +const initialState: AuthState = { | ||
| 12 | + token: localStorage.getItem(TOKEN_STORAGE_KEY), | ||
| 13 | + user: null, | ||
| 14 | +}; | ||
| 15 | + | ||
| 16 | +const authSlice = createSlice({ | ||
| 17 | + name: 'auth', | ||
| 18 | + initialState, | ||
| 19 | + reducers: { | ||
| 20 | + // 写 token + user,并持久化 token(reducer 内副作用为 MVP 取舍,全项目统一,D6) | ||
| 21 | + setCredentials(state, action: PayloadAction<{ token: string; user: AuthUser }>) { | ||
| 22 | + state.token = action.payload.token; | ||
| 23 | + state.user = action.payload.user; | ||
| 24 | + localStorage.setItem(TOKEN_STORAGE_KEY, action.payload.token); | ||
| 25 | + }, | ||
| 26 | + // 清登录态并移除持久化 token | ||
| 27 | + clearCredentials(state) { | ||
| 28 | + state.token = null; | ||
| 29 | + state.user = null; | ||
| 30 | + localStorage.removeItem(TOKEN_STORAGE_KEY); | ||
| 31 | + }, | ||
| 32 | + }, | ||
| 33 | +}); | ||
| 34 | + | ||
| 35 | +export const { setCredentials, clearCredentials } = authSlice.actions; | ||
| 36 | +export default authSlice.reducer; |
frontend/src/store/store.ts
0 → 100644
| 1 | +// REQ-USR-004: Redux store(FE 共享骨架,后续 FE-02~04 复用) | ||
| 2 | +import { configureStore } from '@reduxjs/toolkit'; | ||
| 3 | +import authReducer from './slices/authSlice'; | ||
| 4 | + | ||
| 5 | +export const store = configureStore({ | ||
| 6 | + reducer: { | ||
| 7 | + auth: authReducer, | ||
| 8 | + }, | ||
| 9 | +}); | ||
| 10 | + | ||
| 11 | +export type RootState = ReturnType<typeof store.getState>; | ||
| 12 | +export type AppDispatch = typeof store.dispatch; |
frontend/tests/unit/authSlice.test.ts
0 → 100644
| 1 | +import { describe, it, expect, beforeEach, vi } from 'vitest'; | ||
| 2 | +import authReducer, { | ||
| 3 | + setCredentials, | ||
| 4 | + clearCredentials, | ||
| 5 | + type AuthState, | ||
| 6 | +} from '../../src/store/slices/authSlice'; | ||
| 7 | +import { TOKEN_STORAGE_KEY } from '../../src/api/request'; | ||
| 8 | +import type { AuthUser } from '../../src/api/types'; | ||
| 9 | + | ||
| 10 | +const user: AuthUser = { id: 1, sUserName: 'admin', sUserType: '超级管理员', sLanguage: '中文' }; | ||
| 11 | + | ||
| 12 | +describe('authSlice', () => { | ||
| 13 | + beforeEach(() => { | ||
| 14 | + localStorage.clear(); | ||
| 15 | + }); | ||
| 16 | + | ||
| 17 | + it('setCredentials stores token and user and persists token', () => { | ||
| 18 | + const start: AuthState = { token: null, user: null }; | ||
| 19 | + const next = authReducer(start, setCredentials({ token: 't', user })); | ||
| 20 | + expect(next.token).toBe('t'); | ||
| 21 | + expect(next.user).toEqual(user); | ||
| 22 | + expect(localStorage.getItem(TOKEN_STORAGE_KEY)).toBe('t'); | ||
| 23 | + }); | ||
| 24 | + | ||
| 25 | + it('clearCredentials clears state and removes persisted token', () => { | ||
| 26 | + localStorage.setItem(TOKEN_STORAGE_KEY, 't'); | ||
| 27 | + const start: AuthState = { token: 't', user }; | ||
| 28 | + const next = authReducer(start, clearCredentials()); | ||
| 29 | + expect(next.token).toBeNull(); | ||
| 30 | + expect(next.user).toBeNull(); | ||
| 31 | + expect(localStorage.getItem(TOKEN_STORAGE_KEY)).toBeNull(); | ||
| 32 | + }); | ||
| 33 | + | ||
| 34 | + it('initialState reads persisted token', async () => { | ||
| 35 | + localStorage.setItem(TOKEN_STORAGE_KEY, 'persisted-tk'); | ||
| 36 | + // 重置模块缓存,使 initialState 在带 token 的 localStorage 下重新求值 | ||
| 37 | + vi.resetModules(); | ||
| 38 | + const mod = await import('../../src/store/slices/authSlice'); | ||
| 39 | + const initial = mod.default(undefined, { type: '@@INIT' }); | ||
| 40 | + expect(initial.token).toBe('persisted-tk'); | ||
| 41 | + }); | ||
| 42 | +}); |