fix: align permission navigation and page access

This commit is contained in:
2026-07-11 14:55:49 +08:00
parent 1e1c476bc3
commit b6fca99390
27 changed files with 434 additions and 49 deletions

View File

@@ -21,6 +21,7 @@ import api from '../../api';
import { downloadBlob } from '../../utils/download';
import PermissionButton from '../../components/PermissionButton';
import { message } from '../../ui/app-message';
import { usePermission } from '../../hooks/usePermission';
interface UnavailableDatesResponse {
dates: string[];
@@ -29,6 +30,7 @@ export const unavailableDatesCacheKey = (classroomId: number, date: Dayjs) =>
`${classroomId}:${date.format('YYYY-MM')}`;
const ClassroomRentalsPage: React.FC = () => {
const { hasAnyPermission } = usePermission();
const [data, setData] = useState<any[]>([]);
const [classrooms, setClassrooms] = useState<any[]>([]);
const [organizations, setOrganizations] = useState<any[]>([]);
@@ -82,8 +84,8 @@ const ClassroomRentalsPage: React.FC = () => {
};
useEffect(() => {
fetchMeta();
}, []);
if (hasAnyPermission('rental:create', 'rental:edit')) fetchMeta();
}, [hasAnyPermission]);
useEffect(() => {
fetchData();
}, [filterMonth]);

View File

@@ -21,6 +21,7 @@ import api from '../../api';
import { maskPhone, maskIdNumber } from '../../utils/sensitive';
import PermissionButton from '../../components/PermissionButton';
import { message } from '../../ui/app-message';
import { usePermission } from '../../hooks/usePermission';
const statusMap: Record<string, { text: string; color: string }> = {
paid: { text: '已缴', color: 'green' },
@@ -51,6 +52,7 @@ interface PendingRefund {
}
const DepositsPage: React.FC = () => {
const { hasPermission } = usePermission();
const [data, setData] = useState<any[]>([]);
const [students, setStudents] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
@@ -73,7 +75,10 @@ const DepositsPage: React.FC = () => {
const fetchData = async () => {
setLoading(true);
try {
const [d, s]: any[] = await Promise.all([api.get('/deposits'), api.get('/students')]);
const [d, s]: any[] = await Promise.all([
api.get('/deposits'),
hasPermission('deposit:create') ? api.get('/deposits/student-lookups') : Promise.resolve([]),
]);
setData(d);
setStudents(s);
} catch (e: any) {
@@ -95,7 +100,7 @@ const DepositsPage: React.FC = () => {
useEffect(() => {
fetchData();
}, []);
}, [hasPermission]);
const filteredData = useMemo(() => {
return data.filter((d: any) => {
@@ -444,20 +449,22 @@ const DepositsPage: React.FC = () => {
</>
),
},
{
key: 'pending',
label: '待审批退款',
children: (
<Table
columns={pendingColumns}
dataSource={pendingRefunds}
rowKey="id"
loading={pendingLoading}
scroll={{ x: 1200 }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }}
/>
),
},
...(hasPermission('deposit:approve')
? [{
key: 'pending',
label: '待审批退款',
children: (
<Table
columns={pendingColumns}
dataSource={pendingRefunds}
rowKey="id"
loading={pendingLoading}
scroll={{ x: 1200 }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }}
/>
),
}]
: []),
]}
/>

View File

@@ -114,16 +114,15 @@ const ExpensesPage: React.FC = () => {
const fetchData = useCallback(async () => {
setLoading(true);
try {
const [re, pe, rm, st]: any[] = await Promise.all([
const [re, pe, lookups]: any[] = await Promise.all([
api.get('/expenses/room'),
api.get('/expenses/personal'),
api.get('/rooms'),
api.get('/students'),
api.get('/expenses/lookups').catch(() => ({ rooms: [], students: [] })),
]);
setRoomExpenses(re);
setPersonalExpenses(pe);
setRooms(rm);
setStudents(st);
setRooms(lookups.rooms || []);
setStudents(lookups.students || []);
} catch (e: any) {
message.error(e?.message || '加载失败,请稍后重试');
}

View File

@@ -12,6 +12,8 @@ import type { DataNode } from 'antd/es/tree';
import type { TreeSelectProps } from 'antd/es/tree-select';
import api from '../../api';
import { message } from '../../ui/app-message';
import { usePermission } from '../../hooks/usePermission';
import PermissionButton from '../../components/PermissionButton';
interface DingTalkConfig {
agentId: string;
@@ -63,6 +65,7 @@ interface ImportResult {
}
const IntegrationConfigPage: React.FC = () => {
const { hasAllPermissions } = usePermission();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
@@ -279,7 +282,7 @@ const IntegrationConfigPage: React.FC = () => {
}
};
const syncTabItems = config
const syncTabItems = config && hasAllPermissions('sync:read', 'class:view', 'class:edit')
? [
{
key: 'sync-users',
@@ -461,9 +464,9 @@ const IntegrationConfigPage: React.FC = () => {
<Switch />
</Form.Item>
<Space>
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handleSave}>
<PermissionButton permission="integration:trigger" type="primary" icon={<SaveOutlined />} loading={saving} onClick={handleSave}>
</Button>
</PermissionButton>
<Button icon={<ApiOutlined />} loading={testing} onClick={handleTest}>
</Button>

View File

@@ -5,6 +5,7 @@ import { UserOutlined, LockOutlined } from '@ant-design/icons';
import api from '../../api';
import { message } from '../../ui/app-message';
import { writePermissions } from '../../auth/permission-store';
import { findFirstAccessiblePath } from '../../auth/permission-navigation';
const { Title } = Typography;
@@ -18,9 +19,10 @@ const LoginPage: React.FC = () => {
const res: any = await api.post('/auth/login', values);
localStorage.setItem('token', res.access_token);
localStorage.setItem('user', JSON.stringify(res.user));
writePermissions(res.user.permissions || []);
const permissions = res.user.permissions || [];
writePermissions(permissions);
message.success('登录成功');
navigate('/dashboard');
navigate(findFirstAccessiblePath(permissions) || '/', { replace: true });
} catch (err: any) {
message.error(err?.message || '登录失败');
} finally {

View File

@@ -30,6 +30,17 @@ const PermissionsPage: React.FC = () => {
log: '操作日志',
user: '用户管理',
role: '角色管理',
class: '班级管理',
schedule: '排课管理',
attendance: '考勤管理',
learning: '学习记录',
exam: '考试管理',
sync: '数据同步',
integration: '集成配置',
department: '部门管理',
notification: '通知中心',
profile: '个人资料',
ai: 'AI 模型配置',
};
useEffect(() => {

View File

@@ -34,6 +34,7 @@ import {
import dayjs, { Dayjs } from 'dayjs';
import api from '../../api';
import PermissionButton from '../../components/PermissionButton';
import { usePermission } from '../../hooks/usePermission';
import { message } from '../../ui/app-message';
import {
buildSchedulePayload,
@@ -99,6 +100,7 @@ const WEEKDAY_NUMBERS = [1, 2, 3, 4, 5, 6, 7];
// ---- Component ----
const SchedulesPage: React.FC = () => {
const { hasPermission } = usePermission();
// View mode and navigation
const [viewMode, setViewMode] = useState<'week' | 'month'>('week');
const [viewDate, setViewDate] = useState<Dayjs>(() => dayjs().weekday(1).startOf('day'));
@@ -235,9 +237,8 @@ const SchedulesPage: React.FC = () => {
const fetchData = useCallback(async () => {
setLoading(true);
try {
const [classroomsRes, classesRes, schedulesRes] = await Promise.all([
api.get('/classrooms') as Promise<ClassroomItem[]>,
api.get('/classes') as Promise<ClassItem[]>,
const [lookups, schedulesRes] = await Promise.all([
api.get('/class-schedules/lookups') as Promise<{ classrooms: ClassroomItem[]; classes: ClassItem[] }>,
api.get('/class-schedules/weekly', {
params: {
startDate: startDateStr,
@@ -247,8 +248,8 @@ const SchedulesPage: React.FC = () => {
}) as Promise<Record<string, Record<string, ClassScheduleItem[]>>>,
]);
setClassrooms(classroomsRes);
setClasses(classesRes);
setClassrooms(lookups.classrooms);
setClasses(lookups.classes);
// Convert string keys to numbers
const typedMatrix: Record<number, Record<number, ClassScheduleItem[]>> = {};
@@ -326,7 +327,7 @@ const SchedulesPage: React.FC = () => {
setSelectedSchedules(schedules);
setModalMode('detail');
setModalOpen(true);
} else {
} else if (hasPermission('schedule:create')) {
setSelectedSchedules([]);
setEditingSchedule(null);
setModalMode('create');
@@ -948,7 +949,8 @@ const SchedulesPage: React.FC = () => {
}}
>
<span style={{ fontWeight: 500 }}></span>
<Button
<PermissionButton
permission="schedule:create"
type="primary"
icon={<PlusOutlined />}
onClick={() => {
@@ -964,7 +966,7 @@ const SchedulesPage: React.FC = () => {
}}
>
</Button>
</PermissionButton>
</div>
{selectedSchedules.length === 0 ? (
<Empty description="该时段暂无排课" />