feat: refine admin forms, attendance and finance workflows

Squash merge PR #23.

Included changes:
- complete occupancy check-in required fields/default payload
- improve responsive admin management pages
- fix attendance edge cases and attendance period config
- refine wallet/finance-related workflow handling

Checks:
- npm run typecheck -w apps/admin
- npm run typecheck -w apps/server
This commit is contained in:
2026-07-18 12:54:10 +00:00
parent 92d303ed01
commit 375c7ec60b
64 changed files with 5169 additions and 2404 deletions

View File

@@ -59,22 +59,22 @@ const AttendanceDevicesPage: React.FC = () => {
}, []);
const classroomOptions = useMemo(
() => classrooms.map((item) => ({
value: item.id,
label: item.building ? `${item.name}${item.building}` : item.name,
})),
() =>
classrooms.map((item) => ({
value: item.id,
label: item.building ? `${item.name}${item.building}` : item.name,
})),
[classrooms],
);
const filteredData = useMemo(() => {
const text = keyword.trim().toLocaleLowerCase('zh-CN');
if (!text) return data;
return data.filter((item) => [
item.deviceSn,
item.deviceName,
item.classroom?.name,
item.location,
].some((value) => (value || '').toLocaleLowerCase('zh-CN').includes(text)));
return data.filter((item) =>
[item.deviceSn, item.deviceName, item.classroom?.name, item.location].some((value) =>
(value || '').toLocaleLowerCase('zh-CN').includes(text),
),
);
}, [data, keyword]);
const openCreate = () => {
@@ -131,17 +131,48 @@ const AttendanceDevicesPage: React.FC = () => {
const columns: ColumnsType<AttendanceDeviceRow> = [
{ title: '设备名称', dataIndex: 'deviceName', width: 180 },
{ title: 'SN 码', dataIndex: 'deviceSn', width: 220, render: (value) => <span style={{ fontFamily: 'monospace' }}>{value}</span> },
{ title: '绑定教室', dataIndex: ['classroom', 'name'], width: 160, render: (_value, record) => record.classroom?.name || `教室 ${record.classroomId}` },
{ title: '位置', dataIndex: 'location', render: (value) => value || <span style={{ color: '#999' }}></span> },
{ title: '状态', dataIndex: 'status', width: 90, render: (value: keyof typeof statusMeta) => <Tag color={statusMeta[value]?.color}>{statusMeta[value]?.text || value}</Tag> },
{ title: '备注', dataIndex: 'notes', ellipsis: true, render: (value) => value || <span style={{ color: '#999' }}></span> },
{
title: 'SN 码',
dataIndex: 'deviceSn',
width: 220,
render: (value) => <span style={{ fontFamily: 'monospace' }}>{value}</span>,
},
{
title: '绑定教室',
dataIndex: ['classroom', 'name'],
width: 160,
render: (_value, record) => record.classroom?.name || `教室 ${record.classroomId}`,
},
{
title: '位置',
dataIndex: 'location',
render: (value) => value || <span style={{ color: '#999' }}></span>,
},
{
title: '状态',
dataIndex: 'status',
width: 90,
render: (value: keyof typeof statusMeta) => (
<Tag color={statusMeta[value]?.color}>{statusMeta[value]?.text || value}</Tag>
),
},
{
title: '备注',
dataIndex: 'notes',
ellipsis: true,
render: (value) => value || <span style={{ color: '#999' }}></span>,
},
{
title: '操作',
width: 150,
render: (_, record) => (
<Space>
<PermissionButton permission="classroom:edit" size="small" type="link" onClick={() => openEdit(record)}>
<PermissionButton
permission="classroom:edit"
size="small"
type="link"
onClick={() => openEdit(record)}
>
</PermissionButton>
<Popconfirm title="确定停用此考勤机绑定?" onConfirm={() => handleDelete(record.id)}>
@@ -156,7 +187,15 @@ const AttendanceDevicesPage: React.FC = () => {
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
<div
style={{
marginBottom: 16,
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 8,
}}
>
<Input.Search
allowClear
placeholder="搜索设备/SN/教室"
@@ -164,7 +203,12 @@ const AttendanceDevicesPage: React.FC = () => {
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<PermissionButton permission="classroom:edit" type="primary" icon={<PlusOutlined />} onClick={openCreate}>
<PermissionButton
permission="classroom:edit"
type="primary"
icon={<PlusOutlined />}
onClick={openCreate}
>
</PermissionButton>
</div>
@@ -188,17 +232,39 @@ const AttendanceDevicesPage: React.FC = () => {
okText="保存"
>
<Form form={form} layout="vertical">
<Form.Item name="deviceName" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
<Form.Item
name="deviceName"
label="设备名称"
rules={[{ required: true, message: '请输入设备名称' }]}
>
<Input placeholder="如彼岸游境_N1604" />
</Form.Item>
<Form.Item name="deviceSn" label="SN 码" rules={[{ required: true, message: '请输入钉钉返回的 deviceSN' }]}>
<Form.Item
name="deviceSn"
label="SN 码"
rules={[{ required: true, message: '请输入钉钉返回的 deviceSN' }]}
>
<Input placeholder="如300419260325WN1604" />
</Form.Item>
<Form.Item name="classroomId" label="绑定教室" rules={[{ required: true, message: '请选择绑定教室' }]}>
<Select showSearch optionFilterProp="label" options={classroomOptions} placeholder="选择教室" />
<Form.Item
name="classroomId"
label="绑定教室"
rules={[{ required: true, message: '请选择绑定教室' }]}
>
<Select
showSearch
optionFilterProp="label"
options={classroomOptions}
placeholder="选择教室"
/>
</Form.Item>
<Form.Item name="status" label="状态" initialValue="active">
<Select options={[{ value: 'active', label: '启用' }, { value: 'disabled', label: '停用' }]} />
<Select
options={[
{ value: 'active', label: '启用' },
{ value: 'disabled', label: '停用' },
]}
/>
</Form.Item>
<Form.Item name="location" label="位置">
<Input placeholder="如:教学楼一楼东侧" />