refactor: 前端登录/权限/界面状态迁移至 zustand

This commit is contained in:
2026-08-04 14:41:27 +08:00
parent ce1dcc07ea
commit f07ffdc64c
39 changed files with 970 additions and 211 deletions

View File

@@ -2,12 +2,7 @@ import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
import PermissionButton from '../components/PermissionButton';
import {
beginPermissionVerification,
clearPermissions,
readPermissionState,
writePermissions,
} from './permission-store';
import { usePermissionStore } from '../store/permission/permissionStore';
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
@@ -27,18 +22,25 @@ async function renderPermissionButton() {
});
}
function readPermissionState() {
return {
permissions: usePermissionStore.getState().permissions,
status: usePermissionStore.getState().status,
};
}
afterEach(async () => {
if (root) await act(async () => root?.unmount());
container?.remove();
root = null;
container = null;
clearPermissions();
usePermissionStore.getState().clearPermissions();
});
describe('permission state', () => {
it('ignores cached localStorage permissions until profile verification succeeds', async () => {
localStorage.setItem('permissions', JSON.stringify(['student:edit']));
beginPermissionVerification();
usePermissionStore.getState().beginPermissionVerification();
expect(readPermissionState()).toEqual({ permissions: [], status: 'loading' });
await renderPermissionButton();
@@ -46,20 +48,20 @@ describe('permission state', () => {
});
it('renders permission actions only after verified permissions are written', async () => {
beginPermissionVerification();
usePermissionStore.getState().beginPermissionVerification();
await renderPermissionButton();
expect(container?.textContent).not.toContain('编辑学生');
await act(async () => writePermissions(['student:edit']));
await act(async () => usePermissionStore.getState().writePermissions(['student:edit']));
expect(container?.textContent).toContain('编辑学生');
});
it('stays fail-closed while profile verification is retried after a failure', async () => {
writePermissions(['student:edit']);
beginPermissionVerification();
it('keeps verified permissions while profile verification refreshes in the background', async () => {
usePermissionStore.getState().writePermissions(['student:edit']);
usePermissionStore.getState().beginPermissionVerification();
expect(readPermissionState()).toEqual({ permissions: [], status: 'loading' });
expect(readPermissionState()).toEqual({ permissions: ['student:edit'], status: 'ready' });
await renderPermissionButton();
expect(container?.textContent).not.toContain('编辑学生');
expect(container?.textContent).toContain('编辑学生');
});
});

View File

@@ -1,40 +0,0 @@
export const PERMISSIONS_UPDATED_EVENT = 'permissions-updated';
export type PermissionStatus = 'unknown' | 'loading' | 'ready';
export interface PermissionState {
permissions: string[];
status: PermissionStatus;
}
let permissionState: PermissionState = { permissions: [], status: 'unknown' };
function notifyPermissionStateChanged(): void {
window.dispatchEvent(new Event(PERMISSIONS_UPDATED_EVENT));
}
export function readPermissionState(): PermissionState {
return permissionState;
}
export function readPermissions(): string[] {
return permissionState.status === 'ready' ? permissionState.permissions : [];
}
export function beginPermissionVerification(): void {
permissionState = { permissions: [], status: 'loading' };
notifyPermissionStateChanged();
}
export function writePermissions(permissions: string[]): void {
const uniquePermissions = [...new Set(permissions)];
localStorage.setItem('permissions', JSON.stringify(uniquePermissions));
permissionState = { permissions: uniquePermissions, status: 'ready' };
notifyPermissionStateChanged();
}
export function clearPermissions(status: PermissionStatus = 'unknown'): void {
localStorage.removeItem('permissions');
permissionState = { permissions: [], status };
notifyPermissionStateChanged();
}