- Hours.tsx 参与者列表首列新增复选框:表头全选/半选态(indeterminate)、每行切换选中;选中行整行高亮 primary-50/70 - 工具栏按钮拆分:「全表填时长」原逻辑保留(prompt 全量填);新增「批量设置(选中N)」按钮,勾选 1-N 人后可用 - 批量设置弹窗(max-w-lg):统一小时数必填;备注可勾选同时统一设置后启用;确认后仅覆盖选中人员,顶部显示绿色提示提醒请点击保存登记写入 - 切换活动或重新加载时清空选择集合;批量设置值仅更新前端内存,实际写库仍走原 save→hoursApi.batch - Icons.tsx 新增 IconBatchSet 图标(勾选列表+批量) - build 通过
This commit is contained in:
@@ -122,3 +122,9 @@ export const IconFileImage: React.FC<IconProps> = ({ size, ...rest }) => (
|
|||||||
<circle cx="9.5" cy="11.5" r="1.2" />
|
<circle cx="9.5" cy="11.5" r="1.2" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const IconBatchSet: React.FC<IconProps> = ({ size, ...rest }) => (
|
||||||
|
<svg {...base(size)} {...rest}>
|
||||||
|
<path d="M3 6h13v4H3zM20 8h-3M3 14h13v4H3zM20 16h-3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import Button from '../../components/Button';
|
import Button from '../../components/Button';
|
||||||
import Select from '../../components/Select';
|
import Select from '../../components/Select';
|
||||||
import Input from '../../components/Input';
|
import Input from '../../components/Input';
|
||||||
|
import Modal from '../../components/Modal';
|
||||||
import { activitiesApi, hoursApi, errMsg } from '../../api';
|
import { activitiesApi, hoursApi, errMsg } from '../../api';
|
||||||
import { exportToExcel, exportTableToImage, formatDateRange, utc8DateStamp } from '../../utils/export';
|
import { exportToExcel, exportTableToImage, formatDateRange, utc8DateStamp } from '../../utils/export';
|
||||||
import type { ActivityItem, HoursRow, Participant, HoursActivityResult } from '../../types';
|
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() {
|
export default function Hours() {
|
||||||
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
||||||
@@ -18,6 +19,12 @@ export default function Hours() {
|
|||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
const tableRef = useRef<HTMLDivElement>(null);
|
const tableRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||||
|
const [batchModalOpen, setBatchModalOpen] = useState(false);
|
||||||
|
const [batchHours, setBatchHours] = useState<string>('2');
|
||||||
|
const [batchRemark, setBatchRemark] = useState<string>('');
|
||||||
|
const [batchApplyRemark, setBatchApplyRemark] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
activitiesApi.list().then(setActivities).catch(e => alert(errMsg(e)));
|
activitiesApi.list().then(setActivities).catch(e => alert(errMsg(e)));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -27,6 +34,7 @@ export default function Hours() {
|
|||||||
setMsg('');
|
setMsg('');
|
||||||
setParticipants([]);
|
setParticipants([]);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
|
setSelectedIds(new Set());
|
||||||
try {
|
try {
|
||||||
const [p, r] = await Promise.all([activitiesApi.participants(id), hoursApi.byActivity(id)]);
|
const [p, r] = await Promise.all([activitiesApi.participants(id), hoursApi.byActivity(id)]);
|
||||||
setActivityInfo(p.activity);
|
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(() => {
|
useEffect(() => {
|
||||||
if (selectedId) loadParticipants(Number(selectedId));
|
if (selectedId) loadParticipants(Number(selectedId));
|
||||||
else { setParticipants([]); setActivityInfo(null); setResult(null); }
|
else { setParticipants([]); setActivityInfo(null); setResult(null); }
|
||||||
@@ -127,7 +176,8 @@ export default function Hours() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<Button variant="secondary" onClick={() => { const v = prompt('统一设置时长(小时)', '2'); if (v !== null) fillAll(v); }} disabled={!selectedId}>批量填时长</Button>
|
<Button variant="secondary" onClick={() => { const v = prompt('统一设置时长(小时)', '2'); if (v !== null) fillAll(v); }} disabled={!selectedId}>全表填时长</Button>
|
||||||
|
<Button variant="secondary" onClick={openBatchModal} disabled={!selectedId}><span className="inline-flex items-center gap-2"><IconBatchSet size={18} />批量设置({selectedCount > 0 ? `选中${selectedCount}` : '勾选后可用'})</span></Button>
|
||||||
<Button variant="secondary" onClick={doExcelExport} disabled={!selectedId}><span className="inline-flex items-center gap-2"><IconFileExcel size={18} />导出 Excel</span></Button>
|
<Button variant="secondary" onClick={doExcelExport} disabled={!selectedId}><span className="inline-flex items-center gap-2"><IconFileExcel size={18} />导出 Excel</span></Button>
|
||||||
<Button variant="secondary" onClick={doImageExport} disabled={!selectedId}><span className="inline-flex items-center gap-2"><IconFileImage size={18} />导出图片</span></Button>
|
<Button variant="secondary" onClick={doImageExport} disabled={!selectedId}><span className="inline-flex items-center gap-2"><IconFileImage size={18} />导出图片</span></Button>
|
||||||
<Button onClick={save} disabled={!selectedId || saving}>{saving ? '保存中...' : '保存登记'}</Button>
|
<Button onClick={save} disabled={!selectedId || saving}>{saving ? '保存中...' : '保存登记'}</Button>
|
||||||
@@ -158,6 +208,17 @@ export default function Hours() {
|
|||||||
<table className="w-full text-sm border-collapse">
|
<table className="w-full text-sm border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gray-100 border-b border-gray-200">
|
<tr className="bg-gray-100 border-b border-gray-200">
|
||||||
|
<th className="px-2 py-2.5 text-left font-semibold whitespace-nowrap text-gray-800" style={{ width: '42px' }}>
|
||||||
|
<label className="inline-flex cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
|
||||||
|
checked={isAllSelected}
|
||||||
|
ref={(el) => { if (el) el.indeterminate = isIndeterminate; }}
|
||||||
|
onChange={toggleAll}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
<th className="px-2 py-2.5 text-left font-semibold whitespace-nowrap text-gray-800">序号</th>
|
<th className="px-2 py-2.5 text-left font-semibold whitespace-nowrap text-gray-800">序号</th>
|
||||||
<th className="px-2 py-2.5 text-left font-semibold whitespace-nowrap text-gray-800">班级</th>
|
<th className="px-2 py-2.5 text-left font-semibold whitespace-nowrap text-gray-800">班级</th>
|
||||||
<th className="px-2 py-2.5 text-left font-semibold whitespace-nowrap text-gray-800">姓名</th>
|
<th className="px-2 py-2.5 text-left font-semibold whitespace-nowrap text-gray-800">姓名</th>
|
||||||
@@ -169,11 +230,21 @@ export default function Hours() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{participants.length === 0 ? (
|
{participants.length === 0 ? (
|
||||||
<tr><td colSpan={7} className="px-3 py-10 text-center text-gray-500 border-t border-gray-200">
|
<tr><td colSpan={8} className="px-3 py-10 text-center text-gray-500 border-t border-gray-200">
|
||||||
该活动暂无部员可登记(若选了工作组,请确认该工作组下有部员)
|
该活动暂无部员可登记(若选了工作组,请确认该工作组下有部员)
|
||||||
</td></tr>
|
</td></tr>
|
||||||
) : participants.map((p, idx) => (
|
) : participants.map((p, idx) => (
|
||||||
<tr key={p.id} className={`border-b border-gray-200 ${idx % 2 ? 'bg-gray-50' : 'bg-white'} hover:bg-primary-50 transition-colors`}>
|
<tr key={p.id} className={`border-b border-gray-200 ${selectedIds.has(p.id) ? 'bg-primary-50/70' : idx % 2 ? 'bg-gray-50' : 'bg-white'} hover:bg-primary-50 transition-colors`}>
|
||||||
|
<td className="px-2 py-2">
|
||||||
|
<label className="inline-flex cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
|
||||||
|
checked={selectedIds.has(p.id)}
|
||||||
|
onChange={() => toggleOne(p.id)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
<td className="px-2 py-2 text-gray-700">{idx + 1}</td>
|
<td className="px-2 py-2 text-gray-700">{idx + 1}</td>
|
||||||
<td className="px-2 py-2 whitespace-nowrap text-gray-700">{p.class}</td>
|
<td className="px-2 py-2 whitespace-nowrap text-gray-700">{p.class}</td>
|
||||||
<td className="px-2 py-2 font-semibold whitespace-nowrap text-gray-800">{p.name}</td>
|
<td className="px-2 py-2 font-semibold whitespace-nowrap text-gray-800">{p.name}</td>
|
||||||
@@ -198,6 +269,32 @@ export default function Hours() {
|
|||||||
请先在上方选择一个活动开始登记
|
请先在上方选择一个活动开始登记
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Modal open={batchModalOpen} title={`批量设置时长(已选 ${selectedCount} 人)`} width="max-w-lg" onClose={() => setBatchModalOpen(false)}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold mb-1.5 text-gray-700">时长(小时)<span className="text-red-500 ml-1">*</span></label>
|
||||||
|
<Input type="number" step="0.1" min="0" value={batchHours} onChange={(e) => setBatchHours(e.target.value)} />
|
||||||
|
<div className="muted mt-1.5 text-xs">将为选中的 {selectedCount} 名部员统一设置此时长值</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold mb-1.5 text-gray-700">
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<input type="checkbox" className="h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500" checked={batchApplyRemark} onChange={(e) => setBatchApplyRemark(e.target.checked)} />
|
||||||
|
<span>同时统一设置备注(可选)</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Input value={batchRemark} onChange={(e) => setBatchRemark(e.target.value)} disabled={!batchApplyRemark} placeholder={batchApplyRemark ? '例如:校园摄影宣传 / 公众号推文排版' : '先勾选上方"同时设置备注"后启用'} />
|
||||||
|
</div>
|
||||||
|
<div className="bg-primary-50 border border-primary-100 rounded-md px-3 py-2 text-sm text-primary-800">
|
||||||
|
注意:设置后会立即覆盖选中人员的时长/备注(如勾选)。需点击右侧「保存登记」按钮才会写入数据库。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" className="flex items-center justify-end gap-2 pt-3 mt-4 border-t border-gray-200">
|
||||||
|
<Button variant="secondary" onClick={() => setBatchModalOpen(false)}>取消</Button>
|
||||||
|
<Button onClick={confirmBatchSet}>确认设置</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user