fix: close permission review gaps
fix: harden permission-gated UI — minimum-org endpoint, modal/Popconfirm fail-closed on revocation
This commit is contained in:
@@ -1146,9 +1146,9 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) =>
|
||||
value={studentSearch}
|
||||
onChange={(event) => setStudentSearch(event.target.value)}
|
||||
/>
|
||||
<Button icon={<ExportOutlined />} onClick={handleExport}>
|
||||
<PermissionButton permission="attendance:export" icon={<ExportOutlined />} onClick={handleExport}>
|
||||
导出
|
||||
</Button>
|
||||
</PermissionButton>
|
||||
</div>
|
||||
</header>
|
||||
<div className="student-legend">
|
||||
|
||||
@@ -27,7 +27,7 @@ interface ExamDetail extends ExamItem {
|
||||
}
|
||||
|
||||
const PhoneCell: React.FC<{ row: ScoreRow }> = ({ row }) => {
|
||||
const reveal = useViewSensitive(row.studentId, '考试管理');
|
||||
const reveal = useViewSensitive(row.studentId, '考试管理', hasPermission('log:create'));
|
||||
const { hasPermission } = usePermission();
|
||||
if (!row.phone) return <>-</>;
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
@@ -38,8 +38,11 @@ import { usePermission } from '../../hooks/usePermission';
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const OccupanciesPage: React.FC = () => {
|
||||
const { hasPermission } = usePermission();
|
||||
const canCheckIn = hasPermission('occupancy:checkin');
|
||||
const { hasPermission, permissionsReady } = usePermission();
|
||||
const canCheckIn = permissionsReady && hasPermission('occupancy:checkin');
|
||||
const canCheckOut = permissionsReady && hasPermission('occupancy:checkout');
|
||||
const canTransfer = permissionsReady && hasPermission('occupancy:transfer');
|
||||
const canDelete = permissionsReady && hasPermission('occupancy:delete');
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [students, setStudents] = useState<any[]>([]);
|
||||
const [rooms, setRooms] = useState<any[]>([]);
|
||||
@@ -69,6 +72,12 @@ const OccupanciesPage: React.FC = () => {
|
||||
const selectedCheckInRoomId = Form.useWatch('roomId', checkInForm);
|
||||
const selectedTransferRoomId = Form.useWatch('newRoomId', transferForm);
|
||||
|
||||
// Close modals when the user loses the required permission
|
||||
useEffect(() => { if (!canCheckIn) { setCheckInModal(false); checkInForm.resetFields(); } }, [canCheckIn, checkInForm]);
|
||||
useEffect(() => { if (!canCheckOut && checkOutModal) { setCheckOutModal(null); checkOutForm.resetFields(); } }, [canCheckOut, checkOutModal, checkOutForm]);
|
||||
useEffect(() => { if (!canCheckOut) { setBatchCheckOutModal(false); batchCheckOutForm.resetFields(); } }, [canCheckOut, batchCheckOutForm]);
|
||||
useEffect(() => { if (!canTransfer && transferModal) { setTransferModal(null); transferForm.resetFields(); } }, [canTransfer, transferModal, transferForm]);
|
||||
|
||||
const activeOccupancyByStudentId = useMemo(() => {
|
||||
const map = new Map<number, any>();
|
||||
data.forEach((item) => {
|
||||
@@ -372,27 +381,28 @@ const OccupanciesPage: React.FC = () => {
|
||||
) : (
|
||||
<Space>
|
||||
<Tag>已退宿</Tag>
|
||||
<Popconfirm
|
||||
title="确定归档此记录?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await api.delete(`/occupancies/${record.id}`);
|
||||
message.success('归档成功');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '归档失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="occupancy:delete"
|
||||
size="small"
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
{canDelete ? (
|
||||
<Popconfirm
|
||||
title="确定归档此记录?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await api.delete(`/occupancies/${record.id}`);
|
||||
message.success('归档成功');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '归档失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
>
|
||||
归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -584,15 +594,15 @@ const OccupanciesPage: React.FC = () => {
|
||||
>
|
||||
批量退宿
|
||||
</PermissionButton>
|
||||
) : (
|
||||
</>)}
|
||||
{canDelete ? (
|
||||
<Popconfirm
|
||||
title={`确定归档选中的 ${selectedRowKeys.length} 条入住记录?在住记录会自动跳过`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
>
|
||||
<PermissionButton
|
||||
permission="occupancy:delete"
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<InboxOutlined />}
|
||||
@@ -600,8 +610,9 @@ const OccupanciesPage: React.FC = () => {
|
||||
loading={batchLoading}
|
||||
>
|
||||
批量归档
|
||||
</PermissionButton>
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
)}
|
||||
<Button size="small" onClick={() => setSelectedRowKeys([])} style={{ marginLeft: 8 }}>
|
||||
取消选择
|
||||
@@ -629,8 +640,8 @@ const OccupanciesPage: React.FC = () => {
|
||||
/>
|
||||
<Modal
|
||||
title="入住登记"
|
||||
open={checkInModal}
|
||||
onOk={handleCheckIn}
|
||||
open={checkInModal && canCheckIn}
|
||||
onOk={canCheckIn ? handleCheckIn : undefined}
|
||||
onCancel={() => {
|
||||
setCheckInModal(false);
|
||||
setAvailableBeds([]);
|
||||
@@ -804,8 +815,8 @@ const OccupanciesPage: React.FC = () => {
|
||||
{/* 退宿弹窗 */}
|
||||
<Modal
|
||||
title={`退宿 - ${checkOutModal?.student?.name}`}
|
||||
open={!!checkOutModal}
|
||||
onOk={handleCheckOut}
|
||||
open={!!checkOutModal && canCheckOut}
|
||||
onOk={canCheckOut ? handleCheckOut : undefined}
|
||||
onCancel={() => setCheckOutModal(null)}
|
||||
okText="确认退宿"
|
||||
confirmLoading={saving}
|
||||
@@ -861,8 +872,8 @@ const OccupanciesPage: React.FC = () => {
|
||||
{/* 批量退宿弹窗 */}
|
||||
<Modal
|
||||
title={`批量退宿(${selectedRowKeys.length} 人)`}
|
||||
open={batchCheckOutModal}
|
||||
onOk={handleBatchCheckOut}
|
||||
open={batchCheckOutModal && canCheckOut}
|
||||
onOk={canCheckOut ? handleBatchCheckOut : undefined}
|
||||
onCancel={() => setBatchCheckOutModal(false)}
|
||||
okText="确认批量退宿"
|
||||
width={500}
|
||||
@@ -938,7 +949,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
{/* 换房弹窗 */}
|
||||
<Modal
|
||||
title={`换房 - ${transferModal?.student?.name}`}
|
||||
open={!!transferModal}
|
||||
open={!!transferModal && canTransfer}
|
||||
onOk={handleTransfer}
|
||||
onCancel={() => {
|
||||
setTransferModal(null);
|
||||
|
||||
@@ -85,12 +85,15 @@ function parseRoomNumber(input: string) {
|
||||
}
|
||||
|
||||
const RoomsPage: React.FC = () => {
|
||||
const { hasPermission } = usePermission();
|
||||
const canEditRooms = hasPermission('room:edit');
|
||||
const { hasPermission, permissionsReady } = usePermission();
|
||||
const canEditRooms = permissionsReady && hasPermission('room:edit');
|
||||
const canCreateRooms = permissionsReady && hasPermission('room:create');
|
||||
const canDeleteRooms = permissionsReady && hasPermission('room:delete');
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<any>(null);
|
||||
const canSaveRoom = editing ? canEditRooms : canCreateRooms;
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [archivedCount, setArchivedCount] = useState(0);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
@@ -114,6 +117,9 @@ const RoomsPage: React.FC = () => {
|
||||
const [savingLocker, setSavingLocker] = useState(false);
|
||||
const [batchLoading, setBatchLoading] = useState(false);
|
||||
|
||||
// Close modals when the required permission is lost
|
||||
useEffect(() => { if (!canSaveRoom && modalOpen) { setModalOpen(false); setEditing(null); form.resetFields(); } }, [canSaveRoom, modalOpen, form]);
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
@@ -522,16 +528,17 @@ const RoomsPage: React.FC = () => {
|
||||
return (
|
||||
<Space>
|
||||
{r.status === 'archived' ? (
|
||||
<Popconfirm title="确定恢复此宿舍?" onConfirm={() => handleRestore(r.id)}>
|
||||
<PermissionButton
|
||||
permission="room:edit"
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
type="link"
|
||||
>
|
||||
恢复
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
canEditRooms ? (
|
||||
<Popconfirm title="确定恢复此宿舍?" onConfirm={() => handleRestore(r.id)}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
type="link"
|
||||
>
|
||||
恢复
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null
|
||||
) : (
|
||||
<>
|
||||
<PermissionButton
|
||||
@@ -559,15 +566,16 @@ const RoomsPage: React.FC = () => {
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm title="确定归档?" onConfirm={() => handleArchive(r.id)}>
|
||||
<PermissionButton
|
||||
permission="room:delete"
|
||||
size="small"
|
||||
icon={<InboxOutlined />}
|
||||
>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
{canDeleteRooms ? (
|
||||
<Popconfirm title="确定归档?" onConfirm={() => handleArchive(r.id)}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<InboxOutlined />}
|
||||
>
|
||||
归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
@@ -638,23 +646,24 @@ const RoomsPage: React.FC = () => {
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="room:delete"
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
{canDeleteRooms ? (
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
loading={batchLoading}
|
||||
>
|
||||
批量归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
loading={batchLoading}
|
||||
>
|
||||
批量归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="room:create"
|
||||
type="primary"
|
||||
@@ -732,8 +741,8 @@ const RoomsPage: React.FC = () => {
|
||||
|
||||
<Modal
|
||||
title={editing ? '编辑宿舍' : '添加宿舍'}
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
open={modalOpen && canSaveRoom}
|
||||
onOk={canSaveRoom ? handleSave : undefined}
|
||||
onCancel={() => {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
@@ -994,20 +1003,19 @@ const RoomsPage: React.FC = () => {
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
{r.status !== 'occupied' && (
|
||||
{r.status !== 'occupied' && canEditRooms && (
|
||||
<Popconfirm
|
||||
title="确定归档?"
|
||||
onConfirm={() => handleDeleteBed(r.id)}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="room:edit"
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
disabled={drawerRoom?.status === 'archived'}
|
||||
>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
@@ -1147,20 +1155,19 @@ const RoomsPage: React.FC = () => {
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
{r.status !== 'occupied' && (
|
||||
{r.status !== 'occupied' && canEditRooms && (
|
||||
<Popconfirm
|
||||
title="确定归档?"
|
||||
onConfirm={() => handleDeleteLocker(r.id)}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="room:edit"
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
disabled={drawerRoom?.status === 'archived'}
|
||||
>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
@@ -85,15 +85,24 @@ interface StudentFilterLookups {
|
||||
|
||||
const StudentsPage: React.FC = () => {
|
||||
const { modal } = App.useApp();
|
||||
const { hasPermission, hasAnyPermission } = usePermission();
|
||||
const { hasPermission, hasAnyPermission, hasAllPermissions } = usePermission();
|
||||
const canViewOrganizations = hasPermission('organization:view');
|
||||
const canChooseOrganization =
|
||||
canViewOrganizations && hasAnyPermission('student:create', 'student:edit');
|
||||
const canLoadOrganizations = hasAnyPermission(
|
||||
'organization:view',
|
||||
'student:create',
|
||||
'student:edit',
|
||||
);
|
||||
const canChooseOrganization = hasAnyPermission('student:create', 'student:edit');
|
||||
const canCreateStudent = hasPermission('student:create');
|
||||
const canEditStudent = hasPermission('student:edit');
|
||||
const canDeleteStudent = hasPermission('student:delete');
|
||||
const canSyncJinshuju = hasAllPermissions('sync:read', 'sync:trigger');
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [organizations, setOrganizations] = useState<any[]>([]);
|
||||
const [editing, setEditing] = useState<any>(null);
|
||||
const canSaveStudent = editing ? canEditStudent : canCreateStudent;
|
||||
const [searchName, setSearchName] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
|
||||
const [filterOrganizationId, setFilterOrganizationId] = useState<number | undefined>(undefined);
|
||||
@@ -118,13 +127,40 @@ const StudentsPage: React.FC = () => {
|
||||
|
||||
const [jinshujuOpen, setJinshujuOpen] = useState(false);
|
||||
|
||||
// Sensitive info modal — command-style; destroy when log:create is lost or comp unmounts.
|
||||
// Close the student form modal when the user loses the required permission.
|
||||
useEffect(() => {
|
||||
if (!canSaveStudent && modalOpen) {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [canSaveStudent, modalOpen, form]);
|
||||
|
||||
// Close sensitive modal when log:create is lost (imperative ref already set above).
|
||||
const logCreateRef = React.useRef(hasPermission('log:create'));
|
||||
const sensitiveModalRef = React.useRef<ReturnType<typeof modal.confirm> | null>(null);
|
||||
logCreateRef.current = hasPermission('log:create');
|
||||
useEffect(() => {
|
||||
if (!logCreateRef.current && sensitiveModalRef.current) {
|
||||
sensitiveModalRef.current.destroy();
|
||||
sensitiveModalRef.current = null;
|
||||
}
|
||||
return () => {
|
||||
sensitiveModalRef.current?.destroy();
|
||||
sensitiveModalRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleViewSensitive = (studentId: number, field: string, value: string) => {
|
||||
modal.confirm({
|
||||
if (!logCreateRef.current) return;
|
||||
sensitiveModalRef.current = modal.confirm({
|
||||
title: '查看敏感信息',
|
||||
content: `您即将查看 "${field}" 的完整信息。此操作将被记录。`,
|
||||
okText: '确认查看',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
if (!logCreateRef.current) return;
|
||||
try {
|
||||
await api.post('/operation-logs/audit', {
|
||||
module: '学生管理',
|
||||
@@ -142,6 +178,9 @@ const StudentsPage: React.FC = () => {
|
||||
message.error('审计日志记录失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
afterClose: () => {
|
||||
sensitiveModalRef.current = null;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -194,16 +233,25 @@ const StudentsPage: React.FC = () => {
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canLoadOrganizations) {
|
||||
setOrganizations([]);
|
||||
setFilterOrganizationId(undefined);
|
||||
return;
|
||||
}
|
||||
if (canViewOrganizations) {
|
||||
api
|
||||
.get('/organizations', { params: { includeArchived: 'false' } })
|
||||
.then((res: unknown) => {
|
||||
setOrganizations(res as Array<{ id: number; name: string }>);
|
||||
setOrganizations(res as Array<{ id: number; name: string; isHost?: boolean }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
setOrganizations([]);
|
||||
setFilterOrganizationId(undefined);
|
||||
api
|
||||
.get('/organizations/options')
|
||||
.then((res: unknown) => {
|
||||
setOrganizations(res as Array<{ id: number; name: string; isHost?: boolean }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
api
|
||||
.get<StudentFilterLookups>('/students/filter-lookups')
|
||||
@@ -212,7 +260,7 @@ const StudentsPage: React.FC = () => {
|
||||
setTeacherOptions(res.teachers || []);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [canViewOrganizations]);
|
||||
}, [canLoadOrganizations]);
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
@@ -614,21 +662,18 @@ const StudentsPage: React.FC = () => {
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.status === 'archived' ? (
|
||||
<Popconfirm
|
||||
title="确定恢复此学生?恢复后将重新出现在学生列表中。"
|
||||
onConfirm={() => handleRestore(record.id)}
|
||||
okText="恢复"
|
||||
cancelText="取消"
|
||||
>
|
||||
<PermissionButton
|
||||
permission="student:edit"
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
type="link"
|
||||
canEditStudent ? (
|
||||
<Popconfirm
|
||||
title="确定恢复此学生?恢复后将重新出现在学生列表中。"
|
||||
onConfirm={() => handleRestore(record.id)}
|
||||
okText="恢复"
|
||||
cancelText="取消"
|
||||
>
|
||||
恢复
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">
|
||||
恢复
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null
|
||||
) : (
|
||||
<>
|
||||
<PermissionButton
|
||||
@@ -650,20 +695,21 @@ const StudentsPage: React.FC = () => {
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm
|
||||
title="归档后不会删除数据,可随时恢复。确定归档?"
|
||||
onConfirm={() => handleArchive(record.id)}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
>
|
||||
<PermissionButton
|
||||
permission="student:delete"
|
||||
size="small"
|
||||
{canDeleteStudent ? (
|
||||
<Popconfirm
|
||||
title="归档后不会删除数据,可随时恢复。确定归档?"
|
||||
onConfirm={() => handleArchive(record.id)}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<InboxOutlined />}
|
||||
>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
@@ -765,23 +811,24 @@ const StudentsPage: React.FC = () => {
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="student:delete"
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
{canDeleteStudent ? (
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
loading={batchLoading}
|
||||
>
|
||||
批量归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
loading={batchLoading}
|
||||
>
|
||||
批量归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="student:create"
|
||||
type="primary"
|
||||
@@ -814,13 +861,11 @@ const StudentsPage: React.FC = () => {
|
||||
</Upload>
|
||||
</>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="sync:read"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={() => setJinshujuOpen(true)}
|
||||
>
|
||||
同步金数据
|
||||
</PermissionButton>
|
||||
{canSyncJinshuju ? (
|
||||
<Button icon={<CloudUploadOutlined />} onClick={() => setJinshujuOpen(true)}>
|
||||
同步金数据
|
||||
</Button>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="student:view"
|
||||
icon={<DownloadOutlined />}
|
||||
@@ -929,8 +974,8 @@ const StudentsPage: React.FC = () => {
|
||||
title={editing ? '编辑学生' : '添加学生'}
|
||||
className="student-form-modal"
|
||||
width={720}
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
open={modalOpen && canSaveStudent}
|
||||
onOk={canSaveStudent ? handleSave : undefined}
|
||||
onCancel={() => {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
@@ -1007,7 +1052,7 @@ const StudentsPage: React.FC = () => {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{hasPermission('sync:read') ? (
|
||||
{canSyncJinshuju ? (
|
||||
<JinshujuMatchModal
|
||||
open={jinshujuOpen}
|
||||
onClose={() => setJinshujuOpen(false)}
|
||||
|
||||
Reference in New Issue
Block a user