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

@@ -38,6 +38,7 @@ import PermissionButton from '../../components/PermissionButton';
import EditableCell from '../../components/EditableCell';
import { usePermission } from '../../hooks/usePermission';
import { message } from '../../ui/app-message';
import { useUserStore } from '../../store/user/userStore';
import {
canPullAttendance,
getAttendanceExperience,
@@ -184,12 +185,8 @@ const EMPTY_SUMMARY: AttendanceSummary = {
};
function readCurrentRoles(): string[] {
try {
const user = JSON.parse(localStorage.getItem('user') || '{}') as { roles?: string[] };
return Array.isArray(user.roles) ? user.roles : [];
} catch {
return [];
}
const roles = useUserStore.getState().user?.roles;
return Array.isArray(roles) ? roles : [];
}
function displayAttendanceStatus(status?: string | null): string {
@@ -732,7 +729,7 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) =>
const handleExport = useCallback(() => {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(buildParams(false))) params.set(key, String(value));
const token = localStorage.getItem('token');
const token = useUserStore.getState().token;
fetch(`/api/attendance-records/export?${params.toString()}`, {
headers: { Authorization: `Bearer ${token}` },
})

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useUserStore } from '../../store/user/userStore';
import {
Card,
Tabs,
@@ -572,7 +573,7 @@ const ClassDetailPage: React.FC = () => {
permission="class:view"
icon={<DownloadOutlined />}
onClick={() => {
const token = localStorage.getItem('token');
const token = useUserStore.getState().token;
fetch(`/api/classes/${id}/roster/export`, {
headers: { Authorization: `Bearer ${token}` },
})

View File

@@ -28,9 +28,16 @@ export const buildTeacherCandidateLabel = (user: TeacherCandidateUser) => {
displayName && displayName !== user.username
? `${displayName}${user.username}`
: user.username;
const roleNames = [...new Set((user.roles || []).map((role) => role.name).filter(Boolean))];
const roleNames = [
...new Set((user.roles || []).flatMap((role) => (role.name ? [role.name] : []))),
];
const subjects = [
...new Set((user.profile?.subjects || []).map((subject) => subject.trim()).filter(Boolean)),
...new Set(
(user.profile?.subjects || []).flatMap((subject) => {
const trimmed = subject.trim();
return trimmed ? [trimmed] : [];
}),
),
];
return [identity, roleNames.join('/'), subjects.join('/')].filter(Boolean).join(' · ');

View File

@@ -368,12 +368,12 @@ const ClassroomSchedulePage: React.FC = () => {
{detailModal.startDate} ~ {detailModal.endDate}
{dayjs(detailModal.endDate).diff(dayjs(detailModal.startDate), 'day') + 1}
</div>
{detailModal.dailyRate && (
{detailModal.dailyRate != null && (
<div>
<strong></strong>¥{detailModal.dailyRate}
</div>
)}
{detailModal.totalAmount && (
{detailModal.totalAmount != null && (
<div>
<strong></strong>¥{detailModal.totalAmount}
</div>

View File

@@ -26,6 +26,7 @@ import PermissionButton from '../../components/PermissionButton';
import EditableCell from '../../components/EditableCell';
import { message } from '../../ui/app-message';
import { usePermission } from '../../hooks/usePermission';
import { useUserStore } from '../../store/user/userStore';
const statusMap: Record<string, { text: string; color: string }> = {
available: { text: '可用', color: 'green' },
@@ -143,7 +144,7 @@ const ClassroomsPage: React.FC = () => {
const baseURL = import.meta.env.PROD
? '/api'
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
const token = localStorage.getItem('token');
const token = useUserStore.getState().token;
fetch(`${baseURL}/classrooms/template`, { headers: { Authorization: `Bearer ${token}` } })
.then((res) => res.blob())
.then((blob) => {
@@ -389,7 +390,7 @@ const ClassroomsPage: React.FC = () => {
icon={<DownloadOutlined />}
onClick={() => {
const baseURL = '/api';
const token = localStorage.getItem('token');
const token = useUserStore.getState().token;
fetch(`${baseURL}/classrooms/export`, {
headers: { Authorization: `Bearer ${token}` },
})

View File

@@ -209,7 +209,7 @@ const DashboardPage: React.FC = () => {
setLoading(true);
}
try {
const [s, rr, cr, g] = await Promise.all([
const [s, rr, cr, g, co, cu] = await Promise.all([
api.get<DashboardStats>('/dashboard/stats'),
api.get<Array<{ roomNumber: string; total: string }>>('/dashboard/room-ranking', {
params: { periodStart: period[0], periodEnd: period[1] },
@@ -220,14 +220,14 @@ const DashboardPage: React.FC = () => {
api.get<GanttRoom[]>('/dashboard/gantt', {
params: { periodStart: period[0], periodEnd: period[1] },
}),
api.get<ClassroomOccupancy[]>('/dashboard/classroom-occupancy'),
api.get<ClassroomUtilStats>('/dashboard/classroom-utilization'),
]);
setStats(s);
setRoomRanking(rr);
setClassRanking(cr);
setGanttData(g);
const co = await api.get<ClassroomOccupancy[]>('/dashboard/classroom-occupancy');
setClassroomOccupancy(co);
const cu = await api.get<ClassroomUtilStats>('/dashboard/classroom-utilization');
setClassroomUtil(cu);
loadedRef.current = true;
} catch (e) {

View File

@@ -4,14 +4,18 @@ import { Form, Input, Button, Card, Typography } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import api from '../../api';
import { message } from '../../ui/app-message';
import { clearPermissions, writePermissions } from '../../auth/permission-store';
import { findRoleAwareLandingPath } from '../../auth/menu-policy';
import { usePermissionStore } from '../../store/permission/permissionStore';
import { useUserStore } from '../../store/user/userStore';
const { Title } = Typography;
const LoginPage: React.FC = () => {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const setSession = useUserStore((state) => state.setSession);
const clearPermissions = usePermissionStore((state) => state.clearPermissions);
const writePermissions = usePermissionStore((state) => state.writePermissions);
const onFinish = useCallback(
async (values: any) => {
@@ -19,8 +23,7 @@ const LoginPage: React.FC = () => {
setLoading(true);
try {
const res: any = await api.post('/auth/login', values);
localStorage.setItem('token', res.access_token);
localStorage.setItem('user', JSON.stringify(res.user));
setSession(res.access_token, res.user);
const permissions = res.user.permissions || [];
writePermissions(permissions);
message.success('登录成功');
@@ -33,7 +36,7 @@ const LoginPage: React.FC = () => {
setLoading(false);
}
},
[navigate],
[clearPermissions, navigate, setSession, writePermissions],
);
return (

View File

@@ -49,6 +49,14 @@ function timeAgo(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('zh-CN');
}
const FILTER_ITEMS: Array<{ key: string; icon: React.ReactNode; label: string }> = [
{ key: 'all', icon: <BellOutlined />, label: '全部' },
{ key: 'bill_generated', icon: <DollarOutlined />, label: '账单' },
{ key: 'check_in', icon: <HomeOutlined />, label: '入住' },
{ key: 'class_change', icon: <TeamOutlined />, label: '班级' },
{ key: 'announcement', icon: <SettingOutlined />, label: '公告' },
];
const NotificationsPage: React.FC = () => {
const screens = useBreakpoint();
const isMobile = !screens.sm;
@@ -101,14 +109,6 @@ const NotificationsPage: React.FC = () => {
const filtered =
filter === 'all' ? notifications : notifications.filter((n) => n.type === filter);
const filterItems = [
{ key: 'all', icon: <BellOutlined />, label: '全部' },
{ key: 'bill_generated', icon: <DollarOutlined />, label: '账单' },
{ key: 'check_in', icon: <HomeOutlined />, label: '入住' },
{ key: 'class_change', icon: <TeamOutlined />, label: '班级' },
{ key: 'announcement', icon: <SettingOutlined />, label: '公告' },
];
return (
<Layout className="notifications-layout" style={{ minHeight: '100%', background: '#fff' }}>
{!isMobile && (
@@ -117,7 +117,7 @@ const NotificationsPage: React.FC = () => {
mode="inline"
selectedKeys={[filter]}
onClick={({ key }) => setFilter(key)}
items={filterItems}
items={FILTER_ITEMS}
/>
</Sider>
)}
@@ -132,7 +132,7 @@ const NotificationsPage: React.FC = () => {
<Select
value={filter}
onChange={setFilter}
options={filterItems.map((item) => ({ value: item.key, label: item.label }))}
options={FILTER_ITEMS.map((item) => ({ value: item.key, label: item.label }))}
className="notifications-filter"
/>
)}

View File

@@ -132,8 +132,9 @@ const OccupanciesPage: React.FC = () => {
selectedBatchRecords
.map((item) => item.checkInDate)
.filter(Boolean)
.sort()
.at(-1),
.reduce((latest: string | undefined, date) =>
!latest || date > latest ? date : latest,
undefined),
[selectedBatchRecords],
);
const latestSelectedBillingStartDate = useMemo(
@@ -141,8 +142,9 @@ const OccupanciesPage: React.FC = () => {
selectedBatchRecords
.map((item) => item.billingStartDate || item.checkInDate)
.filter(Boolean)
.sort()
.at(-1),
.reduce((latest: string | undefined, date) =>
!latest || date > latest ? date : latest,
undefined),
[selectedBatchRecords],
);

View File

@@ -181,7 +181,7 @@ const RoomsPage: React.FC = () => {
// 获取楼栋列表用于筛选
const buildings = useMemo(() => {
const set = new Set(data.map((r: any) => r.building).filter(Boolean));
const set = new Set(data.flatMap((r: any) => (r.building ? [r.building] : [])));
return [...set].sort();
}, [data]);