diff --git a/client/src/components/Icons.tsx b/client/src/components/Icons.tsx index 6c260e8..41deac1 100644 --- a/client/src/components/Icons.tsx +++ b/client/src/components/Icons.tsx @@ -122,3 +122,9 @@ export const IconFileImage: React.FC = ({ size, ...rest }) => ( ); + +export const IconBatchSet: React.FC = ({ size, ...rest }) => ( + + + +); diff --git a/client/src/pages/admin/Hours.tsx b/client/src/pages/admin/Hours.tsx index 168a5f9..1c24887 100644 --- a/client/src/pages/admin/Hours.tsx +++ b/client/src/pages/admin/Hours.tsx @@ -2,10 +2,11 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import Button from '../../components/Button'; import Select from '../../components/Select'; import Input from '../../components/Input'; +import Modal from '../../components/Modal'; import { activitiesApi, hoursApi, errMsg } from '../../api'; import { exportToExcel, exportTableToImage, formatDateRange, utc8DateStamp } from '../../utils/export'; import type { ActivityItem, HoursRow, Participant, HoursActivityResult } from '../../types'; -import { IconClock, IconFileExcel, IconFileImage } from '../../components/Icons'; +import { IconClock, IconFileExcel, IconFileImage, IconBatchSet } from '../../components/Icons'; export default function Hours() { const [activities, setActivities] = useState([]); @@ -18,6 +19,12 @@ export default function Hours() { const [msg, setMsg] = useState(''); const tableRef = useRef(null); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [batchModalOpen, setBatchModalOpen] = useState(false); + const [batchHours, setBatchHours] = useState('2'); + const [batchRemark, setBatchRemark] = useState(''); + const [batchApplyRemark, setBatchApplyRemark] = useState(false); + useEffect(() => { activitiesApi.list().then(setActivities).catch(e => alert(errMsg(e))); }, []); @@ -27,6 +34,7 @@ export default function Hours() { setMsg(''); setParticipants([]); setResult(null); + setSelectedIds(new Set()); try { const [p, r] = await Promise.all([activitiesApi.participants(id), hoursApi.byActivity(id)]); setActivityInfo(p.activity); @@ -44,6 +52,47 @@ export default function Hours() { } }; + const selectedCount = selectedIds.size; + + const isAllSelected = participants.length > 0 && selectedCount === participants.length; + const isIndeterminate = selectedCount > 0 && selectedCount < participants.length; + + const toggleAll = () => { + if (isAllSelected) setSelectedIds(new Set()); + else setSelectedIds(new Set(participants.map(p => p.id))); + }; + + const toggleOne = (id: number) => { + setSelectedIds(prev => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const openBatchModal = () => { + if (!selectedId) return; + if (selectedCount === 0) { alert('请先勾选要批量设置时长的部员'); return; } + setBatchHours('2'); + setBatchRemark(''); + setBatchApplyRemark(false); + setBatchModalOpen(true); + }; + + const confirmBatchSet = () => { + const h = Number(batchHours); + if (isNaN(h) || h < 0) { alert('请填写有效的时长(小时数)'); return; } + setParticipants(list => list.map(p => { + if (!selectedIds.has(p.id)) return p; + const out: Participant = { ...p, hours: h }; + if (batchApplyRemark) out.remark = batchRemark; + return out; + })); + setBatchModalOpen(false); + setMsg(`✓ 已为 ${selectedCount} 名部员设置时长 ${h.toFixed(2)} 小时,请点击「保存登记」写入`); + }; + useEffect(() => { if (selectedId) loadParticipants(Number(selectedId)); else { setParticipants([]); setActivityInfo(null); setResult(null); } @@ -127,7 +176,8 @@ export default function Hours() {
- + + @@ -158,6 +208,17 @@ export default function Hours() { + @@ -169,11 +230,21 @@ export default function Hours() { {participants.length === 0 ? ( - ) : participants.map((p, idx) => ( - + + @@ -198,6 +269,32 @@ export default function Hours() { 请先在上方选择一个活动开始登记 )} + + setBatchModalOpen(false)}> +
+
+ + setBatchHours(e.target.value)} /> +
将为选中的 {selectedCount} 名部员统一设置此时长值
+
+
+ + setBatchRemark(e.target.value)} disabled={!batchApplyRemark} placeholder={batchApplyRemark ? '例如:校园摄影宣传 / 公众号推文排版' : '先勾选上方"同时设置备注"后启用'} /> +
+
+ 注意:设置后会立即覆盖选中人员的时长/备注(如勾选)。需点击右侧「保存登记」按钮才会写入数据库。 +
+
+
+ + +
+
); }
+ + 序号 班级 姓名
+
该活动暂无部员可登记(若选了工作组,请确认该工作组下有部员)
+ + {idx + 1} {p.class} {p.name}