feat(console): refine filters and default resource pages to ten rows

This commit is contained in:
2026-09-29 23:00:53 +08:00
parent 96d5a3032a
commit df76dcb9c0
11 changed files with 251 additions and 14 deletions

View File

@@ -4,6 +4,8 @@
## main — 2026-09-29 ## main — 2026-09-29
- 筛选范围改为可搜索的浅色菜单;四个资源列表默认每页 10 条,登录页大屏内容居中,统一筛选区与表格间距。
- 管理端统一浅色工作台、分组导航、紧凑总览、中文状态和移动端布局。 - 管理端统一浅色工作台、分组导航、紧凑总览、中文状态和移动端布局。
- 任务/流程增加服务端状态、精确业务键和时间筛选;任务、计划、流程、订阅增加游标分页、页大小与 ID 定位。刷新保留查询,迟到响应不覆盖新结果。 - 任务/流程增加服务端状态、精确业务键和时间筛选;任务、计划、流程、订阅增加游标分页、页大小与 ID 定位。刷新保留查询,迟到响应不覆盖新结果。
- 应用搜索、接入配置集中展示、低频操作折叠;详情支持复制 ID、键盘焦点与返回入口,Token 查看/复制及原有审计规则保持。 - 应用搜索、接入配置集中展示、低频操作折叠;详情支持复制 ID、键盘焦点与返回入口,Token 查看/复制及原有审计规则保持。

View File

@@ -45,6 +45,7 @@ integration:
./scripts/test-mysql.sh ./scripts/test-mysql.sh
console-check: 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/app.js
node --check internal/transport/httpserver/console/applications.js node --check internal/transport/httpserver/console/applications.js
node --test scripts/test-console-state.cjs node --test scripts/test-console-state.cjs

View File

@@ -330,3 +330,10 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专
- [x] 浏览器验证、查询/翻页回归、基础构建与控制台端到端,本地 Docker 更新和对接说明同步。验收见[管理端改版记录](reports/console-refresh.md)。 - [x] 浏览器验证、查询/翻页回归、基础构建与控制台端到端,本地 Docker 更新和对接说明同步。验收见[管理端改版记录](reports/console-refresh.md)。
不修改业务执行内核、权限、Token、数据库模型或既有业务资源。 不修改业务执行内核、权限、Token、数据库模型或既有业务资源。
## 2026-09-29:筛选、分页与大屏布局细化
- [x] 应用/队列搜索选择、统一状态和每页条数菜单、筛选控件对齐。
- [x] 四个资源列表默认每页 10 条,保留服务端游标与原条件。
- [x] 登录页左侧内容居中、主工作区最大宽度与移动布局调整。
- [ ] 浏览器交互及分页回归、基础构建、本地 Docker 部署与文档同步。

View File

@@ -84,7 +84,7 @@ export ORBIT_CONSOLE_QUEUE_ID='queue_xxx'
## 按应用查看数据 ## 按应用查看数据
页顶「应用 / 环境」选择已创建的应用,再选择队列。任务、定时计划和结果订阅按队列读取;流程、保留策略和归档按应用环境所属命名空间读取,页面有范围提示。应用每页 50 项、队列每页 100 项,选择「加载更多…」获取下一页。没有队列的应用仍能查看流程和归档。 页顶「应用 / 环境」选择已创建的应用,再选择队列。两处下拉菜单支持搜索已加载的选项,应用名称和环境分行展示;支持方向键、Enter 选择及 Escape 关闭。任务、定时计划和结果订阅按队列读取;流程、保留策略和归档按应用环境所属命名空间读取,页面有范围提示。应用每页 50 项、队列每页 100 项,选择「加载更多…」获取下一页。没有队列的应用仍能查看流程和归档。
切换采用完整导航,隔离旧请求和详情;有未保存策略、未确认操作或打开的详情时需先处理完。URL 只保存非敏感 ID。无效范围显示错误,不降级读取默认应用。总览仍是最近查询窗口的统计,不是全库累计。 切换采用完整导航,隔离旧请求和详情;有未保存策略、未确认操作或打开的详情时需先处理完。URL 只保存非敏感 ID。无效范围显示错误,不降级读取默认应用。总览仍是最近查询窗口的统计,不是全库累计。
@@ -100,7 +100,7 @@ export ORBIT_CONSOLE_QUEUE_ID='queue_xxx'
1. 在页顶选择应用环境;任务、计划、订阅再选择队列。 1. 在页顶选择应用环境;任务、计划、订阅再选择队列。
2. 任务与流程可组合状态、完整业务键和创建时间查询。业务键精确匹配,保留大小写、特殊字符和尾部空格;时间输入按浏览器时区换算,开始包含、结束不包含。 2. 任务与流程可组合状态、完整业务键和创建时间查询。业务键精确匹配,保留大小写、特殊字符和尾部空格;时间输入按浏览器时区换算,开始包含、结束不包含。
3. 四个资源列表提供每页 20 / 50 / 100 条、下一页、上一页和回到最新。请求服务端游标,不在前端截取全部数据。自动刷新和手动「刷新数据」保留当前条件及页码;改变条件或页大小回到第一页。分页期间数据可能继续变化,不承诺固定快照。 3. 四个资源列表默认每页 10 条,可选择 10 / 20 / 50 / 100 条、下一页、上一页和回到最新。请求服务端游标,不在前端截取全部数据。自动刷新和手动「刷新数据」保留当前条件及页码;改变条件或页大小回到第一页。分页期间数据可能继续变化,不承诺固定快照。
4. 在页面右上输入完整 32 位资源 ID 可直接定位;详情可复制完整 ID。读取失败保留筛选并提示原因,迟到的旧查询不会覆盖新查询。详情打开后焦点留在抽屉内,Escape 关闭后回到原入口。 4. 在页面右上输入完整 32 位资源 ID 可直接定位;详情可复制完整 ID。读取失败保留筛选并提示原因,迟到的旧查询不会覆盖新查询。详情打开后焦点留在抽屉内,Escape 关闭后回到原入口。
5. 应用接入页可搜索本页应用,先选择接入队列,再查看或复制完整配置。应用标识、权限、有效期调整、退役等低频信息折叠展示;查看与换证沿用原有身份、权限和审计规则。 5. 应用接入页可搜索本页应用,先选择接入队列,再查看或复制完整配置。应用标识、权限、有效期调整、退役等低频信息折叠展示;查看与换证沿用原有身份、权限和审计规则。

View File

@@ -42,3 +42,13 @@
`scripts/orbit-local.py up` 已完成持久 Compose 环境更新。`http://127.0.0.1:18080/version` 返回构建修订 `92a40f4a37ece6c30396fcdf95fe2e73076dc5b5`,`/health/ready` 返回 `ready`。原管理员登录成功,从未登录任务链接进入时,登录后返回原任务页;正式本地入口的任务读取、应用搜索和总览通过,浏览器无 JavaScript 错误。 `scripts/orbit-local.py up` 已完成持久 Compose 环境更新。`http://127.0.0.1:18080/version` 返回构建修订 `92a40f4a37ece6c30396fcdf95fe2e73076dc5b5`,`/health/ready` 返回 `ready`。原管理员登录成功,从未登录任务链接进入时,登录后返回原任务页;正式本地入口的任务读取、应用搜索和总览通过,浏览器无 JavaScript 错误。
SDK、数据库 Schema 34、原应用与凭据保持。部署未重新 bootstrap;前端资源编译进原 Orbit 镜像,无额外前端服务。 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 通过。部署复核另附下方。

View File

@@ -96,6 +96,10 @@
const option = document.createElement('option'); const option = document.createElement('option');
option.value = kind === 'application' ? item.namespace_id : item.id; 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 ? ' · 已暂停' : ''}`; 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; option.disabled = kind === 'application' && !item.enabled;
select.append(option); select.append(option);
} }
@@ -439,7 +443,7 @@
if (!items.length) { body.innerHTML = `<tr><td colspan="${listColumns[kind]}" class="empty-row"><span class="empty-icon">○</span><b>没有符合条件的记录</b><small>检查应用与队列,或调整筛选条件。</small></td></tr>`; return; } if (!items.length) { body.innerHTML = `<tr><td colspan="${listColumns[kind]}" class="empty-row"><span class="empty-icon">○</span><b>没有符合条件的记录</b><small>检查应用与队列,或调整筛选条件。</small></td></tr>`; return; }
body.innerHTML = items.map(item => { body.innerHTML = items.map(item => {
let cells; let cells;
if (kind === 'tasks') cells = `<td><div class="task-id">${resourceButton(kind,item,shortID(item.id))}<small title="${escapeHTML(item.business_key || '')}">${escapeHTML(item.business_key || '未设置业务键')}</small></div></td><td>${scopeBadge()}</td><td class="task-processor">${escapeHTML(item.handler || '—')}<small>${escapeHTML(item.handler_version || '')}</small></td><td>${badge(item.state)}</td><td>${escapeHTML(String(item.attempt_count ?? 0))} 次</td><td>${escapeHTML(formatTime(item.created_at))}</td>`; if (kind === 'tasks') cells = `<td><div class="task-id">${resourceButton(kind,item,shortID(item.id))}<small title="${escapeHTML(item.business_key || '')}">${escapeHTML(item.business_key || '未设置业务键')}</small></div></td><td>${scopeBadge()}</td><td class="task-processor"><span title="${escapeHTML(item.handler || '')}">${escapeHTML(item.handler || '—')}</span><small>${escapeHTML(item.handler_version || '')}</small></td><td>${badge(item.state)}</td><td>${escapeHTML(String(item.attempt_count ?? 0))} 次</td><td>${escapeHTML(formatTime(item.created_at))}</td>`;
if (kind === 'schedules') cells = `<td>${resourceButton(kind,item,item.name)}<small>${escapeHTML(shortID(item.id))}</small></td><td>${scopeBadge()}</td><td>${badge(item.paused ? 'PAUSED' : 'ENABLED')}</td><td>${escapeHTML(String(item.current_version || '—'))}</td><td>${escapeHTML(item.paused ? '已暂停' : formatTime(item.next_fire_at))}</td>`; if (kind === 'schedules') cells = `<td>${resourceButton(kind,item,item.name)}<small>${escapeHTML(shortID(item.id))}</small></td><td>${scopeBadge()}</td><td>${badge(item.paused ? 'PAUSED' : 'ENABLED')}</td><td>${escapeHTML(String(item.current_version || '—'))}</td><td>${escapeHTML(item.paused ? '已暂停' : formatTime(item.next_fire_at))}</td>`;
if (kind === 'workflows') cells = `<td>${resourceButton(kind,item,item.business_key || shortID(item.id))}<small>${escapeHTML(shortID(item.id))}</small></td><td>${scopeBadge()}</td><td>${badge(item.state)}${item.paused ? ' '+badge('PAUSED') : ''}</td><td>${escapeHTML(item.version || '—')}</td><td>${escapeHTML(formatTime(item.created_at))}</td>`; if (kind === 'workflows') cells = `<td>${resourceButton(kind,item,item.business_key || shortID(item.id))}<small>${escapeHTML(shortID(item.id))}</small></td><td>${scopeBadge()}</td><td>${badge(item.state)}${item.paused ? ' '+badge('PAUSED') : ''}</td><td>${escapeHTML(item.version || '—')}</td><td>${escapeHTML(formatTime(item.created_at))}</td>`;
if (kind === 'subscriptions') cells = `<td>${resourceButton(kind,item,item.name)}<small>${escapeHTML(shortID(item.id))}</small></td><td>${scopeBadge()}</td><td>${badge(item.retired_at ? 'RETIRED' : item.enabled === false ? 'PAUSED' : 'ENABLED')}</td><td>${escapeHTML(String(item.pending_count ?? 0))} <span class="muted">/ ${escapeHTML(String(item.max_pending ?? '—'))}</span></td><td>${escapeHTML(formatTime(item.created_at))}</td>`; if (kind === 'subscriptions') cells = `<td>${resourceButton(kind,item,item.name)}<small>${escapeHTML(shortID(item.id))}</small></td><td>${scopeBadge()}</td><td>${badge(item.retired_at ? 'RETIRED' : item.enabled === false ? 'PAUSED' : 'ENABLED')}</td><td>${escapeHTML(String(item.pending_count ?? 0))} <span class="muted">/ ${escapeHTML(String(item.max_pending ?? '—'))}</span></td><td>${escapeHTML(formatTime(item.created_at))}</td>`;
@@ -484,7 +488,7 @@
$(`task-bar-${name}`).style.width = `${total ? tasks.filter(t=>values.includes(t.state)).length/total*100 : 0}%`; $(`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}%`; $('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=>`<tr><td>${resourceButton('tasks',t,shortID(t.id))}<small>${escapeHTML(t.business_key || '未设置业务键')}</small></td><td class="task-processor">${escapeHTML(t.handler)}</td><td>${badge(t.state)}</td><td>${escapeHTML(formatTime(t.created_at))}</td><td>${detailButton('tasks',t)}</td></tr>`).join('') : `<tr><td colspan="5" class="empty-row">${tasks ? '当前队列还没有任务' : '任务暂时无法读取,请检查权限或稍后刷新'}</td></tr>`; $('overview-task-list').innerHTML = tasks?.length ? tasks.slice(0,6).map(t=>`<tr><td>${resourceButton('tasks',t,shortID(t.id))}<small>${escapeHTML(t.business_key || '未设置业务键')}</small></td><td class="task-processor"><span title="${escapeHTML(t.handler)}">${escapeHTML(t.handler)}</span></td><td>${badge(t.state)}</td><td>${escapeHTML(formatTime(t.created_at))}</td><td>${detailButton('tasks',t)}</td></tr>`).join('') : `<tr><td colspan="5" class="empty-row">${tasks ? '当前队列还没有任务' : '任务暂时无法读取,请检查权限或稍后刷新'}</td></tr>`;
const attention = []; const attention = [];
const count = (items, value) => items?.filter(i=>i.state===value).length || 0; 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'); 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); }); $(`${kind}-limit`).addEventListener('change',event=>{ page.limit=Number(event.target.value); page.reset(); loadList(kind); });
const form = $(`${kind}-filters`); const form = $(`${kind}-filters`);
if(form) { 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('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); }); form.addEventListener('reset',()=>{ page.reset({}); loadList(kind); });
} }
@@ -880,7 +886,7 @@
const resource=event.target.closest('[data-resource-id]'); const resource=event.target.closest('[data-resource-id]');
if(resource) detailOpeners[resource.dataset.resourceKind]?.(resource.dataset.resourceId); if(resource) detailOpeners[resource.dataset.resourceKind]?.(resource.dataset.resourceId);
const attention=event.target.closest('[data-attention]'); 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]'); 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 手动复制')); } 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)); window.addEventListener('hashchange', () => setView(currentView(), false));
setView(currentView(), false); setView(currentView(), false);
ensureSession().then(ok => { if (ok) return loadConsoleConfig().then(loadScope).then(() => refresh()); }).catch(() => redirectToLogin()); 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);
})(); })();

View File

@@ -53,7 +53,7 @@
'task.failed': '任务失败', 'task.failed': '任务失败',
'task.cancelled': '任务取消' '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 (!['tasks', 'schedules', 'workflows', 'subscriptions'].includes(kind)) throw new Error('不支持的资源类型');
if (kind !== 'workflows' && !queue) throw new Error('请先选择队列'); if (kind !== 'workflows' && !queue) throw new Error('请先选择队列');
const params = new URLSearchParams({ limit: String(limit) }); const params = new URLSearchParams({ limit: String(limit) });
@@ -80,7 +80,7 @@
return out; return out;
} }
class ListPage { 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; } 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; } 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; } accept(sequence, page) { if (sequence !== this.sequence) return false; this.next = page.next_cursor || ''; this.loading = false; return true; }

View File

@@ -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 });
})();

View File

@@ -50,8 +50,8 @@
<p id="refresh-error" class="page-error" role="alert" hidden></p> <p id="refresh-error" class="page-error" role="alert" hidden></p>
<section class="application-scope" id="application-scope" aria-label="应用筛选"> <section class="application-scope" id="application-scope" aria-label="应用筛选">
<div class="scope-heading"><span class="scope-symbol">⊞</span><div><b>应用范围</b><small id="scope-summary">正在读取…</small></div></div> <div class="scope-heading"><span class="scope-symbol">⊞</span><div><b>应用范围</b><small id="scope-summary">正在读取…</small></div></div>
<label class="scope-field"><span>应用 / 环境</span><select id="scope-application" disabled aria-label="选择应用与环境"><option>正在读取…</option></select></label> <label class="scope-field"><span>应用 / 环境</span><select id="scope-application" data-select data-search="搜索应用 / 环境" disabled aria-label="选择应用与环境"><option>正在读取…</option></select></label>
<label class="scope-field scope-queue"><span>队列</span><select id="scope-queue" disabled aria-label="选择队列"><option>正在读取…</option></select></label> <label class="scope-field scope-queue"><span>队列</span><select id="scope-queue" data-select data-search="搜索队列" disabled aria-label="选择队列"><option>正在读取…</option></select></label>
<div class="scope-note"><small id="scope-hint">任务、计划和订阅按所选队列显示</small><button id="scope-reset" type="button" class="text-button">重置范围 ↗</button></div> <div class="scope-note"><small id="scope-hint">任务、计划和订阅按所选队列显示</small><button id="scope-reset" type="button" class="text-button">重置范围 ↗</button></div>
<p id="scope-error" class="scope-error" role="status"></p> <p id="scope-error" class="scope-error" role="status"></p>
</section> </section>
@@ -86,19 +86,19 @@
</div> </div>
<div class="page-view" id="view-tasks" data-page="tasks"> <div class="page-view" id="view-tasks" data-page="tasks">
<div class="page-heading"><div><h1>任务执行</h1><p>查询任务状态、查看执行历史,按证据处理失败和重试。</p></div><form class="id-lookup" data-lookup="tasks"><label class="sr-only" for="tasks-lookup">任务 ID</label><input id="tasks-lookup" placeholder="输入任务 ID 定位" pattern="[a-f0-9]{32}" maxlength="32" required autocomplete="off"><button class="secondary-button compact" type="submit">定位</button></form></div> <div class="page-heading"><div><h1>任务执行</h1><p>查询任务状态、查看执行历史,按证据处理失败和重试。</p></div><form class="id-lookup" data-lookup="tasks"><label class="sr-only" for="tasks-lookup">任务 ID</label><input id="tasks-lookup" placeholder="输入任务 ID 定位" pattern="[a-f0-9]{32}" maxlength="32" required autocomplete="off"><button class="secondary-button compact" type="submit">定位</button></form></div>
<article class="panel data-panel"><form class="list-filters" id="tasks-filters"><label>状态<select name="state"><option value="">全部状态</option><option value="WAITING">等待执行</option><option value="LEASED">已分配</option><option value="RUNNING">执行中</option><option value="NEEDS_REVIEW">待核查</option><option value="SUCCEEDED">已成功</option><option value="FAILED">已失败</option><option value="CANCELLED">已取消</option></select></label><label class="filter-key">业务键<input name="business_key" placeholder="输入完整业务键,精确匹配" maxlength="191" autocomplete="off"></label><button class="primary-button compact" type="submit">查询</button><button class="secondary-button compact" type="reset">重置</button><details class="filter-more"><summary>时间范围</summary><div class="filter-dates"><label>创建时间从(含)<input name="created_from" type="datetime-local"></label><label>到(不含)<input name="created_before" type="datetime-local"></label><small>按浏览器本地时区查询,设置后点击「查询」。</small></div></details></form><div class="list-summary"><span id="tasks-summary">正在读取…</span><span class="muted">当前应用 / 队列 · 按创建时间倒序</span></div><p id="tasks-error" class="list-error" role="alert" hidden></p><div class="table-wrap"><table><thead><tr><th>任务 / 业务键</th><th>应用 / 环境</th><th>处理器</th><th>状态</th><th>执行次数</th><th>创建时间</th><th>操作</th></tr></thead><tbody id="task-table"><tr><td colspan="7" class="empty-row">正在读取…</td></tr></tbody></table></div><div class="list-pagination"><label>每页 <select id="tasks-limit" aria-label="任务执行每页条数"><option>20</option><option>50</option><option>100</option></select> 条</label><div><button id="tasks-first" class="text-button" type="button">回到最新</button><button id="tasks-prev" class="secondary-button compact" type="button" disabled>上一页</button><span id="tasks-page">第 1 页</span><button id="tasks-next" class="secondary-button compact" type="button" disabled>下一页</button></div></div></article> <article class="panel data-panel"><form class="list-filters" id="tasks-filters"><label>状态<select name="state" data-select aria-label="状态"><option value="">全部状态</option><option value="WAITING">等待执行</option><option value="LEASED">已分配</option><option value="RUNNING">执行中</option><option value="NEEDS_REVIEW">待核查</option><option value="SUCCEEDED">已成功</option><option value="FAILED">已失败</option><option value="CANCELLED">已取消</option></select></label><label class="filter-key">业务键<input name="business_key" placeholder="输入完整业务键,精确匹配" maxlength="191" autocomplete="off"></label><div class="filter-actions"><button class="primary-button compact" type="submit">查询</button><button class="secondary-button compact" type="reset">重置</button></div><button type="button" class="filter-more" aria-expanded="false" aria-controls="tasks-dates">创建时间<span aria-hidden="true">⌄</span></button><div class="filter-dates" id="tasks-dates" hidden><label>创建时间从(含)<input name="created_from" type="datetime-local"></label><label>到(不含)<input name="created_before" type="datetime-local"></label><small>按浏览器本地时区查询,设置后点击「查询」。</small></div></form><div class="list-summary"><span id="tasks-summary">正在读取…</span><span class="muted">当前应用 / 队列 · 按创建时间倒序</span></div><p id="tasks-error" class="list-error" role="alert" hidden></p><div class="table-wrap"><table><thead><tr><th>任务 / 业务键</th><th>应用 / 环境</th><th>处理器</th><th>状态</th><th>执行次数</th><th>创建时间</th><th>操作</th></tr></thead><tbody id="task-table"><tr><td colspan="7" class="empty-row">正在读取…</td></tr></tbody></table></div><div class="list-pagination"><label>每页 <select id="tasks-limit" data-select aria-label="任务执行每页条数"><option>10</option><option>20</option><option>50</option><option>100</option></select> 条</label><div><button id="tasks-first" class="text-button" type="button">回到最新</button><button id="tasks-prev" class="secondary-button compact" type="button" disabled>上一页</button><span id="tasks-page">第 1 页</span><button id="tasks-next" class="secondary-button compact" type="button" disabled>下一页</button></div></div></article>
</div> </div>
<div class="page-view" id="view-schedules" data-page="schedules"> <div class="page-view" id="view-schedules" data-page="schedules">
<div class="page-heading"><div><h1>定时计划</h1><p>查看计划状态与触发历史,暂停或恢复后续调度。</p></div><form class="id-lookup" data-lookup="schedules"><label class="sr-only" for="schedules-lookup">计划 ID</label><input id="schedules-lookup" placeholder="输入计划 ID 定位" pattern="[a-f0-9]{32}" maxlength="32" required autocomplete="off"><button class="secondary-button compact" type="submit">定位</button></form></div> <div class="page-heading"><div><h1>定时计划</h1><p>查看计划状态与触发历史,暂停或恢复后续调度。</p></div><form class="id-lookup" data-lookup="schedules"><label class="sr-only" for="schedules-lookup">计划 ID</label><input id="schedules-lookup" placeholder="输入计划 ID 定位" pattern="[a-f0-9]{32}" maxlength="32" required autocomplete="off"><button class="secondary-button compact" type="submit">定位</button></form></div>
<article class="panel data-panel"><div class="list-summary"><span id="schedules-summary">正在读取…</span><span class="muted">当前应用 / 队列 · 按创建时间倒序</span></div><p id="schedules-error" class="list-error" role="alert" hidden></p><div class="table-wrap"><table><thead><tr><th>计划名称</th><th>应用 / 环境</th><th>状态</th><th>当前版本</th><th>下次触发</th><th>操作</th></tr></thead><tbody id="schedule-list"><tr><td colspan="6" class="empty-row">正在读取…</td></tr></tbody></table></div><div class="list-pagination"><label>每页 <select id="schedules-limit" aria-label="定时计划每页条数"><option>20</option><option>50</option><option>100</option></select> 条</label><div><button id="schedules-first" class="text-button" type="button">回到最新</button><button id="schedules-prev" class="secondary-button compact" type="button" disabled>上一页</button><span id="schedules-page">第 1 页</span><button id="schedules-next" class="secondary-button compact" type="button" disabled>下一页</button></div></div></article> <article class="panel data-panel"><div class="list-summary"><span id="schedules-summary">正在读取…</span><span class="muted">当前应用 / 队列 · 按创建时间倒序</span></div><p id="schedules-error" class="list-error" role="alert" hidden></p><div class="table-wrap"><table><thead><tr><th>计划名称</th><th>应用 / 环境</th><th>状态</th><th>当前版本</th><th>下次触发</th><th>操作</th></tr></thead><tbody id="schedule-list"><tr><td colspan="6" class="empty-row">正在读取…</td></tr></tbody></table></div><div class="list-pagination"><label>每页 <select id="schedules-limit" data-select aria-label="定时计划每页条数"><option>10</option><option>20</option><option>50</option><option>100</option></select> 条</label><div><button id="schedules-first" class="text-button" type="button">回到最新</button><button id="schedules-prev" class="secondary-button compact" type="button" disabled>上一页</button><span id="schedules-page">第 1 页</span><button id="schedules-next" class="secondary-button compact" type="button" disabled>下一页</button></div></div></article>
</div> </div>
<div class="page-view" id="view-workflows" data-page="workflows"> <div class="page-view" id="view-workflows" data-page="workflows">
<div class="page-heading"><div><h1>流程实例</h1><p>跟踪每一步进展,查看等待原因与恢复记录。</p></div><form class="id-lookup" data-lookup="workflows"><label class="sr-only" for="workflows-lookup">流程 ID</label><input id="workflows-lookup" placeholder="输入流程 ID 定位" pattern="[a-f0-9]{32}" maxlength="32" required autocomplete="off"><button class="secondary-button compact" type="submit">定位</button></form></div> <div class="page-heading"><div><h1>流程实例</h1><p>跟踪每一步进展,查看等待原因与恢复记录。</p></div><form class="id-lookup" data-lookup="workflows"><label class="sr-only" for="workflows-lookup">流程 ID</label><input id="workflows-lookup" placeholder="输入流程 ID 定位" pattern="[a-f0-9]{32}" maxlength="32" required autocomplete="off"><button class="secondary-button compact" type="submit">定位</button></form></div>
<article class="panel data-panel"><form class="list-filters" id="workflows-filters"><label>状态<select name="state"><option value="">全部状态</option><option value="RUNNING">执行中</option><option value="WAITING">等待中</option><option value="CANCELLING">取消中</option><option value="NEEDS_REVIEW">待核查</option><option value="SUCCEEDED">已成功</option><option value="FAILED">已失败</option><option value="CANCELLED">已取消</option></select></label><label class="filter-key">业务键<input name="business_key" placeholder="输入完整业务键,精确匹配" maxlength="191" autocomplete="off"></label><button class="primary-button compact" type="submit">查询</button><button class="secondary-button compact" type="reset">重置</button><details class="filter-more"><summary>时间范围</summary><div class="filter-dates"><label>创建时间从(含)<input name="created_from" type="datetime-local"></label><label>到(不含)<input name="created_before" type="datetime-local"></label><small>按浏览器本地时区查询,设置后点击「查询」。</small></div></details></form><div class="list-summary"><span id="workflows-summary">正在读取…</span><span class="muted">当前应用环境 · 按创建时间倒序</span></div><p id="workflows-error" class="list-error" role="alert" hidden></p><div class="table-wrap"><table><thead><tr><th>流程 / 业务键</th><th>应用 / 环境</th><th>状态</th><th>固定版本</th><th>创建时间</th><th>操作</th></tr></thead><tbody id="workflow-list"><tr><td colspan="6" class="empty-row">正在读取…</td></tr></tbody></table></div><div class="list-pagination"><label>每页 <select id="workflows-limit" aria-label="流程实例每页条数"><option>20</option><option>50</option><option>100</option></select> 条</label><div><button id="workflows-first" class="text-button" type="button">回到最新</button><button id="workflows-prev" class="secondary-button compact" type="button" disabled>上一页</button><span id="workflows-page">第 1 页</span><button id="workflows-next" class="secondary-button compact" type="button" disabled>下一页</button></div></div></article> <article class="panel data-panel"><form class="list-filters" id="workflows-filters"><label>状态<select name="state" data-select aria-label="状态"><option value="">全部状态</option><option value="RUNNING">执行中</option><option value="WAITING">等待中</option><option value="CANCELLING">取消中</option><option value="NEEDS_REVIEW">待核查</option><option value="SUCCEEDED">已成功</option><option value="FAILED">已失败</option><option value="CANCELLED">已取消</option></select></label><label class="filter-key">业务键<input name="business_key" placeholder="输入完整业务键,精确匹配" maxlength="191" autocomplete="off"></label><div class="filter-actions"><button class="primary-button compact" type="submit">查询</button><button class="secondary-button compact" type="reset">重置</button></div><button type="button" class="filter-more" aria-expanded="false" aria-controls="workflows-dates">创建时间<span aria-hidden="true">⌄</span></button><div class="filter-dates" id="workflows-dates" hidden><label>创建时间从(含)<input name="created_from" type="datetime-local"></label><label>到(不含)<input name="created_before" type="datetime-local"></label><small>按浏览器本地时区查询,设置后点击「查询」。</small></div></form><div class="list-summary"><span id="workflows-summary">正在读取…</span><span class="muted">当前应用环境 · 按创建时间倒序</span></div><p id="workflows-error" class="list-error" role="alert" hidden></p><div class="table-wrap"><table><thead><tr><th>流程 / 业务键</th><th>应用 / 环境</th><th>状态</th><th>固定版本</th><th>创建时间</th><th>操作</th></tr></thead><tbody id="workflow-list"><tr><td colspan="6" class="empty-row">正在读取…</td></tr></tbody></table></div><div class="list-pagination"><label>每页 <select id="workflows-limit" data-select aria-label="流程实例每页条数"><option>10</option><option>20</option><option>50</option><option>100</option></select> 条</label><div><button id="workflows-first" class="text-button" type="button">回到最新</button><button id="workflows-prev" class="secondary-button compact" type="button" disabled>上一页</button><span id="workflows-page">第 1 页</span><button id="workflows-next" class="secondary-button compact" type="button" disabled>下一页</button></div></div></article>
</div> </div>
<div class="page-view" id="view-subscriptions" data-page="subscriptions"> <div class="page-view" id="view-subscriptions" data-page="subscriptions">
<div class="page-heading"><div><h1>结果订阅</h1><p>查看待确认结果、交付历史和订阅状态。</p></div><form class="id-lookup" data-lookup="subscriptions"><label class="sr-only" for="subscriptions-lookup">订阅 ID</label><input id="subscriptions-lookup" placeholder="输入订阅 ID 定位" pattern="[a-f0-9]{32}" maxlength="32" required autocomplete="off"><button class="secondary-button compact" type="submit">定位</button></form></div> <div class="page-heading"><div><h1>结果订阅</h1><p>查看待确认结果、交付历史和订阅状态。</p></div><form class="id-lookup" data-lookup="subscriptions"><label class="sr-only" for="subscriptions-lookup">订阅 ID</label><input id="subscriptions-lookup" placeholder="输入订阅 ID 定位" pattern="[a-f0-9]{32}" maxlength="32" required autocomplete="off"><button class="secondary-button compact" type="submit">定位</button></form></div>
<article class="panel data-panel"><div class="list-summary"><span id="subscriptions-summary">正在读取…</span><span class="muted">当前应用 / 队列 · 按创建时间倒序</span></div><p id="subscriptions-error" class="list-error" role="alert" hidden></p><div class="table-wrap"><table><thead><tr><th>订阅名称</th><th>应用 / 环境</th><th>状态</th><th>待确认 / 上限</th><th>创建时间</th><th>操作</th></tr></thead><tbody id="subscription-list"><tr><td colspan="6" class="empty-row">正在读取…</td></tr></tbody></table></div><div class="list-pagination"><label>每页 <select id="subscriptions-limit" aria-label="结果订阅每页条数"><option>20</option><option>50</option><option>100</option></select> 条</label><div><button id="subscriptions-first" class="text-button" type="button">回到最新</button><button id="subscriptions-prev" class="secondary-button compact" type="button" disabled>上一页</button><span id="subscriptions-page">第 1 页</span><button id="subscriptions-next" class="secondary-button compact" type="button" disabled>下一页</button></div></div></article> <article class="panel data-panel"><div class="list-summary"><span id="subscriptions-summary">正在读取…</span><span class="muted">当前应用 / 队列 · 按创建时间倒序</span></div><p id="subscriptions-error" class="list-error" role="alert" hidden></p><div class="table-wrap"><table><thead><tr><th>订阅名称</th><th>应用 / 环境</th><th>状态</th><th>待确认 / 上限</th><th>创建时间</th><th>操作</th></tr></thead><tbody id="subscription-list"><tr><td colspan="6" class="empty-row">正在读取…</td></tr></tbody></table></div><div class="list-pagination"><label>每页 <select id="subscriptions-limit" data-select aria-label="结果订阅每页条数"><option>10</option><option>20</option><option>50</option><option>100</option></select> 条</label><div><button id="subscriptions-first" class="text-button" type="button">回到最新</button><button id="subscriptions-prev" class="secondary-button compact" type="button" disabled>上一页</button><span id="subscriptions-page">第 1 页</span><button id="subscriptions-next" class="secondary-button compact" type="button" disabled>下一页</button></div></div></article>
</div> </div>
<div class="page-view" id="view-archives" data-page="archives"> <div class="page-view" id="view-archives" data-page="archives">
<div class="archive-heading"><div><h1>归档作业</h1><p>设置归档周期,查看执行进度和失败原因。</p></div><button class="secondary-button compact" id="archive-refresh">刷新作业 ↻</button></div> <div class="archive-heading"><div><h1>归档作业</h1><p>设置归档周期,查看执行进度和失败原因。</p></div><button class="secondary-button compact" id="archive-refresh">刷新作业 ↻</button></div>
@@ -404,6 +404,7 @@
</dialog> </dialog>
<script src="/console/console-core.js"></script> <script src="/console/console-core.js"></script>
<script src="/console/console-select.js"></script>
<script src="/console/applications.js"></script> <script src="/console/applications.js"></script>
<script src="/console/app.js"></script> <script src="/console/app.js"></script>
</body> </body>

View File

@@ -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: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: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}} @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(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}} @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}}

View File

@@ -16,6 +16,9 @@ test('精确业务键、筛选和游标跨页保持,不静默扩展查询范
assert.equal(query.get('state'), 'NEEDS_REVIEW'); assert.equal(query.get('state'), 'NEEDS_REVIEW');
assert.equal(query.get('queue_id'), 'queue-1'); assert.equal(query.get('queue_id'), 'queue-1');
assert.equal(query.get('cursor'), 'opaque+/='); 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.ok(page.move('prev'));
assert.equal(page.cursor, ''); assert.equal(page.cursor, '');
assert.equal(page.filters.state, 'NEEDS_REVIEW'); assert.equal(page.filters.state, 'NEEDS_REVIEW');