fix(admin): UX improvements — silent fetch failures, empty states, batch loading guards, dashboard refresh
- Replace console.error-only catches with message.error user-facing notifications
across Bills, Classes, ClassroomRentals, ClassroomSchedule, Classrooms, Deposits,
Expenses, OperationLogs, Permissions, Roles, RoomVisual, Rooms, Students,
Tenants, Users
- Add Empty component via Table locale prop on list pages: Bills, Classes,
ClassroomRentals, Classrooms, Deposits, Expenses (room+personal), Occupancies,
Rooms, Students, Tenants, Roles
- Add batchLoading state to batch delete/update operations: Bills (batchDelete,
batchUpdateStatus), Expenses (batchDeleteRoom, batchDeletePersonal),
Occupancies (batchCheckOut, batchDelete), Rooms (batchDelete),
Students (batchDelete)
- Add refreshLoading indicator to Dashboard header when re-fetching data
- Consistent error pattern: catch (e: unknown) { const err = e as { message?: string }; message.error(...); }
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Spin,
|
Spin,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
@@ -54,6 +55,7 @@ const BillsPage: React.FC = () => {
|
|||||||
const [generateForm] = Form.useForm();
|
const [generateForm] = Form.useForm();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [detailLoading, setDetailLoading] = useState(false);
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
|
const [batchLoading, setBatchLoading] = useState(false);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -63,8 +65,8 @@ const BillsPage: React.FC = () => {
|
|||||||
if (filterExpenseType) params.expenseType = filterExpenseType;
|
if (filterExpenseType) params.expenseType = filterExpenseType;
|
||||||
const res = await api.get('/bills', { params }) as unknown[];
|
const res = await api.get('/bills', { params }) as unknown[];
|
||||||
setBills(res);
|
setBills(res);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
message.error(e?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [filterStatus, filterExpenseType]);
|
}, [filterStatus, filterExpenseType]);
|
||||||
@@ -110,8 +112,9 @@ const BillsPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const res = await api.get(`/bills/${id}`);
|
const res = await api.get(`/bills/${id}`);
|
||||||
setDetailModal(res);
|
setDetailModal(res);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
message.error(e?.message || '加载详情失败');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setDetailLoading(false);
|
setDetailLoading(false);
|
||||||
}
|
}
|
||||||
@@ -132,6 +135,7 @@ const BillsPage: React.FC = () => {
|
|||||||
|
|
||||||
const batchUpdateStatus = async (status: string) => {
|
const batchUpdateStatus = async (status: string) => {
|
||||||
if (selectedRows.length === 0) return message.warning('请先选择账单');
|
if (selectedRows.length === 0) return message.warning('请先选择账单');
|
||||||
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.put('/bills/batch/status', { ids: selectedRows, status });
|
await api.put('/bills/batch/status', { ids: selectedRows, status });
|
||||||
message.success(`已批量更新 ${selectedRows.length} 条账单`);
|
message.success(`已批量更新 ${selectedRows.length} 条账单`);
|
||||||
@@ -139,6 +143,8 @@ const BillsPage: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '操作失败');
|
message.error(e?.message || '操作失败');
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,9 +157,9 @@ const BillsPage: React.FC = () => {
|
|||||||
message.error(e?.message || '删除失败');
|
message.error(e?.message || '删除失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const batchDelete = async () => {
|
const batchDelete = async () => {
|
||||||
if (selectedRows.length === 0) return message.warning('请先选择账单');
|
if (selectedRows.length === 0) return message.warning('请先选择账单');
|
||||||
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.post('/bills/batch/delete', { ids: selectedRows });
|
await api.post('/bills/batch/delete', { ids: selectedRows });
|
||||||
message.success(`已删除 ${selectedRows.length} 条账单`);
|
message.success(`已删除 ${selectedRows.length} 条账单`);
|
||||||
@@ -161,6 +167,8 @@ const BillsPage: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '操作失败');
|
message.error(e?.message || '操作失败');
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -334,6 +342,7 @@ const BillsPage: React.FC = () => {
|
|||||||
permission="bill:confirm"
|
permission="bill:confirm"
|
||||||
onClick={() => batchUpdateStatus('confirmed')}
|
onClick={() => batchUpdateStatus('confirmed')}
|
||||||
disabled={selectedRows.length === 0}
|
disabled={selectedRows.length === 0}
|
||||||
|
loading={batchLoading}
|
||||||
>
|
>
|
||||||
批量确认
|
批量确认
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -342,6 +351,7 @@ const BillsPage: React.FC = () => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => batchUpdateStatus('paid')}
|
onClick={() => batchUpdateStatus('paid')}
|
||||||
disabled={selectedRows.length === 0}
|
disabled={selectedRows.length === 0}
|
||||||
|
loading={batchLoading}
|
||||||
>
|
>
|
||||||
批量标记已付
|
批量标记已付
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -357,6 +367,7 @@ const BillsPage: React.FC = () => {
|
|||||||
danger
|
danger
|
||||||
disabled={selectedRows.length === 0}
|
disabled={selectedRows.length === 0}
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
|
loading={batchLoading}
|
||||||
>
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -389,6 +400,7 @@ const BillsPage: React.FC = () => {
|
|||||||
dataSource={filteredBills}
|
dataSource={filteredBills}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
selectedRowKeys: selectedRows,
|
selectedRowKeys: selectedRows,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Table, Button, Input, Select, Space, Tag, Modal, Form, InputNumber,
|
Table, Button, Input, Select, Space, Tag, Modal, Form, InputNumber,
|
||||||
DatePicker, Popconfirm, message, Card, Switch,
|
DatePicker, Popconfirm, message, Card, Switch, Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import { PlusOutlined, SearchOutlined, TeamOutlined, InboxOutlined } from '@ant-design/icons';
|
import { PlusOutlined, SearchOutlined, TeamOutlined, InboxOutlined } from '@ant-design/icons';
|
||||||
@@ -98,8 +98,9 @@ const ClassesPage: React.FC = () => {
|
|||||||
params.isArchived = showArchived;
|
params.isArchived = showArchived;
|
||||||
const res = await api.get<ClassItem[]>('/classes', { params } as Record<string, unknown>);
|
const res = await api.get<ClassItem[]>('/classes', { params } as Record<string, unknown>);
|
||||||
setData(res);
|
setData(res);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
message.error(e?.message || '加载失败,请稍后重试');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -266,6 +267,7 @@ const ClassesPage: React.FC = () => {
|
|||||||
dataSource={filtered}
|
dataSource={filtered}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
pagination={{ pageSize: 20 }}
|
pagination={{ pageSize: 20 }}
|
||||||
scroll={{ x: 1100 }}
|
scroll={{ x: 1100 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
Upload,
|
Upload,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, UploadOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons';
|
import { PlusOutlined, UploadOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
@@ -50,8 +51,8 @@ const ClassroomRentalsPage: React.FC = () => {
|
|||||||
if (filterMonth) params.month = filterMonth.format('YYYY-MM');
|
if (filterMonth) params.month = filterMonth.format('YYYY-MM');
|
||||||
const res: any = await api.get('/classroom-rentals', { params });
|
const res: any = await api.get('/classroom-rentals', { params });
|
||||||
setData(res);
|
setData(res);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
message.error(e?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -61,8 +62,8 @@ const ClassroomRentalsPage: React.FC = () => {
|
|||||||
const [cr, tn]: any = await Promise.all([api.get('/classrooms'), api.get('/tenants')]);
|
const [cr, tn]: any = await Promise.all([api.get('/classrooms'), api.get('/tenants')]);
|
||||||
setClassrooms(cr);
|
setClassrooms(cr);
|
||||||
setTenants(tn);
|
setTenants(tn);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
message.error(e?.message || '加载教室列表失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -311,10 +312,10 @@ const ClassroomRentalsPage: React.FC = () => {
|
|||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1200 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={editing ? '编辑租赁' : '新增租赁'}
|
title={editing ? '编辑租赁' : '新增租赁'}
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Spin,
|
Spin,
|
||||||
Empty,
|
Empty,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { CalendarOutlined, FileTextOutlined } from '@ant-design/icons';
|
import { CalendarOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
@@ -44,8 +45,9 @@ const ClassroomSchedulePage: React.FC = () => {
|
|||||||
params: { year: month.year(), month: month.month() + 1 },
|
params: { year: month.year(), month: month.month() + 1 },
|
||||||
});
|
});
|
||||||
setData(res);
|
setData(res);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [month]);
|
}, [month]);
|
||||||
@@ -85,8 +87,9 @@ const ClassroomSchedulePage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const res: any = await api.get(`/classroom-rentals/${rentalId}`);
|
const res: any = await api.get(`/classroom-rentals/${rentalId}`);
|
||||||
setDetailModal(res);
|
setDetailModal(res);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载详情失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
Upload,
|
Upload,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -69,8 +70,8 @@ const ClassroomsPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const res: any = await api.get('/classrooms', { params: { includeArchived: showArchived } });
|
const res: any = await api.get('/classrooms', { params: { includeArchived: showArchived } });
|
||||||
setData(res);
|
setData(res);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
message.error(e?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -287,9 +288,9 @@ const ClassroomsPage: React.FC = () => {
|
|||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }}
|
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={editing ? '编辑教室' : '添加教室'}
|
title={editing ? '编辑教室' : '添加教室'}
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
|
|||||||
@@ -83,13 +83,19 @@ const DashboardPage: React.FC = () => {
|
|||||||
const [roomRanking, setRoomRanking] = useState<Array<{ roomNumber: string; total: string }>>([]);
|
const [roomRanking, setRoomRanking] = useState<Array<{ roomNumber: string; total: string }>>([]);
|
||||||
const [classroomUtil, setClassroomUtil] = useState<ClassroomUtilStats | null>(null);
|
const [classroomUtil, setClassroomUtil] = useState<ClassroomUtilStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||||
const [period, setPeriod] = useState<[string, string]>([
|
const [period, setPeriod] = useState<[string, string]>([
|
||||||
dayjs().startOf('month').format('YYYY-MM-DD'),
|
dayjs().startOf('month').format('YYYY-MM-DD'),
|
||||||
dayjs().endOf('month').format('YYYY-MM-DD'),
|
dayjs().endOf('month').format('YYYY-MM-DD'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
|
const isRefresh = stats !== null;
|
||||||
|
if (isRefresh) {
|
||||||
|
setRefreshLoading(true);
|
||||||
|
} else {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const [s, rr, cr, g] = await Promise.all([
|
const [s, rr, cr, g] = await Promise.all([
|
||||||
api.get<DashboardStats>('/dashboard/stats'),
|
api.get<DashboardStats>('/dashboard/stats'),
|
||||||
@@ -114,7 +120,8 @@ const DashboardPage: React.FC = () => {
|
|||||||
message.error('数据加载失败,请稍后重试');
|
message.error('数据加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [period]);
|
setRefreshLoading(false);
|
||||||
|
}, [period, stats]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -321,7 +328,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 style={{ margin: 0 }}>数据面板</h2>
|
<h2 style={{ margin: 0 }}>数据面板{refreshLoading && <Spin size="small" style={{ marginLeft: 12 }} />}</h2>
|
||||||
<RangePicker
|
<RangePicker
|
||||||
aria-label="选择日期范围"
|
aria-label="选择日期范围"
|
||||||
value={[dayjs(period[0]), dayjs(period[1])]}
|
value={[dayjs(period[0]), dayjs(period[1])]}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
List,
|
List,
|
||||||
Card,
|
Card,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, DeleteOutlined, DollarOutlined, CheckOutlined } from '@ant-design/icons';
|
import { PlusOutlined, DeleteOutlined, DollarOutlined, CheckOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -75,8 +76,8 @@ const DepositsPage: React.FC = () => {
|
|||||||
const [d, s]: any[] = await Promise.all([api.get('/deposits'), api.get('/students')]);
|
const [d, s]: any[] = await Promise.all([api.get('/deposits'), api.get('/students')]);
|
||||||
setData(d);
|
setData(d);
|
||||||
setStudents(s);
|
setStudents(s);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
message.error(e?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -86,8 +87,8 @@ const DepositsPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const res = await api.get<PendingRefund[]>('/deposits/pending-refunds');
|
const res = await api.get<PendingRefund[]>('/deposits/pending-refunds');
|
||||||
setPendingRefunds(res || []);
|
setPendingRefunds(res || []);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
message.error(e?.message || '加载待退款列表失败');
|
||||||
}
|
}
|
||||||
setPendingLoading(false);
|
setPendingLoading(false);
|
||||||
};
|
};
|
||||||
@@ -436,10 +437,10 @@ const DepositsPage: React.FC = () => {
|
|||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1200 }}
|
||||||
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Upload,
|
Upload,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -51,6 +52,7 @@ const ExpensesPage: React.FC = () => {
|
|||||||
const [selectedRoomKeys, setSelectedRoomKeys] = useState<number[]>([]);
|
const [selectedRoomKeys, setSelectedRoomKeys] = useState<number[]>([]);
|
||||||
const [selectedPersonalKeys, setSelectedPersonalKeys] = useState<number[]>([]);
|
const [selectedPersonalKeys, setSelectedPersonalKeys] = useState<number[]>([]);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [batchLoading, setBatchLoading] = useState(false);
|
||||||
|
|
||||||
// Dynamic expense type options from API
|
// Dynamic expense type options from API
|
||||||
const [typeOptions, setTypeOptions] = useState<{ value: string; label: string }[]>([]);
|
const [typeOptions, setTypeOptions] = useState<{ value: string; label: string }[]>([]);
|
||||||
@@ -78,6 +80,7 @@ const ExpensesPage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleBatchDeleteRoom = async () => {
|
const handleBatchDeleteRoom = async () => {
|
||||||
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: any = await api.post('/expenses/room/batch-delete', { ids: selectedRoomKeys });
|
const res: any = await api.post('/expenses/room/batch-delete', { ids: selectedRoomKeys });
|
||||||
message.success(res?.message || `已删除 ${selectedRoomKeys.length} 条`);
|
message.success(res?.message || `已删除 ${selectedRoomKeys.length} 条`);
|
||||||
@@ -85,10 +88,13 @@ const ExpensesPage: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '批量删除失败');
|
message.error(e?.message || '批量删除失败');
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBatchDeletePersonal = async () => {
|
const handleBatchDeletePersonal = async () => {
|
||||||
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: any = await api.post('/expenses/personal/batch-delete', {
|
const res: any = await api.post('/expenses/personal/batch-delete', {
|
||||||
ids: selectedPersonalKeys,
|
ids: selectedPersonalKeys,
|
||||||
@@ -98,6 +104,8 @@ const ExpensesPage: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '批量删除失败');
|
message.error(e?.message || '批量删除失败');
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,8 +122,9 @@ const ExpensesPage: React.FC = () => {
|
|||||||
setPersonalExpenses(pe);
|
setPersonalExpenses(pe);
|
||||||
setRooms(rm);
|
setRooms(rm);
|
||||||
setStudents(st);
|
setStudents(st);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -410,6 +419,7 @@ const ExpensesPage: React.FC = () => {
|
|||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
disabled={selectedRoomKeys.length === 0}
|
disabled={selectedRoomKeys.length === 0}
|
||||||
|
loading={batchLoading}
|
||||||
>
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -433,6 +443,7 @@ const ExpensesPage: React.FC = () => {
|
|||||||
dataSource={filteredRoomExpenses}
|
dataSource={filteredRoomExpenses}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1200 }}
|
||||||
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
@@ -440,7 +451,6 @@ const ExpensesPage: React.FC = () => {
|
|||||||
onChange: (keys) => setSelectedRoomKeys(keys as number[]),
|
onChange: (keys) => setSelectedRoomKeys(keys as number[]),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -532,6 +542,7 @@ const ExpensesPage: React.FC = () => {
|
|||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
disabled={selectedPersonalKeys.length === 0}
|
disabled={selectedPersonalKeys.length === 0}
|
||||||
|
loading={batchLoading}
|
||||||
>
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -549,12 +560,12 @@ const ExpensesPage: React.FC = () => {
|
|||||||
录入个人费用
|
录入个人费用
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
|
||||||
<Table
|
<Table
|
||||||
columns={personalColumns}
|
columns={personalColumns}
|
||||||
dataSource={filteredPersonalExpenses}
|
dataSource={filteredPersonalExpenses}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1200 }}
|
||||||
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
Upload,
|
Upload,
|
||||||
Switch,
|
Switch,
|
||||||
Alert,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -50,6 +50,7 @@ const OccupanciesPage: React.FC = () => {
|
|||||||
const [batchCheckOutModal, setBatchCheckOutModal] = useState(false);
|
const [batchCheckOutModal, setBatchCheckOutModal] = useState(false);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [batchLoading, setBatchLoading] = useState(false);
|
||||||
const [checkInForm] = Form.useForm();
|
const [checkInForm] = Form.useForm();
|
||||||
const [checkOutForm] = Form.useForm();
|
const [checkOutForm] = Form.useForm();
|
||||||
const [transferForm] = Form.useForm();
|
const [transferForm] = Form.useForm();
|
||||||
@@ -187,6 +188,7 @@ const OccupanciesPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleBatchCheckOut = async () => {
|
const handleBatchCheckOut = async () => {
|
||||||
const values = await batchCheckOutForm.validateFields();
|
const values = await batchCheckOutForm.validateFields();
|
||||||
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: any = await api.post('/occupancies/batch-check-out', {
|
const res: any = await api.post('/occupancies/batch-check-out', {
|
||||||
ids: selectedRowKeys,
|
ids: selectedRowKeys,
|
||||||
@@ -201,10 +203,13 @@ const OccupanciesPage: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '批量退宿失败');
|
message.error(e?.message || '批量退宿失败');
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBatchDelete = async () => {
|
const handleBatchDelete = async () => {
|
||||||
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: any = await api.post('/occupancies/batch-delete', { ids: selectedRowKeys });
|
const res: any = await api.post('/occupancies/batch-delete', { ids: selectedRowKeys });
|
||||||
message.success(res?.message || `已删除 ${selectedRowKeys.length} 条`);
|
message.success(res?.message || `已删除 ${selectedRowKeys.length} 条`);
|
||||||
@@ -212,6 +217,8 @@ const OccupanciesPage: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '批量删除失败');
|
message.error(e?.message || '批量删除失败');
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -447,6 +454,7 @@ const OccupanciesPage: React.FC = () => {
|
|||||||
setBatchCheckOutModal(true);
|
setBatchCheckOutModal(true);
|
||||||
}}
|
}}
|
||||||
style={{ marginLeft: 12 }}
|
style={{ marginLeft: 12 }}
|
||||||
|
loading={batchLoading}
|
||||||
>
|
>
|
||||||
批量退宿
|
批量退宿
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -463,6 +471,7 @@ const OccupanciesPage: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
style={{ marginLeft: 12 }}
|
style={{ marginLeft: 12 }}
|
||||||
|
loading={batchLoading}
|
||||||
>
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -482,12 +491,11 @@ const OccupanciesPage: React.FC = () => {
|
|||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
scroll={{ x: 1300 }}
|
scroll={{ x: 1300 }}
|
||||||
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 入住登记弹窗 */}
|
|
||||||
<Modal
|
<Modal
|
||||||
title="入住登记"
|
title="入住登记"
|
||||||
open={checkInModal}
|
open={checkInModal}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import { Table, Select, DatePicker, Space, Tag, Tooltip } from 'antd';
|
import { Table, Select, DatePicker, Space, Tag, Tooltip, message } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
|
|
||||||
@@ -40,8 +40,9 @@ const OperationLogsPage: React.FC = () => {
|
|||||||
const res: any = await api.get('/operation-logs', { params });
|
const res: any = await api.get('/operation-logs', { params });
|
||||||
setData(res.data);
|
setData(res.data);
|
||||||
setTotal(res.total);
|
setTotal(res.total);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [page, filterModule, dateRange]);
|
}, [page, filterModule, dateRange]);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Tag, Input, Space, Spin, Empty } from 'antd';
|
import { Card, Tag, Input, Space, Spin, Empty, message } from 'antd';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
|
|
||||||
interface PermissionItem {
|
interface PermissionItem {
|
||||||
@@ -36,7 +36,10 @@ const PermissionsPage: React.FC = () => {
|
|||||||
api
|
api
|
||||||
.get('/rbac/permissions/tree')
|
.get('/rbac/permissions/tree')
|
||||||
.then((res: any) => setPermTree(res))
|
.then((res: any) => setPermTree(res))
|
||||||
.catch(console.error)
|
.catch((e: unknown) => {
|
||||||
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载权限失败');
|
||||||
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
message,
|
message,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
@@ -52,8 +53,9 @@ const RolesPage: React.FC = () => {
|
|||||||
]);
|
]);
|
||||||
setData(roles);
|
setData(roles);
|
||||||
setAllPerms(permTree);
|
setAllPerms(permTree);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -224,10 +226,10 @@ const RolesPage: React.FC = () => {
|
|||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
scroll={{ x: 900 }}
|
scroll={{ x: 900 }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import { Row, Col, Card, Tag, Select, Statistic, Modal, Spin, Badge, Tooltip, DatePicker, Alert, Button } from 'antd';
|
import { Row, Col, Card, Tag, Select, Statistic, Modal, Spin, Badge, Tooltip, DatePicker, Alert, Button, message } from 'antd';
|
||||||
import { HomeOutlined, UserOutlined, CalendarOutlined, BankOutlined, HistoryOutlined, ShopOutlined } from '@ant-design/icons';
|
import { HomeOutlined, UserOutlined, CalendarOutlined, BankOutlined, HistoryOutlined, ShopOutlined } from '@ant-design/icons';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
@@ -64,8 +64,9 @@ const RoomVisualPage: React.FC = () => {
|
|||||||
const params = isHistorical ? { asOf: asOf!.format('YYYY-MM-DD') } : undefined;
|
const params = isHistorical ? { asOf: asOf!.format('YYYY-MM-DD') } : undefined;
|
||||||
const res: any = await api.get('/rooms/visual', { params });
|
const res: any = await api.get('/rooms/visual', { params });
|
||||||
setData(res);
|
setData(res);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [isHistorical, asOf]);
|
}, [isHistorical, asOf]);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
Drawer,
|
Drawer,
|
||||||
Tabs,
|
Tabs,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { UploadRequestError, UploadRequestOption } from '@rc-component/upload/lib/interface';
|
import type { UploadRequestError, UploadRequestOption } from '@rc-component/upload/lib/interface';
|
||||||
import {
|
import {
|
||||||
@@ -88,8 +89,10 @@ const RoomsPage: React.FC = () => {
|
|||||||
const [lockerForm] = Form.useForm();
|
const [lockerForm] = Form.useForm();
|
||||||
const [savingBed, setSavingBed] = useState(false);
|
const [savingBed, setSavingBed] = useState(false);
|
||||||
const [savingLocker, setSavingLocker] = useState(false);
|
const [savingLocker, setSavingLocker] = useState(false);
|
||||||
|
const [batchLoading, setBatchLoading] = useState(false);
|
||||||
|
|
||||||
const handleBatchDelete = async () => {
|
const handleBatchDelete = async () => {
|
||||||
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: any = await api.post('/rooms/batch-delete', { ids: selectedRowKeys });
|
const res: any = await api.post('/rooms/batch-delete', { ids: selectedRowKeys });
|
||||||
message.success(res?.message || `已批量归档 ${selectedRowKeys.length} 间`);
|
message.success(res?.message || `已批量归档 ${selectedRowKeys.length} 间`);
|
||||||
@@ -97,6 +100,8 @@ const RoomsPage: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '批量归档失败');
|
message.error(e?.message || '批量归档失败');
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,8 +114,9 @@ const RoomsPage: React.FC = () => {
|
|||||||
setArchivedCount(archived.length);
|
setArchivedCount(archived.length);
|
||||||
const filtered = showArchived ? res : res.filter((r: any) => r.status !== 'archived');
|
const filtered = showArchived ? res : res.filter((r: any) => r.status !== 'archived');
|
||||||
setData(filtered);
|
setData(filtered);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -162,14 +168,20 @@ const RoomsPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const res = await api.get(`/rooms/${roomId}/beds`);
|
const res = await api.get(`/rooms/${roomId}/beds`);
|
||||||
setBeds(res);
|
setBeds(res);
|
||||||
} catch (e) { console.error(e); }
|
} catch (e: unknown) {
|
||||||
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载床位失败');
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchLockers = useCallback(async (roomId: number) => {
|
const fetchLockers = useCallback(async (roomId: number) => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`/rooms/${roomId}/lockers`);
|
const res = await api.get(`/rooms/${roomId}/lockers`);
|
||||||
setLockers(res);
|
setLockers(res);
|
||||||
} catch (e) { console.error(e); }
|
} catch (e: unknown) {
|
||||||
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载柜子失败');
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSaveBed = async () => {
|
const handleSaveBed = async () => {
|
||||||
@@ -425,7 +437,13 @@ const RoomsPage: React.FC = () => {
|
|||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
disabled={selectedRowKeys.length === 0}
|
disabled={selectedRowKeys.length === 0}
|
||||||
>
|
>
|
||||||
<PermissionButton permission="room:delete" danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>
|
<PermissionButton
|
||||||
|
permission="room:delete"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
disabled={selectedRowKeys.length === 0}
|
||||||
|
loading={batchLoading}
|
||||||
|
>
|
||||||
批量归档
|
批量归档
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
@@ -486,6 +504,7 @@ const RoomsPage: React.FC = () => {
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1200 }}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 间` }}
|
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 间` }}
|
||||||
rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')}
|
rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Drawer,
|
Drawer,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
|
Empty,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -72,6 +73,7 @@ const StudentsPage: React.FC = () => {
|
|||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [archivedCount, setArchivedCount] = useState(0);
|
const [archivedCount, setArchivedCount] = useState(0);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||||
|
const [batchLoading, setBatchLoading] = useState(false);
|
||||||
const [enrollmentData, setEnrollmentData] = useState<Record<number, EnrollmentInfo[]>>({});
|
const [enrollmentData, setEnrollmentData] = useState<Record<number, EnrollmentInfo[]>>({});
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [drawerStudentId, setDrawerStudentId] = useState<number | undefined>(undefined);
|
const [drawerStudentId, setDrawerStudentId] = useState<number | undefined>(undefined);
|
||||||
@@ -111,6 +113,7 @@ const StudentsPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBatchDelete = async () => {
|
const handleBatchDelete = async () => {
|
||||||
|
setBatchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res: any = await api.post('/students/batch-delete', { ids: selectedRowKeys });
|
const res: any = await api.post('/students/batch-delete', { ids: selectedRowKeys });
|
||||||
message.success(res?.message || `已批量归档 ${selectedRowKeys.length} 人`);
|
message.success(res?.message || `已批量归档 ${selectedRowKeys.length} 人`);
|
||||||
@@ -118,6 +121,8 @@ const StudentsPage: React.FC = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '批量归档失败');
|
message.error(e?.message || '批量归档失败');
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,8 +137,9 @@ const StudentsPage: React.FC = () => {
|
|||||||
const archived = list.filter((r) => r.status === 'archived');
|
const archived = list.filter((r) => r.status === 'archived');
|
||||||
setArchivedCount(archived.length);
|
setArchivedCount(archived.length);
|
||||||
setData(showArchived ? list : list.filter((r) => r.status !== 'archived'));
|
setData(showArchived ? list : list.filter((r) => r.status !== 'archived'));
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [searchName, showArchived, filterStatus, filterTenantId]);
|
}, [searchName, showArchived, filterStatus, filterTenantId]);
|
||||||
@@ -383,6 +389,7 @@ const StudentsPage: React.FC = () => {
|
|||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
disabled={selectedRowKeys.length === 0}
|
disabled={selectedRowKeys.length === 0}
|
||||||
|
loading={batchLoading}
|
||||||
>
|
>
|
||||||
批量归档
|
批量归档
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -441,6 +448,7 @@ const StudentsPage: React.FC = () => {
|
|||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
scroll={{ x: 1410 }}
|
scroll={{ x: 1410 }}
|
||||||
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 人` }}
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 人` }}
|
||||||
rowClassName={(record: any) => (record.status === 'archived' ? 'archived-row' : '')}
|
rowClassName={(record: any) => (record.status === 'archived' ? 'archived-row' : '')}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useMemo } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { Table, Modal, Form, Input, Select, Space, message, Tag, Popconfirm } from 'antd';
|
import { Table, Modal, Form, Input, Select, Space, message, Tag, Popconfirm, Empty } from 'antd';
|
||||||
import { PlusOutlined, InboxOutlined } from '@ant-design/icons';
|
import { PlusOutlined, InboxOutlined } from '@ant-design/icons';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import PermissionButton from '../../components/PermissionButton';
|
import PermissionButton from '../../components/PermissionButton';
|
||||||
@@ -44,8 +44,9 @@ const TenantsPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const res: any = await api.get('/tenants');
|
const res: any = await api.get('/tenants');
|
||||||
setData(res);
|
setData(res);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -195,9 +196,9 @@ const TenantsPage: React.FC = () => {
|
|||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||||
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }}
|
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={editing ? '编辑租赁方' : '添加租赁方'}
|
title={editing ? '编辑租赁方' : '添加租赁方'}
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ const UsersPage: React.FC = () => {
|
|||||||
]);
|
]);
|
||||||
setData(users);
|
setData(users);
|
||||||
setRoles(rolesRes);
|
setRoles(rolesRes);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载失败,请稍后重试');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [showArchived]);
|
}, [showArchived]);
|
||||||
|
|||||||
Reference in New Issue
Block a user