Files
wangjia 0c1f630dca 前端:任务详情展示审批/驳回记录 + 任务行 ID 不被长标题挤掉 + i18n 五语 (tsk_EmZ6vjtVbrOk)
- TaskDetail 渲染 _raw.approvals:闸名·通过/驳回 + 审批人 + 时间,
  驳回用红色边框+红底高亮块突出 reason(t.rejectReason 标签),
  通过用绿色、reason 走低调 ↳ 行;无审批记录不渲染该分区。
- 安全:reason/actor/gate 全部走 React 文本节点(默认转义),
  不经 MaestroRichDoc/innerHTML,避免同源存储型 XSS。
- TaskRow:taskid 拆为独立 flex:none span,title 加 minWidth:0,
  长标题省略号截断时 id 完整不被挤掉;ArchiveRow 同步处理保持一致。
- i18n:zh/en/es/ja/fr 五语新增 rejectReason 键。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:04:42 +08:00

188 lines
11 KiB
React
Raw Permalink 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={{ flex: '0 1 auto', minWidth: 0, color: 'var(--muted)', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{item.title}
</span>
<span style={{ flex: 'none', color: 'var(--faint)', fontSize: 10.5 }}>{item.id}</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) => (
<div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '10px 12px' }}><window.MaestroRichDoc md={text} /></div>
);
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 });