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

@@ -39,7 +39,7 @@
loadScopeApplications().catch(error => showToast(`应用目录读取失败:${error.message}`));
}
text('scope-hint', ['workflows', 'retention', 'archives'].includes(next) ? '当前应用环境 · 不受队列筛选影响' : '任务、计划和订阅按所选队列显示');
window.scrollTo({ top: 0, behavior: 'instant' });
$('main-content').scrollTo({ top: 0, behavior: 'instant' });
if (matchMedia('(max-width:640px)').matches) document.querySelector('.nav-item.active')?.scrollIntoView({inline:'nearest',block:'nearest'});
if (next === 'retention' && state.scopeReady) loadRetention();
if (next === 'archives' && state.scopeReady) loadArchives();
@@ -863,7 +863,7 @@
if (!state.authenticated || !state.scopeReady) return;
if (lists[view]) await loadList(view, background === true);
else if (view === 'overview') await loadOverview();
else if (view === 'applications') await window.orbitApplications.load();
else if (view === 'applications') await window.orbitApplications.load({ background: background === true });
else if (view === 'retention') await loadRetention();
else if (view === 'archives') await loadArchives();
text('last-updated', `更新于 ${formatTime(new Date())}`);

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);

View File

@@ -7,7 +7,7 @@
<title>Orbit Console · 星轨控制台</title>
<link rel="stylesheet" href="/console/styles.css">
</head>
<body>
<body class="console-body">
<a class="skip-link" href="#main-content">跳到主要内容</a>
<div class="app-shell">
<aside class="sidebar">

View File

@@ -136,6 +136,14 @@ input:focus-visible,textarea:focus-visible { outline:none; }
@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%}}
/* 工作台只滚动内容区,导航和页头保持原位;避免整页滚动、回弹及滚动条切换造成错位。 */
.console-body { height:100dvh; overflow:hidden; overscroll-behavior:none; }
.console-body .app-shell { height:100dvh; min-height:0; overflow:hidden; grid-template-rows:minmax(0,1fr); }
.console-body .sidebar { position:relative; height:100%; min-height:0; overscroll-behavior:contain; }
.console-body .main-content { height:100%; min-height:0; overflow-y:auto; overflow-x:hidden; overscroll-behavior:contain; scrollbar-gutter:stable; scroll-padding-top:100px; }
.console-body .topbar { position:sticky; top:0; z-index:20; background:var(--bg); }
@media(min-width:921px){.access-list-panel{position:sticky;top:100px;max-height:calc(100dvh - 124px);overflow:auto;overscroll-behavior:contain}}
@media(max-width:640px){.console-body .app-shell{display:flex;flex-direction:column}.console-body .sidebar{height:auto;flex:0 0 auto;overflow:visible}.console-body .main-content{flex:1;scroll-padding-top:78px}.nav-list{overscroll-behavior-x:contain}}
@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}}