From fc30f9118372006990e205059322793fb2ce10ea Mon Sep 17 00:00:00 2001 From: cfdaily Date: Sun, 14 Jun 2026 14:09:15 +0800 Subject: [PATCH] =?UTF-8?q?[moz]=20feat(frontend):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=93=BE=20Tab=20=E2=80=94=20=E5=88=97?= =?UTF-8?q?=E8=A1=A8+=E8=AF=A6=E6=83=85+=E6=90=9C=E7=B4=A2=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/src/App.tsx | 2 + .../src/components/ToolchainPanel.tsx | 205 ++++++++++++++++++ src/frontend/src/store.ts | 3 +- 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/frontend/src/components/ToolchainPanel.tsx diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index c860483..d13bc0e 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -17,6 +17,7 @@ import CourtCeremony from './components/CourtCeremony'; import CourtDiscussion from './components/CourtDiscussion'; import UsagePanel from './components/UsagePanel'; import SettingsPanel from './components/SettingsPanel'; +import ToolchainPanel from './components/ToolchainPanel'; import GlobalSearch from './components/GlobalSearch'; import NotificationCenter from './components/NotificationCenter'; @@ -100,6 +101,7 @@ export default function App() { usage: , morning: , settings: , + toolchain: , }; return ( diff --git a/src/frontend/src/components/ToolchainPanel.tsx b/src/frontend/src/components/ToolchainPanel.tsx new file mode 100644 index 0000000..37c421d --- /dev/null +++ b/src/frontend/src/components/ToolchainPanel.tsx @@ -0,0 +1,205 @@ +/** + * ToolchainPanel — 工具链事件(系统级) + * 展示 _toolchain 项目的 tasks:CI/PR/部署/Review 通知 + */ +import { useEffect, useState } from 'react'; + +const STATUS_COLORS: Record = { + pending: '#f59e0b22', claimed: '#6a9eff22', working: '#6a9eff22', + review: '#818cf822', done: '#2ecc8a22', failed: '#ef444422', + cancelled: '#6b728022', blocked: '#ef444422', +}; + +const STATUS_LABELS: Record = { + pending: '待处理', claimed: '已认领', working: '处理中', + review: '审查中', done: '已完成', failed: '失败', + cancelled: '已取消', blocked: '已拦截', +}; + +function fmtTime(iso: string): string { + try { + const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z'); + const now = Date.now(); + const diff = now - d.getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return '刚刚'; + if (mins < 60) return `${mins}分钟前`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}小时前`; + return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`; + } catch { return iso; } +} + +export default function ToolchainPanel() { + const [tasks, setTasks] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [detail, setDetail] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [loading, setLoading] = useState(false); + + const loadTasks = async (q?: string) => { + setLoading(true); + try { + const url = q + ? `/api/projects/_toolchain/tasks?q=${encodeURIComponent(q)}` + : `/api/projects/_toolchain/tasks`; + const res = await fetch(url); + if (res.ok) { + const data = await res.json(); + setTasks(data.tasks || []); + } + } catch { /* */ } + setLoading(false); + }; + + useEffect(() => { loadTasks(); }, []); + + // 搜索防抖 300ms + useEffect(() => { + const timer = setTimeout(() => { + if (searchQuery !== undefined) loadTasks(searchQuery || undefined); + }, 300); + return () => clearTimeout(timer); + }, [searchQuery]); + + useEffect(() => { + if (!selectedId) { setDetail(null); return; } + (async () => { + try { + const res = await fetch( + `/api/projects/_toolchain/tasks/${selectedId}?expand=comments` + ); + if (res.ok) setDetail(await res.json()); + } catch { /* */ } + })(); + }, [selectedId]); + + // 渲染评论列表(兼容 expand 和裸 list 格式) + const renderComments = (comments: any[]) => { + if (!comments || comments.length === 0) return null; + return ( +
+
+ 📋 处理记录 ({comments.length}) +
+ {comments.map((c: any, i: number) => ( +
+
+ + {c.author || 'system'} + + {fmtTime(c.created_at)} +
+
{c.body}
+
+ ))} +
+ ); + }; + + return ( +
+ {/* 左侧列表 */} +
+ {/* 搜索栏 + 刷新 */} +
+ setSearchQuery(e.target.value)} + style={{ + flex: 1, padding: '4px 8px', borderRadius: 4, fontSize: 11, + border: '1px solid #2a3550', background: '#161b2e', color: '#dde4f8', + outline: 'none', + }} + /> + + {tasks.length} 条 +
+ + {/* 事件列表 */} +
+ {tasks.length === 0 && ( +
+ {loading ? '加载中...' : '暂无工具链事件'} +
+ )} + {tasks.map((t: any) => ( +
setSelectedId(t.id)} style={{ + padding: '10px 14px', borderBottom: '1px solid var(--line)', + cursor: 'pointer', transition: 'background .15s', + background: selectedId === t.id ? 'var(--panel2)' : 'transparent', + }} + onMouseEnter={e => e.currentTarget.style.background = 'var(--panel2)'} + onMouseLeave={e => e.currentTarget.style.background = selectedId === t.id ? 'var(--panel2)' : 'transparent'} + > +
+ {STATUS_LABELS[t.status] || t.status} + {fmtTime(t.created_at)} +
+
{t.title}
+
+ ))} +
+
+ + {/* 右侧详情 */} +
+ {!detail ? ( +
+
⛓️
+
选择一条事件查看详情
+
+ ) : ( + <> + {/* 头部 */} +
+
+ + {STATUS_LABELS[detail.status] || detail.status} + + {detail.id} +
+
{detail.title}
+
+ {fmtTime(detail.created_at)} +
+
+ + {/* 正文 */} + {detail.description && ( +
+ {detail.description} +
+ )} + + {/* action_report 评论 — expand 格式 {items, total_count} */} + {detail.comments && detail.comments.items && detail.comments.items.length > 0 && + renderComments(detail.comments.items) + } + {/* 兼容裸 list 格式 */} + {detail.comments && Array.isArray(detail.comments) && detail.comments.length > 0 && + renderComments(detail.comments) + } + + )} +
+
+ ); +} diff --git a/src/frontend/src/store.ts b/src/frontend/src/store.ts index 7ca128b..6bfb4cb 100644 --- a/src/frontend/src/store.ts +++ b/src/frontend/src/store.ts @@ -120,7 +120,7 @@ export function isArchived(t: Task): boolean { export type TabKey = | 'tasks' | 'court' | 'monitor' | 'agents' | 'models' | 'skills' | 'sessions' | 'archives' | 'templates' - | 'usage' | 'settings' | 'officials' | 'morning' | 'mail'; + | 'usage' | 'settings' | 'officials' | 'morning' | 'mail' | 'toolchain'; export const TAB_DEFS: { key: TabKey; label: string; icon: string }[] = [ { key: 'tasks', label: '任务看板', icon: '📜' }, @@ -135,6 +135,7 @@ export const TAB_DEFS: { key: TabKey; label: string; icon: string }[] = [ { key: 'archives', label: '奏折阁', icon: '📜' }, { key: 'morning', label: '早朝简报', icon: '🌅' }, { key: 'templates', label: '任务模板', icon: '📋' }, + { key: 'toolchain', label: '工具链', icon: '⛓️' }, { key: 'settings', label: '系统设置', icon: '⚙️' }, ];