{String(index).padStart(2, '0')} - {schedule.startTime}{schedule.endTime} + + {schedule.startTime} + + {schedule.endTime} + - {schedule.subject}{phaseMeta.label} - {className}教室 {schedule.classroomId} + + {schedule.subject} + {phaseMeta.label} + + + {className} + 教室 {schedule.classroomId} + {phase === 'upcoming' ? ( - 等待上课 + + 等待上课 + ) : ( {phase === 'ongoing' ? '查看当前考勤' : '拉取 / 查看考勤'} @@ -571,7 +668,6 @@ const LessonCard: React.FC<{ ); }; - interface AdminStudentPanel { key: string; studentId: number; @@ -594,7 +690,11 @@ const ADMIN_METRIC_META = [ function pickPrimaryStatus(records: AttendanceRecordItem[]) { const priority = ['absent', 'leave', 'present']; - return priority.find((item) => records.some((record) => displayAttendanceStatus(record.status) === item)) || 'absent'; + return ( + priority.find((item) => + records.some((record) => displayAttendanceStatus(record.status) === item), + ) || 'absent' + ); } function buildAdminStudentPanels(records: AttendanceRecordItem[]): AdminStudentPanel[] { @@ -621,9 +721,7 @@ function buildAdminStudentPanels(records: AttendanceRecordItem[]): AdminStudentP } return Array.from(map.values()).map((item) => { - const checked = item.records.filter( - (record) => record.status === 'present', - ).length; + const checked = item.records.filter((record) => record.status === 'present').length; return { ...item, primaryStatus: pickPrimaryStatus(item.records), @@ -660,7 +758,6 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => const [selectedStudent, setSelectedStudent] = useState(null); const [correctingRecordId, setCorrectingRecordId] = useState(null); - const enabledPeriods = useMemo( () => periods.filter((period) => period.enabled).sort((a, b) => a.sortOrder - b.sortOrder), [periods], @@ -712,7 +809,10 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => enabled: period.enabled ?? true, })), }; - const data = await api.put('/attendance-period-configs', payload); + const data = await api.put( + '/attendance-period-configs', + payload, + ); setPeriods(data); setPeriodModalOpen(false); message.success('考勤时段配置已保存'); @@ -796,14 +896,16 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => } setRefreshingDingTalk(true); try { - const result = await api.post<{ refreshed: number; imported: number; matched: number; errors: string[] }>( - '/attendance-records/refresh-dingtalk', - { - date: attendanceDate.format('YYYY-MM-DD'), - classId, - session, - }, - ); + const result = await api.post<{ + refreshed: number; + imported: number; + matched: number; + errors: string[]; + }>('/attendance-records/refresh-dingtalk', { + date: attendanceDate.format('YYYY-MM-DD'), + classId, + session, + }); if (result.errors.length > 0) { message.warning(result.errors[0]); } else { @@ -933,6 +1035,15 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => } }; + const saveAdminRecordCell = async ( + record: AttendanceRecordItem, + field: 'status' | 'remark', + value: unknown, + ) => { + await api.put(`/attendance-records/${record.id}`, { [field]: value }); + message.success('考勤记录已保存'); + await loadRecords(); + }; const studentPanels = useMemo(() => buildAdminStudentPanels(records), [records]); const visibleStudents = useMemo(() => { @@ -952,7 +1063,8 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => const selectedClassOption = classId ? classOptions.find((item) => item.classId === classId) : undefined; - const selectedClass = selectedClassOption?.className || (classId ? `班级 ${classId}` : '全部班级'); + const selectedClass = + selectedClassOption?.className || (classId ? `班级 ${classId}` : '全部班级'); const overviewTeachers = selectedClassOption?.teachers ?? []; const headTeacherNames = classId ? formatTeacherNames(overviewTeachers.filter((teacher) => teacher.roleType === 'head_teacher')) @@ -1004,7 +1116,20 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => title: '状态', dataIndex: 'status', width: 105, - render: (value: string) => , + render: (value: string, record) => ( + ({ + value: String(item.value), + label: item.label, + }))} + disabled={!canEdit} + onSave={(next) => saveAdminRecordCell(record, 'status', next)} + > + + + ), }, { title: '签到来源', @@ -1025,7 +1150,16 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => title: '备注', dataIndex: 'remark', ellipsis: true, - render: (value: string | null) => value || —, + render: (value: string | null, record) => ( + saveAdminRecordCell(record, 'remark', next)} + > + {value || —} + + ), }, ...(canEdit ? [ @@ -1418,7 +1552,6 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => )} - = ({ canEdit }) => - ); }; diff --git a/apps/admin/src/pages/AttendanceDevices.tsx b/apps/admin/src/pages/AttendanceDevices.tsx index fbfa8da..1a2eedc 100644 --- a/apps/admin/src/pages/AttendanceDevices.tsx +++ b/apps/admin/src/pages/AttendanceDevices.tsx @@ -4,6 +4,7 @@ import type { ColumnsType } from 'antd/es/table'; import { PlusOutlined } from '@ant-design/icons'; import api from '../api'; import PermissionButton from '../components/PermissionButton'; +import EditableCell from '../components/EditableCell'; import { message } from '../ui/app-message'; interface ClassroomOption { @@ -119,6 +120,12 @@ const AttendanceDevicesPage: React.FC = () => { } }; + const saveCell = async (record: AttendanceDeviceRow, field: string, value: unknown) => { + await api.put(`/attendance-devices/${record.id}`, { [field]: value }); + message.success('已保存'); + await loadData(); + }; + const handleDelete = async (id: number) => { try { await api.delete(`/attendance-devices/${id}`); @@ -130,37 +137,102 @@ const AttendanceDevicesPage: React.FC = () => { }; const columns: ColumnsType = [ - { title: '设备名称', dataIndex: 'deviceName', width: 180 }, + { + title: '设备名称', + dataIndex: 'deviceName', + width: 180, + render: (value: string, record) => ( + saveCell(record, 'deviceName', next)} + > + {value} + + ), + }, { title: 'SN 码', dataIndex: 'deviceSn', width: 220, - render: (value) => {value}, + render: (value, record) => ( + saveCell(record, 'deviceSn', next)} + > + {value} + + ), }, { title: '绑定教室', dataIndex: ['classroom', 'name'], width: 160, - render: (_value, record) => record.classroom?.name || `教室 ${record.classroomId}`, + render: (_value, record) => ( + ({ + value: item.id, + label: item.building ? `${item.building} · ${item.name}` : item.name, + }))} + permission="classroom:edit" + required + onSave={(next) => saveCell(record, 'classroomId', next)} + > + {record.classroom?.name || `教室 ${record.classroomId}`} + + ), }, { title: '位置', dataIndex: 'location', - render: (value) => value || —, + render: (value, record) => ( + saveCell(record, 'location', next)} + > + {value || —} + + ), }, { title: '状态', dataIndex: 'status', width: 90, - render: (value: keyof typeof statusMeta) => ( - {statusMeta[value]?.text || value} + render: (value: keyof typeof statusMeta, record) => ( + saveCell(record, 'status', next)} + > + {statusMeta[value]?.text || value} + ), }, { title: '备注', dataIndex: 'notes', ellipsis: true, - render: (value) => value || —, + render: (value, record) => ( + saveCell(record, 'notes', next)} + > + {value || —} + + ), }, { title: '操作', diff --git a/apps/admin/src/pages/Bills/bill-print.ts b/apps/admin/src/pages/Bills/bill-print.ts index a4cff31..529908b 100644 --- a/apps/admin/src/pages/Bills/bill-print.ts +++ b/apps/admin/src/pages/Bills/bill-print.ts @@ -103,7 +103,7 @@ export const buildBillPrintHtml = (bill: BillPrintData) => { 费用类型说明天数总人天金额(元) ${rows || '暂无费用明细'} - +