Compare commits
2 Commits
755cd2d3c3
...
9a15406f76
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a15406f76 | |||
| 23db3d8fe0 |
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
## main — 2026-09-29
|
## main — 2026-09-29
|
||||||
|
|
||||||
|
- 修复长页面滚动体验:固定导航与页头、内容独立滚动,应用详情后台刷新避免重建相同快照及打断交互。
|
||||||
|
|
||||||
- 筛选范围改为可搜索的浅色菜单;四个资源列表默认每页 10 条,登录页大屏内容居中,统一筛选区与表格间距。
|
- 筛选范围改为可搜索的浅色菜单;四个资源列表默认每页 10 条,登录页大屏内容居中,统一筛选区与表格间距。
|
||||||
|
|
||||||
- 管理端统一浅色工作台、分组导航、紧凑总览、中文状态和移动端布局。
|
- 管理端统一浅色工作台、分组导航、紧凑总览、中文状态和移动端布局。
|
||||||
|
|||||||
@@ -337,3 +337,10 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专
|
|||||||
- [x] 四个资源列表默认每页 10 条,保留服务端游标与原条件。
|
- [x] 四个资源列表默认每页 10 条,保留服务端游标与原条件。
|
||||||
- [x] 登录页左侧内容居中、主工作区最大宽度与移动布局调整。
|
- [x] 登录页左侧内容居中、主工作区最大宽度与移动布局调整。
|
||||||
- [x] 浏览器交互及分页回归、基础构建、本地 Docker 部署与文档同步;见[验收记录](reports/console-refresh.md)。
|
- [x] 浏览器交互及分页回归、基础构建、本地 Docker 部署与文档同步;见[验收记录](reports/console-refresh.md)。
|
||||||
|
|
||||||
|
## 2026-09-29:滚动稳定性与登录保护核对
|
||||||
|
|
||||||
|
- [x] 控制台独立内容滚动,固定导航与页头,保留移动端导航和表格横向滚动。
|
||||||
|
- [x] 应用详情相同快照不重建,后台刷新在滚动/编辑/展开详情时让位,检查请求返回时的交互状态。
|
||||||
|
- [x] 核对登录实现:当前没有失败次数封控或登录限流,明确记录缺口。
|
||||||
|
- [x] 浏览器滚动、刷新和切页回归,基础检查及持久 Docker 部署;验收见[管理端改版记录](reports/console-refresh.md)。
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ http://127.0.0.1:8080/console/
|
|||||||
- 在具备 `workflows:control` 权限时,流程详情可以暂停、恢复或取消;操作原因、`control_revision` 和幂等键交给服务端做最终校验。
|
- 在具备 `workflows:control` 权限时,流程详情可以暂停、恢复或取消;操作原因、`control_revision` 和幂等键交给服务端做最终校验。
|
||||||
- `NEEDS_REVIEW` 流程会显示按步骤类型划分的人工恢复入口:任务核查、步骤结论、继续、步骤重试、PENDING 接续和等待重建;每次提交都要求当前修订、证据引用和显式确认,服务端恢复 API 负责最终门控。
|
- `NEEDS_REVIEW` 流程会显示按步骤类型划分的人工恢复入口:任务核查、步骤结论、继续、步骤重试、PENDING 接续和等待重建;每次提交都要求当前修订、证据引用和显式确认,服务端恢复 API 负责最终门控。
|
||||||
- 在具备 `events:manage` 权限时,订阅详情可以启停订阅,并对 `EXPIRED` 交付追加次数预算后重试;原交付身份、已用次数和事件不会被页面重置。
|
- 在具备 `events:manage` 权限时,订阅详情可以启停订阅,并对 `EXPIRED` 交付追加次数预算后重试;原交付身份、已用次数和事件不会被页面重置。
|
||||||
|
- 工作台固定导航和页头,内容区独立滚动;切换模块回到内容顶部。应用详情相同快照不重建,后台刷新会避让正在滚动、编辑或展开详情的操作。
|
||||||
- 左侧八个模块是独立视图:总览、任务、计划、流程、订阅、保留策略、归档作业和应用接入。
|
- 左侧八个模块是独立视图:总览、任务、计划、流程、订阅、保留策略、归档作业和应用接入。
|
||||||
- “保留策略”只允许单管理员查看和修改命名空间级窗口;修改需要版本号、幂等键和原因并留下审计。预览和只读导出不会删除源数据,未确认交付、未知效果和未结束流程不会成为候选。
|
- “保留策略”只允许单管理员查看和修改命名空间级窗口;修改需要版本号、幂等键和原因并留下审计。预览和只读导出不会删除源数据,未确认交付、未知效果和未结束流程不会成为候选。
|
||||||
- 当前视图按 10 秒自动刷新,可在页顶暂停;输入、打开详情或操作弹窗时不自动重载。页面只调用服务端授权接口,不把账号密码或业务凭据上传到独立服务,也不读取数据库。
|
- 当前视图按 10 秒自动刷新,可在页顶暂停;输入、打开详情或操作弹窗时不自动重载。页面只调用服务端授权接口,不把账号密码或业务凭据上传到独立服务,也不读取数据库。
|
||||||
@@ -74,6 +75,8 @@ export ORBIT_CONSOLE_QUEUE_ID='queue_xxx'
|
|||||||
|
|
||||||
## 安全边界
|
## 安全边界
|
||||||
|
|
||||||
|
当前登录接口尚未实现连续密码错误计数、临时锁定或账号/IP 登录限流。错误统一返回 401,密码使用 bcrypt 存储;这些措施不能替代防暴力尝试封控。此项是待补能力,不能视为已具备。
|
||||||
|
|
||||||
控制台通过服务端授权访问数据库。登录会话只保存令牌摘要,服务端每次请求都会校验账号、会话、命名空间、应用启停和凭据权限。取消和人工重试会使用当前任务版本生成一次性幂等键;网络结果不明时不要重复点击,应使用原始操作身份通过 API/SDK 核对。`NEEDS_REVIEW` 任务仍须使用带证据的核查 API/SDK 完成最终裁决。普通账号不自动获得跨命名空间权限;部署指定的单管理员可显式选择应用范围,每个业务事务重新核验原会话及目标应用。各应用分别展示,SDK 权限不变。
|
控制台通过服务端授权访问数据库。登录会话只保存令牌摘要,服务端每次请求都会校验账号、会话、命名空间、应用启停和凭据权限。取消和人工重试会使用当前任务版本生成一次性幂等键;网络结果不明时不要重复点击,应使用原始操作身份通过 API/SDK 核对。`NEEDS_REVIEW` 任务仍须使用带证据的核查 API/SDK 完成最终裁决。普通账号不自动获得跨命名空间权限;部署指定的单管理员可显式选择应用范围,每个业务事务重新核验原会话及目标应用。各应用分别展示,SDK 权限不变。
|
||||||
|
|
||||||
不能把业务 Token 长期放进浏览器本地存储,也不能让控制台直连 MySQL。计划版本发布、跨命名空间管理身份和其他高风险写操作仍应通过受保护 API/SDK 设计后再接入页面。流程恢复入口只调用已经定义的恢复 API,不自动猜测结果、不替操作者补齐证据,也不把冲突重试成“最新版本”。
|
不能把业务 Token 长期放进浏览器本地存储,也不能让控制台直连 MySQL。计划版本发布、跨命名空间管理身份和其他高风险写操作仍应通过受保护 API/SDK 设计后再接入页面。流程恢复入口只调用已经定义的恢复 API,不自动猜测结果、不替操作者补齐证据,也不把冲突重试成“最新版本”。
|
||||||
|
|||||||
@@ -54,3 +54,13 @@ SDK、数据库 Schema 34、原应用与凭据保持。部署未重新 bootstrap
|
|||||||
- 本轮基础 test/vet/build、控制台端到端和 console-check/docs-check 通过。部署复核另附下方。
|
- 本轮基础 test/vet/build、控制台端到端和 console-check/docs-check 通过。部署复核另附下方。
|
||||||
|
|
||||||
第二轮已部署至持久 Compose 环境:`/version` 修订为 `df76dcb9c0b69b3c947d112de619737145c44015`,就绪检查 HTTP 200。实际 18080 入口应用菜单、10 条列表、退出和原管理员登录通过;原应用身份与数据库保留。本次不涉及 SDK 发版。
|
第二轮已部署至持久 Compose 环境:`/version` 修订为 `df76dcb9c0b69b3c947d112de619737145c44015`,就绪检查 HTTP 200。实际 18080 入口应用菜单、10 条列表、退出和原管理员登录通过;原应用身份与数据库保留。本次不涉及 SDK 发版。
|
||||||
|
|
||||||
|
## 第三轮:滚动稳定性
|
||||||
|
|
||||||
|
原布局让整个 document 滚动,页头会被带离视口;应用详情每次轮询都重建 DOM。改为固定视口的工作台、独立内容滚动、固定页头和桌面应用目录;后台请求在返回时再次检查滚动/编辑状态,相同应用快照保留原有 DOM。切换视图重置内容滚动区域,菜单仍按当前控件坐标定位。
|
||||||
|
|
||||||
|
浏览器验证:1440×900 与 390×844 下,document 不滚动,页头和导航坐标固定;鼠标点击页头刷新前后内容 scrollTop 均为 293、scrollHeight 均为 1193;展开状态不因相同快照刷新被折叠;手机切页归零、详情打开/关闭和菜单定位通过,无横向页面溢出或 JS 错误。基础 test/vet/build、console-check/docs-check 通过。
|
||||||
|
|
||||||
|
登录保护核对:`ConsoleAuthStore.Login` 与 HTTP 登录路由目前没有失败计数、临时锁定和登录限流;本轮如实记录缺口,没有改动账号密码或认证策略。
|
||||||
|
|
||||||
|
第三轮持久部署已核验:`/version` 修订 `23db3d8fe0de352be9e756f932b12559ea746ff3`,`/health/ready` 为 200 ready。实际 18080 应用页内容 scrollTop=293 时,document scrollY=0、导航和页头 top=0。服务原账号、SDK、Token 和数据库保持不变。
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
loadScopeApplications().catch(error => showToast(`应用目录读取失败:${error.message}`));
|
loadScopeApplications().catch(error => showToast(`应用目录读取失败:${error.message}`));
|
||||||
}
|
}
|
||||||
text('scope-hint', ['workflows', 'retention', 'archives'].includes(next) ? '当前应用环境 · 不受队列筛选影响' : '任务、计划和订阅按所选队列显示');
|
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 (matchMedia('(max-width:640px)').matches) document.querySelector('.nav-item.active')?.scrollIntoView({inline:'nearest',block:'nearest'});
|
||||||
if (next === 'retention' && state.scopeReady) loadRetention();
|
if (next === 'retention' && state.scopeReady) loadRetention();
|
||||||
if (next === 'archives' && state.scopeReady) loadArchives();
|
if (next === 'archives' && state.scopeReady) loadArchives();
|
||||||
@@ -863,7 +863,7 @@
|
|||||||
if (!state.authenticated || !state.scopeReady) return;
|
if (!state.authenticated || !state.scopeReady) return;
|
||||||
if (lists[view]) await loadList(view, background === true);
|
if (lists[view]) await loadList(view, background === true);
|
||||||
else if (view === 'overview') await loadOverview();
|
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 === 'retention') await loadRetention();
|
||||||
else if (view === 'archives') await loadArchives();
|
else if (view === 'archives') await loadArchives();
|
||||||
text('last-updated', `更新于 ${formatTime(new Date())}`);
|
text('last-updated', `更新于 ${formatTime(new Date())}`);
|
||||||
|
|||||||
@@ -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 地址');
|
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`;
|
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 (ui.loading || ui.busy) return;
|
||||||
|
if (background && editingOrScrolling()) return;
|
||||||
ui.loading = true; $('access-error').textContent = '';
|
ui.loading = true; $('access-error').textContent = '';
|
||||||
try {
|
try {
|
||||||
const page = await api(ui.after ? `?after=${encodeURIComponent(ui.after)}` : ''); ui.next = page.next || ''; $('access-next').disabled = !ui.next;
|
const page = await api(ui.after ? `?after=${encodeURIComponent(ui.after)}` : '');
|
||||||
$('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>';
|
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();
|
filterApplications();
|
||||||
if (!ui.selected && page.items.length) await select(page.items[0].namespace_id);
|
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); }
|
} catch (error) { $('access-error').textContent = message(error); }
|
||||||
finally { ui.loading = false; }
|
finally { ui.loading = false; }
|
||||||
}
|
}
|
||||||
async function select(namespace) {
|
async function select(namespace, { background = false } = {}) {
|
||||||
if (namespace !== ui.selected) {
|
if (namespace !== ui.selected) {
|
||||||
|
detailSignature = '';
|
||||||
retirement = null; $('access-retire-form').hidden = true;
|
retirement = null; $('access-retire-form').hidden = true;
|
||||||
$('access-detail').hidden = true; $('access-empty').hidden = false;
|
$('access-detail').hidden = true; $('access-empty').hidden = false;
|
||||||
$('access-empty').textContent = '正在读取应用配置…';
|
$('access-empty').textContent = '正在读取应用配置…';
|
||||||
}
|
}
|
||||||
const requested = namespace, ticket = ++selectionSequence; ui.selected = namespace;
|
const requested = namespace, ticket = ++selectionSequence; ui.selected = namespace;
|
||||||
try {
|
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-empty').hidden = true; $('access-detail').hidden = false;
|
||||||
$('access-title').textContent = detail.entry.application; $('access-status').textContent = detail.entry.enabled ? '已启用' : '已停用';
|
$('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}`;
|
$('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-copy-template').textContent = availableCredential?.token_stored ? '查看完整接入配置 ↗' : '补存当前 Token ↗';
|
||||||
$('access-config-note').textContent = availableCredential?.token_stored ? '选择正确队列后,可查看、复制含当前 Token 的完整配置。' : '当前 Token 尚未补存。请从已保存的新配置导入当前 Token,再查看或复制;过渡期旧 Token 不能用于补存。';
|
$('access-config-note').textContent = availableCredential?.token_stored ? '选择正确队列后,可查看、复制含当前 Token 的完整配置。' : '当前 Token 尚未补存。请从已保存的新配置导入当前 Token,再查看或复制;过渡期旧 Token 不能用于补存。';
|
||||||
$('access-truncated').hidden = !detail.truncated;
|
$('access-truncated').hidden = !detail.truncated;
|
||||||
const now = Date.now();
|
|
||||||
$('access-credentials').innerHTML = detail.credentials.map(c => {
|
$('access-credentials').innerHTML = detail.credentials.map(c => {
|
||||||
const status = c.revoked_at ? '已撤销' : c.expires_at && Date.parse(c.expires_at) <= now ? '已到期' : '有效';
|
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;
|
const transitioning = !c.revoked_at && Date.parse(c.previous_valid_until) > now;
|
||||||
@@ -92,6 +108,7 @@
|
|||||||
}).join('') || '<p class="panel-description">该环境暂无凭据。</p>';
|
}).join('') || '<p class="panel-description">该环境暂无凭据。</p>';
|
||||||
if ($('access-help-dialog').open) renderCredentialHelp();
|
if ($('access-help-dialog').open) renderCredentialHelp();
|
||||||
document.querySelectorAll('[data-access-id]').forEach(el => el.classList.toggle('selected', el.dataset.accessId === namespace)); renderConfig();
|
document.querySelectorAll('[data-access-id]').forEach(el => el.classList.toggle('selected', el.dataset.accessId === namespace)); renderConfig();
|
||||||
|
detailSignature = signature;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (ticket !== selectionSequence) return;
|
if (ticket !== selectionSequence) return;
|
||||||
$('access-error').textContent = message(error);
|
$('access-error').textContent = message(error);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>Orbit Console · 星轨控制台</title>
|
<title>Orbit Console · 星轨控制台</title>
|
||||||
<link rel="stylesheet" href="/console/styles.css">
|
<link rel="stylesheet" href="/console/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="console-body">
|
||||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
|
|||||||
@@ -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: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: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(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(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}}
|
||||||
|
|||||||
Reference in New Issue
Block a user