fix: remove nested button patterns across all pages, remove Dashboard gantt

This commit is contained in:
2026-07-05 21:35:03 +08:00
parent 1b49d971ae
commit 940b587df9
11 changed files with 214 additions and 288 deletions

View File

@@ -285,18 +285,16 @@ const BillsPage: React.FC = () => {
> >
PDF PDF
</PermissionButton> </PermissionButton>
<PermissionButton permission="bill:delete"> <Popconfirm
<Popconfirm title="确定删除此账单?"
title="确定删除此账单?" onConfirm={() => handleDelete(record.id)}
onConfirm={() => handleDelete(record.id)} okText="删除"
okText="删除" cancelText="取消"
cancelText="取消" >
> <PermissionButton permission="bill:delete" size="small" danger icon={<DeleteOutlined />}>
<Button size="small" danger icon={<DeleteOutlined />}>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
</Space> </Space>
), ),
}, },
@@ -350,19 +348,22 @@ const BillsPage: React.FC = () => {
> >
</PermissionButton> </PermissionButton>
<PermissionButton permission="bill:delete"> <Popconfirm
<Popconfirm title={`确定删除选中的 ${selectedRows.length} 条账单?`}
title={`确定删除选中的 ${selectedRows.length} 条账单?`} onConfirm={batchDelete}
onConfirm={batchDelete} okText="删除"
okText="删除" cancelText="取消"
cancelText="取消" disabled={selectedRows.length === 0}
>
<PermissionButton
permission="bill:delete"
danger
disabled={selectedRows.length === 0} disabled={selectedRows.length === 0}
icon={<DeleteOutlined />}
> >
<Button danger disabled={selectedRows.length === 0} icon={<DeleteOutlined />}>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
</Space> </Space>
<Space> <Space>
<PermissionButton <PermissionButton

View File

@@ -257,16 +257,14 @@ const ClassroomRentalsPage: React.FC = () => {
<PermissionButton permission="rental:edit" size="small" onClick={() => openEdit(record)}> <PermissionButton permission="rental:edit" size="small" onClick={() => openEdit(record)}>
</PermissionButton> </PermissionButton>
<PermissionButton permission="rental:delete"> <Popconfirm
<Popconfirm title="确定删除该租赁订单?合同文件将一并删除。"
title="确定删除该租赁订单?合同文件将一并删除。" onConfirm={() => handleDelete(record.id)}
onConfirm={() => handleDelete(record.id)} >
> <PermissionButton permission="rental:delete" size="small" danger>
<Button size="small" danger>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
</Space> </Space>
), ),
}, },

View File

@@ -150,13 +150,11 @@ const ClassroomsPage: React.FC = () => {
render: (_: any, record: any) => ( render: (_: any, record: any) => (
<Space> <Space>
{record.status === 'archived' ? ( {record.status === 'archived' ? (
<PermissionButton permission="classroom:edit"> <Popconfirm title="确定恢复此教室?" onConfirm={() => handleRestore(record.id)}>
<Popconfirm title="确定恢复此教室?" onConfirm={() => handleRestore(record.id)}> <PermissionButton permission="classroom:edit" size="small" icon={<UndoOutlined />} type="link">
<Button size="small" icon={<UndoOutlined />} type="link">
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
) : ( ) : (
<> <>
<PermissionButton <PermissionButton
@@ -170,18 +168,16 @@ const ClassroomsPage: React.FC = () => {
> >
</PermissionButton> </PermissionButton>
<PermissionButton permission="classroom:delete"> <Popconfirm
<Popconfirm title="归档后数据保留,可随时恢复。存在进行中的租赁将无法归档。"
title="归档后数据保留,可随时恢复。存在进行中的租赁将无法归档。" onConfirm={() => handleArchive(record.id)}
onConfirm={() => handleArchive(record.id)} okText="归档"
okText="归档" cancelText="取消"
cancelText="取消" >
> <PermissionButton permission="classroom:delete" size="small" icon={<InboxOutlined />}>
<Button size="small" icon={<InboxOutlined />}>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
</> </>
)} )}
</Space> </Space>

View File

@@ -22,7 +22,6 @@ const DashboardPage: React.FC = () => {
const screens = Grid.useBreakpoint(); const screens = Grid.useBreakpoint();
const isMobile = !screens.sm; const isMobile = !screens.sm;
const [stats, setStats] = useState<any>(null); const [stats, setStats] = useState<any>(null);
const [ganttData, setGanttData] = useState<any[]>([]);
const [expenseStats, setExpenseStats] = useState<any[]>([]); const [expenseStats, setExpenseStats] = useState<any[]>([]);
const [roomRanking, setRoomRanking] = useState<any[]>([]); const [roomRanking, setRoomRanking] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -34,9 +33,8 @@ const DashboardPage: React.FC = () => {
const fetchData = async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
try { try {
const [s, g, e, r] = await Promise.all([ const [s, e, r] = await Promise.all([
api.get('/dashboard/stats'), api.get('/dashboard/stats'),
api.get('/dashboard/gantt', { params: { periodStart: period[0], periodEnd: period[1] } }),
api.get('/dashboard/expense-stats', { api.get('/dashboard/expense-stats', {
params: { periodStart: period[0], periodEnd: period[1] }, params: { periodStart: period[0], periodEnd: period[1] },
}), }),
@@ -45,7 +43,6 @@ const DashboardPage: React.FC = () => {
}), }),
]); ]);
setStats(s); setStats(s);
setGanttData(g as any);
setExpenseStats(e as any); setExpenseStats(e as any);
setRoomRanking(r as any); setRoomRanking(r as any);
} catch (e) { } catch (e) {
@@ -70,56 +67,6 @@ const DashboardPage: React.FC = () => {
other: '其他', other: '其他',
}; };
// 甘特图配置
const ganttOption = () => {
if (!ganttData.length) return {};
const rooms = ganttData.map((d) => d.roomNumber);
const pStart = new Date(period[0]).getTime();
const pEnd = new Date(period[1]).getTime();
const data: any[] = [];
ganttData.forEach((room, roomIdx) => {
room.occupancies.forEach((occ: any, i: number) => {
const start = Math.max(new Date(occ.checkInDate).getTime(), pStart);
const end = occ.checkOutDate ? Math.min(new Date(occ.checkOutDate).getTime(), pEnd) : pEnd;
data.push({
name: occ.studentName,
value: [roomIdx, start, end, occ.studentName],
itemStyle: { color: COLORS[(occ.studentId || i) % COLORS.length] },
});
});
});
return {
tooltip: {
formatter: (p: any) => {
const v = p.value;
return `${p.name}<br/>宿舍: ${rooms[v[0]]}<br/>入住: ${dayjs(v[1]).format('MM-DD')} ~ ${dayjs(v[2]).format('MM-DD')}`;
},
},
grid: { left: 80, right: 30, top: 20, bottom: 30 },
xAxis: { type: 'time', min: pStart, max: pEnd },
yAxis: { type: 'category', data: rooms, inverse: true },
series: [
{
type: 'custom',
renderItem: (_params: any, api: any) => {
const catIdx = api.value(0);
const start = api.coord([api.value(1), catIdx]);
const end = api.coord([api.value(2), catIdx]);
const height = api.size([0, 1])[1] * 0.6;
return {
type: 'rect',
shape: { x: start[0], y: start[1] - height / 2, width: end[0] - start[0], height },
style: { ...api.style(), fill: api.visual('color'), stroke: '#fff', lineWidth: 1 },
};
},
encode: { x: [1, 2], y: 0 },
data,
},
],
};
};
// 费用饼图 // 费用饼图
const pieOption = { const pieOption = {
@@ -298,17 +245,6 @@ const DashboardPage: React.FC = () => {
</Col> </Col>
</Row> </Row>
<Card title="入住时间线(甘特图)" style={{ marginBottom: 24 }}>
{ganttData.length > 0 ? (
<ReactECharts
option={ganttOption()}
style={{ width: '100%', height: Math.max(300, ganttData.length * 40) }}
/>
) : (
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}></div>
)}
</Card>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}> <Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12}> <Col xs={24} sm={12}>
<Card title="考勤趋势近30天"> <Card title="考勤趋势近30天">

View File

@@ -256,22 +256,25 @@ const DepositsPage: React.FC = () => {
</PermissionButton> </PermissionButton>
</> </>
)} )}
<PermissionButton permission="deposit:delete"> <Popconfirm
<Popconfirm title="确定删除?"
title="确定删除?" onConfirm={async () => {
onConfirm={async () => { try {
try { await api.delete(`/deposits/${record.id}`);
await api.delete(`/deposits/${record.id}`); message.success('删除成功');
message.success('删除成功'); fetchData();
fetchData(); } catch (e: any) {
} catch (e: any) { message.error(e?.message || '删除失败');
message.error(e?.message || '删除失败'); }
} }}
}} >
> <PermissionButton
<Button size="small" danger icon={<DeleteOutlined />} /> permission="deposit:delete"
</Popconfirm> size="small"
</PermissionButton> danger
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space> </Space>
), ),
}, },
@@ -529,14 +532,12 @@ const DepositsPage: React.FC = () => {
</PermissionButton> </PermissionButton>
), ),
<PermissionButton key="del" permission="deposit:delete"> <Popconfirm
<Popconfirm title="确定删除?"
title="确定删除?" onConfirm={() => handleDeleteInstallment(item.id)}
onConfirm={() => handleDeleteInstallment(item.id)} >
> <PermissionButton key="del" permission="deposit:delete" size="small" danger icon={<DeleteOutlined />} />
<Button size="small" danger icon={<DeleteOutlined />} /> </Popconfirm>
</Popconfirm>
</PermissionButton>,
].filter(Boolean)} ].filter(Boolean)}
> >
<List.Item.Meta <List.Item.Meta

View File

@@ -239,18 +239,21 @@ const ExpensesPage: React.FC = () => {
> >
{''} {''}
</PermissionButton> </PermissionButton>
<PermissionButton permission="expense:delete"> <Popconfirm
<Popconfirm title="确定删除?"
title="确定删除?" onConfirm={async () => {
onConfirm={async () => { await api.delete(`/expenses/room/${record.id}`);
await api.delete(`/expenses/room/${record.id}`); message.success('删除成功');
message.success('删除成功'); fetchData();
fetchData(); }}
}} >
> <PermissionButton
<Button size="small" danger icon={<DeleteOutlined />} /> permission="expense:delete"
</Popconfirm> size="small"
</PermissionButton> danger
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space> </Space>
), ),
}, },
@@ -290,18 +293,21 @@ const ExpensesPage: React.FC = () => {
> >
{''} {''}
</PermissionButton> </PermissionButton>
<PermissionButton permission="expense:delete"> <Popconfirm
<Popconfirm title="确定删除?"
title="确定删除?" onConfirm={async () => {
onConfirm={async () => { await api.delete(`/expenses/personal/${record.id}`);
await api.delete(`/expenses/personal/${record.id}`); message.success('删除成功');
message.success('删除成功'); fetchData();
fetchData(); }}
}} >
> <PermissionButton
<Button size="small" danger icon={<DeleteOutlined />} /> permission="expense:delete"
</Popconfirm> size="small"
</PermissionButton> danger
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space> </Space>
), ),
}, },
@@ -401,23 +407,22 @@ const ExpensesPage: React.FC = () => {
</PermissionButton> </PermissionButton>
</Space> </Space>
<Space> <Space>
<PermissionButton permission="expense:delete"> <Popconfirm
<Popconfirm title={`确定删除选中的 ${selectedRoomKeys.length} 条费用?`}
title={`确定删除选中的 ${selectedRoomKeys.length} 条费用?`} onConfirm={handleBatchDeleteRoom}
onConfirm={handleBatchDeleteRoom} okText="删除"
okText="删除" cancelText="取消"
cancelText="取消" disabled={selectedRoomKeys.length === 0}
>
<PermissionButton
permission="expense:delete"
danger
icon={<DeleteOutlined />}
disabled={selectedRoomKeys.length === 0} disabled={selectedRoomKeys.length === 0}
> >
<Button
danger </PermissionButton>
icon={<DeleteOutlined />} </Popconfirm>
disabled={selectedRoomKeys.length === 0}
>
</Button>
</Popconfirm>
</PermissionButton>
<PermissionButton <PermissionButton
permission="expense:create" permission="expense:create"
type="primary" type="primary"
@@ -554,23 +559,22 @@ const ExpensesPage: React.FC = () => {
</PermissionButton> </PermissionButton>
</Space> </Space>
<Space> <Space>
<PermissionButton permission="expense:delete"> <Popconfirm
<Popconfirm title={`确定删除选中的 ${selectedPersonalKeys.length} 条个人费用?`}
title={`确定删除选中的 ${selectedPersonalKeys.length} 条个人费用?`} onConfirm={handleBatchDeletePersonal}
onConfirm={handleBatchDeletePersonal} okText="删除"
okText="删除" cancelText="取消"
cancelText="取消" disabled={selectedPersonalKeys.length === 0}
>
<PermissionButton
permission="expense:delete"
danger
icon={<DeleteOutlined />}
disabled={selectedPersonalKeys.length === 0} disabled={selectedPersonalKeys.length === 0}
> >
<Button
danger </PermissionButton>
icon={<DeleteOutlined />} </Popconfirm>
disabled={selectedPersonalKeys.length === 0}
>
</Button>
</Popconfirm>
</PermissionButton>
<PermissionButton <PermissionButton
permission="expense:create" permission="expense:create"
type="primary" type="primary"

View File

@@ -215,22 +215,20 @@ const OccupanciesPage: React.FC = () => {
) : ( ) : (
<Space> <Space>
<Tag>退宿</Tag> <Tag>退宿</Tag>
<PermissionButton permission="occupancy:delete"> <Popconfirm
<Popconfirm title="确定删除此记录?"
title="确定删除此记录?" onConfirm={async () => {
onConfirm={async () => { try {
try { await api.delete(`/occupancies/${record.id}`);
await api.delete(`/occupancies/${record.id}`); message.success('删除成功');
message.success('删除成功'); fetchData();
fetchData(); } catch (e: any) {
} catch (e: any) { message.error(e?.message || '删除失败');
message.error(e?.message || '删除失败'); }
} }}
}} >
> <PermissionButton permission="occupancy:delete" size="small" danger icon={<DeleteOutlined />} />
<Button size="small" danger icon={<DeleteOutlined />} /> </Popconfirm>
</Popconfirm>
</PermissionButton>
</Space> </Space>
), ),
}, },
@@ -419,23 +417,22 @@ const OccupanciesPage: React.FC = () => {
退宿 退宿
</PermissionButton> </PermissionButton>
) : ( ) : (
<PermissionButton permission="occupancy:delete"> <Popconfirm
<Popconfirm title={`确定删除选中的 ${selectedRowKeys.length} 条入住记录?在住记录会自动跳过`}
title={`确定删除选中的 ${selectedRowKeys.length} 条入住记录?在住记录会自动跳过`} onConfirm={handleBatchDelete}
onConfirm={handleBatchDelete} okText="删除"
okText="删除" cancelText="取消"
cancelText="取消" >
<PermissionButton
permission="occupancy:delete"
danger
size="small"
icon={<DeleteOutlined />}
style={{ marginLeft: 12 }}
> >
<Button
danger </PermissionButton>
size="small" </Popconfirm>
icon={<DeleteOutlined />}
style={{ marginLeft: 12 }}
>
</Button>
</Popconfirm>
</PermissionButton>
)} )}
<Button size="small" onClick={() => setSelectedRowKeys([])} style={{ marginLeft: 8 }}> <Button size="small" onClick={() => setSelectedRowKeys([])} style={{ marginLeft: 8 }}>

View File

@@ -165,13 +165,11 @@ const RolesPage: React.FC = () => {
</PermissionButton> </PermissionButton>
{!record.isSystem && ( {!record.isSystem && (
<PermissionButton permission="role:delete"> <Popconfirm title="确认删除该角色?" onConfirm={() => handleDelete(record.id)}>
<Popconfirm title="确认删除该角色?" onConfirm={() => handleDelete(record.id)}> <PermissionButton permission="role:delete" type="link" size="small" danger icon={<DeleteOutlined />}>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
)} )}
</Space> </Space>
), ),

View File

@@ -187,18 +187,16 @@ const StudentsPage: React.FC = () => {
render: (_: any, record: any) => ( render: (_: any, record: any) => (
<Space> <Space>
{record.status === 'archived' ? ( {record.status === 'archived' ? (
<PermissionButton permission="student:edit"> <Popconfirm
<Popconfirm title="确定恢复此学生?恢复后将重新出现在学生列表中。"
title="确定恢复此学生?恢复后将重新出现在学生列表中。" onConfirm={() => handleRestore(record.id)}
onConfirm={() => handleRestore(record.id)} okText="恢复"
okText="恢复" cancelText="取消"
cancelText="取消" >
> <PermissionButton permission="student:edit" size="small" icon={<UndoOutlined />} type="link">
<Button size="small" icon={<UndoOutlined />} type="link">
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
) : ( ) : (
<> <>
<PermissionButton <PermissionButton
@@ -212,18 +210,16 @@ const StudentsPage: React.FC = () => {
> >
</PermissionButton> </PermissionButton>
<PermissionButton permission="student:delete"> <Popconfirm
<Popconfirm title="归档后不会删除数据,可随时恢复。确定归档?"
title="归档后不会删除数据,可随时恢复。确定归档?" onConfirm={() => handleArchive(record.id)}
onConfirm={() => handleArchive(record.id)} okText="归档"
okText="归档" cancelText="取消"
cancelText="取消" >
> <PermissionButton permission="student:delete" size="small" icon={<InboxOutlined />}>
<Button size="small" icon={<InboxOutlined />}>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
</> </>
)} )}
</Space> </Space>
@@ -251,19 +247,22 @@ const StudentsPage: React.FC = () => {
</Button> </Button>
</Space> </Space>
<Space wrap> <Space wrap>
<PermissionButton permission="student:delete"> <Popconfirm
<Popconfirm title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`} onConfirm={handleBatchDelete}
onConfirm={handleBatchDelete} okText="归档"
okText="归档" cancelText="取消"
cancelText="取消" disabled={selectedRowKeys.length === 0}
>
<PermissionButton
permission="student:delete"
danger
icon={<DeleteOutlined />}
disabled={selectedRowKeys.length === 0} disabled={selectedRowKeys.length === 0}
> >
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
<PermissionButton <PermissionButton
permission="student:create" permission="student:create"
type="primary" type="primary"

View File

@@ -130,18 +130,16 @@ const TenantsPage: React.FC = () => {
> >
</PermissionButton> </PermissionButton>
<PermissionButton permission="tenant:delete"> <Popconfirm
<Popconfirm title="归档后仍可查看历史租赁"
title="归档后仍可查看历史租赁" onConfirm={() => handleArchive(record.id)}
onConfirm={() => handleArchive(record.id)} okText="归档"
okText="归档" cancelText="取消"
cancelText="取消" >
> <PermissionButton permission="tenant:delete" size="small" icon={<InboxOutlined />}>
<Button size="small" icon={<InboxOutlined />}>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
</Space> </Space>
), ),
}, },

View File

@@ -180,13 +180,11 @@ const UsersPage: React.FC = () => {
</PermissionButton> </PermissionButton>
{record.username !== 'admin' && ( {record.username !== 'admin' && (
<PermissionButton permission="user:delete"> <Popconfirm title="确认删除该用户?" onConfirm={() => handleDelete(record.id)}>
<Popconfirm title="确认删除该用户?" onConfirm={() => handleDelete(record.id)}> <PermissionButton permission="user:delete" type="link" size="small" danger icon={<DeleteOutlined />}>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</PermissionButton>
</Button> </Popconfirm>
</Popconfirm>
</PermissionButton>
)} )}
</Space> </Space>
), ),