One Hub 翡翠管理台
One Hub(LLM 网关)的后台皮肤:Berry 模板的骨架 ×
Minimals 的色彩语言 —— 纯白底靠双层阴影分卡片、翡翠绿 #00A76F 主色、
墨色主按钮、灰底表头 + 虚线行分隔的明暗双态 MUI 管理台。demo 是纯 HTML/CSS 缩微复刻(非像素镜像),
真身的实现本体是 MUI v5 的 createTheme 三件套,配方见下节。
要点
- 色板抄 Minimals:主绿
#00A76F(hover 加深到#007867并追加0 8px 16px24% 同色投影)、灰阶以#919EAB为轴心展开(grey200#F4F6F8/ grey600#637381/ grey800#1C252E/ grey900#141A21)、语义色 error#FF5630、warning#FFAB00、info#00B8D9 - 默认按钮不用主色而用「墨色」:浅色态黑底白字(grey800),深色态整体反转成白底黑字;翡翠绿只留给页面唯一主操作和选中态
- 关掉 MUI 出厂感三件套:全局
disableElevation、textTransform: unset、按钮字重 700 - 圆角分层不是一个值走天下:基准 12px(面板/输入框),卡片和对话框翻倍到 24px(demo 统一取 16px 档),按钮/菜单项/图标按钮钉死 8px
- 卡片无边框,立体感全靠双层阴影公式:
0 0 2px 0 rgba(145,158,171,.2), 0 12px 24px -4px rgba(145,158,171,.12)——一圈 2px 描边感 + 一坨下坠柔影,明暗两态同一公式(深色态另加 1px 5% 白边) - 表格四件套:表头灰底(浅
#F4F6F8/ 深#28323D)且无下边框、行分隔一律 1px dashed、单元格居中对齐、末行分隔线转透明;分页条顶边也是 dashed - 侧栏 260px 纸面底 + 1px 实线右边框
rgba(145,158,171,.12);选中项 = 主色文字 + 主色 8% 底色(深色态文字转 primaryLight#5BE49B、底色加浓到 16%)+ 8px 圆角;hover 只上 4% 黑 - 明暗切换只翻 6 个令牌:bg
#fff↔#141A21、paper#fff↔#1C252E、表头#F4F6F8↔#28323D、正文#1C252E↔#fff、次级字#637381↔#919EAB、contained 按钮黑白反转;dividerrgba(145,158,171,.2)两态通用 - 状态与模型名一律 soft 胶囊:16% 透明度语义色打底 + 深一档同色系文字(深色态换浅一档),不用实心色块
- 字体链正文 Public Sans Variable、大标题 Barlow 800 字重;demo 用站内共享 Inter 近似(数字气质相当,中文回退系统字体)
- 顶栏图标按钮做成 38px 方形 + 8px 圆角 + 3% 黑微底,hover 加深到 6%,不用 MUI 默认的圆形涟漪
MUI 配方(真身实现)
以下代码摘自 one-hub 仓库(钉 commit 387f8bf,
内容冻结不随上游变),核心就三个文件:palette.js、typography.js、compStyleOverride.js,
由 themes/index.js 组装:
const themes = createTheme(themeOptions); // palette + typography + shape 先建题
themes.components = componentStyleOverrides(themeOption); // 组件覆盖整体替换,不做 merge
return themes;
值得学的组装手法:
components不塞进createTheme的入参,而是建完 theme 后整体替换, 覆盖函数里直接闭包引用色板对象(theme.colors?.primaryMain),绕开theme.palette回查 —— 换肤时只要换传入的 colors 就能重算整套组件样式。
色板的单一来源是一个 SCSS module(_themes-vars.module.scss),
用 :export 块同时喂给 SCSS 和 JS,改一处两边生效:
$primaryMain: #00A76F; // Minimals 翡翠绿
$grey800: #1C252E;
:export { primaryMain: $primaryMain; grey800: $grey800; /* ... */ }
「墨色主按钮」和签名阴影都在
compStyleOverride.js:
MuiButton: {
defaultProps: { color: 'inherit', disableElevation: true },
styleOverrides: {
root: { fontWeight: 700, borderRadius: '8px', textTransform: 'unset' },
contained: { // 默认按钮 = 墨色,且明暗态整体反转
color: isDark ? theme.colors?.grey800 : '#FFFFFF',
backgroundColor: isDark ? '#FFFFFF' : theme.colors?.grey800
},
containedPrimary: { // 主色按钮 hover 才出同色投影
'&:hover': {
backgroundColor: theme.colors?.primaryDark,
boxShadow: `0 8px 16px 0 ${varAlpha(theme.colors?.primaryMain, 0.24)}`
}
}
}
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: `${(theme?.customization?.borderRadius || 8) * 2}px`, // 卡片圆角 = 基准 ×2
boxShadow: `0 0 2px 0 ${varAlpha(theme.colors?.grey500, 0.2)}, 0 12px 24px -4px ${varAlpha(theme.colors?.grey500, 0.12)}`
}
}
},
MuiTableCell: {
styleOverrides: {
root: { borderBottomStyle: 'dashed', textAlign: 'center' }, // 虚线分隔 + 居中
head: {
backgroundColor: isDark ? '#28323D' : theme.colors?.grey200,
borderBottom: 'none', color: theme.darkTextSecondary, fontWeight: 600
}
}
},
MuiListItemButton: {
styleOverrides: {
root: {
borderRadius: '8px',
'&.Mui-selected': { // 侧栏选中:主色字 + 主色低透明底
color: theme.menuSelected, // 浅色态 primaryMain,深色态 primaryLight
backgroundColor: theme.menuSelectedBack // 主色 8%(深色态 16%)
}
}
}
}
这份覆盖表值得整个抄走的原因:它把「风格」全部下沉到了 theme 层,业务代码里见不到一行颜色 —— 表格虚线、按钮反转、菜单选中全是
styleOverrides声明出来的。给任何 MUI v5 项目换上这套components(加上面的色板),就得到 demo 里的观感;demo 的 CSS 变量则是同一套令牌的非 React 译本。
明暗双态不是两份主题文件,而是一个 GetLightOption() / GetDarkOption() 开关只翻语义令牌
(paper、backgroundDefault、menuSelected、headBackgroundColor 等十来个键),
色板本体不动 —— demo 里 :root / [data-theme="dark"] 两块 CSS 变量就是对这个结构的直译。
源码(折叠)
onehub-berry-admin-demo.html(自包含单页,含内联 style + script)
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>One Hub 翡翠管理台 · 风格缩微 demo</title>
<!-- 字体走全站共享 vendor:真身用 Public Sans Variable + Barlow,此处以 Inter 近似(数字气质相当,中文回退系统字体) -->
<link rel="stylesheet" href="/vendor/fonts/fonts.css">
<style>
/* ==== 设计令牌:抄自 one-hub web/src/assets/scss/_themes-vars.module.scss + themes/index.js ==== */
:root {
--primary: #00A76F; /* primaryMain(Minimals 翡翠绿) */
--primary-dark: #007867;
--primary-light: #5BE49B;
--secondary: #8E33FF;
--info: #00B8D9;
--warning: #FFAB00;
--error: #FF5630;
--bg: #FFFFFF; /* backgroundDefault:浅色态就是纯白,靠阴影分卡片 */
--paper: #FFFFFF;
--text: #1C252E; /* grey800 */
--text-2: #637381; /* grey600 */
--divider: rgba(145,158,171,.2); /* grey500 @ 20%,明暗两态通用 */
--border: rgba(145,158,171,.12); /* 侧栏右边框 */
--table-head: #F4F6F8; /* grey200 */
--hover: rgba(0,0,0,.04);
--chip-bg: rgba(145,158,171,.16);
--menu-sel: #00A76F; /* 浅色态选中 = primaryMain */
--menu-sel-bg: rgba(0,167,111,.08);
--menu-chip: #F4F6F8;
--btn-bg: #1C252E; /* contained 默认按钮:浅色态黑底白字 */
--btn-fg: #FFFFFF;
--btn-hover: #454F5B; /* grey700 */
--icon-btn-bg: rgba(0,0,0,.03);
--icon-btn-hover: rgba(0,0,0,.06);
--success-soft-bg: rgba(34,197,94,.16); --success-soft: #118D57;
--error-soft-bg: rgba(255,86,48,.16); --error-soft: #B71D18;
--warning-soft-bg: rgba(255,171,0,.16); --warning-soft: #B76E00;
/* 卡片阴影双层公式:一圈 2px 描边感 + 一坨下坠柔影,明暗两态同一公式 */
--card-shadow: 0 0 2px 0 rgba(145,158,171,.2), 0 12px 24px -4px rgba(145,158,171,.12);
}
[data-theme="dark"] {
--bg: #141A21; /* grey900 */
--paper: #1C252E; /* grey800 */
--text: #FFFFFF;
--text-2: #919EAB; /* grey500 */
--table-head: #28323D;
--hover: rgba(255,255,255,.08);
--menu-sel: #5BE49B; /* 深色态选中转 primaryLight,底加浓到 16% */
--menu-sel-bg: rgba(0,167,111,.16);
--menu-chip: #28323D;
--btn-bg: #FFFFFF; /* contained 默认按钮:深色态整个反转成白底黑字 */
--btn-fg: #1C252E;
--btn-hover: #DFE3E8; /* grey300 */
--icon-btn-bg: rgba(255,255,255,.05);
--icon-btn-hover: rgba(255,255,255,.1);
--success-soft: #77ED8B;
--error-soft: #FFAC82;
--warning-soft: #FFD666;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: 'Inter', 'Public Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 14px; line-height: 1.5;
background: var(--bg); color: var(--text);
-webkit-font-smoothing: antialiased;
transition: background .25s ease, color .25s ease;
}
.ic { width: 20px; height: 20px; flex: none; }
.ic-sm { width: 16px; height: 16px; flex: none; }
.app { display: flex; min-height: 100%; }
/* ==== 侧栏:260px 纸面底 + 1px 实线右边框(rgba(grey500,.12)) ==== */
.sidebar {
width: 260px; flex: none;
background: var(--paper);
border-right: 1px solid var(--border);
padding: 20px 16px;
display: flex; flex-direction: column; gap: 4px;
}
.logo { display: flex; align-items: center; gap: 10px; padding: 4px 8px 20px; }
.logo-mark {
width: 36px; height: 36px; border-radius: 12px;
background: linear-gradient(135deg, #00A76F, #007867);
display: grid; place-items: center; color: #fff;
}
.logo b { font-size: 17px; font-weight: 800; letter-spacing: .2px; }
.menu-caption {
font-size: 13px; font-weight: 600; color: var(--text-2);
padding: 6px 8px; margin-top: 8px;
}
.nav-item {
display: flex; align-items: center; gap: 14px;
padding: 9px 14px; border-radius: 8px;
font-size: 14px; font-weight: 500; color: var(--text);
cursor: pointer; user-select: none;
transition: background .15s ease, color .15s ease;
}
.nav-item:hover { background: var(--hover); color: var(--menu-sel); }
.nav-item.selected {
background: var(--menu-sel-bg); color: var(--menu-sel); font-weight: 600;
}
/* 侧栏底部额度小卡:menuChip 灰底 + 基准 12px 圆角 */
.quota-card {
margin-top: auto; background: var(--menu-chip);
border-radius: 12px; padding: 14px 16px;
}
.quota-card .cap { font-size: 12px; color: var(--text-2); }
.quota-card .val { font-size: 16px; font-weight: 700; margin: 2px 0 10px; }
.quota-bar { height: 6px; border-radius: 999px; background: var(--divider); overflow: hidden; }
.quota-bar i { display: block; height: 100%; width: 64%; border-radius: 999px; background: var(--primary); }
.shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* ==== 顶栏:64px,透明底,圆角 8px 的方形图标按钮 ==== */
.header {
height: 64px; flex: none;
display: flex; align-items: center; gap: 12px;
padding: 0 24px;
}
.icon-btn {
width: 38px; height: 38px; border: 0; border-radius: 8px;
background: var(--icon-btn-bg); color: var(--text);
display: grid; place-items: center; cursor: pointer;
transition: background .15s ease;
}
.icon-btn:hover { background: var(--icon-btn-hover); }
.crumb { font-size: 13px; color: var(--text-2); }
.crumb b { color: var(--text); font-weight: 600; }
.header .spacer { flex: 1; }
.bell { position: relative; }
.bell::after {
content: ''; position: absolute; top: 9px; right: 10px;
width: 7px; height: 7px; border-radius: 50%;
background: var(--error); border: 2px solid var(--bg);
}
.avatar {
width: 36px; height: 36px; border-radius: 50%;
background: linear-gradient(135deg, #5BE49B, #007867);
color: #fff; font-weight: 700; font-size: 14px;
display: grid; place-items: center; cursor: pointer;
}
.content { padding: 8px 24px 32px; display: flex; flex-direction: column; gap: 24px; }
.page-head { display: flex; align-items: flex-end; gap: 16px; }
.page-head h1 { font-size: 20px; font-weight: 800; line-height: 1.35; }
.page-head p { font-size: 13px; color: var(--text-2); margin-top: 2px; }
.page-head .spacer { flex: 1; }
/* ==== 按钮:全局无大写变换、无涟漪投影、字重 700、圆角钉 8px ==== */
.btn {
height: 38px; padding: 0 14px; border-radius: 8px;
font: inherit; font-size: 14px; font-weight: 700;
display: inline-flex; align-items: center; gap: 6px;
cursor: pointer; transition: all .15s ease;
}
.btn-contained { border: 0; background: var(--btn-bg); color: var(--btn-fg); }
.btn-contained:hover { background: var(--btn-hover); }
.btn-outlined {
border: 1px solid rgba(145,158,171,.32); background: transparent; color: var(--text);
}
.btn-outlined:hover { border-color: currentColor; box-shadow: 0 0 0 .75px currentColor; background: var(--hover); }
/* ==== 卡片:双层阴影 + 大圆角 + 无边框 ==== */
.card {
background: var(--paper); border-radius: 16px;
box-shadow: var(--card-shadow); overflow: hidden;
}
[data-theme="dark"] .card { border: 1px solid rgba(255,255,255,.05); }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.stat-card { padding: 20px 24px; display: flex; align-items: center; gap: 16px; }
.stat-card .meta { flex: 1; min-width: 0; }
.stat-card .cap { font-size: 13px; color: var(--text-2); }
.stat-card .num { font-size: 28px; font-weight: 800; line-height: 1.3; margin: 2px 0 6px; }
.pill {
display: inline-flex; align-items: center; gap: 4px;
font-size: 12px; font-weight: 600;
padding: 2px 8px; border-radius: 8px;
}
.pill.up { background: var(--success-soft-bg); color: var(--success-soft); }
.pill.down { background: var(--error-soft-bg); color: var(--error-soft); }
.charts { display: grid; grid-template-columns: 2fr 1fr; gap: 24px; }
.card-head { padding: 20px 24px 0; display: flex; align-items: center; }
.card-head h2 { font-size: 15px; font-weight: 700; }
.card-head .sub { font-size: 12px; color: var(--text-2); margin-top: 2px; }
.card-head .spacer { flex: 1; }
.card-body { padding: 16px 24px 24px; }
.donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 20px; }
.donut {
width: 170px; height: 170px; border-radius: 50%; position: relative;
background: conic-gradient(var(--primary) 0 38%, var(--secondary) 38% 64%, var(--warning) 64% 84%, var(--info) 84% 100%);
}
.donut::after {
content: '本周'; position: absolute; inset: 22%;
background: var(--paper); border-radius: 50%;
display: grid; place-items: center;
font-size: 13px; color: var(--text-2);
transition: background .25s ease;
}
.legend { width: 100%; display: flex; flex-direction: column; gap: 8px; }
.legend li { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.legend i { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.legend .spacer { flex: 1; }
.legend .pct { color: var(--text-2); font-weight: 600; }
/* ==== 表格:灰底表头无下边框、单元格居中、行间 1px dashed、末行透明 ==== */
table { width: 100%; border-collapse: separate; border-spacing: 0; }
thead th {
background: var(--table-head);
font-size: 13px; font-weight: 600; color: var(--text-2);
padding: 14px 12px; text-align: center; white-space: nowrap;
}
tbody td {
font-size: 14px; padding: 15px 12px; text-align: center;
border-bottom: 1px dashed var(--divider);
}
tbody tr { transition: background .2s ease; }
tbody tr:hover { background: var(--hover); }
tbody tr:last-child td { border-bottom-color: transparent; }
td.num { font-variant-numeric: tabular-nums; }
.chip {
display: inline-flex; align-items: center;
height: 24px; padding: 0 10px; border-radius: 8px;
font-size: 12px; font-weight: 600;
background: var(--chip-bg); color: var(--text);
}
.chip.ok { background: var(--success-soft-bg); color: var(--success-soft); }
.chip.err { background: var(--error-soft-bg); color: var(--error-soft); }
.chip.warn { background: var(--warning-soft-bg); color: var(--warning-soft); }
.pagination {
display: flex; align-items: center; justify-content: flex-end; gap: 18px;
min-height: 56px; padding: 0 16px;
border-top: 1px dashed var(--divider);
font-size: 13px; color: var(--text-2);
}
.pagination .icon-btn { width: 30px; height: 30px; background: transparent; }
.pagination .icon-btn:hover { background: var(--hover); }
.pagination .icon-btn[disabled] { opacity: .4; cursor: default; }
.pagination .icon-btn[disabled]:hover { background: transparent; }
text.axis { font-size: 11px; fill: var(--text-2); font-family: inherit; }
</style>
</head>
<body>
<!-- 图标库:内联 stroke 图标(tabler 风格,1.7px 圆头描边) -->
<svg style="display:none" xmlns="http://www.w3.org/2000/svg">
<defs>
<symbol id="i-hub-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<circle cx="12" cy="12" r="3.2"/><path d="M12 8.8V4M14.8 13.6l4 2.4M9.2 13.6l-4 2.4"/>
<circle cx="12" cy="3" r="1.4"/><circle cx="19.8" cy="16.6" r="1.4"/><circle cx="4.2" cy="16.6" r="1.4"/>
</symbol>
<symbol id="i-dash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="4" width="6" height="7" rx="2"/><rect x="4" y="15" width="6" height="5" rx="2"/>
<rect x="14" y="4" width="6" height="5" rx="2"/><rect x="14" y="13" width="6" height="7" rx="2"/>
</symbol>
<symbol id="i-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3v18h18"/><path d="M7 15l4-5 3 3 5-7"/>
</symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="6" rx="2"/><rect x="3" y="14" width="18" height="6" rx="2"/>
<path d="M7 7h.01M7 17h.01"/>
</symbol>
<symbol id="i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="15" r="4"/><path d="M10.8 12.2 20 3M17 6l2.5 2.5M13.5 9.5l2 2"/>
</symbol>
<symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 3v5h5"/><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/>
<path d="M9 13h6M9 17h4"/>
</symbol>
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="8" r="4"/><path d="M5 21c0-3.5 3-6 7-6s7 2.5 7 6"/>
</symbol>
<symbol id="i-adjust" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 4v6M6 14v6M12 4v2M12 10v10M18 4v10M18 18v2"/>
<circle cx="6" cy="12" r="2"/><circle cx="12" cy="8" r="2"/><circle cx="18" cy="16" r="2"/>
</symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
<path d="M4 6h16M4 12h16M4 18h16"/>
</symbol>
<symbol id="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M19.1 4.9l-1.4 1.4M6.3 17.7l-1.4 1.4"/>
</symbol>
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5z"/>
</symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6"/><path d="M10 19a2 2 0 0 0 4 0"/>
</symbol>
<symbol id="i-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 6l-6 6 6 6"/>
</symbol>
<symbol id="i-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 6l6 6-6 6"/>
</symbol>
<symbol id="i-trend" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 16l6-6 4 4 6-7"/>
</symbol>
</defs>
</svg>
<div class="app">
<!-- ============ 侧栏 ============ -->
<aside class="sidebar">
<div class="logo">
<span class="logo-mark"><svg class="ic"><use href="#i-hub-logo"/></svg></span>
<b>One Hub</b>
</div>
<div class="menu-caption">面板</div>
<div class="nav-item selected"><svg class="ic"><use href="#i-dash"/></svg>总览</div>
<div class="nav-item"><svg class="ic"><use href="#i-chart"/></svg>分析</div>
<div class="menu-caption">管理</div>
<div class="nav-item"><svg class="ic"><use href="#i-server"/></svg>渠道</div>
<div class="nav-item"><svg class="ic"><use href="#i-key"/></svg>令牌</div>
<div class="nav-item"><svg class="ic"><use href="#i-file"/></svg>日志</div>
<div class="nav-item"><svg class="ic"><use href="#i-user"/></svg>用户</div>
<div class="nav-item"><svg class="ic"><use href="#i-adjust"/></svg>设置</div>
<div class="quota-card">
<div class="cap">本月剩余额度</div>
<div class="val">$128.40 <span style="font-size:12px;font-weight:400;color:var(--text-2)">/ $200</span></div>
<div class="quota-bar"><i></i></div>
</div>
</aside>
<!-- ============ 右侧主体 ============ -->
<div class="shell">
<header class="header">
<button class="icon-btn" title="收起菜单"><svg class="ic"><use href="#i-menu"/></svg></button>
<span class="crumb">面板 / <b>总览</b></span>
<span class="spacer"></span>
<button class="icon-btn" id="theme-toggle" title="切换明暗">
<svg class="ic" id="ico-moon"><use href="#i-moon"/></svg>
<svg class="ic" id="ico-sun" style="display:none"><use href="#i-sun"/></svg>
</button>
<button class="icon-btn bell" title="通知"><svg class="ic"><use href="#i-bell"/></svg></button>
<span class="avatar">B</span>
</header>
<main class="content">
<div class="page-head">
<div>
<h1>总览</h1>
<p>欢迎回来,一起看看今天的网关流量</p>
</div>
<span class="spacer"></span>
<button class="btn btn-outlined">导出报表</button>
<button class="btn btn-contained">+ 新建令牌</button>
</div>
<!-- 统计卡:sparkline 三色对应真身 request/quota/token -->
<section class="stats">
<div class="card stat-card">
<div class="meta">
<div class="cap">今日请求数</div>
<div class="num">3,428</div>
<span class="pill up"><svg class="ic-sm"><use href="#i-trend"/></svg>+12.4%</span>
</div>
<svg width="96" height="40" viewBox="0 0 96 40" fill="none">
<path d="M4 30 C14 27, 18 32, 28 28 C38 24, 42 16, 52 19 C62 22, 68 12, 78 13 C84 13.5, 89 9, 92 10"
stroke="#60A5FA" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div class="card stat-card">
<div class="meta">
<div class="cap">今日消费</div>
<div class="num">$12.47</div>
<span class="pill up"><svg class="ic-sm"><use href="#i-trend"/></svg>+3.1%</span>
</div>
<svg width="96" height="40" viewBox="0 0 96 40" fill="none">
<path d="M4 26 C12 28, 20 20, 30 22 C40 24, 46 15, 56 17 C66 19, 74 11, 82 13 C87 14, 90 11, 92 12"
stroke="#FBBF24" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div class="card stat-card">
<div class="meta">
<div class="cap">今日 Tokens</div>
<div class="num">1.94 M</div>
<span class="pill down"><svg class="ic-sm" style="transform:scaleY(-1)"><use href="#i-trend"/></svg>-2.8%</span>
</div>
<svg width="96" height="40" viewBox="0 0 96 40" fill="none">
<path d="M4 18 C12 15, 20 24, 30 21 C40 18, 46 28, 56 25 C66 22, 74 30, 82 27 C87 25, 90 28, 92 27"
stroke="#F87171" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
</section>
<!-- 趋势 + 占比 -->
<section class="charts">
<div class="card">
<div class="card-head">
<div>
<h2>模型调用趋势</h2>
<div class="sub">最近 7 天请求量</div>
</div>
<span class="spacer"></span>
<span class="chip">近 7 天</span>
</div>
<div class="card-body">
<svg viewBox="0 0 640 240" width="100%" fill="none" style="display:block">
<defs>
<linearGradient id="area" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#00A76F" stop-opacity=".26"/>
<stop offset="100%" stop-color="#00A76F" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- 横向虚线网格 -->
<g stroke="var(--divider)" stroke-dasharray="3 4">
<line x1="44" y1="40" x2="620" y2="40"/>
<line x1="44" y1="80" x2="620" y2="80"/>
<line x1="44" y1="120" x2="620" y2="120"/>
<line x1="44" y1="160" x2="620" y2="160"/>
<line x1="44" y1="200" x2="620" y2="200"/>
</g>
<g text-anchor="end">
<text class="axis" x="36" y="44">8k</text>
<text class="axis" x="36" y="84">6k</text>
<text class="axis" x="36" y="124">4k</text>
<text class="axis" x="36" y="164">2k</text>
<text class="axis" x="36" y="204">0</text>
</g>
<path d="M50 132 C87 126, 106 118, 143 118 C180 118, 199 124, 236 124 C273 124, 293 88, 330 88 C367 88, 386 98, 423 98 C460 98, 479 62, 516 62 C553 62, 573 74, 610 74 L610 200 L50 200 Z"
fill="url(#area)"/>
<path d="M50 132 C87 126, 106 118, 143 118 C180 118, 199 124, 236 124 C273 124, 293 88, 330 88 C367 88, 386 98, 423 98 C460 98, 479 62, 516 62 C553 62, 573 74, 610 74"
stroke="#00A76F" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="516" cy="62" r="4" fill="#00A76F" stroke="var(--paper)" stroke-width="2"/>
<g text-anchor="middle">
<text class="axis" x="50" y="226">周一</text>
<text class="axis" x="143" y="226">周二</text>
<text class="axis" x="236" y="226">周三</text>
<text class="axis" x="330" y="226">周四</text>
<text class="axis" x="423" y="226">周五</text>
<text class="axis" x="516" y="226">周六</text>
<text class="axis" x="610" y="226">周日</text>
</g>
</svg>
</div>
</div>
<div class="card">
<div class="card-head"><h2>模型占比</h2></div>
<div class="card-body donut-wrap">
<div class="donut"></div>
<ul class="legend">
<li><i style="background:var(--primary)"></i>gpt-4o<span class="spacer"></span><span class="pct">38%</span></li>
<li><i style="background:var(--secondary)"></i>claude-sonnet<span class="spacer"></span><span class="pct">26%</span></li>
<li><i style="background:var(--warning)"></i>gemini-2.5<span class="spacer"></span><span class="pct">20%</span></li>
<li><i style="background:var(--info)"></i>其他<span class="spacer"></span><span class="pct">16%</span></li>
</ul>
</div>
</div>
</section>
<!-- 调用日志表格 -->
<section class="card">
<div class="card-head" style="padding-bottom:16px">
<div>
<h2>最近调用</h2>
<div class="sub">网关实时日志,每 10 秒刷新</div>
</div>
<span class="spacer"></span>
<button class="btn btn-outlined" style="height:32px;font-size:13px">刷新</button>
</div>
<table>
<thead>
<tr><th>时间</th><th>模型</th><th>渠道</th><th>提示</th><th>补全</th><th>花费</th><th>状态</th></tr>
</thead>
<tbody>
<tr>
<td class="num">14:32:18</td><td><span class="chip">gpt-4o</span></td><td>OpenAI-主力</td>
<td class="num">1,204</td><td class="num">356</td><td class="num">$0.0214</td>
<td><span class="chip ok">成功</span></td>
</tr>
<tr>
<td class="num">14:31:55</td><td><span class="chip">claude-sonnet-4-5</span></td><td>Anthropic-直连</td>
<td class="num">3,872</td><td class="num">1,204</td><td class="num">$0.0862</td>
<td><span class="chip ok">成功</span></td>
</tr>
<tr>
<td class="num">14:31:02</td><td><span class="chip">gemini-2.5-pro</span></td><td>Gemini-备用</td>
<td class="num">2,048</td><td class="num">512</td><td class="num">$0.0387</td>
<td><span class="chip warn">重试</span></td>
</tr>
<tr>
<td class="num">14:30:47</td><td><span class="chip">gpt-4o-mini</span></td><td>OpenAI-主力</td>
<td class="num">864</td><td class="num">220</td><td class="num">$0.0009</td>
<td><span class="chip ok">成功</span></td>
</tr>
<tr>
<td class="num">14:29:33</td><td><span class="chip">deepseek-v3</span></td><td>DeepSeek-低价</td>
<td class="num">5,120</td><td class="num">2,048</td><td class="num">$0.0041</td>
<td><span class="chip err">失败</span></td>
</tr>
</tbody>
</table>
<div class="pagination">
<span>每页行数:10</span>
<span>1–5 / 42</span>
<button class="icon-btn" disabled><svg class="ic-sm"><use href="#i-left"/></svg></button>
<button class="icon-btn"><svg class="ic-sm"><use href="#i-right"/></svg></button>
</div>
</section>
</main>
</div>
</div>
<script>
// 明暗切换:只翻转 data-theme,全部样式差异都在 CSS 变量层
const root = document.documentElement;
const syncThemeIcon = () => {
const dark = root.dataset.theme === 'dark';
document.getElementById('ico-moon').style.display = dark ? 'none' : '';
document.getElementById('ico-sun').style.display = dark ? '' : 'none';
};
syncThemeIcon();
document.getElementById('theme-toggle').addEventListener('click', () => {
root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark';
syncThemeIcon();
});
// 侧栏选中态(仅演示样式)
document.querySelectorAll('.nav-item').forEach((item) => {
item.addEventListener('click', () => {
document.querySelector('.nav-item.selected')?.classList.remove('selected');
item.classList.add('selected');
});
});
</script>
</body>
</html>