From c8f9d8444048ce5ba92b5ab9767d72f16a2ec3bb Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 24 Sep 2026 01:05:27 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(console):=20=E4=B8=BB=E4=BB=8E?= =?UTF-8?q?=E5=8F=8C=E6=A0=8F=E5=B8=83=E5=B1=80=20+=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=81=A5=E5=BA=B7=E5=BA=A6=E6=9D=A1=20+=20=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E9=98=9F=E5=88=97=E6=9D=A1=20+=20=E4=BB=BB=E5=8A=A1=E5=8D=A1?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=81=AF/=E5=8F=82=E6=95=B0=E5=88=86?= =?UTF-8?q?=E7=BB=84=E6=82=AC=E5=81=9C=E5=BC=B9=E5=87=BA=EF=BC=88=E5=85=AD?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- media/console/app.js | 5 + media/console/console.css | 188 ++++++++++++++++++++++++++++++++++++++ media/console/console.js | 82 ++++++++++++++--- media/console/index.html | 35 +++++-- media/console/taskCard.js | 95 ++++++++++++++++++- src/localization.ts | 42 +++++++++ 6 files changed, 421 insertions(+), 26 deletions(-) diff --git a/media/console/app.js b/media/console/app.js index 46ea416..d852a23 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,8 @@ }; renderTasks(state.currentTasks); Console.refreshDrawer(); + Console.renderHealth(); + 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..838157b 100644 --- a/media/console/console.css +++ b/media/console/console.css @@ -1098,6 +1098,194 @@ 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; } + +/* ═══ 任务卡悬停弹出:参数分组概要(毛玻璃 · 只读) ═══════════ */ +.task-card--pop { position: relative; overflow: visible; } +.task-card--pop .pop { + position: absolute; + top: -8px; + left: calc(100% + 12px); + z-index: 80; + width: 250px; + padding: 10px 12px; + border: 1px solid var(--border-strong); + border-radius: 10px; + background: color-mix(in srgb, var(--surface-raised, var(--surface)) 82%, 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; + transform: translateX(-8px) scale(.97); + pointer-events: none; + transition: opacity .18s ease, transform .18s cubic-bezier(.2,.9,.3,1.2); + transition-delay: 0s; +} +.task-card--pop:hover .pop { opacity: 1; transform: translateX(0) scale(1); transition-delay: .22s; } +.task-card--pop .pop::before { + content: ''; + position: absolute; + top: 20px; left: -6px; + width: 10px; height: 10px; + transform: rotate(45deg); + background: var(--surface-raised, var(--surface)); + border-bottom: 1px solid var(--border-strong); + border-right: 1px solid var(--border-strong); +} +@keyframes popfade { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } } +.task-card--pop .pop-title, .task-card--pop .grp { animation: popfade .24s ease both; } +.task-card--pop .pop-title { + display: flex; align-items: baseline; justify-content: space-between; + margin-bottom: 2px; font-size: 12px; font-weight: 700; +} +.task-card--pop .pop-sub { font-size: 10px; font-weight: 400; color: var(--muted); } +.task-card--pop .grp { margin: 8px 0; } +.task-card--pop .grp-head { + display: flex; align-items: center; justify-content: space-between; + padding: 0 2px 2px; + font-size: 10px; letter-spacing: .08em; color: var(--muted); +} +.task-card--pop .grp-head b { font-size: 12px; font-weight: 600; letter-spacing: 0; color: var(--vscode-editor-foreground); } +.task-card--pop .grp-head .cnt { + padding: 1px 8px; border-radius: 999px; + background: color-mix(in srgb, var(--run) 14%, transparent); + color: var(--run); font-size: 10px; +} +.task-card--pop .grp-body { position: relative; margin: 2px 0 0 8px; padding-left: 12px; } +.task-card--pop .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); +} +.task-card--pop .it { + position: relative; + display: flex; align-items: center; justify-content: space-between; gap: 8px; + 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; +} +.task-card--pop .it::before { + content: ''; + position: absolute; + top: 50%; left: -12px; + width: 10px; height: 1px; + background: var(--border-strong); opacity: .7; +} +.task-card--pop .it > span:first-child { + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.task-card--pop .it .st { padding: 1px 7px; border-radius: 999px; font-size: 10px; white-space: nowrap; } +.task-card--pop .it .st.ok { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); } + + /* 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..952748b 100644 --- a/media/console/console.js +++ b/media/console/console.js @@ -16,10 +16,10 @@ // ── 分段切换 ───────────────────────────────────────────────────────── 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'); + 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'; @@ -55,11 +55,56 @@ 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; + const owned = document.querySelectorAll('[data-role="config-toggle"].has-overrides').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) { @@ -638,15 +683,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 +842,7 @@ closeDrawer, refreshDrawer, renderConfig, + renderHealth, renderMultiAccount, }; })(); diff --git a/media/console/index.html b/media/console/index.html index d9cb3a8..c6085f2 100644 --- a/media/console/index.html +++ b/media/console/index.html @@ -9,6 +9,26 @@
+
+ + + + +
@@ -38,16 +58,9 @@ - - - - +
+
+ +
@@ -117,6 +132,8 @@
+
+
diff --git a/media/console/taskCard.js b/media/console/taskCard.js index 4e66ba5..8da1777 100644 --- a/media/console/taskCard.js +++ b/media/console/taskCard.js @@ -126,6 +126,52 @@ return [sync, reset]; } + /** 悬停弹出:参数表单分组概要(数据与 configPanel 同源:configGroups / groupLabels) */ + function buildGroupPop(task, schema) { + const groups = schema?.configGroups && typeof schema.configGroups === 'object' + ? Object.entries(schema.configGroups) : []; + if (!groups.length) return null; + const pop = document.createElement('div'); + pop.className = 'pop'; + const title = document.createElement('div'); + title.className = 'pop-title'; + title.textContent = schema?.displayName || task.displayName || ''; + const sub = document.createElement('span'); + sub.className = 'pop-sub'; + sub.textContent = t('depsPopSub', { count: groups.length }); + title.appendChild(sub); + pop.appendChild(title); + for (const [key, fieldKeys] of groups) { + const list = Array.isArray(fieldKeys) ? fieldKeys : []; + const grp = document.createElement('div'); + grp.className = 'grp'; + const head = document.createElement('div'); + head.className = 'grp-head'; + const name = document.createElement('b'); + name.textContent = schema.groupLabels?.[key] || key; + const cnt = document.createElement('span'); + cnt.className = 'cnt'; + cnt.textContent = t('itemsCount', { count: list.length }); + head.append(name, cnt); + const body = document.createElement('div'); + body.className = 'grp-body'; + for (const fieldKey of list.slice(0, 3)) { + const it = document.createElement('div'); + it.className = 'it'; + const label = document.createElement('span'); + label.textContent = fieldKey; + const st = document.createElement('span'); + st.className = 'st ok'; + st.textContent = t('depsPopTaken'); + it.append(label, st); + body.appendChild(it); + } + grp.append(head, body); + pop.appendChild(grp); + } + return pop; + } + function buildTaskCard(task) { const key = taskKey(task); const schema = state.schemas[key]; @@ -143,6 +189,10 @@ identity.className = 'task-card__identity'; const titleRow = document.createElement('div'); titleRow.className = 'task-card__title-row'; + const dot = document.createElement('span'); + dot.className = 'rc-dot'; + dot.setAttribute('aria-hidden', 'true'); + titleRow.appendChild(dot); const name = document.createElement('div'); name.className = 'task-card__name'; name.textContent = displayName; @@ -175,6 +225,11 @@ // 点击「参数」按钮时由 console.js 把这个节点搬运进抽屉显示。 const configPanel = buildConfigPanel(task, schema); card.append(header, configPanel); + const pop = buildGroupPop(task, schema); + if (pop) { + card.classList.add('task-card--pop'); + card.appendChild(pop); + } return card; } @@ -203,6 +258,7 @@ function updateRunningState() { for (const card of elements.tasks.querySelectorAll('.task-card')) { const status = cardState(card); + card.dataset.run = status || 'idle'; card.classList.toggle('is-active', status === 'polling' || status === 'running'); const badge = card.querySelector('[data-role="status"]'); if (badge) { @@ -226,6 +282,39 @@ updateToolbar(); } + /** 任务显示名:tasks 列表 → schema → 原始 key 兜底 */ + function displayNameOf(key) { + const task = state.currentTasks.find((item) => taskKey(item) === key); + return task?.displayName || state.schemas[key]?.displayName || key; + } + + /** 执行队列条:一次性队列可视化(队列芯片随 executor 消息刷新) */ + function renderQueueStrip() { + const strip = document.getElementById('queueStrip'); + if (!strip) return; + const queue = state.executor.onetimeQueue || []; + strip.textContent = ''; + if (!queue.length) { + strip.hidden = true; + return; + } + const head = document.createElement('div'); + head.className = 'rc-queue__head'; + const title = document.createElement('b'); + title.textContent = t('queueTitle'); + head.appendChild(title); + const chips = document.createElement('div'); + chips.className = 'rc-queue__chips'; + for (const key of queue) { + const chip = document.createElement('span'); + chip.className = 'rc-queue__chip'; + chip.textContent = displayNameOf(key); + chips.appendChild(chip); + } + strip.append(head, chips); + strip.hidden = false; + } + /** 状态条第二行:当前任务 + 一次性队列(空闲时隐藏) */ function updateExecutorSub() { const sub = document.getElementById('executorSub'); @@ -233,16 +322,14 @@ const executor = state.executor; const parts = []; if (executor.current) { - const task = state.currentTasks.find((item) => taskKey(item) === executor.current); - const schema = state.schemas[executor.current]; - const label = task?.displayName || schema?.displayName || executor.current; - parts.push(t('executorCurrent', { task: label })); + parts.push(t('executorCurrent', { task: displayNameOf(executor.current) })); } if (executor.onetimeQueue.length) { parts.push(t('executorQueueCount', { count: executor.onetimeQueue.length })); } sub.hidden = !parts.length; sub.textContent = parts.join(' · '); + renderQueueStrip(); } function updateToolbar() { diff --git a/src/localization.ts b/src/localization.ts index 046465e..2fe4206 100644 --- a/src/localization.ts +++ b/src/localization.ts @@ -242,6 +242,13 @@ const ZH_CN: WebviewStrings = { diffTag: '与出厂不同', runningCount: '● {count} 运行中', projectStoreTag: '项目自建 store', + healthTitle: '配置健康度', + healthGroups: '快照组', + healthFields: '快照字段', + healthOwned: '已定制任务', + queueTitle: '执行队列', + depsPopSub: '参数分 {count} 组', + depsPopTaken: '已接管', jsonEditBtn: 'JSON 编辑', openFileBtn: '打开文件', openDataBtn: '打开数据位置', @@ -432,6 +439,13 @@ const EN: WebviewStrings = { diffTag: 'differs from factory', runningCount: '● {count} running', projectStoreTag: 'project store', + healthTitle: 'Config health', + healthGroups: 'Snapshot groups', + healthFields: 'Snapshot fields', + healthOwned: 'Customized tasks', + queueTitle: 'Execution queue', + depsPopSub: '{count} config group(s)', + depsPopTaken: 'Owned', jsonEditBtn: 'JSON edit', openFileBtn: 'Open file', openDataBtn: 'Open data location', @@ -483,6 +497,13 @@ const ZH_TW: WebviewStrings = { diffTag: '與出廠不同', runningCount: '● {count} 執行中', projectStoreTag: '專案自建 store', + healthTitle: '設定健康度', + healthGroups: '快照組', + healthFields: '快照欄位', + healthOwned: '已客製任務', + queueTitle: '執行佇列', + depsPopSub: '參數分 {count} 組', + depsPopTaken: '已接管', jsonEditBtn: 'JSON 編輯', openFileBtn: '開啟檔案', openDataBtn: '開啟資料位置', @@ -556,6 +577,13 @@ const JA: WebviewStrings = { diffTag: '初期値と差異あり', runningCount: '● {count} 実行中', projectStoreTag: 'プロジェクト store', + healthTitle: '設定ヘルス', + healthGroups: 'スナップショット組', + healthFields: 'スナップショット項目', + healthOwned: 'カスタマイズ済みタスク', + queueTitle: '実行キュー', + depsPopSub: 'パラメータ {count} グループ', + depsPopTaken: '引き継ぎ済み', jsonEditBtn: 'JSON 編集', openFileBtn: 'ファイルを開く', openDataBtn: 'データ場所を開く', @@ -628,6 +656,13 @@ const KO: WebviewStrings = { diffTag: '출고값과 차이', runningCount: '● {count} 실행 중', projectStoreTag: '프로젝트 store', + healthTitle: '설정 상태', + healthGroups: '스냅샷 그룹', + healthFields: '스냅샷 필드', + healthOwned: '커스텀 작업', + queueTitle: '실행 대기열', + depsPopSub: '파라미터 {count}개 그룹', + depsPopTaken: '관리됨', jsonEditBtn: 'JSON 편집', openFileBtn: '파일 열기', openDataBtn: '데이터 위치 열기', @@ -700,6 +735,13 @@ const ES: WebviewStrings = { diffTag: 'difiere de fábrica', runningCount: '● {count} en ejecución', projectStoreTag: 'store del proyecto', + healthTitle: 'Estado de la configuración', + healthGroups: 'Grupos de snapshot', + healthFields: 'Campos de snapshot', + healthOwned: 'Tareas personalizadas', + queueTitle: 'Cola de ejecución', + depsPopSub: '{count} grupo(s) de configuración', + depsPopTaken: 'Gestionado', jsonEditBtn: 'Editar JSON', openFileBtn: 'Abrir archivo', openDataBtn: 'Abrir ubicación de datos', From 198b93e0838271eadb7cc2793cdc48cc6b210baa Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 24 Sep 2026 01:25:34 +0800 Subject: [PATCH 2/7] fix(console): hover pop -> body-level fixed singleton (scroll-clip immune), seg switch 1.2s suppress + fade replay --- media/console/console.css | 77 ++++++++++++++++----------------- media/console/console.js | 11 ++++- media/console/taskCard.js | 91 +++++++++++++++++++++++++++++++-------- 3 files changed, 121 insertions(+), 58 deletions(-) diff --git a/media/console/console.css b/media/console/console.css index 838157b..a1be515 100644 --- a/media/console/console.css +++ b/media/console/console.css @@ -1204,86 +1204,83 @@ button.secondary:hover:not(:disabled) { background: var(--vscode-button-secondar .task-card[data-run="enqueued"] .rc-dot { background: var(--warn); } .task-card[data-run="armed"] .rc-dot { background: var(--ok); opacity: .75; } -/* ═══ 任务卡悬停弹出:参数分组概要(毛玻璃 · 只读) ═══════════ */ -.task-card--pop { position: relative; overflow: visible; } -.task-card--pop .pop { - position: absolute; - top: -8px; - left: calc(100% + 12px); - z-index: 80; +/* ═══ 任务卡悬停弹出: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)) 82%, transparent); + 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; - transform: translateX(-8px) scale(.97); + visibility: hidden; + transform: translateY(4px) scale(.98); pointer-events: none; - transition: opacity .18s ease, transform .18s cubic-bezier(.2,.9,.3,1.2); - transition-delay: 0s; + transition: opacity .16s ease, transform .16s cubic-bezier(.2,.9,.3,1.2), visibility 0s linear .16s; } -.task-card--pop:hover .pop { opacity: 1; transform: translateX(0) scale(1); transition-delay: .22s; } -.task-card--pop .pop::before { - content: ''; - position: absolute; - top: 20px; left: -6px; - width: 10px; height: 10px; - transform: rotate(45deg); - background: var(--surface-raised, var(--surface)); - border-bottom: 1px solid var(--border-strong); - border-right: 1px solid var(--border-strong); -} -@keyframes popfade { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } } -.task-card--pop .pop-title, .task-card--pop .grp { animation: popfade .24s ease both; } -.task-card--pop .pop-title { +.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; +} +@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; } -.task-card--pop .pop-sub { font-size: 10px; font-weight: 400; color: var(--muted); } -.task-card--pop .grp { margin: 8px 0; } -.task-card--pop .grp-head { +.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); } -.task-card--pop .grp-head b { font-size: 12px; font-weight: 600; letter-spacing: 0; color: var(--vscode-editor-foreground); } -.task-card--pop .grp-head .cnt { +.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; } -.task-card--pop .grp-body { position: relative; margin: 2px 0 0 8px; padding-left: 12px; } -.task-card--pop .grp-body::before { +.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); } -.task-card--pop .it { +.gpop .it { position: relative; - display: flex; align-items: center; justify-content: space-between; gap: 8px; + 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; } -.task-card--pop .it::before { +.gpop .it::before { content: ''; position: absolute; top: 50%; left: -12px; width: 10px; height: 1px; background: var(--border-strong); opacity: .7; } -.task-card--pop .it > span:first-child { - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; -} -.task-card--pop .it .st { padding: 1px 7px; border-radius: 999px; font-size: 10px; white-space: nowrap; } -.task-card--pop .it .st.ok { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); } +.gpop .it > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* 分段切换内容淡入重放(弹出层规范配套:切 Tab 后 1.2s 抑制弹出 + 内容重放淡入) */ +.page.is-entering { animation: pagefade .26s ease both; } +@keyframes pagefade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } diff --git a/media/console/console.js b/media/console/console.js index 952748b..2f9c5ba 100644 --- a/media/console/console.js +++ b/media/console/console.js @@ -16,6 +16,8 @@ // ── 分段切换 ───────────────────────────────────────────────────────── function switchSeg(seg) { + // 弹出层规范:切 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); @@ -24,7 +26,14 @@ $('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__::组名) ── diff --git a/media/console/taskCard.js b/media/console/taskCard.js index 8da1777..942a11c 100644 --- a/media/console/taskCard.js +++ b/media/console/taskCard.js @@ -126,13 +126,22 @@ return [sync, reset]; } - /** 悬停弹出:参数表单分组概要(数据与 configPanel 同源:configGroups / groupLabels) */ - function buildGroupPop(task, schema) { + /* ── 悬停弹出(body 级单例 + fixed 定位)────────────────────────── + 卡内 absolute + left:calc(100%+12px) 会被任务列表滚动容器的 + overflow 裁掉(lab 里 B/C 布局同款翻车),挂到 body 上用 + getBoundingClientRect + 视口夹紧定位,右缘对齐卡片、优先下方、 + 底部放不下自动翻上方。只读无按钮;内容与 configPanel 同源。 */ + let popEl = null; + let popHideTimer = 0; + let popSuppressUntil = 0; + + /** 弹层内容:参数分组概要(configGroups / groupLabels / displayKey) */ + function groupPopContent(task, schema) { const groups = schema?.configGroups && typeof schema.configGroups === 'object' ? Object.entries(schema.configGroups) : []; if (!groups.length) return null; - const pop = document.createElement('div'); - pop.className = 'pop'; + const labels = Object.fromEntries((schema?.fields || []).map((f) => [f.key, f.displayKey || f.key])); + const frag = document.createDocumentFragment(); const title = document.createElement('div'); title.className = 'pop-title'; title.textContent = schema?.displayName || task.displayName || ''; @@ -140,7 +149,7 @@ sub.className = 'pop-sub'; sub.textContent = t('depsPopSub', { count: groups.length }); title.appendChild(sub); - pop.appendChild(title); + frag.appendChild(title); for (const [key, fieldKeys] of groups) { const list = Array.isArray(fieldKeys) ? fieldKeys : []; const grp = document.createElement('div'); @@ -155,23 +164,69 @@ head.append(name, cnt); const body = document.createElement('div'); body.className = 'grp-body'; - for (const fieldKey of list.slice(0, 3)) { + for (const fieldKey of list.slice(0, 4)) { const it = document.createElement('div'); it.className = 'it'; const label = document.createElement('span'); - label.textContent = fieldKey; - const st = document.createElement('span'); - st.className = 'st ok'; - st.textContent = t('depsPopTaken'); - it.append(label, st); + label.textContent = labels[fieldKey] || fieldKey; + it.appendChild(label); body.appendChild(it); } grp.append(head, body); - pop.appendChild(grp); + frag.appendChild(grp); } - return pop; + return frag; } + function hideHoverPop(immediate = false) { + window.clearTimeout(popHideTimer); + if (!popEl) return; + if (immediate) { + popEl.classList.remove('is-visible'); + return; + } + popHideTimer = window.setTimeout(() => popEl?.classList.remove('is-visible'), 120); + } + + function showHoverPop(card, task, schema) { + if (Date.now() < popSuppressUntil) return; + const content = groupPopContent(task, schema); + if (!content) return; + if (!popEl) { + popEl = document.createElement('div'); + popEl.className = 'gpop'; + popEl.setAttribute('role', 'tooltip'); + popEl.addEventListener('mouseenter', () => window.clearTimeout(popHideTimer)); + popEl.addEventListener('mouseleave', () => hideHoverPop()); + document.body.appendChild(popEl); + } + popEl.replaceChildren(content); + popEl.classList.remove('is-visible'); // 先归零再测量,避免位置跳变闪烁 + const rect = card.getBoundingClientRect(); + const vw = window.innerWidth; + const vh = window.innerHeight; + const pw = popEl.offsetWidth; + const ph = popEl.offsetHeight; + let left = rect.right - pw - 10; // 右缘对齐卡片右缘 + left = Math.max(8, Math.min(left, vw - pw - 8)); + let top = rect.bottom + 8; // 优先放下方 + if (top + ph > vh - 8) top = rect.top - ph - 8; // 底部放不下 → 翻上方 + top = Math.max(8, Math.min(top, Math.max(8, vh - ph - 8))); + popEl.style.left = `${Math.round(left)}px`; + popEl.style.top = `${Math.round(top)}px`; + popEl.classList.add('is-visible'); + } + + /** 切分段后短暂抑制弹出(弹出层规范:点 Tab 后 1.2s 内不弹 + 内容淡入重放) */ + function suppressPopups() { + popSuppressUntil = Date.now() + 1200; + hideHoverPop(true); + } + + // 列表滚动 / 窗口缩放时弹层立刻收起,避免钉在旧位置 + window.addEventListener('scroll', () => hideHoverPop(true), { capture: true, passive: true }); + window.addEventListener('resize', () => hideHoverPop(true)); + function buildTaskCard(task) { const key = taskKey(task); const schema = state.schemas[key]; @@ -225,10 +280,11 @@ // 点击「参数」按钮时由 console.js 把这个节点搬运进抽屉显示。 const configPanel = buildConfigPanel(task, schema); card.append(header, configPanel); - const pop = buildGroupPop(task, schema); - if (pop) { + // 有参数分组才挂悬停弹出(内容按需构建,body 级单例见 showHoverPop) + if (schema?.configGroups && Object.keys(schema.configGroups).length) { card.classList.add('task-card--pop'); - card.appendChild(pop); + card.addEventListener('mouseenter', () => showHoverPop(card, task, schema)); + card.addEventListener('mouseleave', () => hideHoverPop()); } return card; } @@ -464,6 +520,7 @@ function renderTasks(tasks) { state.currentTasks = tasks; + hideHoverPop(true); // 重渲染后旧弹层位置失效,直接收起 // show_in_task_tab=False 的任务不进列表(框架原生不消费,插件按隐藏对待) const listed = tasks.filter((task) => state.schemas[taskKey(task)]?.showInTaskTab !== false); const visible = listed.filter(matches); @@ -483,5 +540,5 @@ renderTasks(state.currentTasks); } - globalThis.TaskLauncherTaskCard = { renderTasks, updateRunningState, setSearch, toggleGroup }; + globalThis.TaskLauncherTaskCard = { renderTasks, updateRunningState, setSearch, toggleGroup, suppressPopups }; })(); From 220ae27e0d8ee272205cf4640a531727769e5b45 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 24 Sep 2026 01:57:25 +0800 Subject: [PATCH 3/7] fix(console): clear pending pop hide timer on show - fast card-to-card hover killed new popup after ~120ms --- media/console/taskCard.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/media/console/taskCard.js b/media/console/taskCard.js index 942a11c..75663a1 100644 --- a/media/console/taskCard.js +++ b/media/console/taskCard.js @@ -189,6 +189,8 @@ } function showHoverPop(card, task, schema) { + window.clearTimeout(popHideTimer); // 关键:撤掉上一张卡 mouseleave 挂起的隐藏定时器, + // 否则快速移到相邻卡片时,新弹层显示 ~40ms 后被旧定时器藏掉(真机复现过) if (Date.now() < popSuppressUntil) return; const content = groupPopContent(task, schema); if (!content) return; From 32235bf4fecf6793518bcbf7527d8e361a4be2b7 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 24 Sep 2026 02:27:39 +0800 Subject: [PATCH 4/7] fix(console): hover pop fallback for flat configs - tasks without configGroups collapse fields into a single pseudo-group instead of not popping --- media/console/taskCard.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/media/console/taskCard.js b/media/console/taskCard.js index 75663a1..cec0e07 100644 --- a/media/console/taskCard.js +++ b/media/console/taskCard.js @@ -135,36 +135,46 @@ let popHideTimer = 0; let popSuppressUntil = 0; - /** 弹层内容:参数分组概要(configGroups / groupLabels / displayKey) */ + /** + * 弹层内容:参数分组概要(configGroups / groupLabels / displayKey)。 + * 有分组层次 → 按组展示;没层次但有字段 → 折叠成单个「参数」伪组列扁平字段; + * 两者皆无(连字段都没有)→ 不弹。 + */ function groupPopContent(task, schema) { - const groups = schema?.configGroups && typeof schema.configGroups === 'object' + const fields = schema?.fields || []; + const groupEntries = schema?.configGroups && typeof schema.configGroups === 'object' ? Object.entries(schema.configGroups) : []; - if (!groups.length) return null; - const labels = Object.fromEntries((schema?.fields || []).map((f) => [f.key, f.displayKey || f.key])); + const labels = Object.fromEntries(fields.map((f) => [f.key, f.displayKey || f.key])); + const entries = groupEntries.length + ? groupEntries + : (fields.length ? [[null, fields.map((f) => f.key)]] : []); + if (!entries.length) return null; const frag = document.createDocumentFragment(); const title = document.createElement('div'); title.className = 'pop-title'; title.textContent = schema?.displayName || task.displayName || ''; const sub = document.createElement('span'); sub.className = 'pop-sub'; - sub.textContent = t('depsPopSub', { count: groups.length }); + sub.textContent = groupEntries.length + ? t('depsPopSub', { count: groupEntries.length }) + : t('itemsCount', { count: fields.length }); title.appendChild(sub); frag.appendChild(title); - for (const [key, fieldKeys] of groups) { + for (const [key, fieldKeys] of entries) { const list = Array.isArray(fieldKeys) ? fieldKeys : []; const grp = document.createElement('div'); grp.className = 'grp'; const head = document.createElement('div'); head.className = 'grp-head'; const name = document.createElement('b'); - name.textContent = schema.groupLabels?.[key] || key; + name.textContent = key === null ? t('parameters') : (schema.groupLabels?.[key] || key); const cnt = document.createElement('span'); cnt.className = 'cnt'; cnt.textContent = t('itemsCount', { count: list.length }); head.append(name, cnt); const body = document.createElement('div'); body.className = 'grp-body'; - for (const fieldKey of list.slice(0, 4)) { + for (const fieldKey of list.slice(0, key === null ? 6 : 4)) { const it = document.createElement('div'); it.className = 'it'; const label = document.createElement('span'); @@ -282,8 +292,8 @@ // 点击「参数」按钮时由 console.js 把这个节点搬运进抽屉显示。 const configPanel = buildConfigPanel(task, schema); card.append(header, configPanel); - // 有参数分组才挂悬停弹出(内容按需构建,body 级单例见 showHoverPop) - if (schema?.configGroups && Object.keys(schema.configGroups).length) { + // 悬停弹出:有分组层次按组展示,没层次但有字段折叠成「参数」伪组(见 groupPopContent) + if ((schema?.configGroups && Object.keys(schema.configGroups).length) || schema?.fields?.length) { card.classList.add('task-card--pop'); card.addEventListener('mouseenter', () => showHoverPop(card, task, schema)); card.addEventListener('mouseleave', () => hideHoverPop()); From c69b38145e7b76722520342b0743cabddb3b60c5 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 24 Sep 2026 02:38:13 +0800 Subject: [PATCH 5/7] feat(console): popup lists all fields (internally scrollable) and stays open while pointer is over it; own internal scroll no longer auto-closes it --- media/console/console.css | 18 ++++++++++++++++++ media/console/taskCard.js | 15 +++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/media/console/console.css b/media/console/console.css index a1be515..bffe145 100644 --- a/media/console/console.css +++ b/media/console/console.css @@ -1233,6 +1233,24 @@ button.secondary:hover:not(:disabled) { background: var(--vscode-button-secondar pointer-events: auto; /* 内容超高时允许内部滚动 */ transition: opacity .16s ease, transform .16s cubic-bezier(.2,.9,.3,1.2), visibility 0s; } +/* 朝向桥:伪元素补住卡片与弹层之间的 8px 间隙,鼠标从卡片跨进弹层时 + popEl 的 mouseenter 先清掉卡片 mouseleave 挂起的隐藏定时器,弹层不闪收 */ +.gpop[data-pos="below"]::after, +.gpop[data-pos="above"]::after { + content: ''; + position: absolute; + left: 0; right: 0; height: 12px; +} +.gpop[data-pos="below"]::after { top: -12px; } +.gpop[data-pos="above"]::after { bottom: -12px; } +/* 弹层内部滚动条(内容超高时全量翻阅) */ +.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; } @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; diff --git a/media/console/taskCard.js b/media/console/taskCard.js index cec0e07..ff83efd 100644 --- a/media/console/taskCard.js +++ b/media/console/taskCard.js @@ -174,7 +174,7 @@ head.append(name, cnt); const body = document.createElement('div'); body.className = 'grp-body'; - for (const fieldKey of list.slice(0, key === null ? 6 : 4)) { + for (const fieldKey of list) { const it = document.createElement('div'); it.className = 'it'; const label = document.createElement('span'); @@ -222,8 +222,10 @@ let left = rect.right - pw - 10; // 右缘对齐卡片右缘 left = Math.max(8, Math.min(left, vw - pw - 8)); let top = rect.bottom + 8; // 优先放下方 - if (top + ph > vh - 8) top = rect.top - ph - 8; // 底部放不下 → 翻上方 + const below = top + ph <= vh - 8; + if (!below) top = rect.top - ph - 8; // 底部放不下 → 翻上方 top = Math.max(8, Math.min(top, Math.max(8, vh - ph - 8))); + popEl.dataset.pos = below ? 'below' : 'above'; // 桥的朝向:CSS 伪元素补住与卡片间的间隙 popEl.style.left = `${Math.round(left)}px`; popEl.style.top = `${Math.round(top)}px`; popEl.classList.add('is-visible'); @@ -235,8 +237,13 @@ hideHoverPop(true); } - // 列表滚动 / 窗口缩放时弹层立刻收起,避免钉在旧位置 - window.addEventListener('scroll', () => hideHoverPop(true), { capture: true, passive: true }); + // 列表滚动 / 窗口缩放时弹层立刻收起,避免钉在旧位置。 + // 弹层自身内部滚动不收——capture 阶段会收到非冒泡的 scroll 事件, + // 用户正在弹层里翻全量内容,收掉等于毁掉「悬停可进弹层」。 + window.addEventListener('scroll', (event) => { + if (popEl && event.target instanceof Node && popEl.contains(event.target)) return; + hideHoverPop(true); + }, { capture: true, passive: true }); window.addEventListener('resize', () => hideHoverPop(true)); function buildTaskCard(task) { From 0cfb6eb715331cb9347e0a7fcfe5cc32b405ab38 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 24 Sep 2026 02:44:06 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat(console):=20popup=20expands=20to=20the?= =?UTF-8?q?=20LEFT=20of=20the=20card=20(top-aligned,=20never=20covering=20?= =?UTF-8?q?other=20tasks);=20grouped=20schemas=20also=20list=20ungrouped?= =?UTF-8?q?=20fields=20as=20=E5=85=B6=E4=BB=96=E5=8F=82=E6=95=B0=20group?= =?UTF-8?q?=20(6=20locales)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- media/console/console.css | 10 ---------- media/console/taskCard.js | 40 +++++++++++++++++++++++---------------- src/localization.ts | 6 ++++++ 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/media/console/console.css b/media/console/console.css index bffe145..3f16656 100644 --- a/media/console/console.css +++ b/media/console/console.css @@ -1233,16 +1233,6 @@ button.secondary:hover:not(:disabled) { background: var(--vscode-button-secondar pointer-events: auto; /* 内容超高时允许内部滚动 */ transition: opacity .16s ease, transform .16s cubic-bezier(.2,.9,.3,1.2), visibility 0s; } -/* 朝向桥:伪元素补住卡片与弹层之间的 8px 间隙,鼠标从卡片跨进弹层时 - popEl 的 mouseenter 先清掉卡片 mouseleave 挂起的隐藏定时器,弹层不闪收 */ -.gpop[data-pos="below"]::after, -.gpop[data-pos="above"]::after { - content: ''; - position: absolute; - left: 0; right: 0; height: 12px; -} -.gpop[data-pos="below"]::after { top: -12px; } -.gpop[data-pos="above"]::after { bottom: -12px; } /* 弹层内部滚动条(内容超高时全量翻阅) */ .gpop::-webkit-scrollbar { width: 8px; } .gpop::-webkit-scrollbar-thumb { diff --git a/media/console/taskCard.js b/media/console/taskCard.js index ff83efd..842d874 100644 --- a/media/console/taskCard.js +++ b/media/console/taskCard.js @@ -137,7 +137,8 @@ /** * 弹层内容:参数分组概要(configGroups / groupLabels / displayKey)。 - * 有分组层次 → 按组展示;没层次但有字段 → 折叠成单个「参数」伪组列扁平字段; + * 有分组层次 → 按组展示 + 「其他参数」(没进任何 configGroups 的字段也是参数); + * 没层次但有字段 → 折叠成单个「参数」伪组列扁平字段; * 两者皆无(连字段都没有)→ 不弹。 */ function groupPopContent(task, schema) { @@ -145,9 +146,19 @@ const groupEntries = schema?.configGroups && typeof schema.configGroups === 'object' ? Object.entries(schema.configGroups) : []; const labels = Object.fromEntries(fields.map((f) => [f.key, f.displayKey || f.key])); - const entries = groupEntries.length - ? groupEntries - : (fields.length ? [[null, fields.map((f) => f.key)]] : []); + const entries = []; + if (groupEntries.length) { + const grouped = new Set(); + for (const [key, fieldKeys] of groupEntries) { + const list = Array.isArray(fieldKeys) ? fieldKeys : []; + for (const fieldKey of list) grouped.add(fieldKey); + entries.push({ label: schema.groupLabels?.[key] || key, keys: list }); + } + const others = fields.filter((f) => !grouped.has(f.key)).map((f) => f.key); + if (others.length) entries.push({ label: t('depsPopOthers'), keys: others }); + } else if (fields.length) { + entries.push({ label: t('parameters'), keys: fields.map((f) => f.key) }); + } if (!entries.length) return null; const frag = document.createDocumentFragment(); const title = document.createElement('div'); @@ -156,25 +167,24 @@ const sub = document.createElement('span'); sub.className = 'pop-sub'; sub.textContent = groupEntries.length - ? t('depsPopSub', { count: groupEntries.length }) + ? t('depsPopSub', { count: entries.length }) : t('itemsCount', { count: fields.length }); title.appendChild(sub); frag.appendChild(title); - for (const [key, fieldKeys] of entries) { - const list = Array.isArray(fieldKeys) ? fieldKeys : []; + for (const entry of entries) { const grp = document.createElement('div'); grp.className = 'grp'; const head = document.createElement('div'); head.className = 'grp-head'; const name = document.createElement('b'); - name.textContent = key === null ? t('parameters') : (schema.groupLabels?.[key] || key); + name.textContent = entry.label; const cnt = document.createElement('span'); cnt.className = 'cnt'; - cnt.textContent = t('itemsCount', { count: list.length }); + cnt.textContent = t('itemsCount', { count: entry.keys.length }); head.append(name, cnt); const body = document.createElement('div'); body.className = 'grp-body'; - for (const fieldKey of list) { + for (const fieldKey of entry.keys) { const it = document.createElement('div'); it.className = 'it'; const label = document.createElement('span'); @@ -219,13 +229,11 @@ const vh = window.innerHeight; const pw = popEl.offsetWidth; const ph = popEl.offsetHeight; - let left = rect.right - pw - 10; // 右缘对齐卡片右缘 + // 左侧展开:右缘距卡片左缘 8px,垂直顶对齐卡片——向下/向右弹都会盖住别的任务卡 + let left = rect.left - pw - 8; + if (left < 8) left = Math.min(rect.right + 8, vw - pw - 8); // 左侧放不下 → 回退右侧 left = Math.max(8, Math.min(left, vw - pw - 8)); - let top = rect.bottom + 8; // 优先放下方 - const below = top + ph <= vh - 8; - if (!below) top = rect.top - ph - 8; // 底部放不下 → 翻上方 - top = Math.max(8, Math.min(top, Math.max(8, vh - ph - 8))); - popEl.dataset.pos = below ? 'below' : 'above'; // 桥的朝向:CSS 伪元素补住与卡片间的间隙 + const top = Math.max(8, Math.min(rect.top, Math.max(8, vh - ph - 8))); popEl.style.left = `${Math.round(left)}px`; popEl.style.top = `${Math.round(top)}px`; popEl.classList.add('is-visible'); diff --git a/src/localization.ts b/src/localization.ts index 2fe4206..6b98480 100644 --- a/src/localization.ts +++ b/src/localization.ts @@ -249,6 +249,7 @@ const ZH_CN: WebviewStrings = { queueTitle: '执行队列', depsPopSub: '参数分 {count} 组', depsPopTaken: '已接管', + depsPopOthers: '其他参数', jsonEditBtn: 'JSON 编辑', openFileBtn: '打开文件', openDataBtn: '打开数据位置', @@ -446,6 +447,7 @@ const EN: WebviewStrings = { queueTitle: 'Execution queue', depsPopSub: '{count} config group(s)', depsPopTaken: 'Owned', + depsPopOthers: 'Other parameters', jsonEditBtn: 'JSON edit', openFileBtn: 'Open file', openDataBtn: 'Open data location', @@ -504,6 +506,7 @@ const ZH_TW: WebviewStrings = { queueTitle: '執行佇列', depsPopSub: '參數分 {count} 組', depsPopTaken: '已接管', + depsPopOthers: '其他參數', jsonEditBtn: 'JSON 編輯', openFileBtn: '開啟檔案', openDataBtn: '開啟資料位置', @@ -584,6 +587,7 @@ const JA: WebviewStrings = { queueTitle: '実行キュー', depsPopSub: 'パラメータ {count} グループ', depsPopTaken: '引き継ぎ済み', + depsPopOthers: 'その他のパラメータ', jsonEditBtn: 'JSON 編集', openFileBtn: 'ファイルを開く', openDataBtn: 'データ場所を開く', @@ -663,6 +667,7 @@ const KO: WebviewStrings = { queueTitle: '실행 대기열', depsPopSub: '파라미터 {count}개 그룹', depsPopTaken: '관리됨', + depsPopOthers: '기타 파라미터', jsonEditBtn: 'JSON 편집', openFileBtn: '파일 열기', openDataBtn: '데이터 위치 열기', @@ -742,6 +747,7 @@ const ES: WebviewStrings = { queueTitle: 'Cola de ejecución', depsPopSub: '{count} grupo(s) de configuración', depsPopTaken: 'Gestionado', + depsPopOthers: 'Otros parámetros', jsonEditBtn: 'Editar JSON', openFileBtn: 'Abrir archivo', openDataBtn: 'Abrir ubicación de datos', From 78d5f2f346dcdb62b97d0017e8cdb34a6e05410e Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:01:31 +0800 Subject: [PATCH 7/7] fix(console): hover popup is LEFT-ONLY - shrink width (floor 180px) to fit, clamp to viewport edge; right-side fallback removed --- media/console/taskCard.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/media/console/taskCard.js b/media/console/taskCard.js index 842d874..ca46ea5 100644 --- a/media/console/taskCard.js +++ b/media/console/taskCard.js @@ -225,14 +225,16 @@ popEl.replaceChildren(content); popEl.classList.remove('is-visible'); // 先归零再测量,避免位置跳变闪烁 const rect = card.getBoundingClientRect(); - const vw = window.innerWidth; const vh = window.innerHeight; + // 恒定左侧展开(用户要求:不回退右侧)。空间不足先收窄宽度(下限 180px), + // 连下限都放不下就贴视口左缘(left 钳到 8),允许少量压住卡片——悬停即走,无碍。 + const POP_W = 250; // 与 console.css .gpop 的 width 保持一致 + const availLeft = rect.left - 16; // 卡片间隙 8px + 视口边距 8px + popEl.style.width = `${Math.round(Math.max(180, Math.min(POP_W, availLeft)))}px`; const pw = popEl.offsetWidth; const ph = popEl.offsetHeight; // 左侧展开:右缘距卡片左缘 8px,垂直顶对齐卡片——向下/向右弹都会盖住别的任务卡 - let left = rect.left - pw - 8; - if (left < 8) left = Math.min(rect.right + 8, vw - pw - 8); // 左侧放不下 → 回退右侧 - left = Math.max(8, Math.min(left, vw - pw - 8)); + const left = Math.max(8, rect.left - pw - 8); const top = Math.max(8, Math.min(rect.top, Math.max(8, vh - ph - 8))); popEl.style.left = `${Math.round(left)}px`; popEl.style.top = `${Math.round(top)}px`;