diff --git a/frontend/src/store/slices/authSlice.ts b/frontend/src/store/slices/authSlice.ts new file mode 100644 index 0000000..27f3436 --- /dev/null +++ b/frontend/src/store/slices/authSlice.ts @@ -0,0 +1,36 @@ +// REQ-USR-004: 全局登录态(token + user)。token 持久化到 localStorage(D6)。 +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import { TOKEN_STORAGE_KEY } from '../../api/request'; +import type { AuthUser } from '../../api/types'; + +export interface AuthState { + token: string | null; + user: AuthUser | null; +} + +const initialState: AuthState = { + token: localStorage.getItem(TOKEN_STORAGE_KEY), + user: null, +}; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + // 写 token + user,并持久化 token(reducer 内副作用为 MVP 取舍,全项目统一,D6) + setCredentials(state, action: PayloadAction<{ token: string; user: AuthUser }>) { + state.token = action.payload.token; + state.user = action.payload.user; + localStorage.setItem(TOKEN_STORAGE_KEY, action.payload.token); + }, + // 清登录态并移除持久化 token + clearCredentials(state) { + state.token = null; + state.user = null; + localStorage.removeItem(TOKEN_STORAGE_KEY); + }, + }, +}); + +export const { setCredentials, clearCredentials } = authSlice.actions; +export default authSlice.reducer; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts new file mode 100644 index 0000000..2d70715 --- /dev/null +++ b/frontend/src/store/store.ts @@ -0,0 +1,12 @@ +// REQ-USR-004: Redux store(FE 共享骨架,后续 FE-02~04 复用) +import { configureStore } from '@reduxjs/toolkit'; +import authReducer from './slices/authSlice'; + +export const store = configureStore({ + reducer: { + auth: authReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/frontend/tests/unit/authSlice.test.ts b/frontend/tests/unit/authSlice.test.ts new file mode 100644 index 0000000..b66b765 --- /dev/null +++ b/frontend/tests/unit/authSlice.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import authReducer, { + setCredentials, + clearCredentials, + type AuthState, +} from '../../src/store/slices/authSlice'; +import { TOKEN_STORAGE_KEY } from '../../src/api/request'; +import type { AuthUser } from '../../src/api/types'; + +const user: AuthUser = { id: 1, sUserName: 'admin', sUserType: '超级管理员', sLanguage: '中文' }; + +describe('authSlice', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('setCredentials stores token and user and persists token', () => { + const start: AuthState = { token: null, user: null }; + const next = authReducer(start, setCredentials({ token: 't', user })); + expect(next.token).toBe('t'); + expect(next.user).toEqual(user); + expect(localStorage.getItem(TOKEN_STORAGE_KEY)).toBe('t'); + }); + + it('clearCredentials clears state and removes persisted token', () => { + localStorage.setItem(TOKEN_STORAGE_KEY, 't'); + const start: AuthState = { token: 't', user }; + const next = authReducer(start, clearCredentials()); + expect(next.token).toBeNull(); + expect(next.user).toBeNull(); + expect(localStorage.getItem(TOKEN_STORAGE_KEY)).toBeNull(); + }); + + it('initialState reads persisted token', async () => { + localStorage.setItem(TOKEN_STORAGE_KEY, 'persisted-tk'); + // 重置模块缓存,使 initialState 在带 token 的 localStorage 下重新求值 + vi.resetModules(); + const mod = await import('../../src/store/slices/authSlice'); + const initial = mod.default(undefined, { type: '@@INIT' }); + expect(initial.token).toBe('persisted-tk'); + }); +});