Files
maestro/design/ui_kits/console/ArchiveSection.jsx
T
wangjia 1c62059424 feat(web): 控制台 UI 批次——用量卡片精简+详情弹框、项目列表封顶 6、评审弹框增强、live 输出流、标题栏置顶
- 全局 AGENT 卡片精简为 2×2 概览(近 7 天窗口 + 真实运行数),删按项目列表,加「详情」弹框(按天/周/月柱状图 + 分项目下钻表);- 项目列表展开态封顶 6,多余折叠;- plan/exec 评审弹框:markdown 折叠卡片 + 点文件看 diff;- live 输出流提取 tool_use/tool_result;- Agent/审批闸/任务树/已归档标题栏滚动置顶。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:42:39 +08:00

187 lines
11 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 已归档区(done / 取消,时间倒序分页)+ 归档详情模态
function ArchiveRow({ item, t, onOpen }) {
const { ComplexityBadge, StatusChip } = window.MaestroDesignSystem_a6a290;
const [hover, setHover] = React.useState(false);
return (
<div onClick={() => onOpen(item)} title={t.archiveTip}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer',
padding: '7px 12px', fontSize: 12.5,
borderBottom: '1px dashed var(--line-soft)',
background: hover ? 'var(--panel)' : 'transparent',
}}>
<span style={{ color: 'var(--muted)', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{item.title}<span style={{ color: 'var(--faint)', fontSize: 10.5, marginLeft: 6 }}>{item.id}</span>
</span>
{item.subs ? <span style={{ flex: 'none', fontSize: 10.5, color: 'var(--faint)' }}>{t.archiveSubs.replace('{n}', item.subs)}</span> : null}
<span style={{ flex: 1, minWidth: 8 }}></span>
<ComplexityBadge complexity={item.cplx} />
<StatusChip status={item.status} label={t.status[item.status]} />
<span style={{ flex: 'none', fontSize: 11, color: 'var(--faint)' }} title={item.timeFull}>{item.time}</span>
</div>
);
}
function SizeSelect({ size, setSize, t }) {
const [open, setOpen] = React.useState(false);
const [hover, setHover] = React.useState(false);
const ref = React.useRef(null);
React.useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
return (
<span ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
<button onClick={() => setOpen(!open)}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.04em',
background: 'transparent', color: hover || open ? 'var(--green)' : 'var(--muted)',
border: '1px solid ' + (hover || open ? 'var(--green-dim)' : 'var(--line)'),
borderRadius: 'var(--radius-sm, 4px)', padding: '3px 9px', cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', gap: 7, transition: 'color .12s, border-color .12s',
}}>
{t.perPage} <b style={{ color: 'var(--green)', fontSize: 11.5 }}>{size}</b>
<span style={{ fontSize: 8, color: 'var(--faint)', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .12s' }}></span>
</button>
{open ? (
<div style={{
position: 'absolute', bottom: 'calc(100% + 5px)', right: 0, zIndex: 60,
display: 'grid', minWidth: '100%',
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md, 6px)',
padding: 4, boxShadow: '0 10px 30px rgba(0,0,0,.65)', animation: 'maestro-rise .12s ease both',
}}>
{[10, 20, 50, 100].map((n) => {
const active = n === size;
return (
<button key={n} onClick={() => { setSize(n); setOpen(false); }} style={{
fontFamily: 'var(--mono)', fontSize: 11.5, fontWeight: active ? 700 : 400, textAlign: 'left',
background: active ? 'var(--panel-2)' : 'transparent',
color: active ? 'var(--green)' : 'var(--ink)',
border: 'none', borderRadius: 4, padding: '6px 12px 6px 8px', cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: 7,
}}
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel)'; }}
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
<span style={{ width: 8, color: 'var(--green)', fontSize: 10 }}>{active ? '▍' : ''}</span>{n}
</button>
);
})}
</div>
) : null}
</span>
);
}
function ArchivePager({ page, pages, size, setPage, setSize, t }) {
const { Button } = window.MaestroDesignSystem_a6a290;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', justifyContent: 'center', fontFamily: 'var(--mono)' }}>
{pages > 1 ? (
<React.Fragment>
<Button size="xs" disabled={page <= 1} onClick={() => setPage(page - 1)}>{t.pagePrev}</Button>
<span style={{ fontSize: 11, color: 'var(--muted)' }}>{t.pageOf.replace('{a}', page).replace('{b}', pages)}</span>
<Button size="xs" disabled={page >= pages} onClick={() => setPage(page + 1)}>{t.pageNext}</Button>
</React.Fragment>
) : null}
<SizeSelect size={size} setSize={setSize} t={t} />
</div>
);
}
function ArchiveSection({ items, t, onOpen }) {
const [page, setPage] = React.useState(1);
const [size, setSize] = React.useState(20);
const [folded, setFolded] = React.useState(() => localStorage.getItem('maestro-kit-fold-archive') === '1');
const toggleFold = () => setFolded((f) => { localStorage.setItem('maestro-kit-fold-archive', f ? '0' : '1'); return !f; });
if (!items.length) return null;
const pages = Math.max(1, Math.ceil(items.length / size));
const cur = Math.min(page, pages);
const slice = items.slice((cur - 1) * size, cur * size);
return (
<section style={{ opacity: .82 }}>
<window.MaestroKitSectionHead icon="archive" title={t.archive} count={items.length} accent="var(--faint)" folded={folded} onToggleFold={toggleFold} t={t} sticky />
{!folded ? (
<React.Fragment>
<div>{slice.map((it) => <ArchiveRow key={it.id} item={it} t={t} onOpen={onOpen} />)}</div>
<ArchivePager page={cur} pages={pages} size={size} setPage={setPage} setSize={(n) => { setSize(n); setPage(1); }} t={t} />
</React.Fragment>
) : null}
</section>
);
}
// 归档详情模态(只读:属性 / 产出 / 执行历史 / 结果 / 审批 / 时间线)
function ArchiveModal({ item, t, onClose }) {
const { ComplexityBadge, StatusChip, Timeline } = window.MaestroDesignSystem_a6a290;
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
if (!item) return null;
const d = item.detail || {};
const label = (text) => <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', margin: '14px 0 4px' }}>{text}</div>;
const doc = (text) => (
<pre style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '10px 12px', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, color: 'var(--ink)' }}>{text}</pre>
);
const kvGrid = { fontSize: 12, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '3px 12px', margin: 0 };
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.78)', backdropFilter: 'blur(2px)' }}></div>
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', width: 'min(860px, 94vw)', maxHeight: '92vh', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 'var(--radius-lg, 10px)', overflow: 'hidden', boxShadow: '0 24px 64px rgba(0,0,0,.6)', animation: 'maestro-rise .18s ease both' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', padding: '12px 16px', borderBottom: '1px solid var(--line)', background: 'var(--panel-2)' }}>
<span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--bg-deep)', background: 'var(--muted)', padding: '2px 8px', borderRadius: 3 }}>{t.archiveDetail}</span>
<span style={{ fontWeight: 700, fontSize: 15 }}>{item.title}</span>
<ComplexityBadge complexity={item.cplx} />
<StatusChip status={item.status} label={t.status[item.status]} />
<span style={{ flex: 1 }}></span>
<button onClick={onClose} title={t.closeTip} style={{ fontFamily: 'var(--mono)', fontSize: 13, background: 'transparent', color: 'var(--muted)', border: 'none', cursor: 'pointer', padding: '4px 8px' }}></button>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 20px 24px', fontFamily: 'var(--mono)' }}>
{d.attrs ? <dl style={kvGrid}>{d.attrs.map(([k, v], i) => (
<React.Fragment key={i}><dt style={{ color: 'var(--muted)' }}>{k}</dt><dd style={{ margin: 0, wordBreak: 'break-all' }}>{v}</dd></React.Fragment>
))}</dl> : null}
{d.spec ? <React.Fragment>{label('SPEC · 方案')}{doc(d.spec)}</React.Fragment> : null}
{d.ops ? <React.Fragment>{label('OPERATIONS · 操作')}{doc(d.ops)}</React.Fragment> : null}
{d.runs && d.runs.length ? <React.Fragment>
{label(t.runsLabel + '' + d.runs.length + '')}
<dl style={kvGrid}>{d.runs.map((r, i) => (
<React.Fragment key={i}>
<dt style={{ color: 'var(--muted)' }}>{r.kind} · {r.status}</dt>
<dd style={{ margin: 0 }}>{r.span}{r.ref ? <span style={{ display: 'block', color: 'var(--faint)', fontSize: 10.5 }}>{r.ref}</span> : null}</dd>
</React.Fragment>
))}</dl>
</React.Fragment> : null}
{d.result ? <React.Fragment>
{label(t.resultLabel)}
<dl style={kvGrid}>
<dt style={{ color: 'var(--muted)' }}>{t.branch}</dt><dd style={{ margin: 0 }}>{d.result.branch}</dd>
<dt style={{ color: 'var(--muted)' }}>commits</dt><dd style={{ margin: 0 }}>{d.result.commits.join(' · ')}</dd>
<dt style={{ color: 'var(--muted)' }}>diff</dt><dd style={{ margin: 0 }}>{d.result.diff}</dd>
</dl>
</React.Fragment> : null}
{d.approvals && d.approvals.length ? <React.Fragment>
{label(t.approvalsLabel)}
<dl style={kvGrid}>{d.approvals.map((a, i) => (
<React.Fragment key={i}>
<dt style={{ color: a.action === 'accept' ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>{a.gate} · {a.action === 'accept' ? t.accepted : t.rejected}</dt>
<dd style={{ margin: 0 }}>{t.approver} {a.actor} · {a.at}{a.reason ? <span style={{ display: 'block', color: 'var(--amber)' }}> {a.reason}</span> : null}</dd>
</React.Fragment>
))}</dl>
</React.Fragment> : null}
{d.timeline && d.timeline.length ? <React.Fragment>
{label(t.timelineLabel)}
<Timeline items={d.timeline} />
</React.Fragment> : null}
</div>
</div>
</div>
);
}
Object.assign(window, { MaestroKitArchiveSection: ArchiveSection, MaestroKitArchiveModal: ArchiveModal });