feat(安全): 对外暴露面加固 + 界面去 AI 化(v1.5.0)
界面(去 AI 味): - 大屏页清除 114 处生成器残留属性 data-page-node-id - 视觉系统改回工程控制台风格:去 radial/linear-gradient、去辉光、 去标题前彩色装饰条,改为中性灰阶 + 单一蓝色强调色;KPI 色条改状态点 - 精简各页说教式长提示;修掉 profile.html 泄漏到页面上的 Markdown 星号 - 删除登录页过时的「默认账号 admin / admin123」提示(1.4.0 起已无默认口令) 安全与隐私(按「将会被公网访问」收口): - 内部异常只回 8 位事件号,完整堆栈进服务端日志(web/api.py::_internal) - 导出文件名收敛:防响应头注入与路径穿越;manage.py passwd 补用户名校验 - 登录对不存在的账号也走一次哑哈希,抹平用户名枚举的时序差异 - /api/* 读接口限速 240 次 / 60 秒 / 账号(挡住循环调 /api/bundle) - 进程 umask 0077 + 目录 0700 / 文件 0600:对话正文与主密钥的落盘权限 - 表名与库文件路径只对管理员下发;大屏页所有数据插值转义 - --debug 只允许绑定回环地址;新增 Permissions-Policy 与 413 处理器 文档: - DEPLOYMENT 新增第十三节「安全与隐私基线」;迁移表补 1.4.0 → 1.5.0 行 - SECURITY 更新支持范围、新增「信息泄漏收敛」小节与上线检查项 - .codebuddy/ 加入 .gitignore(助手工作记忆不进仓库) 版本:1.4.0 → 1.5.0(无库结构变更,user_version 仍为 4) 验证:python tools/smoke.py → ok=264 fail=0;python tools/check_docs.py → 0 处问题
这个提交包含在:
+108
-125
@@ -1,41 +1,49 @@
|
||||
/* ============================================================
|
||||
WorkBuddy Portal —— 统一视觉系统
|
||||
设计令牌与大屏页(static/dashboard/index.html)保持一致,
|
||||
两处用同一套色板/圆角/间距,避免后台与大屏像两个产品。
|
||||
风格取向:工程控制台。中性灰阶打底,单一强调色(蓝),
|
||||
不用渐变、不用辉光、不做彩色装饰条 —— 信息本身是唯一的主角。
|
||||
设计令牌与大屏页(static/dashboard/index.html)共用一套,
|
||||
两处颜色/圆角/间距保持一致,避免后台与大屏像两个产品。
|
||||
|
||||
约定:模板只使用类名,不依赖任何渐变或阴影细节;
|
||||
因此调整本文件不需要改动任何 Jinja 模板。
|
||||
============================================================ */
|
||||
:root {
|
||||
/* 底色与层次 */
|
||||
--bg: #080d1a;
|
||||
--panel: rgba(255, 255, 255, .045);
|
||||
--panel-hi: rgba(255, 255, 255, .07);
|
||||
--panel-dim: rgba(0, 0, 0, .30);
|
||||
--line: rgba(255, 255, 255, .09);
|
||||
--line-soft: rgba(255, 255, 255, .06);
|
||||
--bg: #0e1013; /* 页面底 */
|
||||
--bg-elev: #14171b; /* 顶栏 / 表头等抬升面 */
|
||||
--panel: #16191e; /* 卡片 */
|
||||
--panel-hi: #1d2128; /* 卡片内交互面(按钮、分段控件) */
|
||||
--panel-dim: #101318; /* 输入框 / 代码块 */
|
||||
|
||||
--line: #262b33; /* 常规分隔线 */
|
||||
--line-soft: #1f242b; /* 表格行分隔线 */
|
||||
|
||||
/* 文本 */
|
||||
--text: #e8edf7;
|
||||
--sub: #8b9bb4;
|
||||
--dim: #64748b;
|
||||
--text: #e4e7eb;
|
||||
--sub: #98a1ac;
|
||||
--dim: #6b7480;
|
||||
|
||||
/* 主色板(与大屏 ECharts PALETTE 同源) */
|
||||
--cyan: #22d3ee;
|
||||
--violet: #a78bfa;
|
||||
--amber: #fbbf24;
|
||||
--green: #34d399;
|
||||
--red: #f87171;
|
||||
--blue: #60a5fa;
|
||||
--pink: #f472b6;
|
||||
/* 强调色与语义色(低饱和,只做状态区分,不做视觉噪音) */
|
||||
--accent: #4c8df6;
|
||||
--accent-dim: rgba(76, 141, 246, .14);
|
||||
--cyan: #4aa8c0;
|
||||
--violet: #8b7ec8;
|
||||
--amber: #c9952f;
|
||||
--green: #4ba97b;
|
||||
--red: #cf6679;
|
||||
--blue: #4c8df6;
|
||||
--pink: #c07a9e;
|
||||
|
||||
/* 语义色(用量口径:升用琥珀、降用青,刻意不用红绿) */
|
||||
/* 语义色(用量口径:升用琥珀、降用蓝,刻意不用红绿) */
|
||||
--up: var(--amber);
|
||||
--down: var(--cyan);
|
||||
--down: var(--accent);
|
||||
|
||||
/* 形状与节奏 */
|
||||
--r-card: 14px;
|
||||
--r-ctl: 9px;
|
||||
--r-pill: 6px;
|
||||
--r-card: 8px;
|
||||
--r-ctl: 6px;
|
||||
--r-pill: 4px;
|
||||
--gap: 16px;
|
||||
--shadow: 0 12px 34px rgba(0, 0, 0, .42);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -44,10 +52,7 @@
|
||||
html, body { margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(1100px 600px at 12% -10%, rgba(34, 211, 238, .10), transparent 60%),
|
||||
radial-gradient(900px 500px at 100% 0%, rgba(167, 139, 250, .10), transparent 55%),
|
||||
var(--bg);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 13.5px/1.65 "Microsoft YaHei", "PingFang SC", system-ui, -apple-system,
|
||||
"Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
@@ -55,7 +60,7 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a { color: var(--cyan); text-decoration: none; transition: color .15s; }
|
||||
a { color: var(--accent); text-decoration: none; transition: color .15s; }
|
||||
a:hover { text-decoration: underline; }
|
||||
/* 表单控件必须显式继承字体:曾经写过 `font: 13px/1.5 inherit`,
|
||||
那是非法声明(简写里不能出现 inherit 作为字族),整条被浏览器丢弃,
|
||||
@@ -63,9 +68,9 @@ a:hover { text-decoration: underline; }
|
||||
button, input, select, textarea { font-family: inherit; font-size: 13px; }
|
||||
|
||||
code {
|
||||
background: rgba(255, 255, 255, .07); padding: 1px 5px; border-radius: 4px;
|
||||
background: var(--panel-hi); padding: 1px 5px; border-radius: 3px;
|
||||
font: 12px/1.5 ui-monospace, Consolas, "Cascadia Mono", monospace;
|
||||
color: var(--cyan);
|
||||
color: var(--text);
|
||||
}
|
||||
.mono, .logbox, .fullprompt {
|
||||
font-family: ui-monospace, Consolas, "Cascadia Mono", monospace;
|
||||
@@ -77,47 +82,43 @@ code {
|
||||
.muted { color: var(--sub); }
|
||||
.right { text-align: right; }
|
||||
/* 页面标题里嵌的关键数字(如总积分),给一点强调但不喧宾夺主 */
|
||||
.hl { color: var(--cyan); font-variant-numeric: tabular-nums; }
|
||||
.hl { color: var(--accent); font-variant-numeric: tabular-nums; }
|
||||
/* 0 积分的记录整行数字变暗,扫一眼就能跳过长尾 */
|
||||
.tbl td.zero { color: var(--dim); }
|
||||
|
||||
/* 可访问性:键盘焦点一定要看得见 */
|
||||
:focus-visible { outline: 2px solid rgba(34, 211, 238, .65); outline-offset: 2px; }
|
||||
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
/* ---------------- 顶栏 ---------------- */
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 18px;
|
||||
padding: 0 22px; height: 58px;
|
||||
background: rgba(10, 16, 28, .86);
|
||||
padding: 0 22px; height: 56px;
|
||||
background: var(--bg-elev);
|
||||
border-bottom: 1px solid var(--line);
|
||||
backdrop-filter: blur(10px);
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 9px; font-size: 14px; letter-spacing: .2px; }
|
||||
.brand { display: flex; align-items: center; gap: 9px; font-size: 14px; }
|
||||
.brand a { color: var(--text); }
|
||||
.brand a:hover { text-decoration: none; color: var(--cyan); }
|
||||
.brand a:hover { text-decoration: none; color: var(--accent); }
|
||||
.brand .dot {
|
||||
width: 9px; height: 9px; border-radius: 50%; background: var(--cyan);
|
||||
box-shadow: 0 0 12px var(--cyan); flex: 0 0 auto;
|
||||
width: 8px; height: 8px; border-radius: 2px; background: var(--accent);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.brand .ver { color: var(--dim); font-size: 11px; }
|
||||
|
||||
.topbar nav { display: flex; gap: 3px; margin-left: 6px; overflow-x: auto; }
|
||||
.topbar nav { display: flex; gap: 2px; margin-left: 6px; overflow-x: auto; }
|
||||
.topbar nav a {
|
||||
padding: 6px 13px; border-radius: var(--r-ctl); color: var(--sub);
|
||||
font-size: 13px; white-space: nowrap; position: relative;
|
||||
padding: 6px 12px; border-radius: var(--r-ctl); color: var(--sub);
|
||||
font-size: 13px; white-space: nowrap;
|
||||
}
|
||||
.topbar nav a:hover { background: var(--panel-hi); color: var(--text); text-decoration: none; }
|
||||
.topbar nav a.on {
|
||||
background: linear-gradient(135deg, rgba(34, 211, 238, .22), rgba(167, 139, 250, .22));
|
||||
color: #fff; font-weight: 600;
|
||||
}
|
||||
.topbar nav a.on { background: var(--accent-dim); color: var(--text); font-weight: 600; }
|
||||
.topbar .me { margin-left: auto; display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
|
||||
.topbar .who { color: var(--sub); font-size: 12.5px; }
|
||||
.topbar .who b { color: var(--text); font-weight: 600; }
|
||||
/* 用户名同时是「个人中心」入口,所以是 <a>:去掉下划线并给悬浮反馈 */
|
||||
.topbar .who { display: flex; align-items: center; gap: 6px; text-decoration: none; }
|
||||
.topbar .who:hover, .topbar .who:hover b { color: var(--cyan); }
|
||||
.topbar .who:hover, .topbar .who:hover b { color: var(--accent); }
|
||||
|
||||
/* ---------------- 布局 ---------------- */
|
||||
.wrap { max-width: 1480px; margin: 0 auto; padding: 20px 22px 60px; }
|
||||
@@ -126,14 +127,9 @@ code {
|
||||
gap: 14px; flex-wrap: wrap; margin-bottom: var(--gap);
|
||||
}
|
||||
.pagehead h1 {
|
||||
font-size: 20px; margin: 0; font-weight: 700; letter-spacing: .3px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-size: 19px; margin: 0; font-weight: 600; color: var(--text);
|
||||
}
|
||||
.pagehead h1::before {
|
||||
content: ""; width: 4px; height: 19px; border-radius: 2px;
|
||||
background: linear-gradient(180deg, var(--cyan), var(--violet)); flex: 0 0 auto;
|
||||
}
|
||||
.pagehead .lead { color: var(--sub); font-size: 12.5px; margin: 4px 0 0 14px; }
|
||||
.pagehead .lead { color: var(--sub); font-size: 12.5px; margin: 5px 0 0; }
|
||||
.pagehead .actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); margin-bottom: var(--gap); }
|
||||
.grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--gap); margin-bottom: var(--gap); }
|
||||
@@ -145,14 +141,9 @@ code {
|
||||
padding: 16px 18px; margin-bottom: var(--gap);
|
||||
}
|
||||
.card > h2, .card > .cardhead > h2 {
|
||||
font-size: 14px; margin: 0 0 12px; font-weight: 600; letter-spacing: .2px;
|
||||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
font-size: 13.5px; margin: 0 0 12px; font-weight: 600; color: var(--text);
|
||||
}
|
||||
.card > .cardhead > h2 { margin-bottom: 0; }
|
||||
.card > h2::before, .card > .cardhead > h2::before {
|
||||
content: ""; width: 3px; height: 13px; border-radius: 2px;
|
||||
background: var(--cyan); flex: 0 0 auto;
|
||||
}
|
||||
.cardhead {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px; flex-wrap: wrap; margin-bottom: 12px;
|
||||
@@ -161,7 +152,7 @@ code {
|
||||
.card h3 { font-size: 13px; margin: 18px 0 10px; font-weight: 600; color: var(--text); }
|
||||
.card h3:first-child { margin-top: 0; }
|
||||
.hint { color: var(--sub); font-size: 12px; line-height: 1.75; }
|
||||
.hint a { text-decoration: underline; text-decoration-color: rgba(34, 211, 238, .4); }
|
||||
.hint a { text-decoration: underline; }
|
||||
.card > .hint:last-child { margin-bottom: 0; }
|
||||
.sect-divider { border: 0; border-top: 1px solid var(--line); margin: 18px 0; }
|
||||
|
||||
@@ -172,18 +163,21 @@ code {
|
||||
}
|
||||
.kpi {
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-card);
|
||||
padding: 14px 16px; position: relative; overflow: hidden;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.kpi::after {
|
||||
content: ""; position: absolute; left: 0; top: 0; width: 3px; height: 100%;
|
||||
background: var(--c, var(--cyan));
|
||||
/* --c 由模板行内给出:只作为一个 8px 的状态点,不做整块着色 */
|
||||
.kpi > span {
|
||||
color: var(--sub); font-size: 12px; display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.kpi > span::before {
|
||||
content: ""; width: 8px; height: 8px; border-radius: 2px; flex: 0 0 auto;
|
||||
background: var(--c, var(--line));
|
||||
}
|
||||
.kpi > span { color: var(--sub); font-size: 12px; display: block; letter-spacing: .2px; }
|
||||
.kpi > b {
|
||||
display: block; font-size: 23px; margin: 7px 0 4px; font-weight: 700;
|
||||
font-variant-numeric: tabular-nums; letter-spacing: .3px;
|
||||
display: block; font-size: 22px; margin: 8px 0 4px; font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.kpi > i { color: var(--sub); font-size: 11.5px; font-style: normal; display: block; }
|
||||
.kpi > i { color: var(--sub); font-size: 11.5px; font-style: normal; display: block; line-height: 1.6; }
|
||||
.kpi > i .delta { color: var(--up); font-variant-numeric: tabular-nums; }
|
||||
.kpi > i .delta.dn { color: var(--down); }
|
||||
|
||||
@@ -191,7 +185,7 @@ code {
|
||||
/* 表格必须包在 .tablewrap 里:窄屏时横向滚动,而不是把卡片撑破 */
|
||||
.tablewrap { overflow-x: auto; margin: 0 -2px; }
|
||||
.tablewrap::-webkit-scrollbar { height: 8px; }
|
||||
.tablewrap::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .14); border-radius: 4px; }
|
||||
.tablewrap::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
|
||||
.tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||
.tbl th, .tbl td {
|
||||
padding: 9px 10px; text-align: left; border-bottom: 1px solid var(--line-soft);
|
||||
@@ -199,18 +193,17 @@ code {
|
||||
}
|
||||
.tbl th {
|
||||
color: var(--sub); font-weight: 500; font-size: 11.5px;
|
||||
white-space: nowrap; letter-spacing: .3px;
|
||||
position: sticky; top: 0; background: rgba(12, 18, 32, .96);
|
||||
backdrop-filter: blur(4px); z-index: 1;
|
||||
white-space: nowrap;
|
||||
position: sticky; top: 0; background: var(--bg-elev); z-index: 1;
|
||||
}
|
||||
.tbl tbody tr:hover { background: rgba(255, 255, 255, .028); }
|
||||
.tbl tbody tr:hover { background: var(--panel-hi); }
|
||||
.tbl tbody tr:last-child td { border-bottom: 0; }
|
||||
.tbl td.num, .tbl th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.tbl td.empty, .tbl td.empty:hover { text-align: center; color: var(--sub); padding: 28px; }
|
||||
.tbl td.empty, .tbl td.empty:hover { text-align: center; color: var(--sub); padding: 28px; background: none; }
|
||||
.tbl td.px { max-width: 460px; min-width: 220px; }
|
||||
.tbl details summary { cursor: pointer; color: var(--sub); }
|
||||
.tbl details summary:hover { color: var(--text); }
|
||||
.tbl details[open] summary { color: var(--cyan); }
|
||||
.tbl details[open] summary { color: var(--text); }
|
||||
|
||||
.scroll-y { max-height: 340px; overflow: auto; }
|
||||
|
||||
@@ -223,7 +216,7 @@ code {
|
||||
|
||||
.fullprompt {
|
||||
margin-top: 9px; padding: 11px 13px; background: var(--panel-dim);
|
||||
border: 1px solid var(--line); border-radius: 8px;
|
||||
border: 1px solid var(--line); border-radius: var(--r-ctl);
|
||||
white-space: pre-wrap; word-break: break-word; color: var(--text);
|
||||
font-size: 12px; line-height: 1.75; max-height: 320px; overflow: auto;
|
||||
}
|
||||
@@ -232,14 +225,14 @@ code {
|
||||
.tag {
|
||||
display: inline-block; padding: 1px 7px; border-radius: var(--r-pill);
|
||||
font-size: 11px; line-height: 17px; white-space: nowrap;
|
||||
background: rgba(255, 255, 255, .08); color: var(--sub); vertical-align: 1px;
|
||||
background: var(--panel-hi); color: var(--sub); vertical-align: 1px;
|
||||
}
|
||||
.tag.ok { background: rgba(52, 211, 153, .16); color: var(--green); }
|
||||
.tag.warn { background: rgba(251, 191, 36, .16); color: var(--amber); }
|
||||
.tag.bad { background: rgba(248, 113, 113, .16); color: var(--red); }
|
||||
.tag.info { background: rgba(96, 165, 250, .16); color: var(--blue); }
|
||||
.tag.accent { background: rgba(167, 139, 250, .18); color: var(--violet); }
|
||||
.tag.mute { background: rgba(255, 255, 255, .06); color: var(--dim); }
|
||||
.tag.ok { background: rgba(75, 169, 123, .16); color: var(--green); }
|
||||
.tag.warn { background: rgba(201, 149, 47, .16); color: var(--amber); }
|
||||
.tag.bad { background: rgba(207, 102, 121, .16); color: var(--red); }
|
||||
.tag.info { background: rgba(76, 141, 246, .16); color: var(--blue); }
|
||||
.tag.accent { background: rgba(139, 126, 200, .16); color: var(--violet); }
|
||||
.tag.mute { background: var(--panel-hi); color: var(--dim); }
|
||||
|
||||
.kbd {
|
||||
display: inline-block; padding: 0 6px; border-radius: 4px;
|
||||
@@ -252,19 +245,18 @@ code {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
padding: 7px 14px; border-radius: var(--r-ctl); border: 1px solid var(--line);
|
||||
background: var(--panel-hi); color: var(--text); font-size: 12.5px;
|
||||
cursor: pointer; transition: background .15s, border-color .15s, box-shadow .15s, filter .15s;
|
||||
cursor: pointer; transition: background .15s, border-color .15s;
|
||||
white-space: nowrap; text-decoration: none;
|
||||
}
|
||||
.btn:hover { background: rgba(255, 255, 255, .11); text-decoration: none; }
|
||||
.btn:hover { background: #232830; text-decoration: none; }
|
||||
.btn.primary {
|
||||
background: linear-gradient(135deg, rgba(34, 211, 238, .26), rgba(167, 139, 250, .26));
|
||||
border-color: rgba(34, 211, 238, .42); color: #fff; font-weight: 600;
|
||||
background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
|
||||
}
|
||||
.btn.primary:hover { border-color: var(--cyan); box-shadow: 0 0 14px rgba(34, 211, 238, .22); }
|
||||
.btn.primary:hover { background: #5e99f7; border-color: #5e99f7; }
|
||||
.btn.ghost { background: transparent; color: var(--sub); }
|
||||
.btn.ghost:hover { color: var(--text); background: var(--panel-hi); }
|
||||
.btn.danger { border-color: rgba(248, 113, 113, .34); color: var(--red); background: rgba(248, 113, 113, .10); }
|
||||
.btn.danger:hover { background: rgba(248, 113, 113, .18); }
|
||||
.btn.danger { border-color: rgba(207, 102, 121, .34); color: var(--red); background: rgba(207, 102, 121, .10); }
|
||||
.btn.danger:hover { background: rgba(207, 102, 121, .18); }
|
||||
.btn.sm { padding: 4px 10px; font-size: 12px; }
|
||||
.btn[disabled], .btn[aria-disabled=true] { opacity: .5; cursor: not-allowed; }
|
||||
.btnrow { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
@@ -273,14 +265,12 @@ form .btn { margin-top: 8px; }
|
||||
/* ---------------- 表单 ---------------- */
|
||||
label { display: block; color: var(--sub); font-size: 12px; }
|
||||
label input, label select, label textarea, .inp {
|
||||
width: 100%; margin-top: 5px; padding: 8px 10px; border-radius: 8px;
|
||||
width: 100%; margin-top: 5px; padding: 8px 10px; border-radius: var(--r-ctl);
|
||||
background: var(--panel-dim); border: 1px solid var(--line); color: var(--text);
|
||||
font-size: 13px; line-height: 1.5; outline: none; transition: border-color .15s, background .15s;
|
||||
}
|
||||
label input:hover, label select:hover, label textarea:hover { border-color: rgba(255, 255, 255, .16); }
|
||||
label input:focus, label select:focus, label textarea:focus {
|
||||
border-color: var(--cyan); background: rgba(0, 0, 0, .42);
|
||||
font-size: 13px; line-height: 1.5; outline: none; transition: border-color .15s;
|
||||
}
|
||||
label input:hover, label select:hover, label textarea:hover { border-color: #333a44; }
|
||||
label input:focus, label select:focus, label textarea:focus { border-color: var(--accent); }
|
||||
label textarea {
|
||||
font-family: ui-monospace, Consolas, "Cascadia Mono", monospace;
|
||||
font-size: 12px; line-height: 1.6; resize: vertical;
|
||||
@@ -293,7 +283,7 @@ label.row.inline { display: inline-flex; margin-bottom: 0; }
|
||||
label.row.inline > span { flex: 0 0 auto; }
|
||||
label .unit { color: var(--dim); font-size: 11.5px; margin-left: 8px; flex: 0 0 auto; }
|
||||
.field-err { color: var(--red); font-size: 11.5px; margin-top: 4px; }
|
||||
select option { background: #111a2e; color: var(--text); }
|
||||
select option { background: var(--bg-elev); color: var(--text); }
|
||||
|
||||
.filters { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
|
||||
.filters label { flex: 1 1 152px; }
|
||||
@@ -317,23 +307,17 @@ select option { background: #111a2e; color: var(--text); }
|
||||
font-size: 12.5px; cursor: pointer; transition: .15s; font-family: inherit;
|
||||
text-decoration: none; white-space: nowrap;
|
||||
}
|
||||
.seg a:hover, .seg button:hover { color: var(--text); background: rgba(255, 255, 255, .06); text-decoration: none; }
|
||||
.seg a.on, .seg button.on {
|
||||
background: linear-gradient(135deg, rgba(34, 211, 238, .28), rgba(167, 139, 250, .28));
|
||||
color: #fff; font-weight: 600;
|
||||
}
|
||||
.seg a:hover, .seg button:hover { color: var(--text); text-decoration: none; }
|
||||
.seg a.on, .seg button.on { background: var(--accent-dim); color: var(--text); font-weight: 600; }
|
||||
|
||||
/* ---------------- 分页 ---------------- */
|
||||
.pager { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; align-items: center; }
|
||||
.pager a, .pager .cur, .pager .gap {
|
||||
padding: 4px 11px; border-radius: 7px; border: 1px solid var(--line);
|
||||
padding: 4px 11px; border-radius: var(--r-ctl); border: 1px solid var(--line);
|
||||
font-size: 12px; color: var(--sub); min-width: 34px; text-align: center;
|
||||
}
|
||||
.pager a:hover { color: var(--text); border-color: rgba(34, 211, 238, .45); text-decoration: none; }
|
||||
.pager .cur {
|
||||
background: linear-gradient(135deg, rgba(34, 211, 238, .24), rgba(167, 139, 250, .24));
|
||||
color: #fff; border-color: transparent; font-weight: 600;
|
||||
}
|
||||
.pager a:hover { color: var(--text); border-color: var(--accent); text-decoration: none; }
|
||||
.pager .cur { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
|
||||
.pager .gap { border-color: transparent; color: var(--dim); }
|
||||
.pager .meta { margin-left: auto; color: var(--sub); font-size: 11.5px; }
|
||||
|
||||
@@ -342,18 +326,18 @@ select option { background: #111a2e; color: var(--text); }
|
||||
.flash {
|
||||
padding: 11px 14px; border-radius: var(--r-ctl); font-size: 12.5px;
|
||||
margin-bottom: 8px; border: 1px solid var(--line);
|
||||
background: rgba(34, 211, 238, .10); color: var(--text);
|
||||
background: var(--panel-hi); color: var(--text);
|
||||
line-height: 1.7;
|
||||
}
|
||||
.flash.error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .3); }
|
||||
.flash.warn { background: rgba(251, 191, 36, .12); border-color: rgba(251, 191, 36, .3); }
|
||||
.flash.ok { background: rgba(52, 211, 153, .12); border-color: rgba(52, 211, 153, .3); }
|
||||
.flash.error { background: rgba(207, 102, 121, .12); border-color: rgba(207, 102, 121, .30); }
|
||||
.flash.warn { background: rgba(201, 149, 47, .12); border-color: rgba(201, 149, 47, .30); }
|
||||
.flash.ok { background: rgba(75, 169, 123, .12); border-color: rgba(75, 169, 123, .30); }
|
||||
.flash:last-child { margin-bottom: 0; }
|
||||
.logbox {
|
||||
background: rgba(0, 0, 0, .40); border: 1px solid var(--line); border-radius: var(--r-ctl);
|
||||
background: var(--panel-dim); border: 1px solid var(--line); border-radius: var(--r-ctl);
|
||||
padding: 12px 14px; max-height: 430px; overflow: auto; margin: 0;
|
||||
font-size: 11.5px; line-height: 1.75; white-space: pre-wrap;
|
||||
word-break: break-word; color: #c6d3e6;
|
||||
word-break: break-word; color: var(--sub);
|
||||
}
|
||||
.logbox:empty::before { content: "(暂无内容)"; color: var(--dim); }
|
||||
.empty-state {
|
||||
@@ -369,12 +353,12 @@ select option { background: #111a2e; color: var(--text); }
|
||||
}
|
||||
.login { width: 100%; max-width: 400px; padding: 28px 28px 22px; margin: 0; }
|
||||
.login .logo {
|
||||
width: 44px; height: 44px; border-radius: 13px; margin-bottom: 14px;
|
||||
background: linear-gradient(135deg, var(--cyan), var(--violet));
|
||||
width: 40px; height: 40px; border-radius: var(--r-ctl); margin-bottom: 14px;
|
||||
background: var(--accent);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px; font-weight: 800; color: #061019;
|
||||
font-size: 19px; font-weight: 700; color: #fff;
|
||||
}
|
||||
.login h1 { font-size: 19px; margin: 0 0 5px; }
|
||||
.login h1 { font-size: 19px; margin: 0 0 5px; font-weight: 600; }
|
||||
.login .hint { margin: 0 0 20px; }
|
||||
.login label { margin-bottom: 14px; }
|
||||
.login .btn { width: 100%; padding: 10px; margin-top: 4px; }
|
||||
@@ -398,14 +382,13 @@ select option { background: #111a2e; color: var(--text); }
|
||||
border: 1px solid var(--line); border-radius: var(--r-ctl);
|
||||
background: var(--panel-dim); cursor: pointer; transition: .15s;
|
||||
}
|
||||
.capimg:hover { border-color: var(--cyan); }
|
||||
.capimg:hover { border-color: var(--accent); }
|
||||
|
||||
/* ---------------- 错误页 ---------------- */
|
||||
.errpage { text-align: center; padding: 66px 24px; }
|
||||
.errpage .code {
|
||||
font-size: 52px; font-weight: 800; line-height: 1; margin: 0 0 12px;
|
||||
background: linear-gradient(135deg, var(--cyan), var(--violet));
|
||||
-webkit-background-clip: text; background-clip: text; color: transparent;
|
||||
font-size: 46px; font-weight: 600; line-height: 1; margin: 0 0 14px;
|
||||
color: var(--sub); font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.errpage p { color: var(--sub); margin: 0 0 20px; }
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-page-node-id="jBpPdqHNtyPDgHiurSUDlP">
|
||||
<head data-page-node-id="dVR2hep3JIj1Ezc0ZtFF6H">
|
||||
<meta charset="UTF-8" data-page-node-id="qqRbAyBRXJtuBQJ1uZTFiM">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" data-page-node-id="hnIdNU8Kp932sqqMuVMJdf">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WorkBuddy Portal · 积分消耗大屏</title>
|
||||
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' data-page-node-id="AHkV6pmcOA23nBLJEuCsK9"><circle cx='8' cy='8' r='6' fill='%2322d3ee' data-page-node-id="2GTvuDAClmE7KNpisaRV1j"/></svg>">
|
||||
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><circle cx='8' cy='8' r='6' fill='%234c8df6'/></svg>">
|
||||
<!-- 必须用**绝对**路径:本页由 /dashboard 直接吐出(无尾斜杠),
|
||||
若写成相对路径会解析成 /vendor/echarts.min.js → 404,结果整页图表全白。
|
||||
静态目录挂载在 /static(见 create_app 的 static_url_path)。 -->
|
||||
@@ -15,135 +15,133 @@
|
||||
明细按窗口裁剪下发(且不含 prompt 全文),页面不再整包搬运整个存档。
|
||||
详见页面脚本里的 loadSources() / refetch()。 -->
|
||||
<style>
|
||||
/* 与后台 app.css 共用同一套令牌:中性灰阶 + 单一强调色,不用渐变与辉光 */
|
||||
:root{
|
||||
--bg:#080d1a; --panel:rgba(255,255,255,.045); --line:rgba(255,255,255,.09);
|
||||
--text:#e8edf7; --sub:#8b9bb4; --cyan:#22d3ee; --violet:#a78bfa;
|
||||
--amber:#fbbf24; --green:#34d399; --pink:#f472b6; --blue:#60a5fa;
|
||||
--bg:#0e1013; --bg-elev:#14171b; --panel:#16191e; --panel-hi:#1d2128; --panel-dim:#101318;
|
||||
--line:#262b33;
|
||||
--text:#e4e7eb; --sub:#98a1ac; --dim:#6b7480;
|
||||
--accent:#4c8df6; --accent-dim:rgba(76,141,246,.14);
|
||||
--cyan:#4aa8c0; --violet:#8b7ec8; --amber:#c9952f; --green:#4ba97b;
|
||||
--pink:#c07a9e; --blue:#4c8df6; --red:#cf6679;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{
|
||||
background:
|
||||
radial-gradient(1100px 600px at 12% -10%, rgba(34,211,238,.13), transparent 60%),
|
||||
radial-gradient(900px 500px at 100% 0%, rgba(167,139,250,.13), transparent 55%),
|
||||
var(--bg);
|
||||
background:var(--bg);
|
||||
color:var(--text); font-family:"Microsoft YaHei","PingFang SC",system-ui,-apple-system,sans-serif;
|
||||
min-height:100vh; padding:18px 22px 34px;
|
||||
}
|
||||
header{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap;margin-bottom:16px}
|
||||
h1{font-size:24px;font-weight:700;letter-spacing:.5px;display:flex;align-items:center;gap:10px}
|
||||
h1::before{content:"";width:5px;height:24px;border-radius:3px;background:linear-gradient(180deg,var(--cyan),var(--violet))}
|
||||
.sub{color:var(--sub);font-size:12.5px;margin-top:4px}
|
||||
.sub code{background:rgba(255,255,255,.07);padding:1px 5px;border-radius:4px;color:var(--cyan);font-size:11.5px}
|
||||
h1{font-size:20px;font-weight:600}
|
||||
.sub{color:var(--sub);font-size:12.5px;margin-top:5px}
|
||||
.sub code{background:var(--panel-hi);padding:1px 5px;border-radius:3px;color:var(--text);font-size:11.5px}
|
||||
/* 大屏是独立静态页,拿不到 Jinja 的导航,所以自己提供回后台的入口 */
|
||||
.navback{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
|
||||
.abtn{
|
||||
display:inline-flex;align-items:center;gap:5px;padding:5px 12px;border-radius:9px;
|
||||
border:1px solid var(--line);background:rgba(255,255,255,.05);color:var(--sub);
|
||||
display:inline-flex;align-items:center;gap:5px;padding:5px 12px;border-radius:6px;
|
||||
border:1px solid var(--line);background:var(--panel-hi);color:var(--sub);
|
||||
font-size:12px;text-decoration:none;transition:.15s;
|
||||
}
|
||||
.abtn:hover{color:var(--text);border-color:rgba(34,211,238,.45);background:rgba(255,255,255,.09);text-decoration:none}
|
||||
.abtn:hover{color:var(--text);border-color:var(--accent);background:#232830;text-decoration:none}
|
||||
.meta{color:var(--sub);font-size:12px;text-align:right;line-height:1.7}
|
||||
.meta b{color:var(--cyan);font-weight:600}
|
||||
.meta b{color:var(--text);font-weight:600}
|
||||
|
||||
.bar{
|
||||
display:flex;gap:14px;flex-wrap:wrap;align-items:center;
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:12px;
|
||||
padding:12px 16px;margin-bottom:16px;backdrop-filter:blur(6px);
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:8px;
|
||||
padding:12px 16px;margin-bottom:16px;
|
||||
}
|
||||
.grp{display:flex;align-items:center;gap:8px}
|
||||
.grp>span{color:var(--sub);font-size:12px;white-space:nowrap}
|
||||
.seg{display:flex;background:rgba(255,255,255,.05);border:1px solid var(--line);border-radius:9px;overflow:hidden}
|
||||
.seg{display:flex;background:var(--panel-hi);border:1px solid var(--line);border-radius:6px;overflow:hidden}
|
||||
.seg button{
|
||||
background:transparent;border:0;color:var(--sub);padding:6px 13px;font-size:12.5px;
|
||||
cursor:pointer;transition:.15s;font-family:inherit;
|
||||
}
|
||||
.seg button:hover{color:var(--text);background:rgba(255,255,255,.06)}
|
||||
.seg button.on{background:linear-gradient(135deg,rgba(34,211,238,.28),rgba(167,139,250,.28));color:#fff;font-weight:600}
|
||||
.seg button:hover{color:var(--text)}
|
||||
.seg button.on{background:var(--accent-dim);color:var(--text);font-weight:600}
|
||||
select,input[type=date]{
|
||||
background:rgba(255,255,255,.06);border:1px solid var(--line);color:var(--text);
|
||||
border-radius:8px;padding:6px 10px;font-size:12.5px;font-family:inherit;outline:none;
|
||||
background:var(--panel-dim);border:1px solid var(--line);color:var(--text);
|
||||
border-radius:6px;padding:6px 10px;font-size:12.5px;font-family:inherit;outline:none;
|
||||
}
|
||||
select option{background:#111a2e;color:var(--text)}
|
||||
select option{background:var(--bg-elev);color:var(--text)}
|
||||
button.act{
|
||||
background:linear-gradient(135deg,rgba(34,211,238,.22),rgba(167,139,250,.22));
|
||||
border:1px solid rgba(34,211,238,.35);color:#fff;border-radius:9px;
|
||||
padding:6px 14px;font-size:12.5px;cursor:pointer;font-family:inherit;transition:.15s;
|
||||
background:var(--accent);border:1px solid var(--accent);color:#fff;border-radius:6px;
|
||||
padding:6px 14px;font-size:12.5px;cursor:pointer;font-family:inherit;font-weight:600;transition:.15s;
|
||||
}
|
||||
button.act:hover{border-color:var(--cyan);box-shadow:0 0 14px rgba(34,211,238,.25)}
|
||||
button.act:hover{background:#5e99f7;border-color:#5e99f7}
|
||||
|
||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:14px;margin-bottom:16px}
|
||||
.kpi{
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:15px 17px;position:relative;overflow:hidden;
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:15px 17px;
|
||||
}
|
||||
.kpi::after{content:"";position:absolute;left:0;top:0;width:3px;height:100%;background:var(--c,var(--cyan))}
|
||||
.kpi .lb{color:var(--sub);font-size:12px;letter-spacing:.3px}
|
||||
.kpi .vl{font-size:26px;font-weight:700;margin-top:7px;font-variant-numeric:tabular-nums;letter-spacing:.5px}
|
||||
.kpi .ex{color:var(--sub);font-size:11.5px;margin-top:5px}
|
||||
.kpi .lb{color:var(--sub);font-size:12px;display:flex;align-items:center;gap:6px}
|
||||
.kpi .lb::before{content:"";width:8px;height:8px;border-radius:2px;background:var(--c,var(--line));flex:0 0 auto}
|
||||
.kpi .vl{font-size:25px;font-weight:600;margin-top:8px;font-variant-numeric:tabular-nums}
|
||||
.kpi .ex{color:var(--sub);font-size:11.5px;margin-top:5px;line-height:1.6}
|
||||
|
||||
.grid{display:grid;grid-template-columns:1.45fr 1fr;gap:16px}
|
||||
.grid.b{grid-template-columns:1fr 1fr;margin-top:16px}
|
||||
.card{
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 16px 10px;
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px 16px 10px;
|
||||
}
|
||||
.card h2{font-size:14px;font-weight:600;margin-bottom:2px;display:flex;align-items:center;gap:8px}
|
||||
.card h2::before{content:"";width:3px;height:13px;border-radius:2px;background:var(--cyan)}
|
||||
.card .hint{color:var(--sub);font-size:11.5px;margin-bottom:6px}
|
||||
.card h2{font-size:13.5px;font-weight:600;color:var(--text)}
|
||||
.card .hint{color:var(--sub);font-size:11.5px;margin-bottom:6px;margin-top:2px}
|
||||
.chart{width:100%;height:290px}
|
||||
.chart.tall{height:330px}
|
||||
|
||||
table{width:100%;border-collapse:collapse;font-size:12.5px;margin-top:6px}
|
||||
th,td{padding:8px 10px;text-align:left;border-bottom:1px solid rgba(255,255,255,.06)}
|
||||
th{color:var(--sub);font-weight:500;font-size:11.5px;letter-spacing:.4px}
|
||||
th,td{padding:8px 10px;text-align:left;border-bottom:1px solid var(--line)}
|
||||
th{color:var(--sub);font-weight:500;font-size:11.5px}
|
||||
td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}
|
||||
tbody tr:hover{background:rgba(255,255,255,.04)}
|
||||
tbody tr:hover{background:var(--panel-hi)}
|
||||
.dot{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:7px;vertical-align:middle}
|
||||
|
||||
#err{
|
||||
display:none;background:rgba(248,113,113,.12);border:1px solid rgba(248,113,113,.4);
|
||||
color:#fca5a5;border-radius:12px;padding:14px 16px;margin-bottom:16px;font-size:13px;line-height:1.8;
|
||||
display:none;background:rgba(207,102,121,.12);border:1px solid rgba(207,102,121,.4);
|
||||
color:#e8b6bf;border-radius:8px;padding:14px 16px;margin-bottom:16px;font-size:13px;line-height:1.8;
|
||||
}
|
||||
#err code{background:rgba(0,0,0,.35);padding:2px 6px;border-radius:4px;color:#fde68a}
|
||||
.foot{color:var(--sub);font-size:11.5px;text-align:center;margin-top:22px}
|
||||
#err code{background:var(--panel-dim);padding:2px 6px;border-radius:3px;color:var(--text)}
|
||||
.foot{color:var(--dim);font-size:11.5px;text-align:center;margin-top:22px;line-height:1.9}
|
||||
|
||||
/* ---- 消耗日历(日志视图) ---- */
|
||||
.calcard{margin-top:16px;padding-bottom:14px}
|
||||
.calhead{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
|
||||
.calnav{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.caltitle{min-width:92px;text-align:center;font-size:13px;font-weight:600;color:var(--text)}
|
||||
button.act.ghost{background:rgba(255,255,255,.05);border-color:var(--line);color:var(--sub);padding:5px 11px}
|
||||
button.act.ghost:hover{color:var(--text);border-color:rgba(34,211,238,.5);box-shadow:none}
|
||||
button.act.ghost{background:var(--panel-hi);border-color:var(--line);color:var(--sub);padding:5px 11px;font-weight:400}
|
||||
button.act.ghost:hover{color:var(--text);border-color:var(--accent);background:#232830}
|
||||
button.act.ghost:disabled{opacity:.32;cursor:not-allowed}
|
||||
button.act.ghost:disabled:hover{border-color:var(--line);color:var(--sub)}
|
||||
button.act.ghost:disabled:hover{border-color:var(--line);color:var(--sub);background:var(--panel-hi)}
|
||||
.callegend{display:flex;align-items:center;justify-content:flex-end;gap:5px;color:var(--sub);font-size:11px;margin:4px 0 9px}
|
||||
.callegend i{display:inline-block;width:17px;height:11px;border-radius:3px;border:1px solid rgba(255,255,255,.10)}
|
||||
.callegend i{display:inline-block;width:17px;height:11px;border-radius:3px;border:1px solid var(--line)}
|
||||
.caldow{display:grid;grid-template-columns:repeat(7,1fr);gap:8px;text-align:center;color:var(--sub);
|
||||
font-size:11px;letter-spacing:.6px;padding-bottom:7px}
|
||||
font-size:11px;padding-bottom:7px}
|
||||
.calgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:8px}
|
||||
.calcell{position:relative;min-height:72px;border-radius:10px;border:1px solid rgba(255,255,255,.07);
|
||||
background:rgba(255,255,255,.03);padding:7px 9px 10px;display:flex;flex-direction:column;gap:2px;
|
||||
overflow:hidden;transition:transform .15s,border-color .15s,box-shadow .15s}
|
||||
.calcell{position:relative;min-height:72px;border-radius:6px;border:1px solid var(--line);
|
||||
background:var(--panel);padding:7px 9px 10px;display:flex;flex-direction:column;gap:2px;
|
||||
overflow:hidden;transition:border-color .15s,background .15s}
|
||||
.calcell.void{background:transparent;border-color:transparent}
|
||||
.calcell.in{border-color:rgba(34,211,238,.42)}
|
||||
.calcell.in{border-color:var(--accent)}
|
||||
.calcell.has{cursor:pointer}
|
||||
.calcell.has:hover{transform:translateY(-2px);border-color:rgba(34,211,238,.7);box-shadow:0 8px 20px rgba(0,0,0,.38)}
|
||||
.calcell.has:hover{border-color:var(--accent);background:var(--panel-hi)}
|
||||
.calcell .cd{font-size:11.5px;color:var(--sub);font-variant-numeric:tabular-nums}
|
||||
.calcell.today .cd{color:var(--cyan);font-weight:700}
|
||||
.calcell .cv{font-size:14px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.2px}
|
||||
.calcell.empty .cv{color:var(--sub);font-weight:500;font-size:13px}
|
||||
.calcell.today .cd{color:var(--text);font-weight:700}
|
||||
.calcell .cv{font-size:14px;font-weight:600;font-variant-numeric:tabular-nums}
|
||||
.calcell.empty .cv{color:var(--sub);font-weight:400;font-size:13px}
|
||||
.calcell .cc{font-size:10.5px;color:var(--sub);font-variant-numeric:tabular-nums}
|
||||
.calcell .cbar{position:absolute;left:0;bottom:0;width:100%;height:3px;background:rgba(255,255,255,.05)}
|
||||
.calcell .cbar>i{display:block;height:100%;width:0;border-radius:0 2px 0 0}
|
||||
.calcell.sel{box-shadow:inset 0 0 0 1.5px var(--cyan)}
|
||||
.calcell.peak::after{content:"峰";position:absolute;right:6px;top:5px;background:var(--amber);color:#0a1120;
|
||||
font-size:9.5px;line-height:14px;padding:0 5px;border-radius:5px;font-weight:700}
|
||||
.calcell .cbar{position:absolute;left:0;bottom:0;width:100%;height:3px;background:var(--panel-hi)}
|
||||
.calcell .cbar>i{display:block;height:100%;width:0}
|
||||
.calcell.sel{box-shadow:inset 0 0 0 1.5px var(--accent)}
|
||||
.calcell.peak::after{content:"峰";position:absolute;right:6px;top:5px;background:var(--amber);color:#0e1013;
|
||||
font-size:9.5px;line-height:14px;padding:0 5px;border-radius:3px;font-weight:700}
|
||||
.calcell.partial .cc::after{content:" · 残";color:var(--amber)}
|
||||
.calstat{display:flex;gap:22px;flex-wrap:wrap;margin-top:12px;padding-top:12px;border-top:1px solid var(--line);
|
||||
font-size:12px;color:var(--sub)}
|
||||
.calstat b{color:var(--text);margin-left:6px;font-variant-numeric:tabular-nums}
|
||||
#calTip{position:fixed;z-index:99;pointer-events:none;display:none;max-width:290px;
|
||||
background:rgba(12,18,32,.96);border:1px solid rgba(255,255,255,.14);border-radius:9px;
|
||||
padding:9px 12px;font-size:12px;line-height:1.8;color:var(--text);box-shadow:0 12px 34px rgba(0,0,0,.5)}
|
||||
#calTip b{color:var(--cyan)}
|
||||
background:var(--bg-elev);border:1px solid var(--line);border-radius:6px;
|
||||
padding:9px 12px;font-size:12px;line-height:1.8;color:var(--text);box-shadow:0 8px 24px rgba(0,0,0,.5)}
|
||||
#calTip b{color:var(--text)}
|
||||
@media(max-width:820px){
|
||||
.calcell{min-height:58px;padding:5px 6px 8px}
|
||||
.calcell .cv{font-size:12px}.calcell .cc{font-size:9.5px}
|
||||
@@ -151,15 +149,15 @@
|
||||
@media(max-width:1180px){.grid,.grid.b{grid-template-columns:1fr}}
|
||||
|
||||
/* ---- 环比 / 标签 / 迷你条 ---- */
|
||||
.dimup{color:var(--amber)} /* 用量上升用琥珀,下降用青色:刻意不用红绿 */
|
||||
.dimdn{color:var(--cyan)}
|
||||
.ok{color:var(--green)} .bad{color:#f87171}
|
||||
.tag{display:inline-block;font-size:10px;line-height:15px;padding:0 5px;border-radius:4px;
|
||||
background:rgba(251,191,36,.18);color:var(--amber);margin-left:6px;vertical-align:1px;white-space:nowrap}
|
||||
.tag.free{background:rgba(96,165,250,.16);color:var(--blue);margin-left:4px}
|
||||
.bar-mini{height:7px;border-radius:4px;background:rgba(255,255,255,.08);overflow:hidden}
|
||||
.bar-mini>div{height:100%;border-radius:4px;background:linear-gradient(90deg,#22d3ee,#a78bfa)}
|
||||
.card .hint b{color:var(--cyan);font-weight:600}
|
||||
.dimup{color:var(--amber)} /* 用量上升用琥珀,下降用蓝:刻意不用红绿 */
|
||||
.dimdn{color:var(--accent)}
|
||||
.ok{color:var(--green)} .bad{color:var(--red)}
|
||||
.tag{display:inline-block;font-size:10px;line-height:15px;padding:0 5px;border-radius:3px;
|
||||
background:rgba(201,149,47,.18);color:var(--amber);margin-left:6px;vertical-align:1px;white-space:nowrap}
|
||||
.tag.free{background:rgba(76,141,246,.16);color:var(--blue);margin-left:4px}
|
||||
.bar-mini{height:6px;border-radius:3px;background:var(--panel-hi);overflow:hidden}
|
||||
.bar-mini>div{height:100%;border-radius:3px;background:var(--accent)}
|
||||
.card .hint b{color:var(--text);font-weight:600}
|
||||
.cardhead{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||||
|
||||
/* ---- TOP 榜 / 数据源 ---- */
|
||||
@@ -172,21 +170,20 @@
|
||||
#tblTop td:nth-child(-n+5){white-space:nowrap}
|
||||
#tblTop td.px{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
||||
color:var(--sub);font-size:11.5px}
|
||||
#tblSrc code{background:rgba(255,255,255,.06);padding:1px 5px;border-radius:4px;
|
||||
font-size:11px;color:var(--cyan);font-family:Consolas,Monaco,monospace}
|
||||
#tblSrc code{background:var(--panel-hi);padding:1px 5px;border-radius:3px;
|
||||
font-size:11px;color:var(--text);font-family:Consolas,Monaco,monospace}
|
||||
#srcCheck{margin-top:10px;font-size:11.5px;color:var(--sub);line-height:1.95;
|
||||
border-top:1px solid var(--line);padding-top:9px}
|
||||
.sub code{background:rgba(255,255,255,.06);padding:1px 5px;border-radius:4px;color:var(--cyan);
|
||||
.sub code{background:var(--panel-hi);padding:1px 5px;border-radius:3px;color:var(--text);
|
||||
font-family:Consolas,Monaco,monospace;font-size:11.5px}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body data-page-node-id="cgE383OQXYMooNAAYOf8Xo">
|
||||
<body>
|
||||
|
||||
<header data-page-node-id="qNmHyKNLVV83WavBjcUDvZ">
|
||||
<div data-page-node-id="nvuOyMOnmQg04RZg7ABko0">
|
||||
<h1 data-page-node-id="bbxwehmAM7hCbojvIUbEn5">WorkBuddy Portal · 积分消耗大屏</h1>
|
||||
<div class="sub" data-page-node-id="LgqD86gtDlqqDDFruEcboi">数据来源:本项目 <code>/api</code>(SQLite 正本 <code>data/usage.sqlite</code>),页面按筛选条件实时聚合</div>
|
||||
<header>
|
||||
<div>
|
||||
<h1>WorkBuddy Portal · 积分消耗大屏</h1>
|
||||
<div class="sub">数据来源:<code>/api</code>(正本 <code>data/usage.sqlite</code>)</div>
|
||||
<div class="navback">
|
||||
<a class="abtn" href="/">← 返回后台</a>
|
||||
<a class="abtn" href="/records">数据明细</a>
|
||||
@@ -195,161 +192,160 @@
|
||||
<a class="abtn" id="abtnLogs" href="/logs" hidden>日志管理</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta" data-page-node-id="4jmZuGBGC8JQmVAE2IOJ7H">
|
||||
数据范围 <b id="mRange" data-page-node-id="Hv9sHK3bEHKC2oi5QrSBHx">-</b><br data-page-node-id="loxYsApIQ9pIhv0bpA4S7c">
|
||||
共 <b id="mDays" data-page-node-id="j7DQYi30ic8FPEQH2mfPSW">-</b> 天 · <b id="mRows" data-page-node-id="I5ZLhC3ODlkutlGRaRlRKp">-</b> 条请求记录<br data-page-node-id="ecCETqYdR2yF04jFxFPiT8">
|
||||
<div class="meta">
|
||||
数据范围 <b id="mRange">-</b><br>
|
||||
共 <b id="mDays">-</b> 天 · <b id="mRows">-</b> 条请求记录<br>
|
||||
数据快照 <b id="mUpdate">-</b>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="err" data-page-node-id="dj4W2yF1bo2dSOqXftYHYb"></div>
|
||||
<div id="err"></div>
|
||||
|
||||
<div class="bar" data-page-node-id="DdktDg1IO90mtTF9ZuSNB9">
|
||||
<div class="grp" data-page-node-id="f4G2ZTZxKEJdVG5RkYwL2Q">
|
||||
<span data-page-node-id="6lFVYOo1P7geY0oSXMscUj">日期范围</span>
|
||||
<div class="seg" id="segRange" data-page-node-id="ev2oBcItYEQrzl7wiXlzBG">
|
||||
<button data-v="day" data-page-node-id="8MbAF6CvYCeQHQ1LFx4L8D">今日</button>
|
||||
<button data-v="7d" class="on" data-page-node-id="hiFAf8qWEb3fE6l2MgUbKR">近 7 天</button>
|
||||
<button data-v="30d" data-page-node-id="N4I8KC6ZmmbwXFqQz4pyBy">近 30 天</button>
|
||||
<button data-v="all" data-page-node-id="esurYqQl7tJrebaUFYae80">全部</button>
|
||||
<button data-v="custom" data-page-node-id="LUr2Jzq9MLGadHwepmYg3B">自定义</button>
|
||||
<div class="bar">
|
||||
<div class="grp">
|
||||
<span>日期范围</span>
|
||||
<div class="seg" id="segRange">
|
||||
<button data-v="day">今日</button>
|
||||
<button data-v="7d" class="on">近 7 天</button>
|
||||
<button data-v="30d">近 30 天</button>
|
||||
<button data-v="all">全部</button>
|
||||
<button data-v="custom">自定义</button>
|
||||
</div>
|
||||
<select id="selDay" title="选择具体某一天" style="display:none" data-page-node-id="AXWOgamGumzhgLyFWDtO5F"></select>
|
||||
<span id="customBox" style="display:none;gap:6px;align-items:center" data-page-node-id="c6BMGtK1rXEGLtPey8eXhK">
|
||||
<input type="date" id="dFrom" data-page-node-id="NZFXC8NZfUMAKOwCeuWi8g"><span style="color:var(--sub)" data-page-node-id="c5MDTzWvPgNNSXYezC5q4d">至</span><input type="date" id="dTo" data-page-node-id="ijDpwRtLUU7VCCFK2s8ad0">
|
||||
<select id="selDay" title="选择具体某一天" style="display:none"></select>
|
||||
<span id="customBox" style="display:none;gap:6px;align-items:center">
|
||||
<input type="date" id="dFrom"><span style="color:var(--sub)">至</span><input type="date" id="dTo">
|
||||
</span>
|
||||
</div>
|
||||
<div class="grp" data-page-node-id="x0nms61tGsgUGorPYjYQnX">
|
||||
<span data-page-node-id="OWHQO4wrcghIFuTngJTrgn">统计维度</span>
|
||||
<div class="seg" id="segGran" data-page-node-id="mMRb516sRLu1kmhM4ZCXo3">
|
||||
<button data-v="auto" class="on" data-page-node-id="9z0JMKlm1sL8Na4FE6eZyX">自动</button>
|
||||
<button data-v="day" data-page-node-id="fMCwiGyoKGl1zmLVyi3fsz">按天</button>
|
||||
<button data-v="week" data-page-node-id="Hpl4cp1jpGMjejxp9zE9L4">按周</button>
|
||||
<button data-v="month" data-page-node-id="ebklBG8V2FMGuvwmlUDSDD">按月</button>
|
||||
<div class="grp">
|
||||
<span>统计维度</span>
|
||||
<div class="seg" id="segGran">
|
||||
<button data-v="auto" class="on">自动</button>
|
||||
<button data-v="day">按天</button>
|
||||
<button data-v="week">按周</button>
|
||||
<button data-v="month">按月</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grp" data-page-node-id="GNZcj0escL1zjzKbpm4wrr">
|
||||
<span data-page-node-id="qFpAW08WBrAvUR1rD9pg1r">拆分维度</span>
|
||||
<div class="seg" id="segDim" data-page-node-id="Ke7fl3DhEfgDU4XOqJA4CC">
|
||||
<button data-v="model" class="on" data-page-node-id="umLIxwwds01h4xJGMTKJ4N">模型</button>
|
||||
<button data-v="client" data-page-node-id="AmKFjL92JpfCZ34bugR90r">客户端</button>
|
||||
<button data-v="hour" data-page-node-id="PMJKA7oulDfEMrMLmYok3Q">时段</button>
|
||||
<div class="grp">
|
||||
<span>拆分维度</span>
|
||||
<div class="seg" id="segDim">
|
||||
<button data-v="model" class="on">模型</button>
|
||||
<button data-v="client">客户端</button>
|
||||
<button data-v="hour">时段</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="act" id="btnReload" title="数据由 fetch_usage.py 采集后加工成 dashboard/data/*.json,重新采集后刷新本页即可">刷新页面</button>
|
||||
<button class="act" id="btnReload" title="重新向 /api 取数并重绘本页">刷新页面</button>
|
||||
</div>
|
||||
|
||||
<div class="kpis" id="kpis" data-page-node-id="ezG8rbfBQAhGqE7X7WvjI0"></div>
|
||||
<div class="kpis" id="kpis"></div>
|
||||
|
||||
<div class="card calcard" data-page-node-id="CalCard0001">
|
||||
<div class="calhead" data-page-node-id="CalHead0001">
|
||||
<div data-page-node-id="CalHead0002">
|
||||
<h2 data-page-node-id="CalHead0003">消耗日历 · 日志视图</h2>
|
||||
<div class="hint" id="calHint" data-page-node-id="CalHead0004">按天汇总积分消耗与调用量,色块深浅表示当日强度(相对所选月份峰值);点击有数据的日期可下钻到当日</div>
|
||||
<div class="card calcard">
|
||||
<div class="calhead">
|
||||
<div>
|
||||
<h2>消耗日历 · 日志视图</h2>
|
||||
<div class="hint" id="calHint">按天汇总积分消耗与调用量,色块深浅表示当日强度(相对所选月份峰值);点击有数据的日期可下钻到当日</div>
|
||||
</div>
|
||||
<div class="calnav" data-page-node-id="CalNav0001">
|
||||
<div class="grp" data-page-node-id="CalNav0002">
|
||||
<span data-page-node-id="CalNav0003">着色依据</span>
|
||||
<div class="seg" id="segCalMode" data-page-node-id="CalNav0004">
|
||||
<button data-v="credits" class="on" data-page-node-id="CalNav0005">积分</button>
|
||||
<button data-v="calls" data-page-node-id="CalNav0006">调用量</button>
|
||||
<div class="calnav">
|
||||
<div class="grp">
|
||||
<span>着色依据</span>
|
||||
<div class="seg" id="segCalMode">
|
||||
<button data-v="credits" class="on">积分</button>
|
||||
<button data-v="calls">调用量</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="act ghost" id="calPrev" title="上一月" data-page-node-id="CalNav0007">‹</button>
|
||||
<span class="caltitle" id="calTitle" data-page-node-id="CalNav0008">-</span>
|
||||
<button class="act ghost" id="calNext" title="下一月" data-page-node-id="CalNav0009">›</button>
|
||||
<button class="act ghost" id="calNow" title="跳到最后有数据的月份" data-page-node-id="CalNav0010">最新</button>
|
||||
<button class="act ghost" id="calPrev" title="上一月">‹</button>
|
||||
<span class="caltitle" id="calTitle">-</span>
|
||||
<button class="act ghost" id="calNext" title="下一月">›</button>
|
||||
<button class="act ghost" id="calNow" title="跳到最后有数据的月份">最新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="callegend" id="calLegend" data-page-node-id="CalLegend01"></div>
|
||||
<div class="caldow" data-page-node-id="CalDow00001">
|
||||
<span data-page-node-id="CalDow00002">一</span><span data-page-node-id="CalDow00003">二</span><span data-page-node-id="CalDow00004">三</span><span data-page-node-id="CalDow00005">四</span><span data-page-node-id="CalDow00006">五</span><span data-page-node-id="CalDow00007">六</span><span data-page-node-id="CalDow00008">日</span>
|
||||
<div class="callegend" id="calLegend"></div>
|
||||
<div class="caldow">
|
||||
<span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span>
|
||||
</div>
|
||||
<div class="calgrid" id="calGrid" data-page-node-id="CalGrid0001"></div>
|
||||
<div class="calstat" id="calStat" data-page-node-id="CalStat0001"></div>
|
||||
<div class="calgrid" id="calGrid"></div>
|
||||
<div class="calstat" id="calStat"></div>
|
||||
</div>
|
||||
<div id="calTip" data-page-node-id="CalTip0001"></div>
|
||||
<div id="calTip"></div>
|
||||
|
||||
<div class="grid" data-page-node-id="Y8L5F0bCd1duWGGhtdnfoI">
|
||||
<div class="card" data-page-node-id="9dkepoT2cxSqyqReGkIdsN">
|
||||
<h2 data-page-node-id="rWK6Yj9FegoLslsQgGDqW6">消耗趋势</h2>
|
||||
<div class="hint" id="trendHint" data-page-node-id="CmcGDzFQGuVCQPHLNjjesn">按选定统计维度展示积分消耗与调用次数</div>
|
||||
<div class="chart tall" id="cTrend" data-page-node-id="Yl7NmMfhUpKb5C0y9vw5nn"></div>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>消耗趋势</h2>
|
||||
<div class="hint" id="trendHint">按选定统计维度展示积分消耗与调用次数</div>
|
||||
<div class="chart tall" id="cTrend"></div>
|
||||
</div>
|
||||
<div class="card" data-page-node-id="MNLST8ygBBvzlhIUWesmDC">
|
||||
<div class="card">
|
||||
<div class="cardhead">
|
||||
<h2 data-page-node-id="6yGT1sOOvBuMJkwikplAeN">维度构成</h2>
|
||||
<div class="seg" id="segPieMetric" data-page-node-id="PieSeg0001">
|
||||
<button data-v="credits" class="on" data-page-node-id="PieSeg0002">按积分</button>
|
||||
<button data-v="calls" data-page-node-id="PieSeg0003">按调用量</button>
|
||||
<h2>维度构成</h2>
|
||||
<div class="seg" id="segPieMetric">
|
||||
<button data-v="credits" class="on">按积分</button>
|
||||
<button data-v="calls">按调用量</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint" id="pieHint" data-page-node-id="2mTXUjnvf1XD2ZG7LSFM9F">按拆分维度统计积分消耗占比</div>
|
||||
<div class="chart tall" id="cPie" data-page-node-id="WSjIdmCWoCmtoWgRk3rR6a"></div>
|
||||
<div class="hint" id="pieHint">按拆分维度统计积分消耗占比</div>
|
||||
<div class="chart tall" id="cPie"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid b" data-page-node-id="p5TYzrQeXYclXwXZc0fUrY">
|
||||
<div class="card" data-page-node-id="7DuEunYEvwgw3tJDDfUmRv">
|
||||
<h2 data-page-node-id="MSJH2nzGRPudHm7yBktel9">维度堆叠分布</h2>
|
||||
<div class="hint" id="stackHint" data-page-node-id="a2cMsTQF1SGPs7Z5Irwi0F">各维度在各时间段上的积分消耗堆叠</div>
|
||||
<div class="chart" id="cStack" data-page-node-id="8GgWmtBxYAkB1vzWEraxTi"></div>
|
||||
<div class="grid b">
|
||||
<div class="card">
|
||||
<h2>维度堆叠分布</h2>
|
||||
<div class="hint" id="stackHint">各维度在各时间段上的积分消耗堆叠</div>
|
||||
<div class="chart" id="cStack"></div>
|
||||
</div>
|
||||
<div class="card" data-page-node-id="GtFQNN1efZkhbOgg4nSwP0">
|
||||
<h2 data-page-node-id="0kKEgioY6RdyLLMw0E7svo">时段 × 日期热力图</h2>
|
||||
<div class="hint" id="heatHint" data-page-node-id="sXCbVdfmqJn7E4fJRJbG4f">颜色越亮表示该时段消耗越高</div>
|
||||
<div class="chart" id="cHeat" data-page-node-id="ZopZiALpWzp3dXaZPJiC7B"></div>
|
||||
<div class="card">
|
||||
<h2>时段 × 日期热力图</h2>
|
||||
<div class="hint" id="heatHint">颜色越亮表示该时段消耗越高</div>
|
||||
<div class="chart" id="cHeat"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid b" data-page-node-id="HistRow0001">
|
||||
<div class="card" data-page-node-id="HistCard001">
|
||||
<h2 data-page-node-id="HistCard002">单笔消耗分布(长尾)</h2>
|
||||
<div class="hint" id="histHint" data-page-node-id="HistCard003">按单笔积分分桶</div>
|
||||
<div class="chart" id="cHist" data-page-node-id="HistCard004"></div>
|
||||
<div class="grid b">
|
||||
<div class="card">
|
||||
<h2>单笔消耗分布(长尾)</h2>
|
||||
<div class="hint" id="histHint">按单笔积分分桶</div>
|
||||
<div class="chart" id="cHist"></div>
|
||||
</div>
|
||||
<div class="card" data-page-node-id="ScatCard001">
|
||||
<h2 data-page-node-id="ScatCard002">模型效率散点</h2>
|
||||
<div class="hint" id="scatterHint" data-page-node-id="ScatCard003">请求数 × 积分,气泡大小 = 单次均价</div>
|
||||
<div class="chart" id="cScatter" data-page-node-id="ScatCard004"></div>
|
||||
<div class="card">
|
||||
<h2>模型效率散点</h2>
|
||||
<div class="hint" id="scatterHint">请求数 × 积分,气泡大小 = 单次均价</div>
|
||||
<div class="chart" id="cScatter"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid b" data-page-node-id="AJe8K1vlk6fFa7VkHQBfk1">
|
||||
<div class="card" data-page-node-id="uXTFSuEDX2uijM5H6FkoCU">
|
||||
<h2 data-page-node-id="AuhjUdYBYmEYYT6SEh50T8">维度汇总表</h2>
|
||||
<div class="hint" data-page-node-id="fVNeKG380KfClp6qHRdP6W">按拆分维度聚合,按积分消耗降序</div>
|
||||
<div style="max-height:300px;overflow:auto" data-page-node-id="nf3LtPrwJZko43cEaHKMhx"><table id="tblDim" data-page-node-id="4E0T1dD39HCRci06VTahNY"></table></div>
|
||||
<div class="grid b">
|
||||
<div class="card">
|
||||
<h2>维度汇总表</h2>
|
||||
<div class="hint">按拆分维度聚合,按积分消耗降序</div>
|
||||
<div style="max-height:300px;overflow:auto"><table id="tblDim"></table></div>
|
||||
</div>
|
||||
<div class="card" data-page-node-id="HTGAo9nJBdjXgrdoBiSqfP">
|
||||
<h2 data-page-node-id="s0AhZQxvSQlKGY2flPkDeF">每日明细表</h2>
|
||||
<div class="hint" data-page-node-id="6CyHLq7WRzpn0PL29I7iJd">当前筛选范围内逐日汇总</div>
|
||||
<div style="max-height:300px;overflow:auto" data-page-node-id="J1mlkJ3uAd9BNXgCW09pHH"><table id="tblDay" data-page-node-id="VUcijHxm29zvPPCXC5oT7l"></table></div>
|
||||
<div class="card">
|
||||
<h2>每日明细表</h2>
|
||||
<div class="hint">当前筛选范围内逐日汇总</div>
|
||||
<div style="max-height:300px;overflow:auto"><table id="tblDay"></table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid b" data-page-node-id="TopRow000001">
|
||||
<div class="card" data-page-node-id="TopCard0001">
|
||||
<div class="grid b">
|
||||
<div class="card">
|
||||
<div class="cardhead">
|
||||
<h2 data-page-node-id="TopCard0002">TOP 单笔请求</h2>
|
||||
<div class="seg" id="segTopMode" data-page-node-id="TopSeg00001">
|
||||
<button data-v="range" class="on" data-page-node-id="TopSeg00002">当前筛选</button>
|
||||
<button data-v="global" data-page-node-id="TopSeg00003">全局 TOP200</button>
|
||||
<h2>TOP 单笔请求</h2>
|
||||
<div class="seg" id="segTopMode">
|
||||
<button data-v="range" class="on">当前筛选</button>
|
||||
<button data-v="global">全局 TOP200</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint" id="topHint" data-page-node-id="TopCard0003">按积分降序</div>
|
||||
<div style="max-height:320px;overflow:auto" data-page-node-id="TopCard0004"><table id="tblTop" data-page-node-id="TopCard0005"></table></div>
|
||||
<div class="hint" id="topHint">按积分降序</div>
|
||||
<div style="max-height:320px;overflow:auto"><table id="tblTop"></table></div>
|
||||
</div>
|
||||
<div class="card" data-page-node-id="SrcCard0001">
|
||||
<h2 data-page-node-id="SrcCard0002">数据源</h2>
|
||||
<div class="hint" data-page-node-id="SrcCard0003">数据全部由本项目 Flask 后端经 SQLite 实时聚合(无中间 JSON 层)</div>
|
||||
<div style="max-height:250px;overflow:auto" data-page-node-id="SrcCard0004"><table id="tblSrc" data-page-node-id="SrcCard0005"></table></div>
|
||||
<div id="srcCheck" data-page-node-id="SrcCard0006"></div>
|
||||
<div class="card">
|
||||
<h2>数据源</h2>
|
||||
<div class="hint">数据由本项目 Flask 后端从 SQLite 实时聚合</div>
|
||||
<div style="max-height:250px;overflow:auto"><table id="tblSrc"></table></div>
|
||||
<div id="srcCheck"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="foot" data-page-node-id="ZUGgx25CirIv3OdO9R3ywV">数据链:本项目 Flask 后端(进程内定时采集 → SQLite 正本)→ <code>/api</code> 实时聚合 → 本页渲染 ·
|
||||
筛选项变更会按窗口重新取数(明细按需下发,不含 prompt 全文)</div>
|
||||
<div class="foot">数据链:进程内定时采集 → <code>data/usage.sqlite</code> → <code>/api</code> 实时聚合 → 本页渲染</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
@@ -367,18 +363,19 @@
|
||||
*/
|
||||
const API = "/api";
|
||||
|
||||
const PALETTE = ["#22d3ee","#a78bfa","#fbbf24","#34d399","#f472b6","#60a5fa","#fb923c","#4ade80","#c084fc","#f87171"];
|
||||
// 低饱和色板:够区分 10 个维度,又不至于把大屏变成调色板
|
||||
const PALETTE = ["#4c8df6","#4aa8c0","#8b7ec8","#c9952f","#4ba97b","#c07a9e","#7d8896","#6d9fd8","#9a8f7a","#5f8ca8"];
|
||||
// 维度数可能超过色板长度(实测 12 个模型 > 10 色),超出部分按黄金角补色,避免同色歧义
|
||||
const dimColor = i => i < PALETTE.length ? PALETTE[i]
|
||||
: "hsl(" + Math.round((i * 137.508) % 360) + ",70%,62%)";
|
||||
// 日历 5 级色阶(沿用页面热力图的 青→紫→粉 渐变语义)
|
||||
: "hsl(" + Math.round((i * 137.508) % 360) + ",32%,62%)";
|
||||
// 日历 5 级色阶(与热力图同一套、由深到浅的蓝色阶)
|
||||
const CAL_LV = [
|
||||
{bg:"rgba(255,255,255,.03)", bar:"rgba(255,255,255,.07)"},
|
||||
{bg:"rgba(34,211,238,.10)", bar:"#155e75"},
|
||||
{bg:"rgba(34,211,238,.17)", bar:"#0891b2"},
|
||||
{bg:"rgba(34,211,238,.25)", bar:"#22d3ee"},
|
||||
{bg:"rgba(167,139,250,.30)", bar:"#a78bfa"},
|
||||
{bg:"rgba(244,114,182,.32)", bar:"#f472b6"}
|
||||
{bg:"transparent", bar:"#1f242b"},
|
||||
{bg:"rgba(76,141,246,.10)", bar:"#22405e"},
|
||||
{bg:"rgba(76,141,246,.17)", bar:"#2f5b86"},
|
||||
{bg:"rgba(76,141,246,.25)", bar:"#4c8df6"},
|
||||
{bg:"rgba(201,149,47,.30)", bar:"#c9952f"},
|
||||
{bg:"rgba(192,122,158,.32)", bar:"#c07a9e"}
|
||||
];
|
||||
// 单笔消耗分布的分桶(0 = 免费,>100 为极端长尾)
|
||||
const HIST_LABELS = ["0(免费)","≤0.1","≤0.5","≤1","≤2","≤5","≤10","≤20","≤50","≤100",">100"];
|
||||
@@ -695,35 +692,35 @@
|
||||
|
||||
charts.trend.setOption({
|
||||
grid:{left:52,right:56,top:44,bottom:52},
|
||||
tooltip:{trigger:"axis",backgroundColor:"rgba(12,18,32,.94)",borderColor:"rgba(255,255,255,.12)",
|
||||
textStyle:{color:"#e8edf7",fontSize:12},
|
||||
tooltip:{trigger:"axis",backgroundColor:"rgba(20,23,27,.96)",borderColor:"#262b33",
|
||||
textStyle:{color:"#e4e7eb",fontSize:12},
|
||||
formatter:p=>{
|
||||
const i = p[0].dataIndex, o = g[i];
|
||||
return `<b>${o.key}</b>${i===partIdx?`(残日,截至 ${pd.hhmm})`:""}<br>`
|
||||
+ `积分 ${fmt(o.credits)}<br>调用 ${fmt0(o.calls)} 次`;
|
||||
}},
|
||||
legend:{data:["积分消耗","调用次数"],top:6,textStyle:{color:"#8b9bb4",fontSize:11.5},icon:"roundRect",itemWidth:11,itemHeight:11},
|
||||
xAxis:{type:"category",data:labels,axisLabel:{color:"#8b9bb4",fontSize:11,rotate:labels.length>16?38:0},
|
||||
axisLine:{lineStyle:{color:"rgba(255,255,255,.12)"}},axisTick:{show:false}},
|
||||
legend:{data:["积分消耗","调用次数"],top:6,textStyle:{color:"#98a1ac",fontSize:11.5},icon:"roundRect",itemWidth:11,itemHeight:11},
|
||||
xAxis:{type:"category",data:labels,axisLabel:{color:"#98a1ac",fontSize:11,rotate:labels.length>16?38:0},
|
||||
axisLine:{lineStyle:{color:"#262b33"}},axisTick:{show:false}},
|
||||
yAxis:[
|
||||
{type:"value",name:"积分",nameTextStyle:{color:"#8b9bb4",fontSize:11},
|
||||
splitLine:{lineStyle:{color:"rgba(255,255,255,.06)"}},axisLabel:{color:"#8b9bb4",fontSize:11}},
|
||||
{type:"value",name:"次数",nameTextStyle:{color:"#8b9bb4",fontSize:11},
|
||||
splitLine:{show:false},axisLabel:{color:"#8b9bb4",fontSize:11}}
|
||||
{type:"value",name:"积分",nameTextStyle:{color:"#98a1ac",fontSize:11},
|
||||
splitLine:{lineStyle:{color:"#1f242b"}},axisLabel:{color:"#98a1ac",fontSize:11}},
|
||||
{type:"value",name:"次数",nameTextStyle:{color:"#98a1ac",fontSize:11},
|
||||
splitLine:{show:false},axisLabel:{color:"#98a1ac",fontSize:11}}
|
||||
],
|
||||
dataZoom:labels.length>24?[{type:"slider",height:14,bottom:14,borderColor:"transparent",
|
||||
backgroundColor:"rgba(255,255,255,.05)",fillerColor:"rgba(34,211,238,.18)",
|
||||
handleStyle:{color:"#22d3ee"},textStyle:{color:"#8b9bb4",fontSize:10}}]:[],
|
||||
backgroundColor:"#1d2128",fillerColor:"rgba(76,141,246,.18)",
|
||||
handleStyle:{color:"#4c8df6"},textStyle:{color:"#98a1ac",fontSize:10}}]:[],
|
||||
series:[
|
||||
{name:"积分消耗",type:"bar",barMaxWidth:26,
|
||||
data:g.map((o,i)=> i===partIdx
|
||||
? {value:+o.credits.toFixed(2), itemStyle:{color:"rgba(34,211,238,.34)",borderColor:"rgba(251,191,36,.6)",
|
||||
? {value:+o.credits.toFixed(2), itemStyle:{color:"rgba(76,141,246,.34)",borderColor:"rgba(201,149,47,.6)",
|
||||
borderWidth:1,borderType:"dashed",borderRadius:[4,4,0,0]}}
|
||||
: +o.credits.toFixed(2)),
|
||||
itemStyle:{borderRadius:[4,4,0,0],
|
||||
color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:"#22d3ee"},{offset:1,color:"rgba(34,211,238,.18)"}])}},
|
||||
color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:"#4c8df6"},{offset:1,color:"rgba(76,141,246,.18)"}])}},
|
||||
{name:"调用次数",type:"line",yAxisIndex:1,smooth:true,symbol:"circle",symbolSize:5,
|
||||
data:g.map(o=>o.calls),lineStyle:{color:"#a78bfa",width:2},itemStyle:{color:"#a78bfa"}}
|
||||
data:g.map(o=>o.calls),lineStyle:{color:"#c9952f",width:2},itemStyle:{color:"#c9952f"}}
|
||||
]
|
||||
},true);
|
||||
}
|
||||
@@ -740,24 +737,24 @@
|
||||
extra:{calls:o.calls, credits:o.credits}
|
||||
}));
|
||||
if(rest.length) items.push({name:"其他 "+rest.length+" 项", value:+restV.toFixed(2),
|
||||
itemStyle:{color:"#64748b"}, extra:{calls:rest.reduce((s,o)=>s+o.calls,0),
|
||||
itemStyle:{color:"#6b7480"}, extra:{calls:rest.reduce((s,o)=>s+o.calls,0),
|
||||
credits:rest.reduce((s,o)=>s+o.credits,0)}});
|
||||
$("#pieHint").textContent = "按"+DIM_NAME+(metric==="credits"?"统计积分消耗占比":"统计调用次数占比")
|
||||
+ (rest.length ? `(TOP ${N} 之外的 ${rest.length} 项已合并为「其他」)` : "");
|
||||
charts.pie.setOption({
|
||||
tooltip:{trigger:"item",backgroundColor:"rgba(12,18,32,.94)",borderColor:"rgba(255,255,255,.12)",
|
||||
textStyle:{color:"#e8edf7",fontSize:12},
|
||||
tooltip:{trigger:"item",backgroundColor:"rgba(20,23,27,.96)",borderColor:"#262b33",
|
||||
textStyle:{color:"#e4e7eb",fontSize:12},
|
||||
formatter:p=>{
|
||||
const e = (p.data.extra)||{};
|
||||
return `<b>${p.name}</b><br>积分 ${fmt(e.credits||0)} / 调用 ${fmt0(e.calls||0)} 次<br>`
|
||||
return `<b>${esc(p.name)}</b><br>积分 ${fmt(e.credits||0)} / 调用 ${fmt0(e.calls||0)} 次<br>`
|
||||
+ `占${metric==="credits"?"积分":"调用"} ${p.percent}%`;
|
||||
}},
|
||||
legend:{type:"scroll",bottom:2,textStyle:{color:"#8b9bb4",fontSize:11},icon:"roundRect",itemWidth:10,itemHeight:10},
|
||||
legend:{type:"scroll",bottom:2,textStyle:{color:"#98a1ac",fontSize:11},icon:"roundRect",itemWidth:10,itemHeight:10},
|
||||
series:[{
|
||||
type:"pie",radius:["46%","70%"],center:["50%","44%"],avoidLabelOverlap:true,
|
||||
itemStyle:{borderColor:"#0b1020",borderWidth:2},
|
||||
label:{color:"#c7d2e3",fontSize:11,formatter:"{b}\n{d}%"},
|
||||
labelLine:{lineStyle:{color:"rgba(255,255,255,.2)"}},
|
||||
itemStyle:{borderColor:"#16191e",borderWidth:2},
|
||||
label:{color:"#e4e7eb",fontSize:11,formatter:"{b}\n{d}%"},
|
||||
labelLine:{lineStyle:{color:"#3a414b"}},
|
||||
data:items
|
||||
}]
|
||||
},true);
|
||||
@@ -778,6 +775,10 @@
|
||||
const metricDim = new Map();
|
||||
rows.forEach(x=>{ const d=DIM_KEY(x); metricDim.set(d,(metricDim.get(d)||0)+x.credits); });
|
||||
const dimList = [...dims].sort((a,b)=>(metricDim.get(b)||0)-(metricDim.get(a)||0));
|
||||
// 名字保持原样存进 series:图例走 ECharts 的 canvas 渲染(是像素不是 HTML),
|
||||
// 转义反而会把 `&` 画成 `&`。真正需要防的是下面的 tooltip ——
|
||||
// ECharts 的 tooltip 默认 renderMode 是 html,内容按 HTML 插入,
|
||||
// 所以那里必须自己转义(dimList 里是 model / client 名,来自上游接口)。
|
||||
const series = dimList.map((d,i)=>({
|
||||
name:(gran==="hour" ? d : DIM_LABEL(d)), type:"bar", stack:"t", barMaxWidth:30,
|
||||
itemStyle:{color:dimColor(i)},
|
||||
@@ -787,13 +788,19 @@
|
||||
$("#stackHint").textContent = "各"+DIM_NAME+"在时间轴上的积分消耗堆叠(图例按消耗降序;无数据的时间段已补 0)";
|
||||
charts.stack.setOption({
|
||||
grid:{left:52,right:18,top:38,bottom:44},
|
||||
tooltip:{trigger:"axis",axisPointer:{type:"shadow"},backgroundColor:"rgba(12,18,32,.94)",
|
||||
borderColor:"rgba(255,255,255,.12)",textStyle:{color:"#e8edf7",fontSize:12}},
|
||||
legend:{type:"scroll",top:4,textStyle:{color:"#8b9bb4",fontSize:11},icon:"roundRect",itemWidth:10,itemHeight:10},
|
||||
tooltip:{trigger:"axis",axisPointer:{type:"shadow"},backgroundColor:"rgba(20,23,27,.96)",
|
||||
borderColor:"#262b33",textStyle:{color:"#e4e7eb",fontSize:12},
|
||||
formatter:ps=>{
|
||||
if(!ps || !ps.length) return "";
|
||||
const head = ps[0].axisValueLabel || ps[0].name || "";
|
||||
return `<b>${esc(head)}</b><br>` + ps.map(
|
||||
p=>`${p.marker}${esc(p.seriesName)}:${fmt(p.value)}`).join("<br>");
|
||||
}},
|
||||
legend:{type:"scroll",top:4,textStyle:{color:"#98a1ac",fontSize:11},icon:"roundRect",itemWidth:10,itemHeight:10},
|
||||
xAxis:{type:"category",data:bks.map(b=>bucketLabel(b)),
|
||||
axisLabel:{color:"#8b9bb4",fontSize:11,rotate:bks.length>12?35:0},
|
||||
axisLine:{lineStyle:{color:"rgba(255,255,255,.12)"}},axisTick:{show:false}},
|
||||
yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(255,255,255,.06)"}},axisLabel:{color:"#8b9bb4",fontSize:11}},
|
||||
axisLabel:{color:"#98a1ac",fontSize:11,rotate:bks.length>12?35:0},
|
||||
axisLine:{lineStyle:{color:"#262b33"}},axisTick:{show:false}},
|
||||
yAxis:{type:"value",splitLine:{lineStyle:{color:"#1f242b"}},axisLabel:{color:"#98a1ac",fontSize:11}},
|
||||
series:series
|
||||
},true);
|
||||
}
|
||||
@@ -845,19 +852,19 @@
|
||||
|
||||
charts.heat.setOption({
|
||||
grid:{left:single?68:78,right:20,top:16,bottom:46},
|
||||
tooltip:{backgroundColor:"rgba(12,18,32,.94)",borderColor:"rgba(255,255,255,.12)",
|
||||
textStyle:{color:"#e8edf7",fontSize:12},
|
||||
formatter:p=>`<b>${yData[p.value[1]]}</b> ${hours[p.value[0]]}:00<br>`
|
||||
tooltip:{backgroundColor:"rgba(20,23,27,.96)",borderColor:"#262b33",
|
||||
textStyle:{color:"#e4e7eb",fontSize:12},
|
||||
formatter:p=>`<b>${esc(yData[p.value[1]])}</b> ${hours[p.value[0]]}:00<br>`
|
||||
+ `消耗 ${fmt(p.value[2])} 积分<br>调用 ${fmt0(p.value[3]||0)} 次`},
|
||||
xAxis:{type:"category",data:xData,splitArea:{show:true,areaStyle:{color:["rgba(255,255,255,.02)","transparent"]}},
|
||||
axisLabel:{color:"#8b9bb4",fontSize:10},axisLine:{lineStyle:{color:"rgba(255,255,255,.12)"}}},
|
||||
yAxis:{type:"category",data:yData,axisLabel:{color:"#8b9bb4",fontSize:10},
|
||||
axisLine:{lineStyle:{color:"rgba(255,255,255,.12)"}}},
|
||||
xAxis:{type:"category",data:xData,splitArea:{show:true,areaStyle:{color:["#14171b","transparent"]}},
|
||||
axisLabel:{color:"#98a1ac",fontSize:10},axisLine:{lineStyle:{color:"#262b33"}}},
|
||||
yAxis:{type:"category",data:yData,axisLabel:{color:"#98a1ac",fontSize:10},
|
||||
axisLine:{lineStyle:{color:"#262b33"}}},
|
||||
visualMap:{min:0,max:max||1,calculable:false,orient:"horizontal",left:"center",bottom:6,
|
||||
itemWidth:11,itemHeight:90,textStyle:{color:"#8b9bb4",fontSize:10},
|
||||
inRange:{color:["#0f2233","#155e75","#0891b2","#22d3ee","#fbbf24","#f472b6"]}},
|
||||
itemWidth:11,itemHeight:90,textStyle:{color:"#98a1ac",fontSize:10},
|
||||
inRange:{color:["#0f2233","#22405e","#2f5b86","#4c8df6","#c9952f","#c07a9e"]}},
|
||||
series:[{type:"heatmap",data:data,
|
||||
itemStyle:{borderColor:"rgba(0,0,0,.25)",borderWidth:.5},
|
||||
itemStyle:{borderColor:"#0e1013",borderWidth:.5},
|
||||
emphasis:{itemStyle:{borderColor:"#fff",borderWidth:1}}}]
|
||||
},true);
|
||||
}
|
||||
@@ -886,31 +893,31 @@
|
||||
|
||||
charts.hist.setOption({
|
||||
grid:{left:52,right:52,top:38,bottom:52},
|
||||
tooltip:{trigger:"axis",backgroundColor:"rgba(12,18,32,.94)",borderColor:"rgba(255,255,255,.12)",
|
||||
textStyle:{color:"#e8edf7",fontSize:12},
|
||||
tooltip:{trigger:"axis",backgroundColor:"rgba(20,23,27,.96)",borderColor:"#262b33",
|
||||
textStyle:{color:"#e4e7eb",fontSize:12},
|
||||
formatter:p=>{
|
||||
const i=p[0].dataIndex;
|
||||
return `<b>${HIST_LABELS[i]}</b><br>${fmt0(cnt[i])} 笔(占笔数 ${(cnt[i]/tk*100).toFixed(1)}%)<br>`
|
||||
+ `积分 ${fmt(cred[i])}(占 ${share[i]}%)<br>累计积分占比 ${cumShare[i]}%`;
|
||||
}},
|
||||
legend:{data:["笔数","积分占比","累计积分占比"],top:4,textStyle:{color:"#8b9bb4",fontSize:11},
|
||||
legend:{data:["笔数","积分占比","累计积分占比"],top:4,textStyle:{color:"#98a1ac",fontSize:11},
|
||||
icon:"roundRect",itemWidth:10,itemHeight:10},
|
||||
xAxis:{type:"category",data:HIST_LABELS,axisLabel:{color:"#8b9bb4",fontSize:10,rotate:32},
|
||||
axisLine:{lineStyle:{color:"rgba(255,255,255,.12)"}},axisTick:{show:false}},
|
||||
xAxis:{type:"category",data:HIST_LABELS,axisLabel:{color:"#98a1ac",fontSize:10,rotate:32},
|
||||
axisLine:{lineStyle:{color:"#262b33"}},axisTick:{show:false}},
|
||||
yAxis:[
|
||||
{type:"value",name:"笔数",nameTextStyle:{color:"#8b9bb4",fontSize:11},
|
||||
splitLine:{lineStyle:{color:"rgba(255,255,255,.06)"}},axisLabel:{color:"#8b9bb4",fontSize:11}},
|
||||
{type:"value",name:"%",max:100,nameTextStyle:{color:"#8b9bb4",fontSize:11},
|
||||
splitLine:{show:false},axisLabel:{color:"#8b9bb4",fontSize:11}}
|
||||
{type:"value",name:"笔数",nameTextStyle:{color:"#98a1ac",fontSize:11},
|
||||
splitLine:{lineStyle:{color:"#1f242b"}},axisLabel:{color:"#98a1ac",fontSize:11}},
|
||||
{type:"value",name:"%",max:100,nameTextStyle:{color:"#98a1ac",fontSize:11},
|
||||
splitLine:{show:false},axisLabel:{color:"#98a1ac",fontSize:11}}
|
||||
],
|
||||
series:[
|
||||
{name:"笔数",type:"bar",barMaxWidth:28,data:cnt,
|
||||
itemStyle:{borderRadius:[4,4,0,0],
|
||||
color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:"#a78bfa"},{offset:1,color:"rgba(167,139,250,.18)"}])}},
|
||||
color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:"#c9952f"},{offset:1,color:"rgba(201,149,47,.18)"}])}},
|
||||
{name:"积分占比",type:"line",yAxisIndex:1,smooth:true,symbol:"circle",symbolSize:5,data:share,
|
||||
lineStyle:{color:"#fbbf24",width:2},itemStyle:{color:"#fbbf24"}},
|
||||
lineStyle:{color:"#c9952f",width:2},itemStyle:{color:"#c9952f"}},
|
||||
{name:"累计积分占比",type:"line",yAxisIndex:1,smooth:true,symbol:"none",data:cumShare,
|
||||
lineStyle:{color:"#22d3ee",width:1.6,type:"dashed"},itemStyle:{color:"#22d3ee"}}
|
||||
lineStyle:{color:"#4c8df6",width:1.6,type:"dashed"},itemStyle:{color:"#4c8df6"}}
|
||||
]
|
||||
},true);
|
||||
}
|
||||
@@ -929,25 +936,25 @@
|
||||
+ "右上角=又多又贵,右下角=多但便宜(免费模型贴在横轴上)";
|
||||
charts.scatter.setOption({
|
||||
grid:{left:62,right:34,top:34,bottom:46},
|
||||
tooltip:{backgroundColor:"rgba(12,18,32,.94)",borderColor:"rgba(255,255,255,.12)",
|
||||
textStyle:{color:"#e8edf7",fontSize:12},
|
||||
tooltip:{backgroundColor:"rgba(20,23,27,.96)",borderColor:"#262b33",
|
||||
textStyle:{color:"#e4e7eb",fontSize:12},
|
||||
formatter:p=>{
|
||||
const d = p.data;
|
||||
return `<b>${d.name}</b><br>调用 ${fmt0(d.calls)} 次<br>积分 ${fmt(d.credits)}<br>`
|
||||
return `<b>${esc(d.name)}</b><br>调用 ${fmt0(d.calls)} 次<br>积分 ${fmt(d.credits)}<br>`
|
||||
+ `单次均价 ${fmt(d.avg)}<br>免费调用 ${fmt0(d.free)} 次(${d.calls?(d.free/d.calls*100).toFixed(1):"0"}%)`;
|
||||
}},
|
||||
xAxis:{type:"value",name:"调用次数",nameTextStyle:{color:"#8b9bb4",fontSize:11},
|
||||
splitLine:{lineStyle:{color:"rgba(255,255,255,.06)"}},axisLabel:{color:"#8b9bb4",fontSize:11}},
|
||||
yAxis:{type:"value",name:"积分消耗",nameTextStyle:{color:"#8b9bb4",fontSize:11},
|
||||
splitLine:{lineStyle:{color:"rgba(255,255,255,.06)"}},axisLabel:{color:"#8b9bb4",fontSize:11}},
|
||||
xAxis:{type:"value",name:"调用次数",nameTextStyle:{color:"#98a1ac",fontSize:11},
|
||||
splitLine:{lineStyle:{color:"#1f242b"}},axisLabel:{color:"#98a1ac",fontSize:11}},
|
||||
yAxis:{type:"value",name:"积分消耗",nameTextStyle:{color:"#98a1ac",fontSize:11},
|
||||
splitLine:{lineStyle:{color:"#1f242b"}},axisLabel:{color:"#98a1ac",fontSize:11}},
|
||||
series:[{
|
||||
type:"scatter", data:pts,
|
||||
symbolSize:v=>6 + Math.sqrt((v[2]||0)/maxAvg) * 32,
|
||||
itemStyle:{color:p=>{
|
||||
const r = p.data.calls/maxCalls;
|
||||
return `rgba(34,211,238,${(0.30 + 0.55*r).toFixed(2)})`;},
|
||||
borderColor:"rgba(167,139,250,.85)",borderWidth:1},
|
||||
label:{show:true,position:"top",color:"#8b9bb4",fontSize:10,formatter:p=>p.data.name},
|
||||
return `rgba(76,141,246,${(0.30 + 0.55*r).toFixed(2)})`;},
|
||||
borderColor:"rgba(201,149,47,.85)",borderWidth:1},
|
||||
label:{show:true,position:"top",color:"#98a1ac",fontSize:10,formatter:p=>p.data.name},
|
||||
emphasis:{itemStyle:{borderColor:"#fff"}}
|
||||
}]
|
||||
},true);
|
||||
@@ -961,7 +968,7 @@
|
||||
`<thead><tr><th>${DIM_NAME}</th><th class="num">调用次数</th><th class="num">计费 / 免费</th>
|
||||
<th class="num">积分消耗</th><th class="num">占比</th><th class="num">单次均值</th></tr></thead>
|
||||
<tbody>${g.map((o,i)=>`<tr>
|
||||
<td><span class="dot" style="background:${dimColor(i)}"></span>${DIM_LABEL(o.key)}</td>
|
||||
<td><span class="dot" style="background:${dimColor(i)}"></span>${esc(DIM_LABEL(o.key))}</td>
|
||||
<td class="num">${fmt0(o.calls)}</td>
|
||||
<td class="num">${fmt0(o.billableCalls)} / <span style="color:var(--blue)">${fmt0(o.freeCalls)}</span></td>
|
||||
<td class="num">${fmt(o.credits)}</td>
|
||||
@@ -990,7 +997,7 @@
|
||||
if(topState.mode==="global"){
|
||||
const t = (state.src && state.src.top) || [];
|
||||
items = t.slice(0,20).map(x=>({rank:x.rank, t:x.t, model:x.m, client:x.cl, credits:x.c, prompt:x.px}));
|
||||
$("#topHint").textContent = "全局 TOP 20(来自 top.json,由脚本按积分降序全量计算,不受上方筛选影响)";
|
||||
$("#topHint").textContent = "全部存档的 TOP 20(按积分降序,不受上方筛选影响)";
|
||||
}else{
|
||||
items = state.rows.slice().sort((a,b)=>b.credits-a.credits).slice(0,20)
|
||||
.map((x,i)=>({rank:i+1, t:x.t, model:x.model, client:x.client, credits:x.credits, prompt:x.prompt}));
|
||||
@@ -1002,8 +1009,8 @@
|
||||
<th class="num">积分</th><th>Prompt 摘要</th></tr></thead>
|
||||
<tbody>${items.map(x=>`<tr>
|
||||
<td class="num">${x.rank}</td>
|
||||
<td>${(x.t||"").slice(5,16)}</td>
|
||||
<td>${x.model}</td><td>${x.client}</td>
|
||||
<td>${esc((x.t||"").slice(5,16))}</td>
|
||||
<td>${esc(x.model)}</td><td>${esc(x.client)}</td>
|
||||
<td class="num">${fmt(x.credits)}
|
||||
<div class="bar-mini" style="width:46px;display:inline-block;vertical-align:2px;margin-left:6px">
|
||||
<div style="width:${(x.credits/max*100).toFixed(1)}%"></div></div></td>
|
||||
@@ -1016,7 +1023,17 @@
|
||||
function renderSources(){
|
||||
const mf = (state.src && state.src.manifest) || {};
|
||||
const srcs = mf.sources || [];
|
||||
const rows = srcs.map(s=>`<tr><td><code>${s.path}</code></td><td>${s.role||"-"}</td>
|
||||
// 服务端对非管理员不下发「数据源」清单(表名 / 库文件属于实现细节),
|
||||
// 这时把表格整块收起来 —— 只隐藏表格本身会留一个只有表头的空壳。
|
||||
const wrap = $("#tblSrc").parentNode;
|
||||
if(wrap){
|
||||
wrap.style.display = srcs.length ? "" : "none";
|
||||
const cap = wrap.previousElementSibling;
|
||||
if(cap) cap.textContent = srcs.length
|
||||
? "数据由本项目 Flask 后端从 SQLite 实时聚合"
|
||||
: "你的数据由服务端实时聚合,口径自检如下";
|
||||
}
|
||||
const rows = srcs.map(s=>`<tr><td><code>${esc(s.path)}</code></td><td>${esc(s.role||"-")}</td>
|
||||
<td class="num">${fmt0(s.count)}</td><td class="num">${s.bytes? (s.bytes/1024).toFixed(1)+" KB" : "查询"}</td></tr>`).join("");
|
||||
$("#tblSrc").innerHTML =
|
||||
`<thead><tr><th>数据源</th><th>角色</th><th class="num">条目</th><th class="num">体积</th></tr></thead>
|
||||
@@ -1031,11 +1048,10 @@
|
||||
$("#srcCheck").innerHTML =
|
||||
`<div>存档总量(SQLite 正本):${fmt0(t.records||0)} 条 / ${fmt(t.credits||0)} 积分 · `
|
||||
+ `${(t.days||[]).length} 个活跃日 · ${(t.months||[]).length} 个月</div>`
|
||||
+ (w ? `<div>当前窗口(SQL 聚合):${fmt0(w.records||0)} 条 / ${fmt(w.credits||0)} 积分 | `
|
||||
+ `页面按明细实时聚合:${fmt0(state.all.length)} 条 / ${fmt(mine)} 积分 `
|
||||
+ `<span class="${ok?"ok":"bad"}">${ok?"✔ 一致":"✖ 不一致"}</span></div>` : "")
|
||||
+ `<div>口径:明细 / 逐日 / 维度 / TOP 全部由 SQLite 实时计算(无中间 JSON 层) · `
|
||||
+ `Cookie ${h.cookie?"已配置":"<b style='color:#f87171'>未配置</b>"}`
|
||||
+ (w ? `<div>当前窗口:SQL 聚合 ${fmt0(w.records||0)} 条 / ${fmt(w.credits||0)} 积分,`
|
||||
+ `页面重算 ${fmt0(state.all.length)} 条 / ${fmt(mine)} 积分 `
|
||||
+ `<span class="${ok?"ok":"bad"}">${ok?"一致":"不一致"}</span></div>` : "")
|
||||
+ `<div>Cookie ${h.cookie?"已配置":"<b style='color:#cf6679'>未配置</b>"}`
|
||||
+ `${h.lastRunAt?` · 最近采集 ${h.lastRunAt}(${h.lastRunStatus||"-"})`:""}</div>`;
|
||||
}
|
||||
|
||||
@@ -1135,8 +1151,8 @@
|
||||
`<b>${d}</b>${(pd && pd.date===d)?`<span class="tag">残日 · 截至 ${pd.hhmm}</span>`:""}<br>`
|
||||
+ `积分消耗 ${fmt(o.credits)}<br>`
|
||||
+ `调用次数 ${fmt0(o.calls)} 次(计费 ${fmt0(o.billableCalls||0)} · 免费 ${fmt0(o.freeCalls||0)})<br>`
|
||||
+ `TOP 模型 ${tm? tm[0]+"("+fmt(tm[1])+")" : "-"}<br>`
|
||||
+ `TOP 时段 ${th? th[0]+":00("+fmt(th[1])+")" : "-"}`;
|
||||
+ `TOP 模型 ${tm? esc(tm[0])+"("+fmt(tm[1])+")" : "-"}<br>`
|
||||
+ `TOP 时段 ${th? esc(th[0])+":00("+fmt(th[1])+")" : "-"}`;
|
||||
this.el.style.display = "block";
|
||||
}
|
||||
const w = this.el.offsetWidth, h = this.el.offsetHeight;
|
||||
@@ -1224,13 +1240,11 @@
|
||||
state.all.forEach(x=>{ b[x.client] = (b[x.client]||0) + x.credits; });
|
||||
const tot = Object.values(b).reduce((s,v)=>s+v,0)||1;
|
||||
const parts = Object.entries(b).sort((a,c)=>c[1]-a[1])
|
||||
.map(([k,v])=>k+" "+(v/tot*100).toFixed(1)+"%").join(" · ");
|
||||
.map(([k,v])=>esc(k)+" "+(v/tot*100).toFixed(1)+"%").join(" · ");
|
||||
const t = (state.mf && state.mf.totals) || {};
|
||||
$(".sub").innerHTML = "数据来源:本项目 Flask 后端采集云端用量明细 → SQLite("
|
||||
+ "<code>data/usage.sqlite</code>),本页经 <code>/api</code> 实时聚合,"
|
||||
+ "无中间 JSON 层;本窗口客户端构成:" + parts
|
||||
+ `(窗口内 ${fmt0(state.all.length)} 条 / ${fmt(state.all.reduce((s,x)=>s+x.credits,0))} 积分,`
|
||||
+ `全量 ${fmt0(t.records||0)} 条)`;
|
||||
$(".sub").innerHTML = "数据来源:<code>data/usage.sqlite</code>,经 <code>/api</code> 实时聚合;"
|
||||
+ `本窗口 ${fmt0(state.all.length)} 条 / ${fmt(state.all.reduce((s,x)=>s+x.credits,0))} 积分,`
|
||||
+ `全量 ${fmt0(t.records||0)} 条 · 客户端构成:` + parts;
|
||||
}
|
||||
|
||||
// 取数 + 渲染:窗口变了先按新窗口拉一次明细,再交给原有的渲染管线
|
||||
|
||||
在新工单中引用
屏蔽一个用户