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>
199 lines
12 KiB
React
199 lines
12 KiB
React
// 移动版 · 四个 Tab 视图:任务 / 审批 / 事件 / 项目
|
||
function MobTaskRow({ task, depth = 0, expandedId, setExpandedId }) {
|
||
const { ComplexityBadge, StatusChip } = window.MaestroDesignSystem_a6a290;
|
||
const kids = task.children || [];
|
||
const expanded = expandedId === task.id;
|
||
const isGate = ['plan_review', 'spec_review', 'exec_review'].includes(task.status);
|
||
return (
|
||
<React.Fragment>
|
||
<div onClick={() => setExpandedId(expanded ? null : task.id)} style={{
|
||
display: 'flex', flexDirection: 'column', gap: 6,
|
||
padding: '12px 14px', minHeight: 44, cursor: 'pointer',
|
||
paddingLeft: 14 + depth * 16,
|
||
borderBottom: '1px solid var(--line-soft)',
|
||
borderLeft: depth ? '1px solid var(--line-soft)' : 'none',
|
||
background: expanded ? 'var(--panel-2)' : isGate ? 'rgba(184,142,245,.05)' : 'transparent',
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
||
<span style={{ fontWeight: 500, fontSize: 14, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{task.title}</span>
|
||
<span style={{ color: 'var(--faint)', fontSize: 10.5, flex: 'none' }}>{task.id}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<ComplexityBadge complexity={task.cplx} />
|
||
<StatusChip status={task.status} />
|
||
<span style={{ marginLeft: 'auto', fontSize: 10.5, color: task.prio === 'P0' ? 'var(--amber)' : 'var(--faint)' }}>{task.prio}</span>
|
||
</div>
|
||
</div>
|
||
{expanded ? (
|
||
<div style={{ background: 'var(--panel)', borderBottom: '1px solid var(--line)', borderLeft: '2px solid var(--green-dim)', padding: '12px 14px', animation: 'maestro-rise .2s ease both' }}>
|
||
{(task.doc || task.ops) ? (
|
||
<React.Fragment>
|
||
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 4 }}>{task.doc ? '改动方案(SPEC)' : '将执行的操作(OPERATIONS)'}</div>
|
||
<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, whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, color: 'var(--ink)', lineHeight: 1.6 }}>{task.doc || task.ops}</pre>
|
||
</React.Fragment>
|
||
) : (
|
||
<div style={{ padding: '10px 12px', border: '1px dashed var(--line)', borderRadius: 6, color: 'var(--faint)', fontStyle: 'italic', fontSize: 12 }}>待 Claude Code 产出(经 MCP 写入并提交评审)</div>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
{kids.map((k) => <MobTaskRow key={k.id} task={k} depth={depth + 1} expandedId={expandedId} setExpandedId={setExpandedId} />)}
|
||
</React.Fragment>
|
||
);
|
||
}
|
||
|
||
function MobTasks({ tasks, agents }) {
|
||
const { SectionHead } = window.MaestroDesignSystem_a6a290;
|
||
const [expandedId, setExpandedId] = React.useState(null);
|
||
return (
|
||
<div>
|
||
{agents.length ? (
|
||
<div style={{ margin: '12px 14px 0', display: 'flex', gap: 10, alignItems: 'center', background: 'var(--panel)', border: '1px solid var(--cyan-dim)', borderRadius: 6, padding: '10px 12px' }}>
|
||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 8px var(--cyan)', animation: 'maestro-pulse .9s infinite', flex: 'none' }}></span>
|
||
<span style={{ fontSize: 12, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{agents[0].title}</span>
|
||
<span style={{ marginLeft: 'auto', flex: 'none', fontSize: 10, letterSpacing: '.1em', color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', borderRadius: 3, padding: '0 6px' }}>{agents[0].kind}</span>
|
||
</div>
|
||
) : null}
|
||
<SectionHead title="任务树" style={{ padding: '14px 14px 8px' }} />
|
||
<div>
|
||
{tasks.map((t) => <MobTaskRow key={t.id} task={t} expandedId={expandedId} setExpandedId={setExpandedId} />)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MobGatePreview({ item, onDecide, onClose }) {
|
||
const { ComplexityBadge, Button, Textarea } = window.MaestroDesignSystem_a6a290;
|
||
const [rejecting, setRejecting] = React.useState(false);
|
||
const [reason, setReason] = React.useState('');
|
||
if (!item) return null;
|
||
return (
|
||
<div style={{ position: 'fixed', inset: 0, zIndex: 1300, display: 'flex', flexDirection: 'column', background: 'var(--bg)', animation: 'maestro-rise .18s ease both' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 14px', borderBottom: '1px solid var(--line)', background: 'var(--panel-2)', flex: 'none' }}>
|
||
<span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--bg)', background: 'var(--violet)', padding: '2px 8px', borderRadius: 3, flex: 'none' }}>{item.gateLabel}</span>
|
||
<span style={{ fontWeight: 700, fontSize: 14, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.title}</span>
|
||
<span style={{ flex: 1 }}></span>
|
||
<button onClick={onClose} title="关闭" style={{ fontFamily: 'var(--mono)', fontSize: 15, background: 'transparent', color: 'var(--muted)', border: 'none', cursor: 'pointer', padding: '8px 10px', minHeight: 44 }}>✕</button>
|
||
</div>
|
||
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 16px 24px' }}>
|
||
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 10 }}>{item.meta}</div>
|
||
{item.docLabel ? <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', margin: '0 0 6px' }}>{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 14px', fontFamily: 'var(--mono)', fontSize: 13.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink)' }}>{item.doc}</div>
|
||
</div>
|
||
<div style={{ flex: 'none', padding: '12px 14px calc(12px + env(safe-area-inset-bottom))', borderTop: '1px solid var(--line)', background: 'var(--panel-2)', display: 'grid', gap: 10 }}>
|
||
{rejecting ? (
|
||
<React.Fragment>
|
||
<Textarea danger placeholder="改进意见(必填)" minHeight={56} value={reason} onChange={setReason} />
|
||
<div style={{ display: 'flex', gap: 10 }}>
|
||
<Button variant="reject" disabled={!reason.trim()} style={{ flex: 1, minHeight: 44 }}
|
||
onClick={() => { onDecide(item, 'reject', reason); onClose(); }}>确认驳回</Button>
|
||
<Button style={{ flex: 1, minHeight: 44 }} onClick={() => { setRejecting(false); setReason(''); }}>取消</Button>
|
||
</div>
|
||
</React.Fragment>
|
||
) : (
|
||
<div style={{ display: 'flex', gap: 10 }}>
|
||
<Button variant="accept" style={{ flex: 1, minHeight: 44 }} onClick={() => { onDecide(item, 'accept'); onClose(); }}>✓ 通过</Button>
|
||
<Button variant="reject" style={{ flex: 1, minHeight: 44 }} onClick={() => setRejecting(true)}>✕ 驳回</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MobGates({ approvals, onDecide }) {
|
||
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);
|
||
if (!approvals.length) {
|
||
return <div style={{ margin: '24px 14px', padding: '38px 0', textAlign: 'center', color: 'var(--faint)', border: '1px dashed var(--line)', borderRadius: 6, fontSize: 12 }}>无待裁决审批</div>;
|
||
}
|
||
return (
|
||
<div style={{ padding: '12px 14px' }}>
|
||
<GateStripe />
|
||
<SectionHead mark="violet" title="审批闸 · 等待裁决" style={{ padding: '10px 0 8px', color: 'var(--violet)' }} />
|
||
{approvals.map((a) => (
|
||
<div key={a.id} style={{ marginBottom: 12 }}>
|
||
<GateCard gate={a.gate} title={a.title} meta={a.meta} docLabel={a.docLabel} doc={a.doc}
|
||
actions={
|
||
<React.Fragment>
|
||
<Button variant="accept" style={{ flex: 1, minHeight: 44 }} onClick={() => onDecide(a, 'accept')}>✓ 通过</Button>
|
||
<Button variant="reject" style={{ flex: 1, minHeight: 44 }} onClick={() => setRejecting(rejecting === a.id ? null : a.id)}>✕ 驳回</Button>
|
||
<Button variant="ghost" style={{ minHeight: 44 }} title="全屏阅读"
|
||
onClick={() => setPreview({ ...a, gateLabel: { plan: '拆解评审', spec: '方案评审', exec: '结果评审' }[a.gate] || a.gate })}>⛶</Button>
|
||
</React.Fragment>
|
||
}>
|
||
{rejecting === a.id ? (
|
||
<div style={{ display: 'grid', gap: 10, marginTop: 10 }}>
|
||
<Textarea danger placeholder="改进意见(必填)" minHeight={56} value={reason} onChange={setReason} />
|
||
<Button variant="reject" disabled={!reason.trim()} style={{ minHeight: 44 }}
|
||
onClick={() => { onDecide(a, 'reject', reason); setRejecting(null); setReason(''); }}>
|
||
确认驳回
|
||
</Button>
|
||
</div>
|
||
) : null}
|
||
</GateCard>
|
||
</div>
|
||
))}
|
||
<MobGatePreview item={preview} onDecide={onDecide} onClose={() => setPreview(null)} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MobEvents({ events }) {
|
||
const { EventItem, SectionHead } = window.MaestroDesignSystem_a6a290;
|
||
return (
|
||
<div>
|
||
<SectionHead title="事件流" style={{ padding: '14px 14px 8px' }} />
|
||
<ul style={{ listStyle: 'none', padding: '0 14px', margin: 0 }}>
|
||
{events.map((e, i) => <EventItem key={i} type={e.type} time={e.time} detail={e.detail} />)}
|
||
</ul>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MobProjects({ projects, currentId, onSelect, project }) {
|
||
const { SectionHead, Select, Input, Button } = window.MaestroDesignSystem_a6a290;
|
||
return (
|
||
<div>
|
||
<SectionHead title="项目" style={{ padding: '14px 14px 8px' }} />
|
||
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||
{projects.map((p) => {
|
||
const active = p.id === currentId;
|
||
return (
|
||
<li key={p.id} onClick={() => onSelect(p.id)} style={{
|
||
display: 'flex', alignItems: 'center', gap: 10, minHeight: 52,
|
||
padding: '10px 14px', cursor: 'pointer',
|
||
borderBottom: '1px solid var(--line-soft)',
|
||
borderLeft: '2px solid ' + (active ? 'var(--green)' : 'transparent'),
|
||
background: active ? 'var(--panel-2)' : 'transparent',
|
||
color: active ? 'var(--green)' : 'var(--muted)',
|
||
}}>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flex: 'none' }}>
|
||
<line x1="6" y1="3" x2="6" y2="15" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /><path d="M18 9a9 9 0 0 1-9 9" />
|
||
</svg>
|
||
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}>
|
||
<span style={{ fontWeight: 600, fontSize: 13, color: active ? 'var(--green)' : 'var(--ink)' }}>{p.name}</span>
|
||
<span style={{ fontSize: 10.5, color: 'var(--faint)' }}>{p.path}</span>
|
||
</span>
|
||
{active ? <span style={{ marginLeft: 'auto', fontSize: 10, letterSpacing: '.12em', color: 'var(--green)' }}>当前</span> : null}
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
<SectionHead title="项目配置" style={{ padding: '18px 14px 8px' }} />
|
||
<div style={{ margin: '0 14px', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: 14, display: 'grid', gap: 12 }}>
|
||
<Input label="最大并发" type="number" defaultValue={String(project.concurrency)} />
|
||
<Select label="工作模式" defaultValue={project.autonomy} options={[
|
||
{ value: 'manual', label: 'manual · 手动' },
|
||
{ value: 'auto-easy', label: 'auto-easy · 自动执行 Easy' },
|
||
{ value: 'auto-approved', label: 'auto-approved · 自动执行已批准' },
|
||
]} />
|
||
<Button variant="solid" style={{ minHeight: 44 }}>保存配置</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
Object.assign(window, { MaestroMobTasks: MobTasks, MaestroMobGates: MobGates, MaestroMobEvents: MobEvents, MaestroMobProjects: MobProjects });
|