('/ai/config/test', body);
setTestResult(res);
@@ -582,6 +589,25 @@ const AiConfigPage: React.FC = () => {
+
+
+
+
{config?.verified && (
} color="success">
diff --git a/apps/admin/src/pages/Attendance/index.tsx b/apps/admin/src/pages/Attendance/index.tsx
index 98b4c83..379c7bd 100644
--- a/apps/admin/src/pages/Attendance/index.tsx
+++ b/apps/admin/src/pages/Attendance/index.tsx
@@ -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}` },
})
diff --git a/apps/admin/src/pages/Classes/detail.tsx b/apps/admin/src/pages/Classes/detail.tsx
index 07247a2..19e0fe6 100644
--- a/apps/admin/src/pages/Classes/detail.tsx
+++ b/apps/admin/src/pages/Classes/detail.tsx
@@ -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={}
onClick={() => {
- const token = localStorage.getItem('token');
+ const token = useUserStore.getState().token;
fetch(`/api/classes/${id}/roster/export`, {
headers: { Authorization: `Bearer ${token}` },
})
diff --git a/apps/admin/src/pages/Classes/teacher-candidate.ts b/apps/admin/src/pages/Classes/teacher-candidate.ts
index cae2d8c..451732a 100644
--- a/apps/admin/src/pages/Classes/teacher-candidate.ts
+++ b/apps/admin/src/pages/Classes/teacher-candidate.ts
@@ -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(' · ');
diff --git a/apps/admin/src/pages/ClassroomSchedule/index.tsx b/apps/admin/src/pages/ClassroomSchedule/index.tsx
index a095863..5677ef5 100644
--- a/apps/admin/src/pages/ClassroomSchedule/index.tsx
+++ b/apps/admin/src/pages/ClassroomSchedule/index.tsx
@@ -368,12 +368,12 @@ const ClassroomSchedulePage: React.FC = () => {
{detailModal.startDate} ~ {detailModal.endDate}(
{dayjs(detailModal.endDate).diff(dayjs(detailModal.startDate), 'day') + 1}天)
- {detailModal.dailyRate && (
+ {detailModal.dailyRate != null && (
日租金:¥{detailModal.dailyRate}
)}
- {detailModal.totalAmount && (
+ {detailModal.totalAmount != null && (
合同总额:¥{detailModal.totalAmount}
diff --git a/apps/admin/src/pages/Classrooms/index.tsx b/apps/admin/src/pages/Classrooms/index.tsx
index c9b894b..dfe611a 100644
--- a/apps/admin/src/pages/Classrooms/index.tsx
+++ b/apps/admin/src/pages/Classrooms/index.tsx
@@ -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 = {
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={}
onClick={() => {
const baseURL = '/api';
- const token = localStorage.getItem('token');
+ const token = useUserStore.getState().token;
fetch(`${baseURL}/classrooms/export`, {
headers: { Authorization: `Bearer ${token}` },
})
diff --git a/apps/admin/src/pages/Dashboard/index.tsx b/apps/admin/src/pages/Dashboard/index.tsx
index 778656e..94f8a10 100644
--- a/apps/admin/src/pages/Dashboard/index.tsx
+++ b/apps/admin/src/pages/Dashboard/index.tsx
@@ -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('/dashboard/stats'),
api.get>('/dashboard/room-ranking', {
params: { periodStart: period[0], periodEnd: period[1] },
@@ -220,14 +220,14 @@ const DashboardPage: React.FC = () => {
api.get('/dashboard/gantt', {
params: { periodStart: period[0], periodEnd: period[1] },
}),
+ api.get('/dashboard/classroom-occupancy'),
+ api.get('/dashboard/classroom-utilization'),
]);
setStats(s);
setRoomRanking(rr);
setClassRanking(cr);
setGanttData(g);
- const co = await api.get('/dashboard/classroom-occupancy');
setClassroomOccupancy(co);
- const cu = await api.get('/dashboard/classroom-utilization');
setClassroomUtil(cu);
loadedRef.current = true;
} catch (e) {
diff --git a/apps/admin/src/pages/Login/index.tsx b/apps/admin/src/pages/Login/index.tsx
index 9abb8a5..cd9f195 100644
--- a/apps/admin/src/pages/Login/index.tsx
+++ b/apps/admin/src/pages/Login/index.tsx
@@ -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 (
diff --git a/apps/admin/src/pages/Notifications/index.tsx b/apps/admin/src/pages/Notifications/index.tsx
index 9dc152a..cd677d7 100644
--- a/apps/admin/src/pages/Notifications/index.tsx
+++ b/apps/admin/src/pages/Notifications/index.tsx
@@ -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: , label: '全部' },
+ { key: 'bill_generated', icon: , label: '账单' },
+ { key: 'check_in', icon: , label: '入住' },
+ { key: 'class_change', icon: , label: '班级' },
+ { key: 'announcement', icon: , 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: , label: '全部' },
- { key: 'bill_generated', icon: , label: '账单' },
- { key: 'check_in', icon: , label: '入住' },
- { key: 'class_change', icon: , label: '班级' },
- { key: 'announcement', icon: , label: '公告' },
- ];
-
return (
{!isMobile && (
@@ -117,7 +117,7 @@ const NotificationsPage: React.FC = () => {
mode="inline"
selectedKeys={[filter]}
onClick={({ key }) => setFilter(key)}
- items={filterItems}
+ items={FILTER_ITEMS}
/>
)}
@@ -132,7 +132,7 @@ const NotificationsPage: React.FC = () => {