balance-dsh 1.0.3 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/client.js +216 -80
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ DSH 插件:在 Web UI 中直接显示 DeepSeek 账户余额和当前会话 tok
|
|
|
7
7
|
- **侧边栏余额行**:侧边栏展开时显示在底部(紧贴"设置"按钮上方),格式:钱包图标 + `余额` + 金额(带货币单位)+ 时段标签(高峰红 / 空闲绿)。金额按余额状态变色:充足绿色、偏低(<10)琥珀黄、为 0 显示红色"去充值"。
|
|
8
8
|
- **收起态 FAB**:侧边栏收起时,余额显示为左下角悬浮胶囊。
|
|
9
9
|
- **自动刷新**:每 60 秒自动刷新一次;每次对话完成(新回复落库)也会立即刷新;右键余额任意区域可手动刷新。
|
|
10
|
-
-
|
|
10
|
+
- **余额与使用统计**:单击余额在余额项旁以轻量动画打开局部快捷菜单,展示账户余额、近 90 天计费 Token、估算费用、扫描会话数和按日使用热力图;点击菜单外区域会播放关闭动画,双击余额打开 DeepSeek 官方 API 用量页。
|
|
11
11
|
- **高峰/空闲计费**:官方高峰窗口为**周一至周五 09:00-12:00、14:00-18:00(北京时间)**,周末全天空闲。消耗金额**跨时段精确计费**:高峰时段的 token 按高峰价 ×2、空闲时段按目录价,按事件时间分桶计算。
|
|
12
12
|
- **会话消耗行**:对话框下方实时显示当前会话消耗(新增 tokens + 估算金额),悬停查看完整明细(模型、时段拆分、计费口径)。
|
|
13
13
|
- **`/balance` 命令**:命令行查询余额明细和当前会话消耗。
|
|
@@ -16,7 +16,7 @@ DSH 插件:在 Web UI 中直接显示 DeepSeek 账户余额和当前会话 tok
|
|
|
16
16
|
|
|
17
17
|
- 热力图按北京时间聚合最近 90 天持久化会话中的 `assistant/message` Token usage。
|
|
18
18
|
- 费用按照每条消息的实际模型和峰谷时段估算,最终金额以 DeepSeek 官方账单为准。
|
|
19
|
-
- 为避免历史会话过多拖慢 DSH,最多扫描最近 200
|
|
19
|
+
- 为避免历史会话过多拖慢 DSH,最多扫描最近 200 个会话;服务端统计和客户端展示结果均缓存 5 分钟,重复打开菜单不会重复请求。
|
|
20
20
|
- 若未配置 API Key,仍可查看本地 Token 与费用统计,但账户余额会显示为不可用。
|
|
21
21
|
|
|
22
22
|
## 安装
|
package/lib/client.js
CHANGED
|
@@ -5,6 +5,7 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
+
let reactDom = require("react-dom");
|
|
8
9
|
let jsx = require("react/jsx-runtime");
|
|
9
10
|
let primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
11
|
|
|
@@ -41,25 +42,40 @@ window.__ModuleLoader__.load({
|
|
|
41
42
|
.balance-fab-dark:hover{background:#cf3a3f}
|
|
42
43
|
.balance-usage-line{box-sizing:border-box;display:flex;justify-content:center;align-items:center;gap:10px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
43
44
|
.balance-usage-line strong{color:var(--dsw-alias-label-primary);font-weight:600}
|
|
44
|
-
/* ──
|
|
45
|
-
.balance-stats-
|
|
46
|
-
.balance-stats-
|
|
47
|
-
.balance-stats-
|
|
48
|
-
.balance-stats-
|
|
45
|
+
/* ── 跨会话统计浮层(余额元素的子级,向右/向上展开) ── */
|
|
46
|
+
.balance-stats-anchor{position:relative;display:flex;min-width:0;overflow:visible}
|
|
47
|
+
.balance-stats-anchor--wide{width:100%}
|
|
48
|
+
.balance-stats-anchor--compact{position:fixed;left:68px;bottom:12px;z-index:10000}
|
|
49
|
+
.balance-stats-anchor--compact .balance-fab{position:static}
|
|
50
|
+
.balance-stats-panel{box-sizing:border-box;position:fixed;z-index:10000;width:460px;max-height:min(620px,calc(100vh - 24px));overflow-x:hidden;overflow-y:auto;scrollbar-gutter:stable;padding:16px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base,#fff);border:1px solid var(--dsw-alias-border-l2,#d9d9df);border-radius:12px;box-shadow:0 14px 36px rgba(0,0,0,.24);transform-origin:left bottom;will-change:transform,opacity;contain:layout style;animation:balance-stats-panel-in .18s var(--ds-ease-in-out,ease-out) both}
|
|
51
|
+
.balance-stats-panel--closing{pointer-events:none;animation:balance-stats-panel-out .14s var(--ds-ease-in-out,ease-in) both}
|
|
52
|
+
@keyframes balance-stats-panel-in{from{opacity:0;transform:translateY(8px) scale(.975)}to{opacity:1;transform:translateY(0) scale(1)}}
|
|
53
|
+
@keyframes balance-stats-panel-out{from{opacity:1;transform:translateY(0) scale(1)}to{opacity:0;transform:translateY(6px) scale(.985)}}
|
|
54
|
+
.balance-stats-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}
|
|
55
|
+
.balance-stats-title{margin:0;font-size:16px;line-height:24px;font-weight:700}
|
|
49
56
|
.balance-stats-subtitle{margin:4px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
50
57
|
.balance-stats-close{width:32px;height:32px;flex:none;border:0;border-radius:8px;color:var(--dsw-alias-label-secondary);background:transparent;font-size:22px;line-height:1;cursor:pointer}
|
|
51
58
|
.balance-stats-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
52
59
|
.balance-stats-error{padding:12px;border-radius:10px;color:var(--dsw-alias-state-error-primary,#e5484d);background:rgba(229,72,77,.10)}
|
|
53
60
|
.balance-stats-loading{padding:48px 12px;text-align:center;color:var(--dsw-alias-label-secondary)}
|
|
54
|
-
.balance-stats-cards{display:grid;grid-template-columns:repeat(
|
|
55
|
-
.balance-stats-card{min-width:0;padding:
|
|
61
|
+
.balance-stats-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-bottom:16px}
|
|
62
|
+
.balance-stats-card{min-width:0;padding:11px;border:1px solid var(--dsw-alias-border-l2,#d9d9df);border-radius:10px;background:var(--dsw-alias-bg-module-platform,rgba(127,127,127,.04));animation:balance-stats-card-in .2s var(--ds-ease-in-out,ease-out) both}
|
|
63
|
+
.balance-stats-card:nth-child(2){animation-delay:.025s}.balance-stats-card:nth-child(3){animation-delay:.05s}.balance-stats-card:nth-child(4){animation-delay:.075s}
|
|
64
|
+
@keyframes balance-stats-card-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
|
|
56
65
|
.balance-stats-card__label{display:block;margin-bottom:6px;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
57
|
-
.balance-stats-card__value{display:block;overflow:hidden;color:var(--dsw-alias-label-primary);font-size:
|
|
66
|
+
.balance-stats-card__value{display:block;overflow:hidden;color:var(--dsw-alias-label-primary);font-size:16px;font-weight:700;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap}
|
|
58
67
|
.balance-stats-section-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 10px;font-size:14px;font-weight:650}
|
|
59
68
|
.balance-stats-legend{display:flex;align-items:center;gap:4px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:400}
|
|
60
|
-
.balance-heatmap-wrap{
|
|
69
|
+
.balance-heatmap-wrap{min-width:0;overflow:hidden;padding:4px 2px 8px}
|
|
61
70
|
.balance-heatmap{display:grid;grid-template-rows:repeat(7,14px);grid-auto-flow:column;grid-auto-columns:14px;gap:4px;width:max-content;min-width:100%}
|
|
62
|
-
.balance-heatmap-cell{width:14px;height:14px;border-radius:3px;background:rgba(127,127,127,.10)}
|
|
71
|
+
.balance-heatmap-cell{display:block;box-sizing:border-box;width:14px;height:14px;padding:0;border:0;border-radius:3px;background:rgba(127,127,127,.10);appearance:none;transition:transform .12s var(--ds-ease-in-out,ease-out),filter .12s var(--ds-ease-in-out,ease-out)}
|
|
72
|
+
.balance-heatmap-cell[aria-label]{cursor:default}
|
|
73
|
+
.balance-heatmap-cell[aria-label]:hover{transform:scale(1.18);filter:brightness(1.14)}
|
|
74
|
+
.balance-heatmap-tooltip{box-sizing:border-box;position:fixed;z-index:10020;max-width:min(300px,calc(100vw - 24px));padding:6px 9px;border:1px solid var(--dsw-alias-border-l2,#45454d);border-radius:6px;background:var(--dsw-alias-bg-module-platform,#27272b);box-shadow:0 6px 18px rgba(0,0,0,.22);color:var(--dsw-alias-label-primary,#f4f4f5);font-size:11px;font-weight:400;line-height:1.45;white-space:nowrap;pointer-events:none;animation:balance-heatmap-tooltip-in .12s var(--ds-ease-in-out,ease-out) both}
|
|
75
|
+
.balance-heatmap-tooltip--left{transform:translateY(-100%)}
|
|
76
|
+
.balance-heatmap-tooltip--center{transform:translate(-50%,-100%)}
|
|
77
|
+
.balance-heatmap-tooltip--right{transform:translateY(-100%)}
|
|
78
|
+
@keyframes balance-heatmap-tooltip-in{from{opacity:0}to{opacity:1}}
|
|
63
79
|
.balance-heatmap-cell--1{background:rgba(34,197,94,.25)}
|
|
64
80
|
.balance-heatmap-cell--2{background:rgba(34,197,94,.45)}
|
|
65
81
|
.balance-heatmap-cell--3{background:rgba(34,197,94,.68)}
|
|
@@ -68,7 +84,8 @@ window.__ModuleLoader__.load({
|
|
|
68
84
|
.balance-stats-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:14px;color:var(--dsw-alias-label-secondary);font-size:11px}
|
|
69
85
|
.balance-stats-link{padding:7px 12px;border:1px solid var(--dsw-alias-border-l2,#d9d9df);border-radius:8px;color:var(--dsw-alias-label-primary);text-decoration:none;background:var(--dsw-alias-bg-module-platform,rgba(127,127,127,.04))}
|
|
70
86
|
.balance-stats-link:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
71
|
-
@media(max-width:680px){.balance-stats-
|
|
87
|
+
@media(max-width:680px){.balance-stats-panel{max-height:calc(100vh - 68px)}}
|
|
88
|
+
@media(prefers-reduced-motion:reduce){.balance-stats-panel,.balance-stats-panel--closing,.balance-stats-card{animation:none}.balance-heatmap-cell{transition:none}}
|
|
72
89
|
/* ── 展开态底部一行:[设置齿轮][余额胶囊],余额在设置右边 ──
|
|
73
90
|
全部使用稳定锚点,不依赖 CSS Modules hash 类名(DSH 升级后仍有效):
|
|
74
91
|
- wide 态特征:底部存在 .balance-footer / .balance-topup(收起态渲染 position:fixed 的 .balance-fab,不影响)
|
|
@@ -82,12 +99,15 @@ window.__ModuleLoader__.load({
|
|
|
82
99
|
`;
|
|
83
100
|
|
|
84
101
|
const tagId = "balance/balance-widget.css";
|
|
85
|
-
if (typeof document !== "undefined"
|
|
86
|
-
|
|
87
|
-
tag
|
|
88
|
-
|
|
102
|
+
if (typeof document !== "undefined") {
|
|
103
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
|
104
|
+
if (tag === null) {
|
|
105
|
+
tag = document.createElement("style");
|
|
106
|
+
tag.dataset.plugin = "balance";
|
|
107
|
+
tag.dataset.pluginCss = tagId;
|
|
108
|
+
document.head.appendChild(tag);
|
|
109
|
+
}
|
|
89
110
|
tag.textContent = css;
|
|
90
|
-
document.head.appendChild(tag);
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
// ─── 价格(与服务端默认保持一致;服务端 /balance-api 返回后会覆盖) ───
|
|
@@ -337,16 +357,38 @@ window.__ModuleLoader__.load({
|
|
|
337
357
|
fetchBalanceOnce();
|
|
338
358
|
}
|
|
339
359
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return
|
|
360
|
+
const USAGE_STATS_CACHE_TTL = 5 * 60_000;
|
|
361
|
+
let usageStatsCache = null;
|
|
362
|
+
let usageStatsCacheAt = 0;
|
|
363
|
+
let usageStatsInFlight = null;
|
|
364
|
+
|
|
365
|
+
function getCachedUsageStats() {
|
|
366
|
+
return usageStatsCache && Date.now() - usageStatsCacheAt < USAGE_STATS_CACHE_TTL
|
|
367
|
+
? usageStatsCache
|
|
368
|
+
: null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function fetchUsageStats() {
|
|
372
|
+
const cached = getCachedUsageStats();
|
|
373
|
+
if (cached) return Promise.resolve(cached);
|
|
374
|
+
if (usageStatsInFlight) return usageStatsInFlight;
|
|
375
|
+
usageStatsInFlight = fetch("/balance-api?stats=1")
|
|
376
|
+
.then(async (res) => {
|
|
377
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
378
|
+
const data = await res.json();
|
|
379
|
+
if (!data || !data.ok) throw new Error((data && data.error) || "统计加载失败");
|
|
380
|
+
if (!data.stats) throw new Error(data.statsError || "暂无可用的会话统计");
|
|
381
|
+
usageStatsCache = data;
|
|
382
|
+
usageStatsCacheAt = Date.now();
|
|
383
|
+
return data;
|
|
384
|
+
})
|
|
385
|
+
.finally(() => {
|
|
386
|
+
usageStatsInFlight = null;
|
|
387
|
+
});
|
|
388
|
+
return usageStatsInFlight;
|
|
347
389
|
}
|
|
348
390
|
|
|
349
|
-
function StatsCard({ label, value }) {
|
|
391
|
+
const StatsCard = react.memo(function StatsCard({ label, value }) {
|
|
350
392
|
return jsx.jsxs("div", {
|
|
351
393
|
className: "balance-stats-card",
|
|
352
394
|
children: [
|
|
@@ -354,20 +396,27 @@ window.__ModuleLoader__.load({
|
|
|
354
396
|
jsx.jsx("span", { className: "balance-stats-card__value", title: value, children: value }),
|
|
355
397
|
],
|
|
356
398
|
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function UsageStatsPanel({ onClose }) {
|
|
360
|
-
const
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
function UsageStatsPanel({ onClose, panelRef, style, closing }) {
|
|
402
|
+
const liveBalance = useBalance();
|
|
403
|
+
const [heatmapTooltip, setHeatmapTooltip] = react.useState(null);
|
|
404
|
+
const [state, setState] = react.useState(() => {
|
|
405
|
+
const cached = getCachedUsageStats();
|
|
406
|
+
return cached
|
|
407
|
+
? { loading: false, data: cached, error: "" }
|
|
408
|
+
: { loading: true, data: null, error: "" };
|
|
409
|
+
});
|
|
361
410
|
react.useEffect(() => {
|
|
362
|
-
|
|
363
|
-
fetchUsageStats(
|
|
364
|
-
(data) => setState({ loading: false, data, error: "" }),
|
|
411
|
+
let disposed = false;
|
|
412
|
+
fetchUsageStats().then(
|
|
413
|
+
(data) => { if (!disposed) setState({ loading: false, data, error: "" }); },
|
|
365
414
|
(error) => {
|
|
366
|
-
if (
|
|
415
|
+
if (disposed) return;
|
|
367
416
|
setState({ loading: false, data: null, error: error && error.message ? error.message : String(error) });
|
|
368
417
|
},
|
|
369
418
|
);
|
|
370
|
-
return () =>
|
|
419
|
+
return () => { disposed = true; };
|
|
371
420
|
}, []);
|
|
372
421
|
react.useEffect(() => {
|
|
373
422
|
const onKeyDown = (event) => { if (event.key === "Escape") onClose(); };
|
|
@@ -378,25 +427,51 @@ window.__ModuleLoader__.load({
|
|
|
378
427
|
const data = state.data;
|
|
379
428
|
const stats = data && data.stats;
|
|
380
429
|
const days = stats && Array.isArray(stats.days) ? stats.days : [];
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
430
|
+
const showHeatmapTooltip = react.useCallback((event, label) => {
|
|
431
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
432
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
433
|
+
const center = rect.left + rect.width / 2;
|
|
434
|
+
const top = Math.max(12, rect.top - 7);
|
|
435
|
+
if (center < 162) {
|
|
436
|
+
setHeatmapTooltip({ label, align: "left", style: { left: Math.max(12, rect.left), top } });
|
|
437
|
+
} else if (center > viewportWidth - 162) {
|
|
438
|
+
setHeatmapTooltip({ label, align: "right", style: { right: Math.max(12, viewportWidth - rect.right), top } });
|
|
439
|
+
} else {
|
|
440
|
+
setHeatmapTooltip({ label, align: "center", style: { left: center, top } });
|
|
441
|
+
}
|
|
442
|
+
}, []);
|
|
443
|
+
const hideHeatmapTooltip = react.useCallback(() => setHeatmapTooltip(null), []);
|
|
444
|
+
const heatmap = react.useMemo(() => {
|
|
445
|
+
const cells = [];
|
|
446
|
+
const maxTokens = Math.max(0, ...days.map((day) => Number(day.totalTokens || 0)));
|
|
447
|
+
const firstDay = days[0] && days[0].date;
|
|
448
|
+
const leading = firstDay ? (new Date(firstDay + "T00:00:00Z").getUTCDay() + 6) % 7 : 0;
|
|
449
|
+
for (let i = 0; i < leading; i++) {
|
|
450
|
+
cells.push(jsx.jsx("span", { className: "balance-heatmap-cell balance-heatmap-cell--blank" }, "blank-" + i));
|
|
451
|
+
}
|
|
452
|
+
for (let dayIndex = 0; dayIndex < days.length; dayIndex++) {
|
|
453
|
+
const day = days[dayIndex];
|
|
454
|
+
const tokens = Number(day.totalTokens || 0);
|
|
455
|
+
const ratio = maxTokens > 0 ? tokens / maxTokens : 0;
|
|
456
|
+
const level = tokens === 0 ? 0 : ratio <= 0.25 ? 1 : ratio <= 0.5 ? 2 : ratio <= 0.75 ? 3 : 4;
|
|
457
|
+
const costText = "¥" + Number(day.cost || 0).toFixed(4);
|
|
458
|
+
const sessionsText = String(Number(day.sessions || 0));
|
|
459
|
+
const ariaLabel = day.date + ",消耗 " + formatTokens(tokens) + " Token,费用 " + costText + ",会话 " + sessionsText;
|
|
460
|
+
const tooltip = day.date + " · 消耗 " + formatTokens(tokens) + " Token · 费用 " + costText + " · 会话 " + sessionsText;
|
|
461
|
+
cells.push(jsx.jsx("button", {
|
|
462
|
+
type: "button",
|
|
463
|
+
tabIndex: -1,
|
|
464
|
+
className: "balance-heatmap-cell" + (level > 0 ? " balance-heatmap-cell--" + level : ""),
|
|
465
|
+
"aria-label": ariaLabel,
|
|
466
|
+
onMouseEnter: (event) => showHeatmapTooltip(event, tooltip),
|
|
467
|
+
onMouseLeave: hideHeatmapTooltip,
|
|
468
|
+
onFocus: (event) => showHeatmapTooltip(event, tooltip),
|
|
469
|
+
onBlur: hideHeatmapTooltip,
|
|
470
|
+
}, day.date));
|
|
471
|
+
}
|
|
472
|
+
return cells;
|
|
473
|
+
}, [days, hideHeatmapTooltip, showHeatmapTooltip]);
|
|
474
|
+
const balance = liveBalance.balance || (data && data.balance);
|
|
400
475
|
const total = stats && stats.total;
|
|
401
476
|
const balanceText = balance
|
|
402
477
|
? String(balance.total_balance ?? "0.00") + " " + String(balance.currency || "CNY")
|
|
@@ -407,7 +482,7 @@ window.__ModuleLoader__.load({
|
|
|
407
482
|
? jsx.jsx("div", { className: "balance-stats-error", children: state.error })
|
|
408
483
|
: jsx.jsxs(jsx.Fragment, {
|
|
409
484
|
children: [
|
|
410
|
-
data.balanceError ? jsx.jsx("div", { className: "balance-stats-error", children: "余额:" + data.balanceError }) : null,
|
|
485
|
+
!liveBalance.balance && data.balanceError ? jsx.jsx("div", { className: "balance-stats-error", children: "余额:" + data.balanceError }) : null,
|
|
411
486
|
jsx.jsxs("div", {
|
|
412
487
|
className: "balance-stats-cards",
|
|
413
488
|
children: [
|
|
@@ -449,31 +524,33 @@ window.__ModuleLoader__.load({
|
|
|
449
524
|
}),
|
|
450
525
|
],
|
|
451
526
|
});
|
|
452
|
-
return jsx.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
"
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
527
|
+
return jsx.jsxs("section", {
|
|
528
|
+
ref: panelRef,
|
|
529
|
+
style,
|
|
530
|
+
className: "balance-stats-panel" + (closing ? " balance-stats-panel--closing" : ""),
|
|
531
|
+
role: "dialog",
|
|
532
|
+
"aria-label": "余额与使用统计",
|
|
533
|
+
children: [
|
|
534
|
+
jsx.jsxs("header", {
|
|
535
|
+
className: "balance-stats-header",
|
|
536
|
+
children: [
|
|
537
|
+
jsx.jsxs("div", {
|
|
538
|
+
children: [
|
|
539
|
+
jsx.jsx("h2", { className: "balance-stats-title", children: "余额与使用统计" }),
|
|
540
|
+
jsx.jsx("p", { className: "balance-stats-subtitle", children: "余额快捷菜单 · 双击打开 DeepSeek API 用量页" }),
|
|
541
|
+
],
|
|
542
|
+
}),
|
|
543
|
+
jsx.jsx("button", { className: "balance-stats-close", type: "button", onClick: onClose, "aria-label": "关闭", children: "×" }),
|
|
544
|
+
],
|
|
545
|
+
}),
|
|
546
|
+
body,
|
|
547
|
+
heatmapTooltip ? reactDom.createPortal(jsx.jsx("div", {
|
|
548
|
+
className: "balance-heatmap-tooltip balance-heatmap-tooltip--" + heatmapTooltip.align,
|
|
549
|
+
style: heatmapTooltip.style,
|
|
550
|
+
role: "tooltip",
|
|
551
|
+
children: heatmapTooltip.label,
|
|
552
|
+
}), document.body) : null,
|
|
553
|
+
],
|
|
477
554
|
});
|
|
478
555
|
}
|
|
479
556
|
|
|
@@ -501,15 +578,61 @@ window.__ModuleLoader__.load({
|
|
|
501
578
|
function BalanceFooter({ wide }) {
|
|
502
579
|
const state = useBalance();
|
|
503
580
|
const [statsOpen, setStatsOpen] = react.useState(false);
|
|
581
|
+
const [statsClosing, setStatsClosing] = react.useState(false);
|
|
582
|
+
const [statsPosition, setStatsPosition] = react.useState({ left: 12, bottom: 56, width: 460 });
|
|
504
583
|
const clickTimer = react.useRef(null);
|
|
584
|
+
const closeTimer = react.useRef(null);
|
|
585
|
+
const anchorRef = react.useRef(null);
|
|
586
|
+
const panelRef = react.useRef(null);
|
|
505
587
|
react.useEffect(() => () => {
|
|
506
588
|
if (clickTimer.current !== null) clearTimeout(clickTimer.current);
|
|
589
|
+
if (closeTimer.current !== null) clearTimeout(closeTimer.current);
|
|
507
590
|
}, []);
|
|
591
|
+
const closeStats = react.useCallback(() => {
|
|
592
|
+
if (!statsOpen || statsClosing) return;
|
|
593
|
+
setStatsClosing(true);
|
|
594
|
+
if (closeTimer.current !== null) clearTimeout(closeTimer.current);
|
|
595
|
+
closeTimer.current = setTimeout(() => {
|
|
596
|
+
closeTimer.current = null;
|
|
597
|
+
setStatsOpen(false);
|
|
598
|
+
setStatsClosing(false);
|
|
599
|
+
}, 140);
|
|
600
|
+
}, [statsOpen, statsClosing]);
|
|
601
|
+
react.useEffect(() => {
|
|
602
|
+
if (!statsOpen) return undefined;
|
|
603
|
+
const closeOnOutsideClick = (event) => {
|
|
604
|
+
if (anchorRef.current && anchorRef.current.contains(event.target)) return;
|
|
605
|
+
if (panelRef.current && panelRef.current.contains(event.target)) return;
|
|
606
|
+
closeStats();
|
|
607
|
+
};
|
|
608
|
+
const closeOnResize = () => closeStats();
|
|
609
|
+
document.addEventListener("mousedown", closeOnOutsideClick);
|
|
610
|
+
window.addEventListener("resize", closeOnResize);
|
|
611
|
+
return () => {
|
|
612
|
+
document.removeEventListener("mousedown", closeOnOutsideClick);
|
|
613
|
+
window.removeEventListener("resize", closeOnResize);
|
|
614
|
+
};
|
|
615
|
+
}, [statsOpen, closeStats]);
|
|
508
616
|
const openStats = (event) => {
|
|
509
617
|
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
510
618
|
if (clickTimer.current !== null) clearTimeout(clickTimer.current);
|
|
619
|
+
if (closeTimer.current !== null) clearTimeout(closeTimer.current);
|
|
620
|
+
closeTimer.current = null;
|
|
621
|
+
setStatsClosing(false);
|
|
622
|
+
const trigger = event && event.currentTarget;
|
|
623
|
+
if (trigger && typeof trigger.getBoundingClientRect === "function") {
|
|
624
|
+
const rect = trigger.getBoundingClientRect();
|
|
625
|
+
const width = Math.max(1, Math.min(460, window.innerWidth - 24));
|
|
626
|
+
const desiredLeft = wide ? rect.right + 8 : rect.left;
|
|
627
|
+
const left = Math.max(12, Math.min(desiredLeft, window.innerWidth - width - 12));
|
|
628
|
+
const bottom = wide
|
|
629
|
+
? Math.max(12, window.innerHeight - rect.bottom)
|
|
630
|
+
: Math.max(12, window.innerHeight - rect.top + 8);
|
|
631
|
+
setStatsPosition({ left, bottom, width });
|
|
632
|
+
}
|
|
511
633
|
clickTimer.current = setTimeout(() => {
|
|
512
634
|
clickTimer.current = null;
|
|
635
|
+
setStatsClosing(false);
|
|
513
636
|
setStatsOpen(true);
|
|
514
637
|
}, 230);
|
|
515
638
|
};
|
|
@@ -517,6 +640,9 @@ window.__ModuleLoader__.load({
|
|
|
517
640
|
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
518
641
|
if (clickTimer.current !== null) clearTimeout(clickTimer.current);
|
|
519
642
|
clickTimer.current = null;
|
|
643
|
+
if (closeTimer.current !== null) clearTimeout(closeTimer.current);
|
|
644
|
+
closeTimer.current = null;
|
|
645
|
+
setStatsClosing(false);
|
|
520
646
|
setStatsOpen(false);
|
|
521
647
|
window.open("https://platform.deepseek.com/usage", "_blank", "noopener");
|
|
522
648
|
};
|
|
@@ -526,8 +652,18 @@ window.__ModuleLoader__.load({
|
|
|
526
652
|
onContextMenu: onBalanceContextMenu,
|
|
527
653
|
title: "单击查看余额与使用统计,双击打开 DeepSeek API 用量页,右键刷新",
|
|
528
654
|
};
|
|
529
|
-
const withPanel = (node) => jsx.jsxs(
|
|
530
|
-
|
|
655
|
+
const withPanel = (node) => jsx.jsxs("div", {
|
|
656
|
+
ref: anchorRef,
|
|
657
|
+
className: "balance-stats-anchor balance-stats-anchor--" + (wide ? "wide" : "compact"),
|
|
658
|
+
children: [
|
|
659
|
+
node,
|
|
660
|
+
statsOpen ? reactDom.createPortal(jsx.jsx(UsageStatsPanel, {
|
|
661
|
+
onClose: closeStats,
|
|
662
|
+
panelRef,
|
|
663
|
+
style: statsPosition,
|
|
664
|
+
closing: statsClosing,
|
|
665
|
+
}), document.body) : null,
|
|
666
|
+
],
|
|
531
667
|
});
|
|
532
668
|
|
|
533
669
|
if (!wide) {
|