fix(console): stabilize workspace scrolling and background refresh

This commit is contained in:
2026-09-29 23:36:57 +08:00
parent 755cd2d3c3
commit 23db3d8fe0
8 changed files with 55 additions and 10 deletions

View File

@@ -38,27 +38,44 @@
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash || /[\s'"`$\\;&|<>(){}]/.test(url)) throw new Error('服务地址必须是不含账号、查询参数或特殊字符的 HTTP / HTTPS 地址');
return `ORBIT_URL=${url.replace(/\/$/, '')}\nORBIT_APPLICATION_ID=${ui.detail?.entry.application_id || ''}\nORBIT_NAMESPACE_ID=${namespace}\nORBIT_QUEUE_ID=${queue}\nORBIT_TOKEN=${token}\n`;
}
async function load() {
let lastScrollAt = -Infinity, detailSignature = '', previousListHTML = '';
document.addEventListener('scroll', () => { lastScrollAt = performance.now(); }, true);
function editingOrScrolling() {
return performance.now() - lastScrollAt < 1500 || document.activeElement?.matches('input,select,textarea') ||
!!document.querySelector('dialog[open], #access-detail details[open], #access-retire-form:not([hidden])');
}
async function load({ background = false } = {}) {
if (ui.loading || ui.busy) return;
if (background && editingOrScrolling()) return;
ui.loading = true; $('access-error').textContent = '';
try {
const page = await api(ui.after ? `?after=${encodeURIComponent(ui.after)}` : ''); ui.next = page.next || ''; $('access-next').disabled = !ui.next;
$('access-list').innerHTML = page.items.length ? page.items.map(entry => `<button class="access-item ${entry.namespace_id === ui.selected ? 'selected' : ''}" data-access-id="${escape(entry.namespace_id)}"><span class="access-avatar">${escape(entry.application.slice(0, 1).toUpperCase())}</span><span><strong>${escape(entry.application)}</strong><small>${escape(entry.environment)} / ${escape(entry.namespace)} · ${entry.enabled ? '已启用' : '已停用'}</small></span></button>`).join('') : '<p class="panel-description">还没有接入应用,点击右上角新建。</p>';
const page = await api(ui.after ? `?after=${encodeURIComponent(ui.after)}` : '');
if (background && editingOrScrolling()) return;
ui.next = page.next || ''; $('access-next').disabled = !ui.next;
const listHTML = page.items.length ? page.items.map(entry => `<button class="access-item ${entry.namespace_id === ui.selected ? 'selected' : ''}" data-access-id="${escape(entry.namespace_id)}"><span class="access-avatar">${escape(entry.application.slice(0, 1).toUpperCase())}</span><span><strong>${escape(entry.application)}</strong><small>${escape(entry.environment)} / ${escape(entry.namespace)} · ${entry.enabled ? '已启用' : '已停用'}</small></span></button>`).join('') : '<p class="panel-description">还没有接入应用,点击右上角新建。</p>';
if (previousListHTML !== listHTML) { $('access-list').innerHTML = listHTML; previousListHTML = listHTML; }
filterApplications();
if (!ui.selected && page.items.length) await select(page.items[0].namespace_id);
else if (ui.selected && !$('access-dialog').open && !$('access-token-dialog').open) await select(ui.selected);
else if (ui.selected && !$('access-dialog').open && !$('access-token-dialog').open) await select(ui.selected, { background });
} catch (error) { $('access-error').textContent = message(error); }
finally { ui.loading = false; }
}
async function select(namespace) {
async function select(namespace, { background = false } = {}) {
if (namespace !== ui.selected) {
detailSignature = '';
retirement = null; $('access-retire-form').hidden = true;
$('access-detail').hidden = true; $('access-empty').hidden = false;
$('access-empty').textContent = '正在读取应用配置…';
}
const requested = namespace, ticket = ++selectionSequence; ui.selected = namespace;
try {
const detail = await api(`/${encodeURIComponent(namespace)}`); if (ui.selected !== requested || ticket !== selectionSequence) return; ui.detail = detail;
const detail = await api(`/${encodeURIComponent(namespace)}`); if (ui.selected !== requested || ticket !== selectionSequence) return;
if (background && editingOrScrolling()) return;
const now = Date.now();
// 相同快照不重建详情,保留滚动锚点、焦点和折叠状态;到期状态仍按当前时间更新。
const signature = JSON.stringify([detail, detail.credentials.map(c => [!!c.expires_at && Date.parse(c.expires_at) <= now, Date.parse(c.previous_valid_until) > now])]);
ui.detail = detail;
if (signature === detailSignature && !$('access-detail').hidden) return;
$('access-empty').hidden = true; $('access-detail').hidden = false;
$('access-title').textContent = detail.entry.application; $('access-status').textContent = detail.entry.enabled ? '已启用' : '已停用';
$('access-app-id').textContent = detail.entry.application_id; $('access-ns-id').textContent = namespace; $('access-scope').textContent = `${detail.entry.environment} / ${detail.entry.namespace}`;
@@ -71,7 +88,6 @@
$('access-copy-template').textContent = availableCredential?.token_stored ? '查看完整接入配置 ↗' : '补存当前 Token ↗';
$('access-config-note').textContent = availableCredential?.token_stored ? '选择正确队列后,可查看、复制含当前 Token 的完整配置。' : '当前 Token 尚未补存。请从已保存的新配置导入当前 Token,再查看或复制;过渡期旧 Token 不能用于补存。';
$('access-truncated').hidden = !detail.truncated;
const now = Date.now();
$('access-credentials').innerHTML = detail.credentials.map(c => {
const status = c.revoked_at ? '已撤销' : c.expires_at && Date.parse(c.expires_at) <= now ? '已到期' : '有效';
const transitioning = !c.revoked_at && Date.parse(c.previous_valid_until) > now;
@@ -92,6 +108,7 @@
}).join('') || '<p class="panel-description">该环境暂无凭据。</p>';
if ($('access-help-dialog').open) renderCredentialHelp();
document.querySelectorAll('[data-access-id]').forEach(el => el.classList.toggle('selected', el.dataset.accessId === namespace)); renderConfig();
detailSignature = signature;
} catch (error) {
if (ticket !== selectionSequence) return;
$('access-error').textContent = message(error);