diff --git a/media/console/app.js b/media/console/app.js index 46ea416..4fc2c1b 100644 --- a/media/console/app.js +++ b/media/console/app.js @@ -21,6 +21,7 @@ state.schemas = message.schemas || state.schemas; renderTasks(message.tasks || []); Console.refreshDrawer(); + Console.renderHealth(); // 多账户存储概要随 tasks 消息一起到达(probe 探测结果);store 数据保持独立状态 Console.renderMultiAccount(message.multiAccount || state.multiAccount, state.accountStore); break; @@ -31,6 +32,7 @@ Console.renderMultiAccount(message.multiAccount || state.multiAccount, state.accountStore); renderTasks(state.currentTasks); Console.refreshDrawer(); + Console.renderHealth(); break; case 'taskConfigs': state.taskConfigs = message.configs || {}; @@ -38,6 +40,7 @@ if (message.uiState) state.uiState = message.uiState; renderTasks(state.currentTasks); Console.refreshDrawer(); + Console.renderHealth(); break; // 配置接管:全局配置组 + 快照(配置分段数据源) case 'globalGroups': @@ -55,6 +58,7 @@ }; renderTasks(state.currentTasks); Console.refreshDrawer(); + Console.renderHealth(); } else if (message.target === 'global') { state.globalSnapshots[message.name] = message.values || {}; Console.renderConfig(state.globalGroups, state.globalSnapshots, state.expandedGlobalGroups || []); diff --git a/media/console/console.css b/media/console/console.css index 783e492..fa15be8 100644 --- a/media/console/console.css +++ b/media/console/console.css @@ -1098,6 +1098,229 @@ button.secondary:hover:not(:disabled) { background: var(--vscode-button-secondar .config-section-title--toggle { cursor: pointer; user-select: none; } .config-section-title__chev { display: inline-block; width: 11px; margin-right: 2px; color: var(--muted); } +/* ═══ 主从双栏 shell(侧栏导航 + 主内容) ═════════════════════ */ +.shell { display: flex; gap: 10px; align-items: stretch; } +.lay-side { + position: sticky; + top: 0; + align-self: flex-start; + display: flex; + flex-direction: column; + gap: 2px; + width: 158px; + flex: 0 0 auto; + max-height: 100vh; + padding: 6px 6px 6px 0; + border-right: 1px solid var(--border); + background: var(--vscode-editor-background); +} +.lay-side__item { + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 7px 12px; + border-radius: var(--radius-sm); + color: var(--muted); + font-size: 13px; + cursor: pointer; + user-select: none; +} +.lay-side__item:hover { background: var(--surface-hover); color: var(--vscode-editor-foreground); } +.lay-side__item.is-active { + background: var(--surface-hover); + color: var(--vscode-editor-foreground); + box-shadow: inset 2px 0 0 var(--border-strong); +} +.lay-side__ico { + width: 14px; height: 14px; + flex: 0 0 auto; + border-radius: 4px; + background: var(--border-strong); + opacity: .7; +} +.lay-side__item.is-active .lay-side__ico { opacity: 1; } +.lay-side__foot { margin-top: auto; padding: 10px 12px; font-size: 11px; color: var(--muted); } +.lay-main { flex: 1; min-width: 0; } + +/* ═══ 配置健康度条(任务分段顶部,真实快照数据) ══════════════ */ +.rc-health { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 18px; + margin: 0 0 10px; + padding: 9px 12px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); +} +.rc-health__head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px; flex: 1; min-width: 0; } +.rc-health__title { font-size: 12px; font-weight: 700; } +.rc-health__cells { display: flex; gap: 8px; flex: 1; min-width: 0; } +.rc-cell { flex: 1; min-width: 96px; padding: 6px 12px; border-radius: var(--radius-sm); background: var(--surface-hover); } +.rc-cell__label { font-size: 10px; color: var(--muted); } +.rc-cell__value { font-size: 16px; font-weight: 700; } +.rc-cell.is-ok .rc-cell__value { color: var(--ok); } +.rc-cell.is-warn .rc-cell__value { color: var(--warn); } + +/* ═══ 执行队列条(一次性队列可视化) ══════════════════════════ */ +.rc-queue { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; + margin: 0 0 10px; + padding: 8px 12px; + border: 1px dashed var(--border-strong); + border-radius: var(--radius-md); + background: var(--surface); +} +.rc-queue__head { display: flex; align-items: baseline; gap: 8px; } +.rc-queue__head b { font-size: 12px; } +.rc-queue__chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; flex: 1; min-width: 0; } +.rc-queue__chip { + padding: 3px 10px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface-hover); + font-size: 11px; +} + +/* ═══ 运行状态灯(任务卡标题行) ══════════════════════════════ */ +.rc-dot { + flex: 0 0 auto; + width: 8px; height: 8px; + border-radius: 50%; + background: color-mix(in srgb, var(--muted) 55%, transparent); +} +.task-card[data-run="running"] .rc-dot, +.task-card[data-run="polling"] .rc-dot { + background: var(--run); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--run) 18%, transparent); + animation: gbar-pulse 1.8s ease-in-out infinite; +} +.task-card[data-run="queued"] .rc-dot, +.task-card[data-run="enqueued"] .rc-dot { background: var(--warn); } +.task-card[data-run="armed"] .rc-dot { background: var(--ok); opacity: .75; } + +/* ═══ 任务卡悬停弹出:body 级固定定位(滚动容器裁剪免疫 · 毛玻璃 · 只读) ═══ */ +.task-card--pop:hover { border-color: color-mix(in srgb, var(--run) 40%, var(--border-strong)); } + +.gpop { + position: fixed; + z-index: 9999; + width: 250px; + max-height: min(58vh, 420px); + overflow-y: auto; + padding: 10px 12px; + border: 1px solid var(--border-strong); + border-radius: 10px; + background: color-mix(in srgb, var(--surface-raised, var(--surface)) 88%, transparent); + backdrop-filter: blur(18px) saturate(1.5); + -webkit-backdrop-filter: blur(18px) saturate(1.5); + box-shadow: 0 14px 38px color-mix(in srgb, #000 40%, transparent); + opacity: 0; + visibility: hidden; + transform: translateY(4px) scale(.98); + pointer-events: none; + transition: opacity .16s ease, transform .16s cubic-bezier(.2,.9,.3,1.2), visibility 0s linear .16s; +} +.gpop.is-visible { + opacity: 1; + visibility: visible; + transform: none; + pointer-events: auto; /* 内容超高时允许内部滚动 */ + transition: opacity .16s ease, transform .16s cubic-bezier(.2,.9,.3,1.2), visibility 0s; +} +/* 弹层内部滚动条(内容超高时全量翻阅) */ +.gpop::-webkit-scrollbar { width: 8px; } +.gpop::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--border-strong) 80%, transparent); + border-radius: 4px; +} +.gpop::-webkit-scrollbar-thumb:hover { background: var(--border-strong); } +.gpop::-webkit-scrollbar-track { background: transparent; } +/* 条件组(field.type.sub_configs 显隐组):与静态组一视同仁,蓝色系区分语义 */ +.gpop .grp-tag { + padding: 1px 7px; + border-radius: 999px; + font-size: 10px; + background: color-mix(in srgb, var(--vscode-charts-blue, #3794ff) 16%, transparent); + color: var(--vscode-charts-blue, #3794ff); +} +.gpop .it--rule { + border-left-color: var(--vscode-charts-blue, #3794ff); + background: color-mix(in srgb, var(--vscode-charts-blue, #3794ff) 10%, transparent); + font-weight: 600; +} +/* 条件组内的嵌套子组(条件组套条件组 / 静态组)保持节奏一致的间距 */ +.gpop .grp-body .grp { margin: 6px 0 2px; } +@keyframes popfade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } +.gpop .pop-title { + display: flex; align-items: baseline; justify-content: space-between; + margin-bottom: 2px; font-size: 12px; font-weight: 700; + animation: popfade .24s ease both; +} +.gpop .pop-sub { font-size: 10px; font-weight: 400; color: var(--muted); } +.gpop .grp { margin: 8px 0; animation: popfade .24s ease both; } +.gpop .grp-head { + display: flex; align-items: center; justify-content: space-between; + padding: 0 2px 2px; + font-size: 10px; letter-spacing: .08em; color: var(--muted); +} +.gpop .grp-head b { font-size: 12px; font-weight: 600; letter-spacing: 0; color: var(--vscode-editor-foreground); } +.gpop .grp-head .cnt { + padding: 1px 8px; border-radius: 999px; + background: color-mix(in srgb, var(--run) 14%, transparent); + color: var(--run); font-size: 10px; +} +.gpop .grp-body { position: relative; margin: 2px 0 0 8px; padding-left: 12px; } +.gpop .grp-body::before { + content: ''; + position: absolute; + top: 2px; bottom: 6px; left: 0; + width: 2px; border-radius: 1px; + background: linear-gradient(180deg, var(--border-strong), transparent); +} +.gpop .it { + position: relative; + display: flex; align-items: center; + margin: 4px 0; padding: 4px 9px; + border-left: 2px solid var(--border-strong); + border-radius: 6px; + background: color-mix(in srgb, var(--surface-hover) 72%, transparent); + font-size: 12px; overflow: hidden; +} +.gpop .it::before { + content: ''; + position: absolute; + top: 50%; left: -12px; + width: 10px; height: 1px; + background: var(--border-strong); opacity: .7; +} +.gpop .it > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* 拍平行(嵌套到 cap 后第 3 层起):字段名 + 条件摘要徽标换行平铺,蓝系对齐条件组语义。 + 特异性必须压过上方 .gpop .it > span 的 nowrap/ellipsis,否则长徽标被裁成一行 */ +.gpop .it--flat { flex-wrap: wrap; row-gap: 1px; } +.gpop .it--flat > .it__cond { + flex: 1 1 100%; + min-width: 0; + display: block; + font-size: 10px; + line-height: 1.4; + max-height: 2.8em; /* 最长两条规则摘要封顶两行 */ + color: var(--vscode-charts-blue, #3794ff); + white-space: normal; + word-break: break-all; + overflow: hidden; +} + +/* 分段切换内容淡入重放(弹出层规范配套:切 Tab 后 1.2s 抑制弹出 + 内容重放淡入) */ +.page.is-entering { animation: pagefade .26s ease both; } +@keyframes pagefade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } + + /* hidden 属性兜底:作者样式里的 display(.config-fields 的 grid、卡片 body 的 flex 等) 会覆盖 UA 的 [hidden]{display:none},导致所有基于 hidden 的折叠/显隐在真机上失效 diff --git a/media/console/console.js b/media/console/console.js index 5c544cb..84ee169 100644 --- a/media/console/console.js +++ b/media/console/console.js @@ -16,15 +16,24 @@ // ── 分段切换 ───────────────────────────────────────────────────────── function switchSeg(seg) { - for (const button of document.querySelectorAll('.seg__btn')) { - const active = button.dataset.seg === seg; - button.classList.toggle('is-active', active); - button.setAttribute('aria-selected', active ? 'true' : 'false'); + // 弹出层规范:切 Tab 后 1.2s 内抑制悬停弹出 + 目标页内容淡入重放 + globalThis.TaskLauncherTaskCard?.suppressPopups?.(); + for (const item of document.querySelectorAll('.lay-side__item')) { + const active = item.dataset.seg === seg; + item.classList.toggle('is-active', active); + item.setAttribute('aria-selected', active ? 'true' : 'false'); } $('pageTasks').hidden = seg !== 'tasks'; $('pageGame').hidden = seg !== 'game'; $('pageConfig').hidden = seg !== 'config'; - $('pageAccounts').hidden = seg !== 'accounts'; } + $('pageAccounts').hidden = seg !== 'accounts'; + const target = document.getElementById(`page${seg.charAt(0).toUpperCase()}${seg.slice(1)}`); + if (target) { + target.classList.remove('is-entering'); + void target.offsetWidth; // 强制 reflow,保证动画每次都重放 + target.classList.add('is-entering'); + } + } // ── 配置分段:全局配置组卡片(复用 configPanel,伪 task = __global__::组名) ── @@ -43,6 +52,10 @@ } function renderConfig(groups, snapshots, expanded) { + // 折叠切换/快照刷新会重建全部配置卡:旧卡被移除后悬停弹层成了幽灵,先收掉。 + // 不用 suppressPopups——那会上 1.2s 抑制窗口,初始加载时 globalGroups 先于 + // tasks 到达会把任务卡的悬停弹出一起闷掉(CDP 场景实测踩中)。 + globalThis.TaskLauncherTaskCard?.hideHoverPop?.(true); state.globalGroups = groups || []; state.globalSnapshots = snapshots || {}; state.expandedGlobalGroups = Array.isArray(expanded) ? expanded : []; @@ -55,11 +68,62 @@ empty.className = 'config-empty'; empty.textContent = t('noConfigParameters'); host.appendChild(empty); + renderHealth(); return; } for (const group of state.globalGroups) { host.appendChild(buildGlobalCard(group, state.expandedGlobalGroups.includes(group.name))); } + renderHealth(); + } + + /** 配置健康度条:快照组 / 快照字段 / 已定制任务(全部来自既有真实数据) */ + function renderHealth() { + const strip = $('healthStrip'); + if (!strip) return; + const groups = state.globalGroups || []; + if (!groups.length) { + strip.hidden = true; + strip.textContent = ''; + return; + } + let fieldCount = 0; + for (const group of groups) fieldCount += (group.fields || []).length; + // 已定制任务按数据计数(state.currentTasks + 快照≠出厂),不依赖卡片 DOM—— + // 搜索过滤/分组折叠会改变可见卡片数,DOM 计数会跟着漂移(CodeRabbit review) + const listed = (state.currentTasks || []) + .filter((task) => state.schemas[taskKey(task)]?.showInTaskTab !== false); + const owned = listed + .filter((task) => globalThis.TaskLauncherTaskCard.snapshotDiffersFromFactory(taskKey(task))) + .length; + strip.textContent = ''; + const head = document.createElement('div'); + head.className = 'rc-health__head'; + const title = document.createElement('span'); + title.className = 'rc-health__title'; + title.textContent = t('healthTitle'); + const cells = document.createElement('div'); + cells.className = 'rc-health__cells'; + const defs = [ + ['healthGroups', groups.length, 'ok'], + ['healthFields', fieldCount, 'ok'], + ['healthOwned', owned, owned > 0 ? 'warn' : 'ok'], + ]; + for (const [key, value, tone] of defs) { + const cell = document.createElement('div'); + cell.className = `rc-cell is-${tone}`; + const label = document.createElement('div'); + label.className = 'rc-cell__label'; + label.textContent = t(key); + const num = document.createElement('div'); + num.className = 'rc-cell__value'; + num.textContent = String(value); + cell.append(label, num); + cells.appendChild(cell); + } + head.append(title, cells); + strip.append(head); + strip.hidden = false; } function buildGlobalCard(group, expanded) { @@ -131,6 +195,9 @@ } card.append(head, body); + // 悬停参数概要弹层(与任务卡同款):全局组字段与任务 schema 同构,直接复用 + // groupPopContent —— 条件链按「分组吸收显隐」归属,深层拍平徽标。 + globalThis.TaskLauncherTaskCard?.bindHoverPop?.(card, fakeTask, fakeSchema); return card; } @@ -638,15 +705,25 @@ function init() { document.title = t('consoleTitle'); - // 分段导航 - $('segTasks').textContent = t('consoleTabTasks'); - $('segGame').textContent = t('consoleTabGame'); - $('segConfig').textContent = t('consoleTabConfig'); - $('segAccounts').textContent = t('consoleTabAccounts'); - $('segTasks').addEventListener('click', () => switchSeg('tasks')); - $('segGame').addEventListener('click', () => switchSeg('game')); - $('segConfig').addEventListener('click', () => switchSeg('config')); - $('segAccounts').addEventListener('click', () => switchSeg('accounts')); + // 侧栏导航(主从双栏):文案 + 点击/键盘切换 + const sideTabs = [ + ['segTasks', 'consoleTabTasks'], + ['segGame', 'consoleTabGame'], + ['segConfig', 'consoleTabConfig'], + ['segAccounts', 'consoleTabAccounts'], + ]; + for (const [id, key] of sideTabs) { + const item = $(id); + item.querySelector('.lay-side__label').textContent = t(key); + item.addEventListener('click', () => switchSeg(item.dataset.seg)); + item.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + switchSeg(item.dataset.seg); + } + }); + } + $('sideFoot').textContent = t('consoleTitle'); // 任务分段 const search = $('taskSearch'); @@ -787,6 +864,7 @@ closeDrawer, refreshDrawer, renderConfig, + renderHealth, renderMultiAccount, }; })(); diff --git a/media/console/index.html b/media/console/index.html index d9cb3a8..30be6c2 100644 --- a/media/console/index.html +++ b/media/console/index.html @@ -9,6 +9,26 @@