fix: close permission review gaps

fix: harden permission-gated UI — minimum-org endpoint, modal/Popconfirm fail-closed on revocation
This commit is contained in:
2026-07-23 12:23:33 +08:00
parent c98d37307e
commit a7a7af1667
17 changed files with 391 additions and 227 deletions

View File

@@ -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);