refactor: 前端登录/权限/界面状态迁移至 zustand
This commit is contained in:
@@ -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}` },
|
||||
})
|
||||
|
||||
@@ -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}` },
|
||||
})
|
||||
|
||||
@@ -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(' · ');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}` },
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user