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 @@
任务执行
查询任务状态、查看执行历史,按证据处理失败和重试。
-
正在读取…当前应用 / 队列 · 按创建时间倒序
| 任务 / 业务键 | 应用 / 环境 | 处理器 | 状态 | 执行次数 | 创建时间 | 操作 |
|---|
| 正在读取… |
+
正在读取…当前应用 / 队列 · 按创建时间倒序
| 任务 / 业务键 | 应用 / 环境 | 处理器 | 状态 | 执行次数 | 创建时间 | 操作 |
|---|
| 正在读取… |
定时计划
查看计划状态与触发历史,暂停或恢复后续调度。
-
正在读取…当前应用 / 队列 · 按创建时间倒序
| 计划名称 | 应用 / 环境 | 状态 | 当前版本 | 下次触发 | 操作 |
|---|
| 正在读取… |
+
正在读取…当前应用 / 队列 · 按创建时间倒序
| 计划名称 | 应用 / 环境 | 状态 | 当前版本 | 下次触发 | 操作 |
|---|
| 正在读取… |
-
正在读取…当前应用环境 · 按创建时间倒序
| 流程 / 业务键 | 应用 / 环境 | 状态 | 固定版本 | 创建时间 | 操作 |
|---|
| 正在读取… |
+
正在读取…当前应用环境 · 按创建时间倒序
| 流程 / 业务键 | 应用 / 环境 | 状态 | 固定版本 | 创建时间 | 操作 |
|---|
| 正在读取… |
-
正在读取…当前应用 / 队列 · 按创建时间倒序
| 订阅名称 | 应用 / 环境 | 状态 | 待确认 / 上限 | 创建时间 | 操作 |
|---|
| 正在读取… |
+
正在读取…当前应用 / 队列 · 按创建时间倒序
| 订阅名称 | 应用 / 环境 | 状态 | 待确认 / 上限 | 创建时间 | 操作 |
|---|
| 正在读取… |