Commit 8e6cce3f4976bb0c698b67dabdfe66c6fffd4dd9

Authored by zichun
1 parent bd940f2e

feat(fe-login): authSlice 登录态与 token 持久化 REQ-USR-004

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 +});
... ...