auto-sync: 2026-05-19 13:45:03
This commit is contained in:
@@ -409,26 +409,65 @@ export default function EdictBoard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选 + 搜索 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 0', flexWrap: 'wrap' }}>
|
||||
{/* 第 1 行:归档控制 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
|
||||
{(['active', 'archived', 'all'] as const).map(f => (
|
||||
<button key={f} onClick={() => setArchiveFilter(f)} style={{
|
||||
padding: '3px 10px', borderRadius: 6, fontSize: 11,
|
||||
border: `1px solid ${archiveFilter === f ? 'var(--acc)' : '#2a3550'}`,
|
||||
background: archiveFilter === f ? 'var(--acc)22' : '#161b2e',
|
||||
color: archiveFilter === f ? 'var(--acc)' : '#8899aa',
|
||||
cursor: 'pointer', transition: 'all .15s',
|
||||
}}>
|
||||
{f === 'active' ? '活跃' : f === 'archived' ? '归档' : '全部'}
|
||||
</button>
|
||||
))}
|
||||
<span style={{ fontSize: 10, color: 'var(--muted)', marginLeft: 4 }}>活跃{activeCount} · 归档{archivedCount} · 共{topLevelTasks.length}</span>
|
||||
<button onClick={async () => {
|
||||
// 一键归档已完成
|
||||
const pid = selectedProjectId;
|
||||
if (!pid) return;
|
||||
const doneTasks = topLevelTasks.filter(t => t.status === 'done' && !t.archived);
|
||||
if (doneTasks.length === 0) { toast('没有可归档的任务'); return; }
|
||||
try {
|
||||
let count = 0;
|
||||
for (const t of doneTasks) {
|
||||
const res = await fetch(`/api/projects/${pid}/tasks/${t.id}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ archived: 1 }),
|
||||
});
|
||||
if (res.ok) count++;
|
||||
}
|
||||
toast(`📦 已归档 ${count} 个任务`);
|
||||
loadV2Tasks();
|
||||
} catch { toast('归档失败', 'err'); }
|
||||
}} style={{
|
||||
marginLeft: 'auto', padding: '3px 10px', borderRadius: 6, fontSize: 11,
|
||||
border: '1px solid #6b728044', background: '#161b2e', color: '#6b7280',
|
||||
cursor: 'pointer', transition: 'all .15s',
|
||||
}}>📦 一键归档</button>
|
||||
</div>
|
||||
|
||||
{/* 第 2 行:状态筛选 + 搜索 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '0 0 6px', flexWrap: 'wrap' }}>
|
||||
{filters.map(f => (
|
||||
<button key={f.key} onClick={() => setStatusFilter(f.key)} style={{
|
||||
padding: '3px 10px', borderRadius: 6, fontSize: 11,
|
||||
padding: '2px 8px', borderRadius: 5, fontSize: 10,
|
||||
border: `1px solid ${statusFilter === f.key ? 'var(--acc)' : '#2a3550'}`,
|
||||
background: statusFilter === f.key ? 'var(--acc)22' : '#161b2e',
|
||||
color: statusFilter === f.key ? 'var(--acc)' : '#8899aa',
|
||||
cursor: 'pointer', transition: 'all .15s',
|
||||
display: 'flex', alignItems: 'center', gap: 3,
|
||||
display: 'flex', alignItems: 'center', gap: 2,
|
||||
}}>
|
||||
<span>{f.icon}</span><span>{f.label}</span>
|
||||
{(counts[f.key] || 0) > 0 && (
|
||||
<span style={{ fontSize: 10, background: statusFilter === f.key ? 'var(--acc)' : '#2a3550', color: statusFilter === f.key ? '#000' : '#8899aa', borderRadius: 8, padding: '0 4px', minWidth: 16, textAlign: 'center' }}>{counts[f.key]}</span>
|
||||
<span style={{ fontSize: 9, background: statusFilter === f.key ? 'var(--acc)' : '#2a3550', color: statusFilter === f.key ? '#000' : '#8899aa', borderRadius: 8, padding: '0 4px', minWidth: 14, textAlign: 'center' }}>{counts[f.key]}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<div style={{ marginLeft: 'auto', position: 'relative' }}>
|
||||
<input type="text" placeholder="搜索任务..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)}
|
||||
style={{ padding: '4px 10px 4px 28px', borderRadius: 6, fontSize: 12, border: '1px solid #2a3550', background: '#161b2e', color: '#c0ccdd', width: 200, outline: 'none' }}
|
||||
style={{ padding: '4px 10px 4px 28px', borderRadius: 6, fontSize: 11, border: '1px solid #2a3550', background: '#161b2e', color: '#c0ccdd', width: 180, outline: 'none' }}
|
||||
/>
|
||||
<span style={{ position: 'absolute', left: 8, top: '50%', transform: 'translateY(-50%)', fontSize: 12, color: '#556' }}>🔍</span>
|
||||
</div>
|
||||
@@ -438,14 +477,14 @@ export default function EdictBoard() {
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 12, marginTop: 12 }}>
|
||||
{filtered.map(t => {
|
||||
const sub = subtaskIndex[t.id] || { total: 0, done: 0, activeStage: null };
|
||||
return <TaskCard key={t.id} task={t} subtaskCount={sub.total} subtaskDone={sub.done} activeStage={sub.activeStage} onOpen={() => setModalTaskId(t.id)} />;
|
||||
})}
|
||||
return <TaskCard key={t.id} task={t} subtaskCount={sub.total} subtaskDone={sub.done} activeStage={sub.activeStage} onOpen={() => setModalTaskId(t.id)} onAction={(a) => handleCardAction(t.id, a)} />;
|
||||
})}}
|
||||
{filtered.length === 0 && tasks.length > 0 && (
|
||||
<div style={{ gridColumn: '1/-1', textAlign: 'center', padding: 40, color: 'var(--muted)' }}>当前筛选无匹配任务</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tasks.length === 0 && <EmptyState hasProject={true} />}
|
||||
{archiveFiltered.length === 0 && <EmptyState hasProject={true} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user