Files
maestro/design/ui_kits/console_mobile/MobileViews.jsx
T
wangjia 5a1e185b1a docs(design): 解压 Maestro Design System 到 design/(重构参照材料)
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>
2026-06-13 14:29:07 +08:00

199 lines
12 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.
// 移动版 · 四个 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 });