From df76dcb9c0b69b3c947d112de619737145c44015 Mon Sep 17 00:00:00 2001 From: k2safe Date: Tue, 29 Sep 2026 23:00:53 +0800 Subject: [PATCH] feat(console): refine filters and default resource pages to ten rows --- CHANGELOG.md | 2 + Makefile | 1 + docs/08-development-plan.md | 7 + docs/console.md | 4 +- docs/reports/console-refresh.md | 10 ++ internal/transport/httpserver/console/app.js | 14 +- .../httpserver/console/console-core.js | 4 +- .../httpserver/console/console-select.js | 147 ++++++++++++++++++ .../transport/httpserver/console/index.html | 13 +- .../transport/httpserver/console/styles.css | 60 +++++++ scripts/test-console-state.cjs | 3 + 11 files changed, 251 insertions(+), 14 deletions(-) create mode 100644 internal/transport/httpserver/console/console-select.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ec54a1d..4ece07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## main — 2026-09-29 +- 筛选范围改为可搜索的浅色菜单;四个资源列表默认每页 10 条,登录页大屏内容居中,统一筛选区与表格间距。 + - 管理端统一浅色工作台、分组导航、紧凑总览、中文状态和移动端布局。 - 任务/流程增加服务端状态、精确业务键和时间筛选;任务、计划、流程、订阅增加游标分页、页大小与 ID 定位。刷新保留查询,迟到响应不覆盖新结果。 - 应用搜索、接入配置集中展示、低频操作折叠;详情支持复制 ID、键盘焦点与返回入口,Token 查看/复制及原有审计规则保持。 diff --git a/Makefile b/Makefile index 7059795..4996b92 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ integration: ./scripts/test-mysql.sh console-check: + node --check internal/transport/httpserver/console/console-select.js node --check internal/transport/httpserver/console/app.js node --check internal/transport/httpserver/console/applications.js node --test scripts/test-console-state.cjs diff --git a/docs/08-development-plan.md b/docs/08-development-plan.md index 57f5f7e..5cbafae 100644 --- a/docs/08-development-plan.md +++ b/docs/08-development-plan.md @@ -330,3 +330,10 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专 - [x] 浏览器验证、查询/翻页回归、基础构建与控制台端到端,本地 Docker 更新和对接说明同步。验收见[管理端改版记录](reports/console-refresh.md)。 不修改业务执行内核、权限、Token、数据库模型或既有业务资源。 + +## 2026-09-29:筛选、分页与大屏布局细化 + +- [x] 应用/队列搜索选择、统一状态和每页条数菜单、筛选控件对齐。 +- [x] 四个资源列表默认每页 10 条,保留服务端游标与原条件。 +- [x] 登录页左侧内容居中、主工作区最大宽度与移动布局调整。 +- [ ] 浏览器交互及分页回归、基础构建、本地 Docker 部署与文档同步。 diff --git a/docs/console.md b/docs/console.md index e209cbd..69501cd 100644 --- a/docs/console.md +++ b/docs/console.md @@ -84,7 +84,7 @@ export ORBIT_CONSOLE_QUEUE_ID='queue_xxx' ## 按应用查看数据 -页顶「应用 / 环境」选择已创建的应用,再选择队列。任务、定时计划和结果订阅按队列读取;流程、保留策略和归档按应用环境所属命名空间读取,页面有范围提示。应用每页 50 项、队列每页 100 项,选择「加载更多…」获取下一页。没有队列的应用仍能查看流程和归档。 +页顶「应用 / 环境」选择已创建的应用,再选择队列。两处下拉菜单支持搜索已加载的选项,应用名称和环境分行展示;支持方向键、Enter 选择及 Escape 关闭。任务、定时计划和结果订阅按队列读取;流程、保留策略和归档按应用环境所属命名空间读取,页面有范围提示。应用每页 50 项、队列每页 100 项,选择「加载更多…」获取下一页。没有队列的应用仍能查看流程和归档。 切换采用完整导航,隔离旧请求和详情;有未保存策略、未确认操作或打开的详情时需先处理完。URL 只保存非敏感 ID。无效范围显示错误,不降级读取默认应用。总览仍是最近查询窗口的统计,不是全库累计。 @@ -100,7 +100,7 @@ export ORBIT_CONSOLE_QUEUE_ID='queue_xxx' 1. 在页顶选择应用环境;任务、计划、订阅再选择队列。 2. 任务与流程可组合状态、完整业务键和创建时间查询。业务键精确匹配,保留大小写、特殊字符和尾部空格;时间输入按浏览器时区换算,开始包含、结束不包含。 -3. 四个资源列表提供每页 20 / 50 / 100 条、下一页、上一页和回到最新。请求服务端游标,不在前端截取全部数据。自动刷新和手动「刷新数据」保留当前条件及页码;改变条件或页大小回到第一页。分页期间数据可能继续变化,不承诺固定快照。 +3. 四个资源列表默认每页 10 条,可选择 10 / 20 / 50 / 100 条、下一页、上一页和回到最新。请求服务端游标,不在前端截取全部数据。自动刷新和手动「刷新数据」保留当前条件及页码;改变条件或页大小回到第一页。分页期间数据可能继续变化,不承诺固定快照。 4. 在页面右上输入完整 32 位资源 ID 可直接定位;详情可复制完整 ID。读取失败保留筛选并提示原因,迟到的旧查询不会覆盖新查询。详情打开后焦点留在抽屉内,Escape 关闭后回到原入口。 5. 应用接入页可搜索本页应用,先选择接入队列,再查看或复制完整配置。应用标识、权限、有效期调整、退役等低频信息折叠展示;查看与换证沿用原有身份、权限和审计规则。 diff --git a/docs/reports/console-refresh.md b/docs/reports/console-refresh.md index 49f3bb0..a7c400c 100644 --- a/docs/reports/console-refresh.md +++ b/docs/reports/console-refresh.md @@ -42,3 +42,13 @@ `scripts/orbit-local.py up` 已完成持久 Compose 环境更新。`http://127.0.0.1:18080/version` 返回构建修订 `92a40f4a37ece6c30396fcdf95fe2e73076dc5b5`,`/health/ready` 返回 `ready`。原管理员登录成功,从未登录任务链接进入时,登录后返回原任务页;正式本地入口的任务读取、应用搜索和总览通过,浏览器无 JavaScript 错误。 SDK、数据库 Schema 34、原应用与凭据保持。部署未重新 bootstrap;前端资源编译进原 Orbit 镜像,无额外前端服务。 + +## 第二轮:筛选与布局细化 + +- 应用/队列菜单支持已加载选项搜索、环境副标题、选中标记和无匹配提示;状态、页大小沿用同一菜单。原 select 继续承担表单值和 change 事件,无新依赖。 +- 默认页大小从 20 调整为 10,另提供 20/50/100;首页最近 20 条快照与详情历史口径保持。 +- 创建时间按需整行展开;长处理器名称省略显示并可悬停查看全文;主工作区限制大屏宽度,登录品牌内容在左半屏居中。 +- 浏览器实测 25 条记录按 10/10/5 三页读取;切换 20 条回到第一页,查询保留特殊字符与尾部空格。 +- 验证鼠标搜索并切换应用/队列、方向键/Home/End/Enter/Escape/Tab、无匹配提示、重置恢复原始状态、错误时间范围提示、未保存策略阻止范围切换且菜单恢复原值。 +- 八个页面可正常打开;1440×1000、2048×1195 和 390×844 检查大屏及移动布局;手机菜单保持在视口内。 +- 本轮基础 test/vet/build、控制台端到端和 console-check/docs-check 通过。部署复核另附下方。 diff --git a/internal/transport/httpserver/console/app.js b/internal/transport/httpserver/console/app.js index fde332c..ce102cd 100644 --- a/internal/transport/httpserver/console/app.js +++ b/internal/transport/httpserver/console/app.js @@ -96,6 +96,10 @@ const option = document.createElement('option'); option.value = kind === 'application' ? item.namespace_id : item.id; option.textContent = kind === 'application' ? `${item.application} · ${item.environment} / ${item.namespace}${item.enabled ? '' : ' · 已停用'}` : `${item.name}${item.retired_at ? ' · 已退役' : item.paused ? ' · 已暂停' : ''}`; + if (kind === 'application') { + option.dataset.title = item.application; + option.dataset.subtitle = `${item.environment} / ${item.namespace}${item.enabled ? '' : ' · 已停用'}`; + } option.disabled = kind === 'application' && !item.enabled; select.append(option); } @@ -439,7 +443,7 @@ if (!items.length) { body.innerHTML = `○没有符合条件的记录检查应用与队列,或调整筛选条件。`; return; } body.innerHTML = items.map(item => { let cells; - if (kind === 'tasks') cells = `
${resourceButton(kind,item,shortID(item.id))}${escapeHTML(item.business_key || '未设置业务键')}
${scopeBadge()}${escapeHTML(item.handler || '—')}${escapeHTML(item.handler_version || '')}${badge(item.state)}${escapeHTML(String(item.attempt_count ?? 0))} 次${escapeHTML(formatTime(item.created_at))}`; + if (kind === 'tasks') cells = `
${resourceButton(kind,item,shortID(item.id))}${escapeHTML(item.business_key || '未设置业务键')}
${scopeBadge()}${escapeHTML(item.handler || '—')}${escapeHTML(item.handler_version || '')}${badge(item.state)}${escapeHTML(String(item.attempt_count ?? 0))} 次${escapeHTML(formatTime(item.created_at))}`; if (kind === 'schedules') cells = `${resourceButton(kind,item,item.name)}${escapeHTML(shortID(item.id))}${scopeBadge()}${badge(item.paused ? 'PAUSED' : 'ENABLED')}${escapeHTML(String(item.current_version || '—'))}${escapeHTML(item.paused ? '已暂停' : formatTime(item.next_fire_at))}`; if (kind === 'workflows') cells = `${resourceButton(kind,item,item.business_key || shortID(item.id))}${escapeHTML(shortID(item.id))}${scopeBadge()}${badge(item.state)}${item.paused ? ' '+badge('PAUSED') : ''}${escapeHTML(item.version || '—')}${escapeHTML(formatTime(item.created_at))}`; if (kind === 'subscriptions') cells = `${resourceButton(kind,item,item.name)}${escapeHTML(shortID(item.id))}${scopeBadge()}${badge(item.retired_at ? 'RETIRED' : item.enabled === false ? 'PAUSED' : 'ENABLED')}${escapeHTML(String(item.pending_count ?? 0))} / ${escapeHTML(String(item.max_pending ?? '—'))}${escapeHTML(formatTime(item.created_at))}`; @@ -484,7 +488,7 @@ $(`task-bar-${name}`).style.width = `${total ? tasks.filter(t=>values.includes(t.state)).length/total*100 : 0}%`; }); $('subscription-meter-fill').style.width = `${subscriptions?.length ? subscriptions.filter(s=>s.enabled&&!s.retired_at).length/subscriptions.length*100 : 0}%`; - $('overview-task-list').innerHTML = tasks?.length ? tasks.slice(0,6).map(t=>`${resourceButton('tasks',t,shortID(t.id))}${escapeHTML(t.business_key || '未设置业务键')}${escapeHTML(t.handler)}${badge(t.state)}${escapeHTML(formatTime(t.created_at))}${detailButton('tasks',t)}`).join('') : `${tasks ? '当前队列还没有任务' : '任务暂时无法读取,请检查权限或稍后刷新'}`; + $('overview-task-list').innerHTML = tasks?.length ? tasks.slice(0,6).map(t=>`${resourceButton('tasks',t,shortID(t.id))}${escapeHTML(t.business_key || '未设置业务键')}${escapeHTML(t.handler)}${badge(t.state)}${escapeHTML(formatTime(t.created_at))}${detailButton('tasks',t)}`).join('') : `${tasks ? '当前队列还没有任务' : '任务暂时无法读取,请检查权限或稍后刷新'}`; const attention = []; const count = (items, value) => items?.filter(i=>i.state===value).length || 0; const taskReview = count(tasks,'NEEDS_REVIEW'), failed = count(tasks,'FAILED'), flowReview = count(workflows,'NEEDS_REVIEW'); @@ -871,6 +875,8 @@ $(`${kind}-limit`).addEventListener('change',event=>{ page.limit=Number(event.target.value); page.reset(); loadList(kind); }); const form = $(`${kind}-filters`); if(form) { + const more = form.querySelector('.filter-more'); + more.addEventListener('click', () => { const dates = $(more.getAttribute('aria-controls')); dates.hidden = !dates.hidden; more.setAttribute('aria-expanded', String(!dates.hidden)); }); form.addEventListener('submit',event=>{ event.preventDefault(); try { page.reset(core.readFilters(Object.fromEntries(new FormData(form)))); loadList(kind); } catch(error) { text(`${kind}-error`,error.message); $(`${kind}-error`).hidden=false; } }); form.addEventListener('reset',()=>{ page.reset({}); loadList(kind); }); } @@ -880,7 +886,7 @@ const resource=event.target.closest('[data-resource-id]'); if(resource) detailOpeners[resource.dataset.resourceKind]?.(resource.dataset.resourceId); const attention=event.target.closest('[data-attention]'); - if(attention) { const kind=attention.dataset.attention, value=attention.dataset.filterState; lists[kind].reset(value ? {state:value} : {}); const form=$(`${kind}-filters`); if(form) { for(const input of form.querySelectorAll('input,select')) input.value=''; if(value) form.elements.state.value=value; } setView(kind); } + if(attention) { const kind=attention.dataset.attention, value=attention.dataset.filterState; lists[kind].reset(value ? {state:value} : {}); const form=$(`${kind}-filters`); if(form) { for(const input of form.querySelectorAll('input,select')) input.value=''; if(value) form.elements.state.value=value; window.OrbitSelect?.sync(form.elements.state); } setView(kind); } const copy=event.target.closest('[data-copy-id]'); if(copy) { const value=$(copy.dataset.copyId)?.textContent; if(value && value!=='—') navigator.clipboard.writeText(value).then(()=>showToast('资源 ID 已复制')).catch(()=>showToast('复制失败,请选择 ID 手动复制')); } }); @@ -907,5 +913,5 @@ window.addEventListener('hashchange', () => setView(currentView(), false)); setView(currentView(), false); ensureSession().then(ok => { if (ok) return loadConsoleConfig().then(loadScope).then(() => refresh()); }).catch(() => redirectToLogin()); - state.timer = setInterval(() => { if (state.authenticated && $('auto-refresh').checked && !document.hidden && !document.querySelector('.drawer-backdrop:not([hidden]), dialog[open]') && !lists[currentView()]?.loading && !document.activeElement?.matches('input,select,textarea') && !(currentView()==='applications' && document.querySelector('#access-detail details[open]'))) refresh(true); }, 10000); + state.timer = setInterval(() => { if (state.authenticated && $('auto-refresh').checked && !document.hidden && !document.querySelector('.drawer-backdrop:not([hidden]), dialog[open]') && !lists[currentView()]?.loading && !window.OrbitSelect?.isOpen() && !document.activeElement?.matches('input,select,textarea') && !(currentView()==='applications' && document.querySelector('#access-detail details[open]'))) refresh(true); }, 10000); })(); diff --git a/internal/transport/httpserver/console/console-core.js b/internal/transport/httpserver/console/console-core.js index adfbde8..212e040 100644 --- a/internal/transport/httpserver/console/console-core.js +++ b/internal/transport/httpserver/console/console-core.js @@ -53,7 +53,7 @@ 'task.failed': '任务失败', 'task.cancelled': '任务取消' }; - function listURL(kind, queue, filters = {}, cursor = '', limit = 20) { + function listURL(kind, queue, filters = {}, cursor = '', limit = 10) { if (!['tasks', 'schedules', 'workflows', 'subscriptions'].includes(kind)) throw new Error('不支持的资源类型'); if (kind !== 'workflows' && !queue) throw new Error('请先选择队列'); const params = new URLSearchParams({ limit: String(limit) }); @@ -80,7 +80,7 @@ return out; } class ListPage { - constructor() { this.filters = {}; this.limit = 20; this.cursor = ''; this.history = []; this.next = ''; this.sequence = 0; this.loading = false; } + constructor() { this.filters = {}; this.limit = 10; this.cursor = ''; this.history = []; this.next = ''; this.sequence = 0; this.loading = false; } reset(filters = this.filters) { this.sequence++; this.filters = { ...filters }; this.cursor = ''; this.history = []; this.next = ''; this.loading = false; } begin() { this.loading = true; return ++this.sequence; } accept(sequence, page) { if (sequence !== this.sequence) return false; this.next = page.next_cursor || ''; this.loading = false; return true; } diff --git a/internal/transport/httpserver/console/console-select.js b/internal/transport/httpserver/console/console-select.js new file mode 100644 index 0000000..b5d19db --- /dev/null +++ b/internal/transport/httpserver/console/console-select.js @@ -0,0 +1,147 @@ +/* 渐进增强原生 select:表单值和 change 事件仍由原控件提供。 */ +(() => { + const controls = new Map(); + let opened = null; + let serial = 0; + function enhance(select) { + const searchable = select.hasAttribute('data-search'); + const name = select.getAttribute('aria-label') || '选择选项'; + const id = `orbit-select-${++serial}`; + const button = document.createElement('button'); + button.type = 'button'; button.className = 'select-trigger'; + button.setAttribute('aria-label', name); + button.setAttribute('aria-haspopup', 'listbox'); + button.setAttribute('aria-controls', id); + button.setAttribute('aria-expanded', 'false'); + const caption = document.createElement('span'); caption.className = 'select-caption'; + const chevron = document.createElement('span'); chevron.className = 'select-chevron'; chevron.setAttribute('aria-hidden', 'true'); + button.append(caption, chevron); + const popup = document.createElement('div'); popup.className = 'select-popover'; popup.hidden = true; + const search = document.createElement('input'); + search.type = 'search'; search.placeholder = select.dataset.search || '搜索已加载选项'; + search.setAttribute('aria-label', search.placeholder); + search.setAttribute('role', 'combobox'); search.setAttribute('aria-autocomplete', 'list'); + search.setAttribute('aria-controls', id); search.setAttribute('aria-expanded', 'false'); + if (searchable) { + const head = document.createElement('div'); head.className = 'select-search'; head.append(search); popup.append(head); + } + const list = document.createElement('div'); list.id = id; list.className = 'select-options'; + list.setAttribute('role', 'listbox'); list.setAttribute('aria-label', name); list.tabIndex = -1; + const empty = document.createElement('p'); empty.className = 'select-empty'; empty.setAttribute('role', 'status'); + empty.textContent = '没有匹配项,请更换关键词'; empty.hidden = true; + popup.append(list, empty); document.body.append(popup); + select.after(button); select.hidden = true; + let rows = [], active = -1; + const focusOwner = () => searchable ? search : list; + function position() { + if (popup.hidden) return; + const rect = button.getBoundingClientRect(); + if (!rect.width || rect.bottom < 0 || rect.top > innerHeight) { close(); return; } + const width = Math.min(Math.max(rect.width, searchable ? 280 : 150), innerWidth - 24); + popup.style.width = `${width}px`; + popup.style.left = `${Math.max(12, Math.min(rect.left, innerWidth - width - 12))}px`; + const below = innerHeight - rect.bottom - 16, above = rect.top - 16; + const up = below < Math.min(popup.scrollHeight, 300) && above > below; + const room = Math.max(80, up ? above : below); + list.style.maxHeight = `${Math.min(280, room - (searchable ? 61 : 12))}px`; + popup.style.top = `${up ? Math.max(8, rect.top - popup.getBoundingClientRect().height - 6) : rect.bottom + 6}px`; + } + function highlight(index, scroll = false) { + active = index; + rows.forEach((row, i) => row.node.classList.toggle('is-active', i === index)); + if (index < 0) focusOwner().removeAttribute('aria-activedescendant'); + else { + focusOwner().setAttribute('aria-activedescendant', rows[index].node.id); + if (scroll) rows[index].node.scrollIntoView({ block: 'nearest' }); + } + } + function choose(option) { + if (option.disabled || select.disabled) return; + const changed = select.value !== option.value; + select.value = option.value; + close(true); + if (changed) select.dispatchEvent(new Event('change', { bubbles: true })); + // 范围切换可能被未提交表单拦截,以业务监听器确认后的值为准。 + sync(); + } + function render() { + const query = search.value.toLocaleLowerCase(); + list.replaceChildren(); rows = []; + [...select.options].forEach((option, index) => { + if (option.hidden || (query && option.value !== '__more__' && !option.textContent.toLocaleLowerCase().includes(query))) return; + const node = document.createElement('div'); node.className = 'select-option'; node.id = `${id}-${index}`; + node.setAttribute('role', 'option'); node.setAttribute('aria-selected', String(option.selected)); + node.setAttribute('aria-disabled', String(option.disabled)); + const copy = document.createElement('span'); copy.className = 'select-option-copy'; + const title = document.createElement('b'); title.textContent = option.dataset.title || option.textContent; copy.append(title); + if (option.dataset.subtitle) { const sub = document.createElement('small'); sub.textContent = option.dataset.subtitle; copy.append(sub); } + const check = document.createElement('span'); check.className = 'select-check'; check.textContent = option.selected ? '✓' : ''; check.setAttribute('aria-hidden', 'true'); + node.append(copy, check); + const rowIndex = rows.length; + node.addEventListener('pointermove', () => { if (!option.disabled) highlight(rowIndex); }); + node.addEventListener('mousedown', event => event.preventDefault()); + node.addEventListener('click', () => choose(option)); + rows.push({ option, node }); list.append(node); + }); + empty.hidden = rows.length > 0; + const selected = rows.findIndex(row => row.option.selected && !row.option.disabled); + highlight(selected >= 0 ? selected : rows.findIndex(row => !row.option.disabled)); + position(); + } + function sync() { + caption.textContent = select.selectedOptions[0]?.textContent || '请选择'; + button.title = caption.textContent; button.disabled = select.disabled; + if (select.disabled) close(); + else if (!popup.hidden) render(); + } + function close(focus = false) { + if (popup.hidden) return; + popup.hidden = true; button.setAttribute('aria-expanded', 'false'); search.setAttribute('aria-expanded', 'false'); + list.removeAttribute('aria-activedescendant'); search.removeAttribute('aria-activedescendant'); + if (opened?.button === button) opened = null; + if (focus && !button.disabled) button.focus(); + } + function open() { + if (select.disabled) return; + opened?.close(); sync(); search.value = ''; popup.hidden = false; + opened = { button, popup, close, position }; + button.setAttribute('aria-expanded', 'true'); search.setAttribute('aria-expanded', 'true'); + render(); focusOwner().focus(); highlight(active, true); + } + function keydown(event) { + if (event.key === 'Escape' && !popup.hidden) { event.preventDefault(); event.stopPropagation(); close(true); return; } + if (event.key === 'Tab' && !popup.hidden) { close(true); return; } + if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) { + // 搜索输入的 Home/End 保留文本编辑语义。 + if (event.target === search && ['Home', 'End'].includes(event.key)) return; + event.preventDefault(); + if (popup.hidden) { open(); return; } + const enabled = rows.map((row, index) => row.option.disabled ? -1 : index).filter(index => index >= 0); + if (!enabled.length) return; + const current = enabled.indexOf(active), last = enabled.length - 1; + const next = event.key === 'Home' ? 0 : event.key === 'End' ? last : Math.max(0, Math.min(last, current + (event.key === 'ArrowDown' ? 1 : -1))); + highlight(enabled[next], true); + } else if (event.key === 'Enter' || (event.key === ' ' && event.target !== search)) { + event.preventDefault(); + if (popup.hidden) open(); else if (active >= 0) choose(rows[active].option); + } else if (event.key.length === 1 && searchable && event.target === button && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault(); open(); search.value = event.key; render(); + } + } + button.addEventListener('click', event => { event.preventDefault(); popup.hidden ? open() : close(); }); + button.addEventListener('keydown', keydown); search.addEventListener('keydown', keydown); list.addEventListener('keydown', keydown); + search.addEventListener('input', render); + select.addEventListener('change', () => queueMicrotask(sync)); + // reset 事件先于浏览器恢复默认值;下一个任务再同步显示。 + select.form?.addEventListener('reset', () => setTimeout(sync, 0)); + new MutationObserver(sync).observe(select, { childList: true, subtree: true, attributes: true, characterData: true }); + controls.set(select, sync); sync(); + } + document.querySelectorAll('select[data-select]').forEach(enhance); + document.addEventListener('pointerdown', event => { if (opened && !opened.popup.contains(event.target) && !opened.button.contains(event.target)) opened.close(); }); + document.addEventListener('focusin', event => { if (opened && !opened.popup.contains(event.target) && !opened.button.contains(event.target)) opened.close(); }); + document.addEventListener('scroll', event => { if (opened && !opened.popup.contains(event.target)) opened.position(); }, true); + window.addEventListener('resize', () => opened?.position()); + window.addEventListener('hashchange', () => opened?.close()); + window.OrbitSelect = Object.freeze({ sync: select => controls.get(select)?.(), isOpen: () => !!opened }); +})(); diff --git a/internal/transport/httpserver/console/index.html b/internal/transport/httpserver/console/index.html index 0b26635..8f05dca 100644 --- a/internal/transport/httpserver/console/index.html +++ b/internal/transport/httpserver/console/index.html @@ -50,8 +50,8 @@
⊞
应用范围正在读取…
- - + +
任务、计划和订阅按所选队列显示

@@ -86,19 +86,19 @@

任务执行

查询任务状态、查看执行历史,按证据处理失败和重试。

-
时间范围
按浏览器本地时区查询,设置后点击「查询」。
正在读取…当前应用 / 队列 · 按创建时间倒序
任务 / 业务键应用 / 环境处理器状态执行次数创建时间操作
正在读取…
第 1 页
+
正在读取…当前应用 / 队列 · 按创建时间倒序
任务 / 业务键应用 / 环境处理器状态执行次数创建时间操作
正在读取…
第 1 页

定时计划

查看计划状态与触发历史,暂停或恢复后续调度。

-
正在读取…当前应用 / 队列 · 按创建时间倒序
计划名称应用 / 环境状态当前版本下次触发操作
正在读取…
第 1 页
+
正在读取…当前应用 / 队列 · 按创建时间倒序
计划名称应用 / 环境状态当前版本下次触发操作
正在读取…
第 1 页

流程实例

跟踪每一步进展,查看等待原因与恢复记录。

-
时间范围
按浏览器本地时区查询,设置后点击「查询」。
正在读取…当前应用环境 · 按创建时间倒序
流程 / 业务键应用 / 环境状态固定版本创建时间操作
正在读取…
第 1 页
+
正在读取…当前应用环境 · 按创建时间倒序
流程 / 业务键应用 / 环境状态固定版本创建时间操作
正在读取…
第 1 页

结果订阅

查看待确认结果、交付历史和订阅状态。

-
正在读取…当前应用 / 队列 · 按创建时间倒序
订阅名称应用 / 环境状态待确认 / 上限创建时间操作
正在读取…
第 1 页
+
正在读取…当前应用 / 队列 · 按创建时间倒序
订阅名称应用 / 环境状态待确认 / 上限创建时间操作
正在读取…
第 1 页

归档作业

设置归档周期,查看执行进度和失败原因。

@@ -404,6 +404,7 @@ + diff --git a/internal/transport/httpserver/console/styles.css b/internal/transport/httpserver/console/styles.css index d0c498a..9378afe 100644 --- a/internal/transport/httpserver/console/styles.css +++ b/internal/transport/httpserver/console/styles.css @@ -76,6 +76,66 @@ summary:hover { color:var(--accent); } @media(max-width:1200px){.app-shell{grid-template-columns:185px minmax(0,1fr)}.main-content{padding:0 22px 20px}.sidebar{padding:23px 11px}.topbar-actions{gap:9px}.last-updated{display:none}.scope-heading{display:none}.overview-grid{grid-template-columns:minmax(0,1.35fr) minmax(250px,1fr)}.stat-grid{grid-template-columns:repeat(2,1fr)}.access-grid{grid-template-columns:220px minmax(0,1fr)}.archive-grid{grid-template-columns:290px minmax(0,1fr)}.id-lookup input{width:165px}.page-heading{align-items:start}.page-heading h1{font-size:24px}} @media(max-width:920px){.app-shell{grid-template-columns:160px minmax(0,1fr)}.brand{padding:0 5px}.brand strong{font-size:20px}.brand small{font-size:10px}.nav-item{font-size:12px;padding:10px}.nav-icon{font-size:17px}.main-content{padding:0 18px 20px}.metric-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid,.archive-grid{grid-template-columns:1fr}.access-grid{grid-template-columns:1fr}.access-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.page-heading{flex-direction:column;align-items:stretch;gap:14px}.page-heading>.secondary-button{align-self:start}.id-lookup input{width:240px}.snapshot-note{flex-wrap:wrap}.snapshot-note>span:last-child{margin-left:0}.scope-note{flex-basis:100%;justify-content:flex-end}.stat-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.login-brand{padding:40px}.login-intro h1{font-size:42px}.login-brand .brand small{display:none}} @media(max-width:640px){.app-shell{display:block}.sidebar{position:relative;height:auto;display:block;padding:15px 16px 0;border-right:0;border-bottom:1px solid var(--line)}.brand{padding:0}.brand small{display:inline-block;margin-left:10px;letter-spacing:1px}.brand-mark{width:30px;height:30px}.workspace-label,.nav-group-label,.sidebar-bottom{display:none}.nav-list{flex-direction:row;overflow-x:auto;gap:3px;margin-top:14px;padding-bottom:10px}.nav-item{flex-shrink:0;padding:9px 11px;font-size:12px;gap:6px}.nav-icon{font-size:14px;width:16px}.main-content{padding:0 14px 16px}.topbar{height:58px;margin-bottom:16px;gap:10px}.crumbs{font-size:11px;gap:8px}.live-chip{display:none}.topbar-actions{gap:8px}.auto-refresh{font-size:10px}.application-scope{padding:12px;gap:10px;margin-bottom:20px}.scope-field{display:block;min-width:120px}.scope-field select{margin-top:5px}.scope-note{display:none}.page-heading h1,.archive-heading h1,.access-heading h1{font-size:23px}.page-heading p,.archive-heading p,.access-heading p{font-size:11px}.page-heading,.access-heading,.archive-heading{margin-bottom:18px}.access-heading,.archive-heading{align-items:start}.metric-grid{gap:10px}.metric-card{padding:16px}.metric-card strong{font-size:27px;margin:10px 0 5px}.metric-card small{font-size:9px}.metric-top{font-size:11px}.snapshot-note{font-size:9px;line-height:1.8}.panel{padding:17px}.data-panel{padding:0}.panel-heading{margin-bottom:16px}h2{font-size:15px}.stat-grid{gap:14px;grid-template-columns:repeat(2,minmax(0,1fr))}.list-filters{padding:15px;gap:10px}.filter-key{max-width:none;min-width:170px!important}.filter-more{flex-basis:100%}.list-summary{padding:12px 15px}.list-summary .muted{display:none}.list-pagination{padding:12px 15px;flex-wrap:wrap;justify-content:center}.list-pagination>label{font-size:10px}.list-pagination>div{gap:8px}.list-pagination button{font-size:10px;padding:6px 9px}.list-pagination .text-button{padding:3px 0}.list-pagination>div>span{font-size:10px}.id-lookup input{width:100%}.table-wrap table{min-width:660px}.recent-panel table{min-width:540px}td{padding:13px 11px}.access-list{grid-template-columns:1fr}.access-config-grid{grid-template-columns:1fr}.access-meta>div{flex-direction:column;gap:5px}.access-credential{padding:14px}.access-credential-header{flex-wrap:wrap;gap:6px}.access-dialog{padding:20px;max-height:calc(100dvh - 24px);width:calc(100vw - 24px)}.access-form-actions{gap:10px}.access-form-actions button{font-size:11px}.retention-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.retention-meta{flex-direction:column;gap:7px}.archive-stats{gap:9px}.archive-stats article{padding:13px}.archive-stats strong{font-size:24px}.archive-stats small{font-size:9px}.task-drawer{padding:22px 17px}.drawer-header h2{font-size:19px}.detail-grid{padding:15px;gap:14px}.page-footer{flex-direction:column;gap:7px;font-size:9px}.service-detail-grid{grid-template-columns:1fr}.login-page{display:flex;flex-direction:column;min-height:100dvh}.login-brand{padding:24px;background:#edf5f2}.login-brand .brand small{display:inline-block}.login-intro{margin:28px 0 12px}.login-intro .page-eyebrow,.login-flow,.login-brand>small{display:none}.login-intro h1{font-size:28px;letter-spacing:-.5px;margin:0 0 9px}.login-intro h1 br{display:none}.login-intro p{font-size:11px}.login-intro p br{display:none}.login-panel{padding:35px 26px 70px;flex:1}.login-card{width:100%;max-width:380px}.login-card h2{font-size:22px}.login-copy{margin-bottom:24px}.login-footer{bottom:22px}} +/* 筛选工作区:限制大屏拉伸,范围与查询使用一致的控件。 */ +.main-content { max-width:1580px; margin:0 auto; } +.application-scope { gap:16px 22px; padding:16px 20px; margin-bottom:28px; } +.scope-heading { padding-right:6px; } +.scope-field { display:block; flex:0 1 300px; min-width:180px; } +.scope-field>span { display:block; margin-bottom:7px; font-size:11px; } +.scope-queue { flex-basis:300px; } +.scope-note { margin-left:auto; } +.scope-field select { height:40px; } +.select-trigger { display:flex; align-items:center; justify-content:space-between; gap:14px; width:100%; min-width:0; height:40px; padding:0 12px; border:1px solid #d6dee7; border-radius:7px; background:#fff; color:var(--text); text-align:left; font-size:12px; font-weight:400; } +.select-trigger:hover:not(:disabled) { border-color:#9abbb4; background:#fcfefd; } +.select-trigger[aria-expanded=true] { border-color:var(--accent); box-shadow:0 0 0 3px #087f7512; } +.select-caption { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.select-chevron { border-right:1.5px solid #7d8f9d; border-bottom:1.5px solid #7d8f9d; width:6px; height:6px; transform:rotate(45deg); margin:0 2px 3px; flex-shrink:0; } +.select-trigger[aria-expanded=true] .select-chevron { transform:rotate(225deg); margin-top:6px; } +.select-popover { position:fixed; z-index:45; padding:5px; background:#fff; border:1px solid #dde7e4; border-radius:10px; box-shadow:0 10px 32px #183b2f1c,0 2px 6px #183b2f09; } +.select-search { padding:6px 6px 10px; } +.select-search input { margin:0; height:36px; background:var(--soft); font-size:12px; padding:8px 10px; } +.select-options { overflow-y:auto; overscroll-behavior:contain; } +.select-option { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 12px; border-radius:6px; cursor:pointer; color:var(--text); font-size:12px; } +.select-option[aria-selected=true] { color:var(--accent); background:#edf7f3; } +.select-option.is-active { background:#f0f5f3; } +.select-option.is-active[aria-selected=true] { background:#e5f2ed; } +.select-option[aria-disabled=true] { opacity:.45; cursor:not-allowed; } +.select-option-copy { min-width:0; } +.select-option b { display:block; font-weight:500; overflow-wrap:anywhere; } +.select-option small { display:block; font-size:10px; margin-top:3px; color:#83948e; } +.select-check { color:var(--accent); font-size:13px; flex-shrink:0; } +.select-empty { padding:22px 12px; font-size:12px; text-align:center; color:var(--muted); } +.select-options:focus { outline:none; } +input:focus-visible,textarea:focus-visible { outline:none; } +.task-processor>span { display:block; max-width:240px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.list-filters { display:grid; grid-template-columns:155px minmax(200px,1fr) auto auto; gap:16px; padding:22px; align-items:end; } +.list-filters>label { font-size:12px; min-width:0; } +.list-filters>label>.select-trigger { margin-top:7px; } +.list-filters input,.list-filters select { height:40px; } +.filter-key { max-width:none; } +.filter-actions { display:flex; gap:9px; grid-column:4; grid-row:1; } +.filter-actions button { height:40px; padding:0 17px; } +.filter-more { display:flex; gap:12px; align-items:center; align-self:end; justify-content:center; height:40px; border:1px solid #d6dee7; background:#fff; color:var(--muted); border-radius:7px; padding:0 13px; grid-column:3; grid-row:1; font-size:12px; font-weight:400; list-style:none; } +.filter-more::-webkit-details-marker { display:none; } +.filter-more:hover { background:var(--soft); border-color:#9abbb4; } +.filter-more[aria-expanded=true] { background:var(--accent-soft); border-color:#b8d9ce; color:var(--accent); } +.filter-more[aria-expanded=true] span { transform:rotate(180deg); } +.filter-dates { grid-column:1/-1; border-top:1px solid var(--line); padding-top:17px; margin:0; gap:16px; } +.filter-dates label { flex:0 1 230px; } +.filter-dates small { font-size:11px; } +.list-pagination { padding:15px 22px; background:#fcfdfd; } +.list-pagination>label { white-space:nowrap; } +.list-pagination .select-trigger { width:72px; height:34px; font-size:12px; } +.list-pagination>div>span { padding:5px 8px; font-size:12px; color:var(--text); } +/* 品牌内容在左半屏内居中,与右侧表单形成平衡的两列。 */ +.login-brand { padding:54px clamp(32px,5vw,80px); } +.login-brand>* { width:min(100%,400px); margin-left:auto; margin-right:auto; } +.login-intro { margin-top:70px; margin-bottom:70px; } +.login-intro h1 { font-size:clamp(40px,3.4vw,54px); } +.login-card { width:360px; } +@media(max-width:1200px){.scope-field{flex:1;min-width:160px}.scope-note{margin-left:0}} +@media(max-width:920px){.application-scope{gap:12px}.scope-note{flex-basis:100%;margin-top:-4px}.list-filters{grid-template-columns:145px minmax(160px,1fr)}.filter-more{grid-column:1;grid-row:2}.filter-actions{grid-column:2;grid-row:2}.login-brand{padding:40px}.login-intro h1{font-size:40px}} +@media(max-width:640px){.application-scope{padding:14px;gap:12px}.scope-field{flex:1 1 100%;min-width:0}.scope-note{display:none}.scope-field>span{margin-bottom:5px}.list-filters{padding:16px;grid-template-columns:1fr 1fr;gap:14px}.list-filters>label{grid-column:1/-1;min-width:0!important}.filter-more{grid-column:1;grid-row:3}.filter-actions{grid-column:2;grid-row:3;gap:7px}.filter-actions button{flex:1;padding:0 11px}.filter-dates{grid-row:4;gap:14px}.filter-dates label{flex:1 1 100%;min-width:0}.filter-dates small{padding:0}.list-pagination{padding:14px 10px;gap:12px}.list-pagination>div{gap:6px}.list-pagination .select-trigger{width:65px;height:32px}.login-brand{padding:28px}.login-brand>*{width:100%;max-width:380px}.login-intro{margin:30px auto 8px}.login-intro h1{font-size:28px}.login-card{width:100%}} @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}} @media(max-width:640px){.data-panel .table-wrap::before{content:"左右滑动查看完整表格 →";display:block;padding:0 15px 10px;color:var(--muted);font-size:10px}.retention-panel{max-width:none}} diff --git a/scripts/test-console-state.cjs b/scripts/test-console-state.cjs index 40319c1..db818d2 100644 --- a/scripts/test-console-state.cjs +++ b/scripts/test-console-state.cjs @@ -16,6 +16,9 @@ test('精确业务键、筛选和游标跨页保持,不静默扩展查询范 assert.equal(query.get('state'), 'NEEDS_REVIEW'); assert.equal(query.get('queue_id'), 'queue-1'); assert.equal(query.get('cursor'), 'opaque+/='); + assert.equal(page.limit, 10); + assert.equal(query.get('limit'), '10'); + assert.equal(new URL(listURL('tasks', 'queue-1', page.filters, page.cursor, 50), 'http://localhost').searchParams.get('limit'), '50'); assert.ok(page.move('prev')); assert.equal(page.cursor, ''); assert.equal(page.filters.state, 'NEEDS_REVIEW');