系统运行概览
` - + `集中查看服务状态、资源用量与外部能力,数据默认每 10 秒自动更新。
diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3fd4b..26c009a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - Mermaid 图表采用更清晰的科研配色与更精致的图框,新生成的流程图还会按数据、处理、判断、结果等角色使用协调的语义色,减少单调的灰白图。 -- 管理后台总览按“当前运行”和“运营资源”重新组织,执行容量改为精简摘要并可从右侧详情面板查看队列、容器、用户占用与宿主资源,异常状态和近期指标也更容易识别。 +- 管理后台改为常驻的紧凑总览与“运行、用量、用户、资源、集成、质量”分组工作区;总览卡可直接定位到同口径详情,执行容量仍可从侧栏快速查看,桌面端和移动端都能更快进入具体管理内容。 ## 0.70.0 — 2026-09-02 diff --git a/PROGRESS.md b/PROGRESS.md index d10c0a9..f8a2429 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-09-02(0.70.0 正式发布:容器共享容量、专业软件节点、模型与交互更新) +最后更新:2026-09-03(Unreleased:管理后台紧凑总览与分组工作区) --- @@ -20,6 +20,8 @@ --- ## 已完成关键能力 +- **09-03 / Unreleased / 管理后台紧凑总览与分组工作区**:Admin 由纵向长页改为顶部常驻的两级总览和“运行、用量、用户、资源、集成、质量”六个页签,四项运行状态使用主卡、四项运营指标收进紧凑指标栏;点击总览可切换页签并精确定位同口径详情,任务、容量、用户活跃、Token/成本和存储均补齐详情指标。页签 hash 支持刷新与前进后退,桌面端页签吸顶,移动端顶栏操作收进菜单、运营指标与页签可横向滑动;1440px/390px 浏览器渲染、六页签与深链交互检查通过,无 API、schema、migration 或运行方式变化。 + - **09-02 / Unreleased / 单用户重型执行容量与排队提示**:共享执行容量保持整机 6、后台 4,单用户上限由 2 提升到 3;前台重型任务首次等待时通过 SSE 告知单用户、整机、内存压力或队列顺序原因,放行后恢复正常执行提示,排队仍不计入命令 timeout 且可取消。无 schema、migration 或 API 变化。 - **09-02 / Unreleased / Admin Provider API 凭据管理与余额提醒**:新增代码内静态可信 Provider 注册表和单行当前凭据控制面,0039 以 AES-GCM 字段级 AAD 密文保存;Admin 可查看 DB/env 来源与尾号、先测后原子替换、手动测试和删除覆盖,LLM/媒体/平台来源/语音均按新请求热解析。DeepSeek 每 30 分钟检查官方 CNY 余额,低于 ¥30 及明确 402/认证失败进入统一状态与邮件提醒,蓝绿实例以 PostgreSQL advisory lock 单轮选主;持续异常每日最多一次、恢复重置。未连接生产数据库、未调用真实第三方 API。 diff --git a/RUN.md b/RUN.md index c0d07a8..b14e923 100644 --- a/RUN.md +++ b/RUN.md @@ -177,7 +177,7 @@ 容器留着复用即可(alpine 版很轻);schema 升级后重跑一次 `db upgrade head`。 - **Auth env**:`PLATFORM_KEY` + `JWT_SECRET` 任一缺失 web 启动 fail-fast。生成随机串:`python -c "import secrets; print(secrets.token_urlsafe(48))"`。 - **用户管理**(`users.email/password_hash/role`,0005 UNIQUE(email)、0009 role):dev SPA 登录后端。发用户两条路径任选:CLI `main.py user add`(下方),或在登录页右下角"+ 管理员添加用户"链接(需先设 `ZCBOT_ADMIN_TOKEN` env,弹窗输入 email/密码/管理员口令/角色)。撤用户 `DELETE FROM users WHERE email=...`(先 DELETE 该 user 的 tasks)。**用户自助改密**:登录后顶栏「改密码」按钮(走 `POST /v1/auth/change_password`,需知道旧密码);改邮箱 / 用户忘了旧密码无法自助 → 手动 SQL(见故障兜底)。 -- **角色与管理后台**(`users.role` ∈ `user`/`admin`):admin 才显顶栏"管理"入口 → `/static/admin.html`(非 admin 403)。页面:左侧目录(点击滚到对应区)+ 运行态/任务/用户用量/按模型/各用户用量/存储;「按模型」「各用户用量」支持时间筛选(全部/近7天/近30天)+ 排序(按成本/按用量),「各用户用量」「存储」分页;顶栏「导出 PDF」走浏览器打印(在打印对话框选"另存为 PDF",列表取前 10)。提管理员 `main.py user role --email X --role admin`(改完即时生效,role 走 DB 查不进 JWT)。`ZCBOT_ADMIN_TOKEN` 是另一回事(发用户共享口令),与 role 互不相干。 +- **角色与管理后台**(`users.role` ∈ `user`/`admin`):admin 才显顶栏"管理"入口 → `/static/admin.html`(非 admin 403)。页面顶部常驻紧凑总览,下方按“运行/用量/用户/资源/集成/质量”页签切换;点击总览卡会进入对应页签并定位同口径详情。执行容量也可从详情侧栏查看;「按模型」「各用户用量」支持时间筛选(全部/近7天/近30天)+ 排序(按成本/按用量),「各用户用量」「存储」分页;顶栏「导出 PDF」走浏览器打印(在打印对话框选"另存为 PDF",列表取前 10)。提管理员 `main.py user role --email X --role admin`(改完即时生效,role 走 DB 查不进 JWT)。`ZCBOT_ADMIN_TOKEN` 是另一回事(发用户共享口令),与 role 互不相干。 --- diff --git a/tests/test_static_vendor.py b/tests/test_static_vendor.py index 3ab1148..3315a3f 100644 --- a/tests/test_static_vendor.py +++ b/tests/test_static_vendor.py @@ -96,12 +96,14 @@ class StaticVendorTests(unittest.TestCase): self.assertIn("永久删除", admin_js) self.assertNotIn("localStorage.setItem", admin_js) - def test_admin_consolidates_overview_into_eight_summary_cards(self) -> None: + def test_admin_uses_compact_overview_and_grouped_workspace_tabs(self) -> None: html = ADMIN_HTML.read_text(encoding="utf-8") admin_js = (JS_DIR / "admin.js").read_text(encoding="utf-8") self.assertIn('id="ops-summary" class="ops-summary"', admin_js) self.assertIn(".ops-summary {", html) + self.assertIn('class="summary-primary"', admin_js) + self.assertIn('class="summary-secondary"', admin_js) labels = ( "任务运行", "执行容量", @@ -117,24 +119,30 @@ class StaticVendorTests(unittest.TestCase): self.assertIn(f'label: "{label}"', admin_js) positions.append(admin_js.index(f'label: "{label}"')) self.assertEqual(positions, sorted(positions)) - self.assertNotIn("function renderRuntime", admin_js) - self.assertNotIn("function renderTasks", admin_js) - self.assertNotIn("function renderUsersAndUsage", admin_js) - self.assertIn('["s-overview", "总览"]', admin_js) - self.assertIn('["s-usage", "用量趋势"]', admin_js) + self.assertIn("function renderRuntimeDetails()", admin_js) + self.assertIn("function renderCapacitySection()", admin_js) + self.assertIn("function renderUsageSection(d)", admin_js) + for tab in ("运行", "用量", "用户", "资源", "集成", "质量"): + self.assertIn(f'"{tab}"', admin_js) + self.assertIn('class="workspace-tabs"', admin_js) + self.assertIn('role="tabpanel"', admin_js) + self.assertIn('tab: "run", target: "s-runtime"', admin_js) + self.assertIn('tab: "run", target: "s-sandbox-capacity"', admin_js) + self.assertIn('tab: "resources", target: "s-windows-node"', admin_js) + self.assertIn('tab: "quality", target: "s-toolfail"', admin_js) + self.assertIn('tab: "users", target: "s-users"', admin_js) + self.assertIn('tab: "usage", target: "s-usage"', admin_js) + self.assertIn('tab: "resources", target: "s-storage"', admin_js) self.assertIn('unit: softwareNodesLoaded ? "在线 / 总数"', admin_js) self.assertIn('value: fmtTokens(tokens7d), unit: "近 7 天"', admin_js) self.assertIn("storageTotals.physical_bytes", admin_js) self.assertIn("repeat(auto-fit, minmax(190px, 1fr))", html) self.assertIn('apiGet("/v1/admin/storage/users?page=0&page_size=1")', admin_js) - self.assertIn('target: "s-windows-node"', admin_js) - self.assertIn('target: "s-toolfail"', admin_js) - self.assertNotIn('"s-sandbox-capacity"', admin_js) - self.assertNotIn("function renderSandboxCapacity", admin_js) - self.assertIn('["s-sandbox-packages", "容器依赖"]', admin_js) + self.assertIn('id="s-sandbox-capacity"', admin_js) + self.assertIn('id="s-sandbox-packages"', admin_js) self.assertIn('sandboxCapacityData = await apiGet("/v1/admin/sandbox/capacity")', admin_js) self.assertNotIn("containerStatusHTML(capacity)", admin_js) - self.assertIn('data-summary-action="${escapeHtml(action)}"', admin_js) + self.assertIn('data-summary-tab="${escapeHtml(tab)}"', admin_js) self.assertIn("function renderCapacityDrawer()", admin_js) self.assertIn("function openCapacityDrawer()", admin_js) self.assertIn('id="capacity-drawer" class="capacity-drawer"', html) diff --git a/web/static/admin.html b/web/static/admin.html index 21eea1c..77e0b24 100644 --- a/web/static/admin.html +++ b/web/static/admin.html @@ -43,6 +43,9 @@ header .spacer { flex: 1; } header .meta { color: var(--muted); font-size: 12px; font-family: var(--mono); } header .actions { display: flex; align-items: center; gap: 6px; } + .action-menu { display: contents; } + .action-menu > summary { display: none; } + .action-menu-body { display: flex; align-items: center; gap: 6px; } header a, header button { min-height: 30px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 550; color: var(--text); text-decoration: none; @@ -58,28 +61,9 @@ .msg { padding: 28px 14px; text-align: center; color: var(--muted); } .msg a { color: var(--accent); } - /* 目录 + 内容两栏;目录 sticky 跟随滚动 */ - #layout { display: grid; grid-template-columns: 158px minmax(0, 1fr); gap: 14px; align-items: start; } - #toc { - position: sticky; top: 70px; display: flex; flex-direction: column; gap: 3px; - padding: 8px; background: rgba(255,255,255,.88); border: 1px solid var(--border); - border-radius: var(--r-lg); box-shadow: var(--shadow-soft); - } - #toc .toc-label { padding: 4px 9px 7px; color: #8a9299; font-size: 10px; font-weight: 700; letter-spacing: .8px; } - #toc a { - position: relative; display: flex; align-items: center; gap: 8px; min-height: 34px; - color: var(--muted); text-decoration: none; font-size: 12.5px; padding: 6px 8px; - border-radius: 6px; border: 1px solid transparent; transition: .15s ease; - } - #toc .nav-index { width: 20px; flex: 0 0 auto; color: #9aa2a9; font: 10px/1 var(--mono); } - #toc a > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - #toc a:hover { background: var(--panel-soft); color: var(--text-strong); } - #toc a.active { color: var(--accent); border-color: #f0d5d2; background: var(--accent-soft); font-weight: 650; } - #toc a.active::before { content: ""; position: absolute; left: -1px; top: 8px; bottom: 8px; width: 3px; border-radius: 0 3px 3px 0; background: var(--accent); } - #toc a.active .nav-index { color: var(--accent); } - #content { min-width: 0; } + #layout, #content { min-width: 0; } .page-intro { - position: relative; overflow: hidden; margin-bottom: 10px; padding: 13px 16px; + position: relative; overflow: hidden; padding: 12px 16px 10px; border: 1px solid #e6d9d7; border-radius: var(--r-lg); background: linear-gradient(120deg, #fff 0%, #fff 54%, #fdf4f2 100%); box-shadow: var(--shadow-soft); } @@ -87,22 +71,29 @@ content: ""; position: absolute; right: -55px; top: -75px; width: 170px; height: 170px; border: 32px solid rgba(184,58,49,.045); border-radius: 50%; pointer-events: none; } - .page-intro h1 { position: relative; margin: 0; color: var(--text-strong); font-size: 17px; line-height: 1.3; } - .page-intro p { position: relative; margin: 3px 0 0; color: var(--muted); font-size: 11px; } - .anchor { scroll-margin-top: 68px; } + .overview-heading { position: relative; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } + .page-intro h1 { margin: 0; color: var(--text-strong); font-size: 17px; line-height: 1.3; } + .page-intro p { margin: 2px 0 0; color: var(--muted); font-size: 11px; } + .overview-health { position: relative; flex: 0 0 auto; } + .anchor { scroll-margin-top: 116px; } - /* 首屏运营摘要:第一排业务运行,第二排基础设施与质量。 */ - .ops-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; } + /* 总览分两级:运行状态使用主卡,运营指标使用紧凑指标栏。 */ + .ops-summary { position: relative; margin-top: 10px; } + .summary-primary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } + .summary-secondary { + display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 8px; + overflow: hidden; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--panel); + } .summary-card { - min-width: 0; min-height: 142px; display: flex; flex-direction: column; padding: 13px 14px 11px; + min-width: 0; min-height: 82px; display: flex; flex-direction: column; padding: 10px 12px; color: inherit; text-decoration: none; background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow-soft); transition: .15s ease; text-align: left; } button.summary-card { width: 100%; appearance: none; font: inherit; cursor: pointer; } - .summary-card[href]:hover, button.summary-card:hover { border-color: #c9ced2; box-shadow: 0 3px 10px rgba(18,24,30,.06); transform: translateY(-1px); } - .summary-card[href]:focus-visible, button.summary-card:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; border-color: var(--accent); } + button.summary-card:hover { border-color: #c9ced2; box-shadow: 0 3px 10px rgba(18,24,30,.06); transform: translateY(-1px); } + button.summary-card:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; border-color: var(--accent); } .summary-card .label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } - .summary-card .label { min-width: 0; color: #626b73; font-size: 13px; font-weight: 650; } + .summary-card .label { min-width: 0; color: #626b73; font-size: 12px; font-weight: 650; } .summary-card-actions { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 6px; } .summary-card .jump { flex: 0 0 auto; color: #929aa1; font-size: 14px; } .summary-detail { color: #899198; font-size: 10px; white-space: nowrap; } @@ -113,10 +104,10 @@ .summary-badge.ok { color: var(--ok); background: #e8f5e9; } .summary-badge.warn { color: var(--warn); background: #fff0d8; } .summary-badge.danger { color: var(--danger); background: var(--accent-soft); } - .summary-card .summary-value { margin-top: 6px; color: var(--text-strong); font: 680 28px/1.15 var(--mono); letter-spacing: -.7px; } - .summary-card .summary-value .unit { color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: 0; white-space: nowrap; } - .summary-card .summary-sub { margin-top: 5px; overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } - .summary-card .summary-meta { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; margin-top: auto; padding-top: 8px; color: var(--muted); font-size: 11px; } + .summary-card .summary-value { margin-top: 3px; color: var(--text-strong); font: 680 22px/1.12 var(--mono); letter-spacing: -.5px; } + .summary-card .summary-value .unit { color: var(--muted); font-size: 10px; font-weight: 600; letter-spacing: 0; white-space: nowrap; } + .summary-card .summary-sub { margin-top: 3px; overflow: hidden; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } + .summary-card .summary-meta { display: none; } .summary-card .summary-meta span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .summary-card .summary-meta span:last-child { text-align: right; } .summary-card .summary-meter { height: 4px; margin-top: 6px; overflow: hidden; border-radius: 999px; background: #e8ebed; } @@ -127,6 +118,47 @@ .summary-card.danger { border-color: #edc1bd; background: var(--accent-soft); } .summary-card.danger .summary-value { color: var(--danger); } .summary-card.danger .summary-meter > i { background: var(--danger); } + .summary-secondary .summary-card { + min-height: 58px; padding: 8px 12px; border: 0; border-right: 1px solid var(--border-soft); + border-radius: 0; box-shadow: none; + } + .summary-secondary .summary-card:last-child { border-right: 0; } + .summary-secondary .summary-card .summary-value { margin-top: 2px; font-size: 18px; } + .summary-secondary .summary-card .summary-sub, + .summary-secondary .summary-card .summary-badge { display: none; } + .summary-secondary .summary-card:hover { transform: none; box-shadow: none; background: var(--panel-soft); } + + /* 总览下方是唯一工作区;tab 保持可见,panel 只显示当前分组。 */ + .workspace { margin-top: 10px; } + .workspace-tabs { + position: sticky; top: 54px; z-index: 6; display: flex; gap: 3px; padding: 5px; + overflow-x: auto; border: 1px solid var(--border); border-radius: var(--r-lg); + background: rgba(255,255,255,.94); backdrop-filter: blur(10px); box-shadow: var(--shadow-soft); + scrollbar-width: none; + } + .workspace-tabs::-webkit-scrollbar { display: none; } + .workspace-tab { + min-width: 76px; min-height: 34px; flex: 1 0 auto; padding: 5px 13px; border: 1px solid transparent; + border-radius: 6px; color: var(--muted); background: transparent; font-size: 12px; font-weight: 600; cursor: pointer; + } + .workspace-tab:hover { color: var(--text-strong); background: var(--panel-soft); } + .workspace-tab[aria-selected="true"] { color: var(--accent); border-color: #f0d5d2; background: var(--accent-soft); } + .tab-panel { padding-top: 10px; } + .tab-panel[hidden] { display: none; } + .tab-intro { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 2px 2px 9px; } + .tab-intro h2 { margin: 0; color: var(--text-strong); font-size: 15px; } + .tab-intro p { margin: 0; color: var(--muted); font-size: 11px; text-align: right; } + .detail-anchor { scroll-margin-top: 108px; } + .detail-anchor.focus-pulse { animation: focus-pulse 1.2s ease; } + @keyframes focus-pulse { 0%, 100% { opacity: 1; } 35% { opacity: .62; } } + .detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } + .detail-stat { min-width: 0; padding: 11px 12px; border: 1px solid var(--border-soft); border-radius: var(--r-lg); background: var(--panel-soft); } + .detail-stat span { display: block; color: var(--muted); font-size: 10px; } + .detail-stat strong { display: block; margin-top: 2px; color: var(--text-strong); font: 700 20px/1.2 var(--mono); } + .detail-stat.warn { border-color: #efd8b7; background: #fffaf2; } + .detail-stat.danger { border-color: #edc1bd; background: var(--accent-soft); } + .detail-stat.warn strong { color: var(--warn); } + .detail-stat.danger strong { color: var(--danger); } .card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .card-head h2 { margin: 0; } .ctrl { display: flex; gap: 6px; } @@ -239,6 +271,11 @@ font-size: 12px; padding: 4px 8px; border: 1px solid var(--border); border-radius: var(--r-md); background: #fff; cursor: pointer; } + #s-sandbox-capacity button { + min-height: 30px; padding: 4px 9px; border: 1px solid var(--border); + border-radius: var(--r-md); color: var(--text); background: #fff; cursor: pointer; + } + #s-sandbox-capacity button:disabled { opacity: .45; cursor: default; } #s-provider-credentials button:disabled { opacity: .45; cursor: default; } .capacity-drawer.show { display: flex; } .capacity-drawer-panel { @@ -384,23 +421,35 @@ } select:focus, input:focus, textarea:focus { border-color: var(--accent) !important; box-shadow: 0 0 0 3px var(--accent-ring); outline: none; } - @media (min-width: 821px) and (max-width: 1240px) { - #layout { grid-template-columns: 148px minmax(0, 1fr); gap: 12px; } - #toc .nav-index { display: none; } - .ops-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } + @media (min-width: 821px) and (max-width: 1100px) { + .summary-primary { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 820px) { main { padding: 10px; } .grid { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); } /* header 紧凑化:缩 padding/gap、字号,gen-at 时间戳截断不撑宽 */ - header { padding: 8px 10px; gap: 8px; flex-wrap: wrap; } + header { min-height: 52px; padding: 8px 10px; gap: 8px; flex-wrap: nowrap; } header .title { font-size: 14px; } header .eyebrow { display: none; } header .meta { font-size: 11px; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } header a, header button { padding: 4px 8px; } - header .actions { order: 3; width: 100%; overflow-x: auto; padding-bottom: 1px; } - header .actions > * { flex: 0 0 auto; } + header .actions { position: relative; flex: 0 0 auto; } + .action-menu { position: relative; display: block; } + .action-menu > summary { + min-height: 30px; display: inline-flex; align-items: center; padding: 4px 8px; + border: 1px solid var(--border); border-radius: var(--r-md); background: #fff; + color: var(--text); font-size: 12px; font-weight: 550; cursor: pointer; list-style: none; + } + .action-menu > summary::-webkit-details-marker { display: none; } + .action-menu-body { + position: absolute; top: calc(100% + 7px); right: 0; width: 170px; display: none; + padding: 7px; border: 1px solid var(--border); border-radius: var(--r-lg); background: #fff; + box-shadow: 0 10px 26px rgba(18,24,30,.14); + } + .action-menu[open] .action-menu-body { display: grid; gap: 5px; } + .action-menu-body > * { width: 100%; justify-content: flex-start; } /* card-head 允许换行,标题长 + 下拉不再撑出横向溢出 */ .card-head { flex-wrap: wrap; } .ctrl { flex-wrap: wrap; } @@ -408,28 +457,31 @@ .ext-form-grid { grid-template-columns: 1fr; } .ext-form-grid > label, .ext-form-section { grid-column: 1 !important; } .node-code-wrap { flex-direction: column; } - /* 窄屏:目录变顶部横向 chip 条 */ - #layout { grid-template-columns: 1fr; gap: 10px; } - #toc { - flex-direction: row; overflow-x: auto; gap: 6px; top: 109px; - background: rgba(244,246,248,.94); backdrop-filter: blur(8px); padding: 6px 0; z-index: 5; - border: 0; border-radius: 0; box-shadow: none; - } - #toc .toc-label, #toc .nav-index { display: none; } - #toc a { min-height: 32px; white-space: nowrap; border: 1px solid var(--border); background: #fff; } - #toc a.active { border-color: var(--accent); } .page-intro { padding: 12px 14px; } .page-intro h1 { font-size: 17px; } - .ops-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .summary-card { min-height: 140px; } - .summary-card .summary-value { font-size: 26px; } - .anchor { scroll-margin-top: 150px; } + .summary-primary { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .summary-card { min-height: 76px; padding: 9px 10px; } + .summary-card .summary-value { font-size: 20px; } + .summary-secondary { display: flex; overflow-x: auto; scroll-snap-type: x proximity; scrollbar-width: none; } + .summary-secondary::-webkit-scrollbar { display: none; } + .summary-secondary .summary-card { min-width: 145px; flex: 1 0 42%; scroll-snap-align: start; } + .workspace-tabs { top: 52px; margin-inline: -10px; padding-inline: 10px; border-inline: 0; border-radius: 0; } + .workspace-tab { min-width: 68px; } + .tab-intro { display: block; } + .tab-intro p { margin-top: 2px; text-align: left; } + .detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .anchor, .detail-anchor { scroll-margin-top: 100px; } .card { padding: 12px; } } @media (max-width: 480px) { - .ops-summary { grid-template-columns: 1fr; } - .summary-card { min-height: 138px; } + header .meta { display: none; } + .overview-heading p { max-width: 250px; } + .summary-card .summary-badge { padding-inline: 5px; } + .summary-detail { display: none; } + .detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .detail-stat { padding: 9px 10px; } + .detail-stat strong { font-size: 18px; } .capacity-drawer { align-items: flex-end; } .capacity-drawer-panel { width: 100%; height: min(90vh, 760px); border: 0; border-radius: 14px 14px 0 0; } .capacity-hero { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -472,10 +524,15 @@
| ${r.scanned_at ? fmtTime(r.scanned_at) : "—"} | ` + ``; }).join("") || `||||||
| 无数据 | ||||||
| 用户 | 配额占用 | 占配额 | 文件数 | 回收站 | 物理合计 | 扫描于 |
|---|
集中查看服务状态、资源用量与外部能力,数据默认每 10 秒自动更新。
运行状态与关键运营指标,每 10 秒自动更新
${escapeHtml(tab[2])}