5a1e185b1a
phosphor console v1.1 设计系统(从现网 web/style.css 演化):tokens(colors/effects/ typography/fonts) + components(core/forms/surfaces .jsx) + ui_kits(console + console_mobile) + assets/icons(15 双色 SVG) + guidelines + CLAUDE/SKILL/readme。供 B1–B6 重构任务对照实现。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
6.7 KiB
React
102 lines
6.7 KiB
React
// 全屏预览:读完整方案 + 就地裁决
|
|
function GatePreview({ item, t, onDecide, onClose }) {
|
|
const { ComplexityBadge, Button, Textarea } = window.MaestroDesignSystem_a6a290;
|
|
const [rejecting, setRejecting] = React.useState(false);
|
|
const [reason, setReason] = React.useState('');
|
|
React.useEffect(() => {
|
|
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [onClose]);
|
|
if (!item) return null;
|
|
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,.86)', backdropFilter: 'blur(3px)' }}></div>
|
|
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', width: 'min(1080px, 94vw)', height: '94vh', background: 'var(--panel)', border: '1px solid var(--violet-dim)', borderRadius: 'var(--radius-lg, 10px)', overflow: 'hidden', boxShadow: '0 0 0 1px var(--line-soft), 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)', background: 'var(--violet)', padding: '2px 8px', borderRadius: 3 }}>{t.gates[item.gate]}</span>
|
|
<span style={{ fontWeight: 700, fontSize: 15 }}>{item.title}</span>
|
|
<span style={{ fontSize: 11, color: 'var(--muted)' }}>{item.meta}</span>
|
|
<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: '16px 20px 28px', fontFamily: 'var(--mono)' }}>
|
|
{item.docLabel ? <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 6 }}>{item.docLabel}</div> : null}
|
|
<div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '12px 16px', fontSize: 13.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink)' }}>{item.doc}</div>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', padding: '12px 16px', borderTop: '1px solid var(--line)', background: 'var(--panel-2)' }}>
|
|
{rejecting ? (
|
|
<React.Fragment>
|
|
<span style={{ flexBasis: '100%', display: 'flex' }}>
|
|
<Textarea danger placeholder={t.rejectPlaceholder} minHeight={56} value={reason} onChange={setReason} />
|
|
</span>
|
|
<Button variant="reject" disabled={!reason.trim()} onClick={() => { onDecide(item, 'reject', reason); onClose(); }}>{t.confirmReject}</Button>
|
|
<Button onClick={() => { setRejecting(false); setReason(''); }}>{t.cancel}</Button>
|
|
</React.Fragment>
|
|
) : (
|
|
<React.Fragment>
|
|
<Button variant="accept" onClick={() => { onDecide(item, 'accept'); onClose(); }}>{t.accept}</Button>
|
|
<Button variant="reject" onClick={() => setRejecting(true)}>{t.reject}</Button>
|
|
</React.Fragment>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 审批闸区:斜纹条 + GateCard 列表(accept/reject + 驳回必填理由)
|
|
function GateSection({ approvals, onDecide, t }) {
|
|
const { GateCard, GateStripe, Button, Textarea, SectionHead } = window.MaestroDesignSystem_a6a290;
|
|
const [rejecting, setRejecting] = React.useState(null);
|
|
const [reason, setReason] = React.useState('');
|
|
const [preview, setPreview] = React.useState(null);
|
|
const [folded, setFolded] = React.useState(() => new Set());
|
|
const toggleFold = (id) => setFolded((prev) => {
|
|
const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next;
|
|
});
|
|
if (approvals.length === 0) return null;
|
|
return (
|
|
<section style={{ marginTop: 18 }}>
|
|
<GateStripe />
|
|
<SectionHead mark="violet" title={t.gateSection} style={{ paddingTop: 12, color: 'var(--violet)' }} />
|
|
{approvals.map((a) => {
|
|
const isFolded = folded.has(a.id);
|
|
return (
|
|
<div key={a.id} style={{ marginBottom: 12 }}>
|
|
<GateCard gate={a.gate} kindLabel={t.gates[a.gate]} title={a.title} meta={a.meta} docLabel={a.docLabel} doc={a.doc}
|
|
collapsed={isFolded} foldable onToggleFold={() => toggleFold(a.id)}
|
|
foldTitle={isFolded ? t.gateExpand : t.gateCollapse}
|
|
onHeaderDoubleClick={() => setPreview(a)} headerTitle={t.gateDblTip}
|
|
headerExtra={<button type="button" title={t.fullRead} onClick={() => setPreview(a)}
|
|
style={{ fontFamily: 'var(--mono)', fontSize: 12, lineHeight: 1, cursor: 'pointer', background: 'transparent', color: 'var(--muted)', border: '1px solid var(--line)', borderRadius: 'var(--radius-sm,4px)', padding: '3px 7px' }}
|
|
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--violet)'; e.currentTarget.style.borderColor = 'var(--violet-dim)'; }}
|
|
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>⛶</button>}
|
|
actions={
|
|
<React.Fragment>
|
|
<Button variant="accept" onClick={() => onDecide(a, 'accept')}>{t.accept}</Button>
|
|
<Button variant="reject" onClick={() => setRejecting(rejecting === a.id ? null : a.id)}>{t.reject}</Button>
|
|
<span style={{ marginLeft: 'auto' }}>
|
|
<Button variant="ghost" size="xs" title={t.fullRead} onClick={() => setPreview(a)}>⛶ {t.fullRead}</Button>
|
|
</span>
|
|
</React.Fragment>
|
|
}>
|
|
{rejecting === a.id ? (
|
|
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginTop: 10 }}>
|
|
<Textarea danger placeholder={t.rejectPlaceholder} minHeight={56} value={reason} onChange={setReason} />
|
|
<Button variant="reject" disabled={!reason.trim()}
|
|
onClick={() => { onDecide(a, 'reject', reason); setRejecting(null); setReason(''); }}>
|
|
{t.confirmReject}
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
</GateCard>
|
|
</div>
|
|
);
|
|
})}
|
|
<GatePreview item={preview} t={t} onDecide={onDecide} onClose={() => setPreview(null)} />
|
|
</section>
|
|
);
|
|
}
|
|
window.MaestroKitGateSection = GateSection;
|