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>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// 移动版 · 头部 + 底部 Tab 栏
|
||||
const mIcon = { fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', viewBox: '0 0 24 24' };
|
||||
function MIconTask({ s = 22 }) {
|
||||
return <svg {...mIcon} width={s} height={s}><rect x="4" y="4" width="16" height="16" rx="2.5" /><polyline points="8 12 11 15 16 9" /></svg>;
|
||||
}
|
||||
function MIconGate({ s = 22 }) {
|
||||
return <svg {...mIcon} width={s} height={s}><path d="M12 3 L22 21 L2 21 Z" /><line x1="12" y1="10" x2="12" y2="14" /><line x1="12" y1="17.5" x2="12" y2="17.51" /></svg>;
|
||||
}
|
||||
function MIconEvent({ s = 22 }) {
|
||||
return <svg {...mIcon} width={s} height={s}><polyline points="2 12 7 12 10 5 14 19 17 12 22 12" /></svg>;
|
||||
}
|
||||
function MIconGit({ s = 22 }) {
|
||||
return <svg {...mIcon} width={s} height={s}><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>;
|
||||
}
|
||||
|
||||
function MobileMark({ scale = 1.6 }) {
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
const MAP = ['.....LLLLL.....','...LLLLLLLLL...','..GGGGGGGGGGG..','..GGEEGGGEEGG..','..GGEEGGGEEGG..','..GGGGGGGGGGG..','...GGGGGGGGG...','...G..G.G..G...','...G..G.G..G...','..G...G.G...G..','..D...D.D...D..','.D...D...D...D.'];
|
||||
const INK = { L: '#79ec94', G: '#5fdd7d', D: '#2e6b3d', E: '#070908' };
|
||||
const ctx = ref.current.getContext('2d');
|
||||
MAP.forEach((row, y) => [...row].forEach((ch, x) => {
|
||||
if (INK[ch]) { ctx.fillStyle = INK[ch]; ctx.fillRect(x, y, 1, 1); }
|
||||
}));
|
||||
}, []);
|
||||
return <canvas ref={ref} width="15" height="12"
|
||||
style={{ width: 15 * scale, height: 12 * scale, imageRendering: 'pixelated', filter: 'drop-shadow(0 0 5px rgba(95,221,125,.4))' }} />;
|
||||
}
|
||||
|
||||
function MobileHeader({ project, counts }) {
|
||||
const { CountBadge } = window.MaestroDesignSystem_a6a290;
|
||||
return (
|
||||
<header style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px',
|
||||
borderBottom: '1px solid var(--line)', background: 'var(--bg-deep)',
|
||||
position: 'sticky', top: 0, zIndex: 20,
|
||||
}}>
|
||||
<MobileMark />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, letterSpacing: '.04em', whiteSpace: 'nowrap' }}>{project.name}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{project.branch} · {project.autonomy}</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 6 }}>
|
||||
<CountBadge kind="gate" count={counts.gate} />
|
||||
<CountBadge kind="run" count={counts.run} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileTabBar({ tab, setTab, gateCount }) {
|
||||
const tabs = [
|
||||
{ id: 'tasks', label: '任务', icon: MIconTask },
|
||||
{ id: 'gates', label: '审批', icon: MIconGate, badge: gateCount },
|
||||
{ id: 'events', label: '事件', icon: MIconEvent },
|
||||
{ id: 'projects', label: '项目', icon: MIconGit },
|
||||
];
|
||||
return (
|
||||
<nav style={{
|
||||
display: 'grid', gridTemplateColumns: 'repeat(4,1fr)',
|
||||
borderTop: '1px solid var(--line)', background: 'var(--bg-deep)',
|
||||
position: 'sticky', bottom: 0, zIndex: 20,
|
||||
}}>
|
||||
{tabs.map((t) => {
|
||||
const active = tab === t.id;
|
||||
const Icon = t.icon;
|
||||
return (
|
||||
<button key={t.id} onClick={() => setTab(t.id)} style={{
|
||||
fontFamily: 'var(--mono)', background: 'transparent', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
|
||||
padding: '9px 0 10px', minHeight: 56, position: 'relative',
|
||||
color: active ? 'var(--green)' : 'var(--faint)',
|
||||
textShadow: active ? '0 0 10px rgba(95,221,125,.45)' : 'none',
|
||||
}}>
|
||||
<span style={{ position: 'relative', filter: active ? 'drop-shadow(0 0 6px rgba(95,221,125,.5))' : 'none' }}>
|
||||
<Icon />
|
||||
{t.badge ? (
|
||||
<span style={{
|
||||
position: 'absolute', top: -4, right: -8, minWidth: 15, height: 15,
|
||||
fontSize: 9.5, fontWeight: 700, lineHeight: '15px', textAlign: 'center',
|
||||
background: 'var(--violet)', color: 'var(--bg-deep)', borderRadius: 8, padding: '0 3px',
|
||||
}}>{t.badge}</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span style={{ fontSize: 10, letterSpacing: '.12em' }}>{t.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { MaestroMobHeader: MobileHeader, MaestroMobTabBar: MobileTabBar });
|
||||
@@ -0,0 +1,198 @@
|
||||
// 移动版 · 四个 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 });
|
||||
@@ -0,0 +1,90 @@
|
||||
<!-- @dsCard group="Console" viewport="390x760" name="移动版调度台" subtitle="单栏 + 底部 Tab(任务/审批/事件/项目)· 触控目标 ≥44px · 可点击" -->
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MAESTRO · 移动版调度台</title>
|
||||
<link rel="stylesheet" href="../../styles.css">
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<script src="../../_ds_bundle.js"></script>
|
||||
<script src="../console/data.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body { background: var(--bg); color: var(--ink); font-family: var(--mono); font-size: 13px; line-height: 1.55; display: flex; justify-content: center; }
|
||||
body::before {
|
||||
content: ''; position: fixed; inset: 0; z-index: 999; pointer-events: none;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(0,0,0,.16) 0 1px, transparent 1px 3px),
|
||||
radial-gradient(ellipse at 50% 40%, transparent 55%, rgba(0,0,0,.5));
|
||||
opacity: .5;
|
||||
}
|
||||
::selection { background: var(--green-dim); color: #fff; }
|
||||
#root { width: 100%; max-width: 430px; height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="MobileChrome.jsx"></script>
|
||||
<script type="text/babel" src="MobileViews.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const { Toast } = window.MaestroDesignSystem_a6a290;
|
||||
const M = window.MAESTRO_MOCK;
|
||||
|
||||
function App() {
|
||||
const [tab, setTab] = React.useState('tasks');
|
||||
const [currentId, setCurrentId] = React.useState('p1');
|
||||
const [approvals, setApprovals] = React.useState(M.approvals);
|
||||
const [events, setEvents] = React.useState(M.events);
|
||||
const [toasts, setToasts] = React.useState([]);
|
||||
const project = M.projects.find((p) => p.id === currentId);
|
||||
const isMain = currentId === 'p1';
|
||||
const tasks = isMain ? M.tasks : [];
|
||||
const agents = isMain ? M.agents : [];
|
||||
const gates = isMain ? approvals : [];
|
||||
|
||||
const toast = (kind, text) => {
|
||||
const id = Date.now();
|
||||
setToasts((t) => [...t, { id, kind, text }]);
|
||||
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2600);
|
||||
};
|
||||
const now = () => new Date().toTimeString().slice(0, 8);
|
||||
const decide = (a, action, reason) => {
|
||||
setApprovals((list) => list.filter((x) => x.id !== a.id));
|
||||
if (action === 'accept') {
|
||||
setEvents((ev) => [{ type: 'approval.granted', time: now(), detail: a.title + ' · ' + a.gate + '_review 通过' }, ...ev]);
|
||||
toast('ok', '已通过 · ' + a.title);
|
||||
} else {
|
||||
setEvents((ev) => [{ type: 'approval.rejected', time: now(), detail: a.title + ' ↳ ' + reason }, ...ev]);
|
||||
toast('warn', '已驳回 · 退回重做');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', borderLeft: '1px solid var(--line-soft)', borderRight: '1px solid var(--line-soft)', background: 'var(--bg)' }}>
|
||||
<window.MaestroMobHeader project={project} counts={{ gate: gates.length, run: agents.length }} />
|
||||
<main style={{ flex: 1, overflowY: 'auto', paddingBottom: 16 }}>
|
||||
{tab === 'tasks' ? (
|
||||
tasks.length
|
||||
? <window.MaestroMobTasks tasks={tasks} agents={agents} />
|
||||
: <div style={{ margin: '24px 14px', padding: '38px 0', textAlign: 'center', color: 'var(--faint)', border: '1px dashed var(--line)', borderRadius: 6, fontSize: 12 }}>该项目暂无任务</div>
|
||||
) : null}
|
||||
{tab === 'gates' ? <window.MaestroMobGates approvals={gates} onDecide={decide} /> : null}
|
||||
{tab === 'events' ? <window.MaestroMobEvents events={events} /> : null}
|
||||
{tab === 'projects' ? <window.MaestroMobProjects projects={M.projects} currentId={currentId} project={project}
|
||||
onSelect={(id) => { setCurrentId(id); toast('ok', '已切换 · ' + M.projects.find((p) => p.id === id).name); }} /> : null}
|
||||
</main>
|
||||
<window.MaestroMobTabBar tab={tab} setTab={setTab} gateCount={gates.length} />
|
||||
<div style={{ position: 'fixed', bottom: 72, left: '50%', transform: 'translateX(-50%)', zIndex: 1200, display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center', width: 'max-content', maxWidth: '90vw' }}>
|
||||
{toasts.map((t) => <Toast key={t.id} kind={t.kind}>{t.text}</Toast>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user