feat: 支持表格单元格快捷编辑
Some checks failed
CI 检查 / lint (pull_request) Has been cancelled
CI 检查 / typecheck (pull_request) Has been cancelled
CI 检查 / test (pull_request) Has been cancelled

This commit is contained in:
2026-07-21 14:17:08 +08:00
parent fe0b71a0a9
commit a585fd42d5
17 changed files with 2197 additions and 303 deletions

View File

@@ -29,6 +29,7 @@ import {
import api from '../../api';
import { downloadBlob } from '../../utils/download';
import PermissionButton from '../../components/PermissionButton';
import EditableCell from '../../components/EditableCell';
import { message } from '../../ui/app-message';
const statusMap: Record<string, { text: string; color: string }> = {
@@ -199,6 +200,26 @@ const RoomsPage: React.FC = () => {
setSaving(false);
}
};
const saveRoomCell = async (record: any, field: string, value: unknown) => {
await api.put(`/rooms/${record.id}`, { [field]: value });
message.success('已保存');
await fetchData();
};
const saveBedCell = async (record: BedItem, field: string, value: unknown) => {
if (!drawerRoom) return;
await api.put(`/rooms/${drawerRoom.id}/beds/${record.id}`, { [field]: value });
message.success('已保存');
await fetchBeds(drawerRoom.id);
};
const saveLockerCell = async (record: LockerItem, field: string, value: unknown) => {
if (!drawerRoom) return;
await api.put(`/rooms/${drawerRoom.id}/lockers/${record.id}`, { [field]: value });
message.success('已保存');
await fetchLockers(drawerRoom.id);
};
const fetchBeds = useCallback(async (roomId: number) => {
try {
const res = await api.get<BedItem[]>(`/rooms/${roomId}/beds`);
@@ -337,27 +358,123 @@ const RoomsPage: React.FC = () => {
dataIndex: 'roomNumber',
width: 100,
sorter: (a: any, b: any) => a.roomNumber.localeCompare(b.roomNumber),
render: (v: string, r: any) => (
<EditableCell
value={v}
required
permission="room:edit"
disabled={r.status === 'archived'}
onSave={(next) => saveRoomCell(r, 'roomNumber', next)}
>
{v}
</EditableCell>
),
},
{
title: '楼栋',
dataIndex: 'building',
width: 80,
render: (v: string, r: any) => (
<EditableCell
value={v}
permission="room:edit"
disabled={r.status === 'archived'}
onSave={(next) => saveRoomCell(r, 'building', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '楼层',
dataIndex: 'floor',
width: 80,
render: (v: number, r: any) => (
<EditableCell
value={v}
editor="number"
permission="room:edit"
disabled={r.status === 'archived'}
onSave={(next) => saveRoomCell(r, 'floor', next)}
>
{v ?? '-'}
</EditableCell>
),
},
{
title: '类型',
dataIndex: 'roomType',
width: 90,
render: (v: any, r: any) => (
<EditableCell
value={v}
permission="room:edit"
disabled={r.status === 'archived'}
onSave={(next) => saveRoomCell(r, 'roomType', next)}
>
{v || '-'}
</EditableCell>
),
},
{ title: '楼栋', dataIndex: 'building', width: 80 },
{ title: '楼层', dataIndex: 'floor', width: 80 },
{ title: '类型', dataIndex: 'roomType', width: 90, render: (v: any) => v || '-' },
{
title: '租赁类型',
dataIndex: 'rentalCategory',
width: 100,
render: (v: string) => {
if (v === 'long') return <Tag color="blue"></Tag>;
if (v === 'short') return <Tag color="green"></Tag>;
return '-';
},
render: (v: string, r: any) => (
<EditableCell
value={v}
editor="select"
options={[
{ value: 'long', label: '长租' },
{ value: 'short', label: '短租' },
]}
permission="room:edit"
disabled={r.status === 'archived'}
onSave={(next) => saveRoomCell(r, 'rentalCategory', next)}
>
{(() => {
if (v === 'long') return <Tag color="blue"></Tag>;
if (v === 'short') return <Tag color="green"></Tag>;
return '-';
})()}
</EditableCell>
),
},
{
title: '月租金',
dataIndex: 'monthlyRate',
width: 100,
render: (v: number) => (v ? `¥${v}` : '-'),
render: (v: number, r: any) => (
<EditableCell
value={v}
editor="money"
min={0}
permission="room:edit"
disabled={r.status === 'archived'}
onSave={(next) => saveRoomCell(r, 'monthlyRate', next)}
>
{v ? `¥${v}` : '-'}
</EditableCell>
),
},
{
title: '额定人数',
dataIndex: 'capacity',
width: 80,
render: (v: number, r: any) => (
<EditableCell
value={v}
editor="number"
min={1}
required
permission="room:edit"
disabled={r.status === 'archived'}
onSave={(next) => saveRoomCell(r, 'capacity', next)}
>
{v}
</EditableCell>
),
},
{ title: '额定人数', dataIndex: 'capacity', width: 80 },
{
title: '当前入住',
width: 80,
@@ -377,7 +494,22 @@ const RoomsPage: React.FC = () => {
title: '状态',
dataIndex: 'status',
width: 80,
render: (s: string) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.text || s}</Tag>,
render: (s: string, r: any) => (
<EditableCell
value={s}
editor="select"
options={[
{ value: 'available', label: '可入住' },
{ value: 'full', label: '已满' },
{ value: 'maintenance', label: '维修中' },
]}
permission="room:edit"
disabled={r.status === 'archived'}
onSave={(next) => saveRoomCell(r, 'status', next)}
>
<Tag color={statusMap[s]?.color}>{statusMap[s]?.text || s}</Tag>
</EditableCell>
),
},
{
title: '操作',
@@ -778,21 +910,65 @@ const RoomsPage: React.FC = () => {
pagination={false}
size="small"
columns={[
{ title: '编号', dataIndex: 'bedNumber', width: 80 },
{
title: '编号',
dataIndex: 'bedNumber',
width: 80,
render: (v: string, r: BedItem) => (
<EditableCell
value={v}
required
permission="room:edit"
disabled={drawerRoom?.status === 'archived'}
onSave={(next) => saveBedCell(r, 'bedNumber', next)}
>
{v}
</EditableCell>
),
},
{
title: '状态',
dataIndex: 'status',
width: 80,
render: (s: string) => {
const map: Record<string, { text: string; color: string }> = {
available: { text: '空闲', color: 'green' },
occupied: { text: '占用', color: 'blue' },
maintenance: { text: '维修', color: 'orange' },
};
return <Tag color={map[s]?.color}>{map[s]?.text || s}</Tag>;
},
render: (s: string, r: BedItem) => (
<EditableCell
value={s}
editor="select"
options={[
{ value: 'available', label: '空闲' },
{ value: 'occupied', label: '占用' },
{ value: 'maintenance', label: '维修' },
]}
permission="room:edit"
disabled={drawerRoom?.status === 'archived'}
onSave={(next) => saveBedCell(r, 'status', next)}
>
{(() => {
const map: Record<string, { text: string; color: string }> = {
available: { text: '空闲', color: 'green' },
occupied: { text: '占用', color: 'blue' },
maintenance: { text: '维修', color: 'orange' },
};
return <Tag color={map[s]?.color}>{map[s]?.text || s}</Tag>;
})()}
</EditableCell>
),
},
{
title: '备注',
dataIndex: 'notes',
render: (v: string, r: BedItem) => (
<EditableCell
value={v}
editor="textarea"
permission="room:edit"
disabled={drawerRoom?.status === 'archived'}
onSave={(next) => saveBedCell(r, 'notes', next)}
>
{v || '-'}
</EditableCell>
),
},
{ title: '备注', dataIndex: 'notes', render: (v: string) => v || '-' },
{
title: '操作',
width: 120,
@@ -885,21 +1061,65 @@ const RoomsPage: React.FC = () => {
pagination={false}
size="small"
columns={[
{ title: '编号', dataIndex: 'lockerNumber', width: 80 },
{
title: '编号',
dataIndex: 'lockerNumber',
width: 80,
render: (v: string, r: LockerItem) => (
<EditableCell
value={v}
required
permission="room:edit"
disabled={drawerRoom?.status === 'archived'}
onSave={(next) => saveLockerCell(r, 'lockerNumber', next)}
>
{v}
</EditableCell>
),
},
{
title: '状态',
dataIndex: 'status',
width: 80,
render: (s: string) => {
const map: Record<string, { text: string; color: string }> = {
available: { text: '空闲', color: 'green' },
occupied: { text: '占用', color: 'blue' },
maintenance: { text: '维修', color: 'orange' },
};
return <Tag color={map[s]?.color}>{map[s]?.text || s}</Tag>;
},
render: (s: string, r: LockerItem) => (
<EditableCell
value={s}
editor="select"
options={[
{ value: 'available', label: '空闲' },
{ value: 'occupied', label: '占用' },
{ value: 'maintenance', label: '维修' },
]}
permission="room:edit"
disabled={drawerRoom?.status === 'archived'}
onSave={(next) => saveLockerCell(r, 'status', next)}
>
{(() => {
const map: Record<string, { text: string; color: string }> = {
available: { text: '空闲', color: 'green' },
occupied: { text: '占用', color: 'blue' },
maintenance: { text: '维修', color: 'orange' },
};
return <Tag color={map[s]?.color}>{map[s]?.text || s}</Tag>;
})()}
</EditableCell>
),
},
{
title: '备注',
dataIndex: 'notes',
render: (v: string, r: LockerItem) => (
<EditableCell
value={v}
editor="textarea"
permission="room:edit"
disabled={drawerRoom?.status === 'archived'}
onSave={(next) => saveLockerCell(r, 'notes', next)}
>
{v || '-'}
</EditableCell>
),
},
{ title: '备注', dataIndex: 'notes', render: (v: string) => v || '-' },
{
title: '操作',
width: 120,