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

@@ -26,6 +26,7 @@ import {
import dayjs from 'dayjs';
import api from '../../api';
import PermissionButton from '../../components/PermissionButton';
import EditableCell from '../../components/EditableCell';
import { downloadBlob } from '../../utils/download';
import { message } from '../../ui/app-message';
@@ -197,6 +198,24 @@ const ExpensesPage: React.FC = () => {
}
};
const saveRoomCell = useCallback(
async (record: any, field: string, value: unknown) => {
await api.put(`/expenses/room/${record.id}`, { [field]: value });
message.success('已保存');
await fetchData();
},
[fetchData],
);
const savePersonalCell = useCallback(
async (record: any, field: string, value: unknown) => {
await api.put(`/expenses/personal/${record.id}`, { [field]: value });
message.success('已保存');
await fetchData();
},
[fetchData],
);
const handleStudentUtility = async () => {
const values = await utilityForm.validateFields();
setSaving(true);
@@ -257,25 +276,87 @@ const ExpensesPage: React.FC = () => {
const roomColumns = useMemo(
() => [
{ title: '宿舍', width: 120, render: (_: any, r: any) => r.room?.roomNumber || '-' },
{
title: '宿舍',
width: 120,
render: (_: any, r: any) => (
<EditableCell
value={r.roomId}
editor="select"
options={rooms.map((item) => ({ value: item.id, label: item.roomNumber }))}
permission="expense:edit"
required
onSave={(next) => saveRoomCell(r, 'roomId', next)}
>
{r.room?.roomNumber || '-'}
</EditableCell>
),
},
{
title: '费用类型',
width: 100,
dataIndex: 'expenseType',
render: (v: string) => <Tag>{typeMap[v] || v}</Tag>,
render: (v: string, r: any) => (
<EditableCell
value={v}
editor="select"
options={typeOptions}
permission="expense:edit"
required
onSave={(next) => saveRoomCell(r, 'expenseType', next)}
>
<Tag>{typeMap[v] || v}</Tag>
</EditableCell>
),
},
{
title: '金额',
dataIndex: 'amount',
width: 100,
render: (v: number) => `¥${Number(v).toFixed(2)}`,
render: (v: number, r: any) => (
<EditableCell
value={v}
editor="money"
min={0.01}
permission="expense:edit"
required
onSave={(next) => saveRoomCell(r, 'amount', next)}
>{`¥${Number(v).toFixed(2)}`}</EditableCell>
),
},
{
title: '账单周期',
width: 200,
render: (_: any, r: any) => `${r.periodStart} ~ ${r.periodEnd}`,
render: (_: any, r: any) => (
<EditableCell
value={[r.periodStart, r.periodEnd]}
editor="date-range"
permission="expense:edit"
required
onSave={async (next) => {
const [periodStart, periodEnd] = next as unknown as [string, string];
await api.put(`/expenses/room/${r.id}`, { periodStart, periodEnd });
message.success('已保存');
await fetchData();
}}
>{`${r.periodStart} ~ ${r.periodEnd}`}</EditableCell>
),
},
{
title: '说明',
dataIndex: 'description',
width: 150,
render: (v: string, r: any) => (
<EditableCell
value={v}
editor="textarea"
permission="expense:edit"
onSave={(next) => saveRoomCell(r, 'description', next)}
>
{v || '-'}
</EditableCell>
),
},
{ title: '说明', dataIndex: 'description', width: 150 },
{
title: '录入时间',
width: 160,
@@ -326,21 +407,89 @@ const ExpensesPage: React.FC = () => {
),
},
],
[setEditingRoom, roomForm, setRoomModal, fetchData, typeMap],
[rooms, typeOptions, typeMap, saveRoomCell, roomForm, fetchData],
);
const personalColumns = useMemo(
() => [
{ title: '学生', width: 120, render: (_: any, r: any) => r.student?.name || '-' },
{
title: '学生',
width: 120,
render: (_: any, r: any) => (
<EditableCell
value={r.studentId}
editor="select"
options={students.map((item) => ({ value: item.id, label: item.name }))}
permission="expense:edit"
required
onSave={(next) => savePersonalCell(r, 'studentId', next)}
>
{r.student?.name || '-'}
</EditableCell>
),
},
{
title: '费用类型',
width: 100,
dataIndex: 'expenseType',
render: (v: string) => <Tag color="orange">{typeMap[v] || v}</Tag>,
render: (v: string, r: any) => (
<EditableCell
value={v}
editor="select"
options={personalTypeOptions}
permission="expense:edit"
required
onSave={(next) => savePersonalCell(r, 'expenseType', next)}
>
<Tag color="orange">{typeMap[v] || v}</Tag>
</EditableCell>
),
},
{
title: '金额',
dataIndex: 'amount',
render: (v: number, r: any) => (
<EditableCell
value={v}
editor="money"
min={0.01}
permission="expense:edit"
required
onSave={(next) => savePersonalCell(r, 'amount', next)}
>{`¥${Number(v).toFixed(2)}`}</EditableCell>
),
},
{
title: '日期',
dataIndex: 'expenseDate',
width: 110,
render: (v: string, r: any) => (
<EditableCell
value={v}
editor="date"
permission="expense:edit"
required
onSave={(next) => savePersonalCell(r, 'expenseDate', next)}
>
{v}
</EditableCell>
),
},
{
title: '说明',
dataIndex: 'description',
width: 150,
render: (v: string, r: any) => (
<EditableCell
value={v}
editor="textarea"
permission="expense:edit"
onSave={(next) => savePersonalCell(r, 'description', next)}
>
{v || '-'}
</EditableCell>
),
},
{ title: '金额', dataIndex: 'amount', render: (v: number) => `¥${Number(v).toFixed(2)}` },
{ title: '日期', dataIndex: 'expenseDate', width: 110 },
{ title: '说明', dataIndex: 'description', width: 150 },
{
title: '操作',
width: 120,
@@ -386,7 +535,7 @@ const ExpensesPage: React.FC = () => {
),
},
],
[setEditingPersonal, personalForm, setPersonalModal, fetchData, typeMap],
[students, personalTypeOptions, typeMap, savePersonalCell, personalForm, fetchData],
);
return (