fix: align permission-gated UI actions

This commit is contained in:
2026-07-23 11:32:13 +08:00
parent adf738288f
commit c98d37307e
28 changed files with 1340 additions and 718 deletions

View File

@@ -33,10 +33,13 @@ import { maskPhone, maskIdNumber } from '../../utils/sensitive';
import PermissionButton from '../../components/PermissionButton';
import { message } from '../../ui/app-message';
import { buildCheckInPayload, buildTransferPayload } from './occupancy-form';
import { usePermission } from '../../hooks/usePermission';
const { RangePicker } = DatePicker;
const OccupanciesPage: React.FC = () => {
const { hasPermission } = usePermission();
const canCheckIn = hasPermission('occupancy:checkin');
const [data, setData] = useState<any[]>([]);
const [students, setStudents] = useState<any[]>([]);
const [rooms, setRooms] = useState<any[]>([]);
@@ -93,7 +96,12 @@ const OccupanciesPage: React.FC = () => {
[data, selectedRowKeys],
);
const latestSelectedCheckInDate = useMemo(
() => selectedBatchRecords.map((item) => item.checkInDate).filter(Boolean).sort().at(-1),
() =>
selectedBatchRecords
.map((item) => item.checkInDate)
.filter(Boolean)
.sort()
.at(-1),
[selectedBatchRecords],
);
const latestSelectedBillingStartDate = useMemo(
@@ -106,7 +114,8 @@ const OccupanciesPage: React.FC = () => {
[selectedBatchRecords],
);
const dateNotBefore = (start: string | Dayjs | null | undefined, messageText: string) =>
const dateNotBefore =
(start: string | Dayjs | null | undefined, messageText: string) =>
(_: unknown, value?: Dayjs | null) => {
if (!value || !start) return Promise.resolve();
const startDate = dayjs.isDayjs(start) ? start : dayjs(start);
@@ -457,46 +466,77 @@ const OccupanciesPage: React.FC = () => {
>
</PermissionButton>
<Upload
accept=".xlsx,.xls"
showUploadList={false}
customRequest={async ({ file, onSuccess, onError }: any) => {
const formData = new FormData();
formData.append('file', file);
const params = new URLSearchParams();
if (autoDeposit) {
params.set('autoDeposit', 'true');
params.set('depositAmount', String(depositAmount));
}
try {
const res: any = await api.post(
`/occupancies/import?${params.toString()}`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } },
);
if (res.errors?.length > 0) {
Modal.warning({
title: res.message,
content: res.errors.join('\n'),
width: 500,
});
} else {
message.success(res.message);
}
onSuccess?.(res);
fetchData();
} catch (e: any) {
message.error(e?.message || '导入失败');
onError?.(e);
}
}}
>
<Tooltip title="按手机号关联学生,并自动创建缺失的学生、宿舍和入住记录">
<Button type="primary" ghost icon={<UploadOutlined />}>
</Button>
</Tooltip>
</Upload>
{canCheckIn ? (
<>
<Upload
accept=".xlsx,.xls"
showUploadList={false}
customRequest={async ({ file, onSuccess, onError }: any) => {
const formData = new FormData();
formData.append('file', file);
const params = new URLSearchParams();
if (autoDeposit) {
params.set('autoDeposit', 'true');
params.set('depositAmount', String(depositAmount));
}
try {
const res: any = await api.post(
`/occupancies/import?${params.toString()}`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } },
);
if (res.errors?.length > 0) {
Modal.warning({
title: res.message,
content: res.errors.join('\n'),
width: 500,
});
} else {
message.success(res.message);
}
onSuccess?.(res);
fetchData();
} catch (e: any) {
message.error(e?.message || '导入失败');
onError?.(e);
}
}}
>
<Tooltip title="按手机号关联学生,并自动创建缺失的学生、宿舍和入住记录">
<Button type="primary" ghost icon={<UploadOutlined />}>
</Button>
</Tooltip>
</Upload>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
<Switch size="small" checked={autoDeposit} onChange={setAutoDeposit} />
{autoDeposit && (
<Space.Compact>
<InputNumber
size="small"
min={0}
value={depositAmount}
onChange={(v) => setDepositAmount(v || 500)}
style={{ width: 60 }}
/>
<span
style={{
padding: '0 8px',
display: 'flex',
alignItems: 'center',
border: '1px solid #d9d9d9',
backgroundColor: '#fafafa',
fontSize: 12,
}}
>
</span>
</Space.Compact>
)}
</span>
</>
) : null}
<PermissionButton
permission="occupancy:view"
icon={<DownloadOutlined />}
@@ -521,33 +561,6 @@ const OccupanciesPage: React.FC = () => {
>
</PermissionButton>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
<Switch size="small" checked={autoDeposit} onChange={setAutoDeposit} />
{autoDeposit && (
<Space.Compact>
<InputNumber
size="small"
min={0}
value={depositAmount}
onChange={(v) => setDepositAmount(v || 500)}
style={{ width: 60 }}
/>
<span
style={{
padding: '0 8px',
display: 'flex',
alignItems: 'center',
border: '1px solid #d9d9d9',
backgroundColor: '#fafafa',
fontSize: 12,
}}
>
</span>
</Space.Compact>
)}
</span>
</Space>
</div>
{selectedRowKeys.length > 0 && (
@@ -642,7 +655,11 @@ const OccupanciesPage: React.FC = () => {
.filter((s: any) => s.status === 'active')
.map((s: any) => {
const activeOccupancy = activeOccupancyByStudentId.get(s.id);
const identifier = s.idNumber ? maskIdNumber(s.idNumber) : s.phone ? maskPhone(s.phone) : '';
const identifier = s.idNumber
? maskIdNumber(s.idNumber)
: s.phone
? maskPhone(s.phone)
: '';
return {
value: s.id,
label: `${s.name} (${identifier})${activeOccupancy ? ` · 已入住${activeOccupancy.room?.roomNumber ? ` ${activeOccupancy.room.roomNumber}` : ''}` : ''}`,
@@ -668,18 +685,25 @@ const OccupanciesPage: React.FC = () => {
}))}
/>
</Form.Item>
<Form.Item name="checkInDate" label="入住日期" rules={[{ required: true, message: '请选择入住日期' }]}>
<Form.Item
name="checkInDate"
label="入住日期"
rules={[{ required: true, message: '请选择入住日期' }]}
>
<DatePicker style={{ width: '100%' }} placeholder="选择入住日期" format="YYYY-MM-DD" />
</Form.Item>
<Form.Item
name="billingStartDate"
label="计费起始日"
dependencies={["checkInDate"]}
dependencies={['checkInDate']}
extra="默认与入住日期相同,可调整(如学生要求从次日开始计费)"
rules={[
{ required: true, message: '请选择计费起始日' },
({ getFieldValue }) => ({
validator: dateNotBefore(getFieldValue('checkInDate'), '计费起始日不能早于入住日期'),
validator: dateNotBefore(
getFieldValue('checkInDate'),
'计费起始日不能早于入住日期',
),
}),
]}
>
@@ -689,7 +713,11 @@ const OccupanciesPage: React.FC = () => {
format="YYYY-MM-DD"
/>
</Form.Item>
<Form.Item name="stayType" label="入住类型" rules={[{ required: true, message: '请选择入住类型' }]}>
<Form.Item
name="stayType"
label="入住类型"
rules={[{ required: true, message: '请选择入住类型' }]}
>
<Select
options={[
{ value: 'short', label: '短租' },
@@ -714,7 +742,9 @@ const OccupanciesPage: React.FC = () => {
<Select
placeholder={selectedCheckInRoomId ? '请选择床位' : '请先选择房间'}
loading={availableResourcesLoading}
disabled={!selectedCheckInRoomId || availableResourcesLoading || availableBeds.length === 0}
disabled={
!selectedCheckInRoomId || availableResourcesLoading || availableBeds.length === 0
}
options={availableBeds.map((b) => ({
value: b.id,
label: b.bedNumber,
@@ -732,7 +762,9 @@ const OccupanciesPage: React.FC = () => {
allowClear
placeholder="可选分配柜子"
loading={availableResourcesLoading}
disabled={!selectedCheckInRoomId || availableResourcesLoading || availableLockers.length === 0}
disabled={
!selectedCheckInRoomId || availableResourcesLoading || availableLockers.length === 0
}
options={availableLockers.map((l) => ({
value: l.id,
label: l.lockerNumber,
@@ -792,12 +824,14 @@ const OccupanciesPage: React.FC = () => {
<Form.Item
name="billingEndDate"
label="计费截止日"
dependencies={["checkOutDate"]}
dependencies={['checkOutDate']}
extra="默认与退宿日期相同"
rules={[
({ getFieldValue }) => ({
validator: dateNotBefore(
checkOutModal?.billingStartDate || checkOutModal?.checkInDate || getFieldValue('checkOutDate'),
checkOutModal?.billingStartDate ||
checkOutModal?.checkInDate ||
getFieldValue('checkOutDate'),
'计费截止日不能早于计费起始日',
),
}),
@@ -948,7 +982,11 @@ const OccupanciesPage: React.FC = () => {
<Select
placeholder={selectedTransferRoomId ? '请选择目标床位' : '请先选择目标宿舍'}
loading={transferResourcesLoading}
disabled={!selectedTransferRoomId || transferResourcesLoading || transferAvailableBeds.length === 0}
disabled={
!selectedTransferRoomId ||
transferResourcesLoading ||
transferAvailableBeds.length === 0
}
options={transferAvailableBeds.map((bed) => ({
value: bed.id,
label: bed.bedNumber,
@@ -966,7 +1004,11 @@ const OccupanciesPage: React.FC = () => {
allowClear
placeholder="可选分配目标宿舍柜子"
loading={transferResourcesLoading}
disabled={!selectedTransferRoomId || transferResourcesLoading || transferAvailableLockers.length === 0}
disabled={
!selectedTransferRoomId ||
transferResourcesLoading ||
transferAvailableLockers.length === 0
}
options={transferAvailableLockers.map((locker) => ({
value: locker.id,
label: locker.lockerNumber,
@@ -979,7 +1021,9 @@ const OccupanciesPage: React.FC = () => {
label="换房日期"
rules={[
{ required: true, message: '请选择换房日期' },
{ validator: dateNotBefore(transferModal?.checkInDate, '换房日期不能早于原入住日期') },
{
validator: dateNotBefore(transferModal?.checkInDate, '换房日期不能早于原入住日期'),
},
]}
>
<DatePicker style={{ width: '100%' }} placeholder="选择换房日期" format="YYYY-MM-DD" />
@@ -987,12 +1031,14 @@ const OccupanciesPage: React.FC = () => {
<Form.Item
name="oldBillingEndDate"
label="旧房计费截止日"
dependencies={["transferDate"]}
dependencies={['transferDate']}
extra="默认为换房当天"
rules={[
({ getFieldValue }) => ({
validator: dateNotBefore(
transferModal?.billingStartDate || transferModal?.checkInDate || getFieldValue('transferDate'),
transferModal?.billingStartDate ||
transferModal?.checkInDate ||
getFieldValue('transferDate'),
'旧房计费截止日不能早于计费起始日',
),
}),
@@ -1007,11 +1053,14 @@ const OccupanciesPage: React.FC = () => {
<Form.Item
name="newBillingStartDate"
label="新房计费起始日"
dependencies={["transferDate"]}
dependencies={['transferDate']}
extra="默认为换房次日"
rules={[
({ getFieldValue }) => ({
validator: dateNotBefore(getFieldValue('transferDate'), '新房计费起始日不能早于换房日期'),
validator: dateNotBefore(
getFieldValue('transferDate'),
'新房计费起始日不能早于换房日期',
),
}),
]}
>