dsh-plugin-usage 0.1.0

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.
@@ -0,0 +1,2209 @@
1
+ // Auto-generated module entry wrapping the shared client body
2
+ // (source: src/client/index.js). Do not edit directly.
3
+ export function apply(ctx) {
4
+ // ============================================================================
5
+ // Token 用量统计 — settings.section Client UI(纯 JS,动态插件沙箱)
6
+ // 可用全局:React / ctx / styles / host / console;浏览器全局 Date / Intl 等。
7
+ // CSS 常量内联自 styles.js(动态插件沙箱无 import,样式随本文件加载)。
8
+ // ============================================================================
9
+ const CSS = `
10
+ /* ============ 布局 ============ */
11
+ .tsd-section {
12
+ width: 100%;
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 16px;
16
+ color: var(--dsw-alias-label-primary);
17
+ animation: tsd-fade-in 0.4s var(--ds-ease-in-out);
18
+ }
19
+ @keyframes tsd-fade-in {
20
+ from { opacity: 0; transform: translateY(8px); }
21
+ to { opacity: 1; transform: none; }
22
+ }
23
+
24
+ /* ============ Header ============ */
25
+ .tsd-header {
26
+ display: flex;
27
+ align-items: flex-end;
28
+ justify-content: space-between;
29
+ gap: 12px;
30
+ flex-wrap: wrap;
31
+ }
32
+ .tsd-title {
33
+ margin: 0;
34
+ font-size: 24px;
35
+ font-weight: 700;
36
+ letter-spacing: -0.02em;
37
+ color: var(--dsw-alias-label-primary);
38
+ }
39
+ .tsd-subtitle {
40
+ margin: 4px 0 0;
41
+ font-size: 14px;
42
+ color: var(--dsw-alias-label-secondary);
43
+ }
44
+ .tsd-icon-btn {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ width: 34px;
49
+ height: 34px;
50
+ padding: 0;
51
+ border: 1px solid var(--dsw-alias-border-l2);
52
+ background: var(--dsw-alias-bg-layer-1);
53
+ color: var(--dsw-alias-label-secondary);
54
+ border-radius: 8px;
55
+ cursor: pointer;
56
+ font: inherit;
57
+ }
58
+ .tsd-icon-btn:hover {
59
+ color: var(--dsw-alias-label-primary);
60
+ background: var(--dsw-alias-interactive-bg-hover);
61
+ }
62
+ .tsd-spinning svg {
63
+ animation: tsd-spin 0.9s linear infinite;
64
+ }
65
+ @keyframes tsd-spin {
66
+ to { transform: rotate(360deg); }
67
+ }
68
+
69
+ /* ============ 筛选栏 ============ */
70
+ .tsd-filterbar {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 8px;
74
+ flex-wrap: wrap;
75
+ }
76
+ .tsd-seg-wrap {
77
+ position: relative;
78
+ display: inline-flex;
79
+ }
80
+ .tsd-seg {
81
+ display: inline-flex;
82
+ gap: 2px;
83
+ padding: 4px;
84
+ border-radius: 10px;
85
+ background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 12%, transparent);
86
+ }
87
+ .tsd-seg-btn {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ height: 28px;
91
+ padding: 0 12px;
92
+ border: none;
93
+ background: transparent;
94
+ color: var(--dsw-alias-label-secondary);
95
+ border-radius: 8px;
96
+ font: inherit;
97
+ font-size: 13px;
98
+ line-height: 1;
99
+ cursor: pointer;
100
+ white-space: nowrap;
101
+ }
102
+ .tsd-seg-btn:hover {
103
+ color: var(--dsw-alias-label-primary);
104
+ background: var(--dsw-alias-interactive-bg-hover);
105
+ }
106
+ .tsd-seg-active {
107
+ background: var(--dsw-alias-bg-base);
108
+ color: var(--dsw-alias-brand-primary);
109
+ font-weight: 600;
110
+ box-shadow: var(--dsw-shadow-lv1);
111
+ }
112
+ .tsd-seg-custom {
113
+ max-width: 220px;
114
+ }
115
+ .tsd-seg-custom .tsd-seg-label {
116
+ overflow: hidden;
117
+ text-overflow: ellipsis;
118
+ white-space: nowrap;
119
+ }
120
+ .tsd-seg-custom.tsd-seg-active {
121
+ background: var(--dsw-alias-brand-primary);
122
+ color: var(--dsw-alias-label-primary-inverted, #fff);
123
+ }
124
+ .tsd-seg-custom.tsd-seg-active:hover {
125
+ background: var(--dsw-alias-button-primary-hover);
126
+ }
127
+
128
+ /* ============ 通用按钮 ============ */
129
+ .tsd-btn {
130
+ height: 30px;
131
+ padding: 0 14px;
132
+ border: 1px solid var(--dsw-alias-border-l2);
133
+ background: var(--dsw-alias-bg-layer-1);
134
+ color: var(--dsw-alias-label-primary);
135
+ border-radius: 8px;
136
+ font: inherit;
137
+ font-size: 13px;
138
+ cursor: pointer;
139
+ white-space: nowrap;
140
+ }
141
+ .tsd-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }
142
+ .tsd-btn:disabled { opacity: 0.5; cursor: not-allowed; }
143
+ .tsd-btn-primary {
144
+ background: var(--dsw-alias-button-primary-fill);
145
+ border-color: transparent;
146
+ color: var(--dsw-alias-label-primary-inverted, #fff);
147
+ }
148
+ .tsd-btn-primary:hover { background: var(--dsw-alias-button-primary-hover); }
149
+ .tsd-btn-ghost { background: transparent; }
150
+ .tsd-btn-ghost:hover { background: var(--dsw-alias-interactive-bg-hover); }
151
+
152
+ /* ============ 浮层 Popover ============ */
153
+ .tsd-popover-anchor {
154
+ position: relative;
155
+ display: inline-flex;
156
+ }
157
+ .tsd-popover {
158
+ position: absolute;
159
+ top: calc(100% + 6px);
160
+ left: 0;
161
+ z-index: 40;
162
+ padding: 12px;
163
+ border: 1px solid var(--dsw-alias-border-l2);
164
+ background: var(--dsw-alias-bg-overlay);
165
+ border-radius: 12px;
166
+ box-shadow: var(--dsw-shadow-lv3);
167
+ }
168
+ .tsd-model-popover {
169
+ width: 320px;
170
+ max-width: calc(100vw - 24px);
171
+ }
172
+ .tsd-range-popover {
173
+ width: 300px;
174
+ max-width: calc(100vw - 24px);
175
+ }
176
+ .tsd-popover-title {
177
+ font-size: 14px;
178
+ font-weight: 600;
179
+ color: var(--dsw-alias-label-primary);
180
+ margin: 0 0 10px;
181
+ }
182
+ .tsd-popover-tools {
183
+ display: flex;
184
+ justify-content: space-between;
185
+ margin-bottom: 8px;
186
+ }
187
+ .tsd-link-btn {
188
+ border: none;
189
+ background: none;
190
+ color: var(--dsw-alias-state-business-primary);
191
+ font: inherit;
192
+ font-size: 12px;
193
+ cursor: pointer;
194
+ padding: 2px 4px;
195
+ border-radius: 4px;
196
+ }
197
+ .tsd-link-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }
198
+ .tsd-popover-actions {
199
+ display: flex;
200
+ justify-content: flex-end;
201
+ gap: 8px;
202
+ margin-top: 12px;
203
+ }
204
+ .tsd-popover-note {
205
+ margin: 8px 0 0;
206
+ font-size: 11px;
207
+ color: var(--dsw-alias-label-tertiary);
208
+ }
209
+ .tsd-popover-empty {
210
+ margin: 0;
211
+ padding: 10px 0;
212
+ text-align: center;
213
+ font-size: 12px;
214
+ color: var(--dsw-alias-label-tertiary);
215
+ }
216
+
217
+ /* ============ 模型下拉搜索 ============ */
218
+ .tsd-select-btn {
219
+ display: inline-flex;
220
+ align-items: center;
221
+ gap: 6px;
222
+ min-width: 120px;
223
+ max-width: 260px;
224
+ height: 36px;
225
+ padding: 0 10px;
226
+ border: 1px solid var(--dsw-alias-border-l2);
227
+ background: var(--dsw-alias-bg-layer-1);
228
+ color: var(--dsw-alias-label-primary);
229
+ border-radius: 8px;
230
+ font: inherit;
231
+ font-size: 13px;
232
+ cursor: pointer;
233
+ }
234
+ .tsd-select-btn:hover { border-color: var(--dsw-alias-border-l3); }
235
+ .tsd-select-has { border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2)); }
236
+ .tsd-select-label {
237
+ overflow: hidden;
238
+ text-overflow: ellipsis;
239
+ white-space: nowrap;
240
+ min-width: 0;
241
+ }
242
+ .tsd-chevron {
243
+ flex: none;
244
+ transition: transform var(--ds-transition-duration-fast) var(--ds-ease-in-out);
245
+ }
246
+ .tsd-chevron-open { transform: rotate(180deg); }
247
+ .tsd-search {
248
+ position: relative;
249
+ margin-bottom: 8px;
250
+ }
251
+ .tsd-search-icon {
252
+ position: absolute;
253
+ left: 8px;
254
+ top: 50%;
255
+ transform: translateY(-50%);
256
+ color: var(--dsw-alias-label-tertiary);
257
+ pointer-events: none;
258
+ }
259
+ .tsd-search input {
260
+ box-sizing: border-box;
261
+ width: 100%;
262
+ height: 32px;
263
+ padding: 0 8px 0 28px;
264
+ border: 1px solid var(--dsw-alias-border-l2);
265
+ background: var(--dsw-alias-bg-layer-1);
266
+ color: var(--dsw-alias-label-primary);
267
+ border-radius: 8px;
268
+ font: inherit;
269
+ font-size: 13px;
270
+ outline: none;
271
+ }
272
+ .tsd-search input:focus {
273
+ border-color: var(--dsw-alias-state-business-primary);
274
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
275
+ }
276
+ .tsd-model-list {
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 2px;
280
+ max-height: 240px;
281
+ overflow-y: auto;
282
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
283
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
284
+ }
285
+ .tsd-model-item {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 8px;
289
+ padding: 5px 6px;
290
+ border-radius: 6px;
291
+ cursor: pointer;
292
+ font-size: 13px;
293
+ color: var(--dsw-alias-label-primary);
294
+ min-width: 0;
295
+ }
296
+ .tsd-model-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
297
+ .tsd-model-item input { margin: 0; accent-color: var(--dsw-alias-brand-primary); }
298
+ .tsd-model-name {
299
+ font-family: var(--ds-font-family-code);
300
+ font-size: 12px;
301
+ overflow: hidden;
302
+ text-overflow: ellipsis;
303
+ white-space: nowrap;
304
+ min-width: 0;
305
+ }
306
+
307
+ /* ============ 自定义日期浮层 ============ */
308
+ .tsd-range-fields {
309
+ display: flex;
310
+ flex-direction: column;
311
+ gap: 10px;
312
+ }
313
+ .tsd-field {
314
+ display: flex;
315
+ flex-direction: column;
316
+ gap: 4px;
317
+ }
318
+ .tsd-field-label {
319
+ font-size: 12px;
320
+ color: var(--dsw-alias-label-secondary);
321
+ }
322
+ .tsd-field input {
323
+ box-sizing: border-box;
324
+ width: 100%;
325
+ height: 32px;
326
+ padding: 0 8px;
327
+ border: 1px solid var(--dsw-alias-border-l2);
328
+ background: var(--dsw-alias-bg-layer-1);
329
+ color: var(--dsw-alias-label-primary);
330
+ border-radius: 8px;
331
+ font: inherit;
332
+ font-size: 13px;
333
+ outline: none;
334
+ }
335
+ .tsd-field input:focus {
336
+ border-color: var(--dsw-alias-state-business-primary);
337
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
338
+ }
339
+ .tsd-field-error {
340
+ margin: 6px 0 0;
341
+ font-size: 12px;
342
+ color: var(--dsw-alias-state-error-primary);
343
+ }
344
+ .tsd-field-hint {
345
+ margin: 6px 0 0;
346
+ font-size: 11px;
347
+ color: var(--dsw-alias-label-tertiary);
348
+ }
349
+
350
+ /* ============ 汇总指标卡 ============ */
351
+ .tsd-cards {
352
+ display: grid;
353
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
354
+ gap: 10px;
355
+ }
356
+ .tsd-card {
357
+ min-width: 0;
358
+ padding: 14px 16px;
359
+ border: 1px solid var(--dsw-alias-border-l2);
360
+ background: var(--dsw-alias-bg-layer-3);
361
+ border-radius: 12px;
362
+ box-shadow: var(--dsw-shadow-lv1);
363
+ }
364
+ .tsd-card-head {
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 6px;
368
+ }
369
+ .tsd-card-icon {
370
+ display: inline-flex;
371
+ }
372
+ .tsd-card-label {
373
+ font-size: 12px;
374
+ color: var(--dsw-alias-label-secondary);
375
+ overflow: hidden;
376
+ text-overflow: ellipsis;
377
+ white-space: nowrap;
378
+ }
379
+ .tsd-card-value {
380
+ margin-top: 8px;
381
+ font-size: 20px;
382
+ font-weight: 700;
383
+ letter-spacing: -0.01em;
384
+ color: var(--dsw-alias-label-primary);
385
+ font-variant-numeric: tabular-nums;
386
+ overflow: hidden;
387
+ text-overflow: ellipsis;
388
+ white-space: nowrap;
389
+ }
390
+ .tsd-card-sub {
391
+ margin-top: 4px;
392
+ font-size: 12px;
393
+ color: var(--dsw-alias-label-tertiary);
394
+ }
395
+ .tsd-card-note {
396
+ margin-top: 4px;
397
+ font-size: 11px;
398
+ color: var(--dsw-alias-state-warn-primary);
399
+ }
400
+
401
+ /* ============ 卡片头部(图表 / 表格共用) ============ */
402
+ .tsd-card-header {
403
+ display: flex;
404
+ align-items: center;
405
+ justify-content: space-between;
406
+ gap: 8px;
407
+ margin-bottom: 12px;
408
+ }
409
+ .tsd-card-title {
410
+ margin: 0;
411
+ font-size: 15px;
412
+ font-weight: 600;
413
+ color: var(--dsw-alias-label-primary);
414
+ }
415
+ .tsd-card-tag {
416
+ font-size: 12px;
417
+ color: var(--dsw-alias-label-tertiary);
418
+ font-variant-numeric: tabular-nums;
419
+ white-space: nowrap;
420
+ overflow: hidden;
421
+ text-overflow: ellipsis;
422
+ }
423
+
424
+ /* ============ 每日用量柱状图 ============ */
425
+ .tsd-chart-card { padding: 14px 16px 10px; }
426
+ .tsd-legend {
427
+ display: flex;
428
+ gap: 14px;
429
+ margin-bottom: 8px;
430
+ }
431
+ .tsd-legend-item {
432
+ display: inline-flex;
433
+ align-items: center;
434
+ gap: 6px;
435
+ font-size: 12px;
436
+ color: var(--dsw-alias-label-secondary);
437
+ }
438
+ .tsd-legend-dot,
439
+ .tsd-tooltip-dot {
440
+ display: inline-block;
441
+ width: 10px;
442
+ height: 10px;
443
+ border-radius: 2px;
444
+ flex: none;
445
+ }
446
+ .tsd-legend-in { background: var(--dsw-static-deepseek-500); }
447
+ .tsd-legend-out { background: var(--dsw-static-green-500); }
448
+ .tsd-legend-total { background: var(--dsw-alias-label-secondary); }
449
+
450
+ .tsd-chart {
451
+ position: relative;
452
+ display: flex;
453
+ align-items: stretch;
454
+ gap: 2px;
455
+ height: 240px;
456
+ padding: 0 4px;
457
+ }
458
+ .tsd-chart-col {
459
+ flex: 1;
460
+ min-width: 0;
461
+ display: flex;
462
+ flex-direction: column;
463
+ align-items: center;
464
+ border-radius: 4px;
465
+ cursor: default;
466
+ transition: opacity var(--ds-transition-duration-fast) var(--ds-ease-in-out);
467
+ }
468
+ .tsd-chart-col:focus-visible {
469
+ outline: 2px solid var(--dsw-alias-state-business-primary);
470
+ outline-offset: 2px;
471
+ }
472
+ .tsd-chart:hover .tsd-chart-col { opacity: 0.4; }
473
+ .tsd-chart:hover .tsd-chart-col:hover,
474
+ .tsd-chart:focus-within .tsd-chart-col:focus { opacity: 1; }
475
+
476
+ .tsd-chart-plot {
477
+ flex: 1;
478
+ width: 100%;
479
+ display: flex;
480
+ align-items: flex-end;
481
+ justify-content: center;
482
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
483
+ background-image: linear-gradient(to top, var(--dsw-alias-border-l1) 1px, transparent 1px);
484
+ background-size: 100% 25%;
485
+ background-repeat: no-repeat;
486
+ }
487
+ .tsd-chart-stack {
488
+ width: 62%;
489
+ height: 100%;
490
+ display: flex;
491
+ flex-direction: column;
492
+ justify-content: flex-end;
493
+ }
494
+ .tsd-chart-seg {
495
+ width: 100%;
496
+ }
497
+ .tsd-seg-in {
498
+ background: var(--dsw-static-deepseek-500);
499
+ }
500
+ .tsd-seg-out {
501
+ background: var(--dsw-static-green-500);
502
+ border-radius: 3px 3px 0 0;
503
+ }
504
+ .tsd-chart-label {
505
+ height: 14px;
506
+ line-height: 14px;
507
+ margin-top: 6px;
508
+ font-size: 11px;
509
+ color: var(--dsw-alias-label-tertiary);
510
+ font-variant-numeric: tabular-nums;
511
+ white-space: nowrap;
512
+ }
513
+ .tsd-chart-label-hidden { visibility: hidden; }
514
+
515
+ /* tooltip */
516
+ .tsd-tooltip {
517
+ position: absolute;
518
+ top: 10px;
519
+ transform: translateX(-50%);
520
+ z-index: 6;
521
+ min-width: 156px;
522
+ padding: 8px 10px;
523
+ border: 1px solid var(--dsw-alias-border-l2);
524
+ background: var(--dsw-alias-bg-overlay);
525
+ backdrop-filter: blur(8px);
526
+ border-radius: 8px;
527
+ box-shadow: var(--dsw-shadow-lv3);
528
+ pointer-events: none;
529
+ }
530
+ .tsd-tooltip-date {
531
+ margin-bottom: 6px;
532
+ font-size: 12px;
533
+ font-weight: 600;
534
+ color: var(--dsw-alias-label-primary);
535
+ }
536
+ .tsd-tooltip-row {
537
+ display: flex;
538
+ align-items: center;
539
+ gap: 6px;
540
+ margin: 2px 0;
541
+ font-size: 12px;
542
+ color: var(--dsw-alias-label-secondary);
543
+ font-variant-numeric: tabular-nums;
544
+ }
545
+ .tsd-tooltip-val {
546
+ margin-left: auto;
547
+ padding-left: 12px;
548
+ color: var(--dsw-alias-label-primary);
549
+ }
550
+ .tsd-tooltip-total {
551
+ display: flex;
552
+ justify-content: space-between;
553
+ margin-top: 6px;
554
+ padding-top: 6px;
555
+ border-top: 1px solid var(--dsw-alias-border-l2);
556
+ font-size: 12px;
557
+ font-weight: 600;
558
+ color: var(--dsw-alias-label-primary);
559
+ font-variant-numeric: tabular-nums;
560
+ }
561
+
562
+ /* ============ 按模型明细表 ============ */
563
+ .tsd-table-card { padding: 0; }
564
+ .tsd-table-card .tsd-card-header { padding: 14px 16px 0; }
565
+ .tsd-table-wrap {
566
+ overflow-x: auto;
567
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
568
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
569
+ }
570
+ .tsd-table {
571
+ width: 100%;
572
+ border-collapse: collapse;
573
+ font-size: 13px;
574
+ }
575
+ .tsd-th {
576
+ padding: 10px 12px;
577
+ text-align: left;
578
+ font-size: 12px;
579
+ font-weight: 500;
580
+ color: var(--dsw-alias-label-secondary);
581
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
582
+ white-space: nowrap;
583
+ }
584
+ .tsd-th-num { text-align: right; }
585
+ .tsd-sort-btn {
586
+ display: inline-flex;
587
+ align-items: center;
588
+ gap: 2px;
589
+ padding: 0;
590
+ border: none;
591
+ background: none;
592
+ font: inherit;
593
+ font-size: 12px;
594
+ color: inherit;
595
+ cursor: pointer;
596
+ }
597
+ .tsd-sort-btn:hover { color: var(--dsw-alias-label-primary); }
598
+ .tsd-sort-active { color: var(--dsw-alias-brand-primary); }
599
+ .tsd-sort-arrow {
600
+ font-size: 10px;
601
+ line-height: 1;
602
+ }
603
+ .tsd-tr:hover { background: var(--dsw-alias-interactive-bg-hover); }
604
+ .tsd-td {
605
+ padding: 9px 12px;
606
+ border-bottom: 1px solid var(--dsw-alias-border-l1);
607
+ color: var(--dsw-alias-label-secondary);
608
+ white-space: nowrap;
609
+ }
610
+ .tsd-td-model { max-width: 240px; }
611
+ .tsd-model-mono {
612
+ display: block;
613
+ max-width: 220px;
614
+ overflow: hidden;
615
+ text-overflow: ellipsis;
616
+ white-space: nowrap;
617
+ font-family: var(--ds-font-family-code);
618
+ font-size: 12px;
619
+ color: var(--dsw-alias-label-primary);
620
+ }
621
+ .tsd-num {
622
+ text-align: right;
623
+ font-variant-numeric: tabular-nums;
624
+ }
625
+ .tsd-total {
626
+ font-weight: 600;
627
+ color: var(--dsw-alias-label-primary);
628
+ }
629
+ .tsd-unpriced {
630
+ color: var(--dsw-alias-label-tertiary);
631
+ font-style: italic;
632
+ }
633
+ .tsd-td-ratio { text-align: right; }
634
+ .tsd-ratio {
635
+ display: inline-flex;
636
+ align-items: center;
637
+ justify-content: flex-end;
638
+ gap: 8px;
639
+ }
640
+ .tsd-ratio-bar {
641
+ width: 60px;
642
+ height: 6px;
643
+ border-radius: 3px;
644
+ background: var(--dsw-alias-bg-layer-1);
645
+ overflow: hidden;
646
+ flex: none;
647
+ }
648
+ .tsd-ratio-fill {
649
+ height: 100%;
650
+ border-radius: 3px;
651
+ background: linear-gradient(90deg, var(--dsw-static-deepseek-500), color-mix(in srgb, var(--dsw-static-deepseek-500) 55%, var(--dsw-static-deepseek-300)));
652
+ }
653
+ .tsd-ratio-pct {
654
+ min-width: 42px;
655
+ text-align: right;
656
+ font-size: 12px;
657
+ color: var(--dsw-alias-label-secondary);
658
+ font-variant-numeric: tabular-nums;
659
+ }
660
+
661
+ /* ============ 三态:空 / 错误 / 加载 ============ */
662
+ .tsd-empty {
663
+ display: flex;
664
+ flex-direction: column;
665
+ align-items: center;
666
+ gap: 6px;
667
+ padding: 40px 16px;
668
+ text-align: center;
669
+ }
670
+ .tsd-empty-icon {
671
+ color: var(--dsw-alias-label-tertiary);
672
+ margin-bottom: 4px;
673
+ }
674
+ .tsd-empty-title {
675
+ margin: 0;
676
+ font-size: 15px;
677
+ font-weight: 600;
678
+ color: var(--dsw-alias-label-primary);
679
+ }
680
+ .tsd-empty-desc {
681
+ margin: 0;
682
+ font-size: 13px;
683
+ color: var(--dsw-alias-label-secondary);
684
+ }
685
+ .tsd-empty-block {
686
+ padding: 24px;
687
+ text-align: center;
688
+ font-size: 13px;
689
+ color: var(--dsw-alias-label-tertiary);
690
+ }
691
+
692
+ .tsd-error {
693
+ display: flex;
694
+ align-items: center;
695
+ gap: 12px;
696
+ padding: 20px 16px;
697
+ border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);
698
+ background: color-mix(in srgb, var(--dsw-alias-state-error-secondary) 30%, var(--dsw-alias-bg-layer-3));
699
+ }
700
+ .tsd-error-icon {
701
+ flex: none;
702
+ color: var(--dsw-alias-state-error-primary);
703
+ }
704
+ .tsd-error-text {
705
+ flex: 1;
706
+ min-width: 0;
707
+ }
708
+ .tsd-error-title {
709
+ margin: 0;
710
+ font-size: 14px;
711
+ font-weight: 600;
712
+ color: var(--dsw-alias-label-primary);
713
+ }
714
+ .tsd-error-detail {
715
+ margin: 2px 0 0;
716
+ font-size: 12px;
717
+ color: var(--dsw-alias-label-secondary);
718
+ overflow: hidden;
719
+ text-overflow: ellipsis;
720
+ white-space: nowrap;
721
+ }
722
+
723
+ .tsd-alert-banner {
724
+ display: flex;
725
+ align-items: center;
726
+ gap: 10px;
727
+ padding: 10px 14px;
728
+ border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);
729
+ border-radius: 10px;
730
+ background: color-mix(in srgb, var(--dsw-alias-state-error-secondary) 30%, var(--dsw-alias-bg-layer-3));
731
+ color: var(--dsw-alias-state-error-primary);
732
+ font-size: 13px;
733
+ }
734
+
735
+ .tsd-loading {
736
+ display: flex;
737
+ flex-direction: column;
738
+ gap: 16px;
739
+ }
740
+ .tsd-card-skeleton { min-height: 84px; }
741
+ .tsd-skeleton {
742
+ background: var(--dsw-alias-bg-skeleton);
743
+ border-radius: 6px;
744
+ animation: tsd-pulse 1.6s var(--ds-ease-in-out) infinite;
745
+ }
746
+ .tsd-skeleton-row {
747
+ height: 14px;
748
+ margin: 12px 0;
749
+ background: var(--dsw-alias-bg-skeleton);
750
+ border-radius: 4px;
751
+ animation: tsd-pulse 1.6s var(--ds-ease-in-out) infinite;
752
+ }
753
+ @keyframes tsd-pulse {
754
+ 0%, 100% { opacity: 1; }
755
+ 50% { opacity: 0.45; }
756
+ }
757
+
758
+ /* ============ 刷新时的内容态 ============ */
759
+ .tsd-content {
760
+ display: flex;
761
+ flex-direction: column;
762
+ gap: 16px;
763
+ transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
764
+ }
765
+ .tsd-content[data-busy=true] { opacity: 0.55; }
766
+
767
+ /* ============ 无障碍 ============ */
768
+ .tsd-visually-hidden {
769
+ position: absolute;
770
+ width: 1px;
771
+ height: 1px;
772
+ padding: 0;
773
+ margin: -1px;
774
+ overflow: hidden;
775
+ clip: rect(0 0 0 0);
776
+ white-space: nowrap;
777
+ border: 0;
778
+ }
779
+ .tsd-icon {
780
+ flex: none;
781
+ }
782
+ .tsd-seg-btn:focus-visible,
783
+ .tsd-select-btn:focus-visible,
784
+ .tsd-btn:focus-visible,
785
+ .tsd-icon-btn:focus-visible,
786
+ .tsd-link-btn:focus-visible,
787
+ .tsd-sort-btn:focus-visible {
788
+ outline: 2px solid var(--dsw-alias-state-business-primary);
789
+ outline-offset: 2px;
790
+ }
791
+ /* ============ 粒度切换 ============ */
792
+ .tsd-card-actions {
793
+ display: inline-flex;
794
+ align-items: center;
795
+ gap: 8px;
796
+ }
797
+ .tsd-gran {
798
+ display: inline-flex;
799
+ gap: 2px;
800
+ padding: 2px;
801
+ border-radius: 8px;
802
+ background: var(--dsw-alias-bg-layer-1);
803
+ }
804
+ .tsd-gran-btn {
805
+ border: none;
806
+ background: transparent;
807
+ color: var(--dsw-alias-label-tertiary);
808
+ font: inherit;
809
+ font-size: 11px;
810
+ line-height: 16px;
811
+ border-radius: 6px;
812
+ padding: 2px 8px;
813
+ cursor: pointer;
814
+ }
815
+ .tsd-gran-btn[data-active="true"] {
816
+ background: var(--dsw-alias-bg-layer-3);
817
+ color: var(--dsw-alias-label-primary);
818
+ box-shadow: var(--dsw-shadow-lv1);
819
+ }
820
+ .tsd-gran-btn:disabled {
821
+ opacity: 0.4;
822
+ cursor: not-allowed;
823
+ }
824
+ /* ============ 折线图 ============ */
825
+ .tsd-linechart {
826
+ position: relative;
827
+ padding: 6px 10px 8px;
828
+ }
829
+ .tsd-linechart svg {
830
+ display: block;
831
+ width: 100%;
832
+ height: auto;
833
+ }
834
+ .tsd-linechart .tsd-line-grid {
835
+ stroke: var(--dsw-alias-border-l2);
836
+ stroke-width: 1;
837
+ stroke-dasharray: 3 3;
838
+ }
839
+ .tsd-linechart .tsd-line-axis {
840
+ stroke: var(--dsw-alias-border-l2);
841
+ stroke-width: 1;
842
+ }
843
+ .tsd-linechart .tsd-line-in {
844
+ stroke: var(--dsw-static-deepseek-500);
845
+ stroke-width: 2;
846
+ fill: none;
847
+ }
848
+ .tsd-linechart .tsd-line-out {
849
+ stroke: var(--dsw-static-green-500);
850
+ stroke-width: 2;
851
+ fill: none;
852
+ }
853
+ .tsd-linechart .tsd-line-total {
854
+ stroke: var(--dsw-alias-label-secondary);
855
+ stroke-width: 1.5;
856
+ stroke-dasharray: 4 3;
857
+ fill: none;
858
+ }
859
+ .tsd-linechart .tsd-line-dot {
860
+ fill: var(--dsw-alias-bg-layer-2);
861
+ stroke: var(--dsw-alias-label-secondary);
862
+ stroke-width: 1;
863
+ }
864
+ .tsd-linechart .tsd-line-hover {
865
+ fill: transparent;
866
+ cursor: crosshair;
867
+ }
868
+ .tsd-linechart .tsd-line-cursor {
869
+ stroke: var(--dsw-alias-border-l3);
870
+ stroke-width: 1;
871
+ stroke-dasharray: 3 3;
872
+ pointer-events: none;
873
+ }
874
+ .tsd-linechart .tsd-line-hl {
875
+ stroke: var(--dsw-alias-bg-base);
876
+ stroke-width: 1.5;
877
+ pointer-events: none;
878
+ }
879
+ .tsd-linechart .tsd-line-hl-in { fill: var(--dsw-static-deepseek-500); }
880
+ .tsd-linechart .tsd-line-hl-out { fill: var(--dsw-static-green-500); }
881
+ .tsd-linechart .tsd-line-hl-total { fill: var(--dsw-alias-label-secondary); }
882
+ .tsd-linechart .tsd-line-text {
883
+ fill: var(--dsw-alias-label-tertiary);
884
+ font-size: 10px;
885
+ }
886
+ /* ============ 年度热力图 ============ */
887
+ .tsd-heatmap {
888
+ padding: 6px 14px 14px;
889
+ overflow-x: auto;
890
+ }
891
+ .tsd-heatmap-inner {
892
+ display: inline-flex;
893
+ flex-direction: column;
894
+ gap: 4px;
895
+ min-width: min-content;
896
+ }
897
+ .tsd-heatmap-months {
898
+ display: flex;
899
+ margin-left: 26px;
900
+ height: 14px;
901
+ position: relative;
902
+ }
903
+ .tsd-heatmap-month {
904
+ font-size: 9px;
905
+ line-height: 14px;
906
+ color: var(--dsw-alias-label-tertiary);
907
+ white-space: nowrap;
908
+ position: absolute;
909
+ top: 0;
910
+ }
911
+ .tsd-heatmap-body {
912
+ display: flex;
913
+ gap: 4px;
914
+ }
915
+ .tsd-heatmap-days {
916
+ display: flex;
917
+ flex-direction: column;
918
+ gap: 2px;
919
+ width: 22px;
920
+ flex: none;
921
+ }
922
+ .tsd-heatmap-day {
923
+ height: 10px;
924
+ font-size: 9px;
925
+ line-height: 10px;
926
+ color: var(--dsw-alias-label-tertiary);
927
+ text-align: right;
928
+ padding-right: 4px;
929
+ }
930
+ .tsd-heatmap-grid {
931
+ display: flex;
932
+ gap: 2px;
933
+ }
934
+ .tsd-heatmap-col {
935
+ display: flex;
936
+ flex-direction: column;
937
+ gap: 2px;
938
+ }
939
+ .tsd-heatmap-cell {
940
+ width: 10px;
941
+ height: 10px;
942
+ border-radius: 2px;
943
+ background: var(--dsw-alias-interactive-bg-hover);
944
+ transition: transform var(--ds-transition-duration-fast) var(--ds-ease-in-out), box-shadow var(--ds-transition-duration-fast) var(--ds-ease-in-out);
945
+ }
946
+ .tsd-heatmap-cell:hover {
947
+ outline: 1px solid var(--dsw-alias-label-secondary);
948
+ transform: scale(1.35);
949
+ box-shadow: var(--dsw-shadow-lv2);
950
+ position: relative;
951
+ z-index: 2;
952
+ }
953
+ .tsd-heatmap-legend {
954
+ display: flex;
955
+ align-items: center;
956
+ gap: 4px;
957
+ margin: 8px 14px 0;
958
+ padding-top: 8px;
959
+ border-top: 1px solid var(--dsw-alias-border-l2);
960
+ font-size: 10px;
961
+ color: var(--dsw-alias-label-tertiary);
962
+ }
963
+ `;
964
+ const h = React.createElement
965
+
966
+ /* ---------------------------------------------------------------------------
967
+ * 国际化:zh/en 字典 + Context(locale 服务经 slot 注册注入 t,子组件经 Context 取用)
968
+ * ------------------------------------------------------------------------- */
969
+ const NS = 'usage'
970
+ const ZH = {
971
+ nav: '用量统计',
972
+ subtitle: '查看 AI 模型的使用情况和成本统计',
973
+ refresh: '刷新',
974
+ today: '今日',
975
+ last7d: '近7天',
976
+ last30d: '近30天',
977
+ custom: '自定义',
978
+ allModels: '全部模型',
979
+ selectedModels: '已选 {count} 个模型',
980
+ selectAll: '全选',
981
+ clear: '清空',
982
+ searchModels: '搜索模型',
983
+ noMatchModels: '无匹配模型',
984
+ modelFilterHint: '勾选即时生效,仅列出当前范围内有数据的模型',
985
+ customRangeTitle: '自定义时间范围',
986
+ customRangeAria: '自定义日期范围',
987
+ startTime: '开始时间',
988
+ endTime: '结束时间',
989
+ rangeInvalid: '开始时间不能晚于结束时间',
990
+ rangeHint: '支持选择日期,最长 30 天',
991
+ cancel: '取消',
992
+ confirm: '确定',
993
+ dateRangeGroup: '日期范围',
994
+ modelFilterGroup: '模型筛选',
995
+ chartTypeGroup: '图表类型',
996
+ totalTokens: '总 Token',
997
+ inputTokens: '输入 Token',
998
+ outputTokens: '输出 Token',
999
+ calls: '调用次数',
1000
+ cost: '成本',
1001
+ approxIO: '≈ 输入+输出',
1002
+ unpricedNote: '{count} 次调用未定价',
1003
+ dailyTrend: '每日用量趋势',
1004
+ barView: '柱状图',
1005
+ lineView: '折线图',
1006
+ byHour: '按小时',
1007
+ byDay: '按天',
1008
+ everyNDays: '每 {n} 天',
1009
+ legendInput: '输入',
1010
+ legendOutput: '输出',
1011
+ legendTotal: '总 Token',
1012
+ tooltipTotal: '合计',
1013
+ startNDays: '起 {n} 天',
1014
+ lineChartAria: 'Token 用量趋势折线图',
1015
+ yearlyHeatmap: '年度用量热力图',
1016
+ yearDaily: '{year} 年每日 Token 用量',
1017
+ monthLabel: '{n}月',
1018
+ weekdayMon: '一',
1019
+ weekdayWed: '三',
1020
+ weekdayFri: '五',
1021
+ less: '少',
1022
+ more: '多',
1023
+ byModelTitle: '按模型明细',
1024
+ modelCount: '共 {n} 个模型',
1025
+ modelTableCaption: '按模型用量明细',
1026
+ colModel: '模型',
1027
+ colCalls: '请求数',
1028
+ colInput: '输入 Token',
1029
+ colOutput: '输出 Token',
1030
+ colTotal: '总 Token',
1031
+ colCost: '成本',
1032
+ colShare: '占比',
1033
+ unpriced: '未定价',
1034
+ unpricedRowHint: '该模型未配置定价,按 0 计价',
1035
+ noData: '暂无数据',
1036
+ emptyDesc: '该时间段内没有调用记录',
1037
+ emptyFiltered: '当前筛选条件下没有数据,试试调整时间范围或模型',
1038
+ resetFilters: '重置筛选',
1039
+ loadFailed: '数据加载失败:{message}',
1040
+ loadFailedTitle: '数据加载失败',
1041
+ retry: '重试',
1042
+ barAria: '{key},输入 {in} Token,输出 {out} Token,合计 {tot} Token',
1043
+ hmTitle: '{key}:{tokens} Token{calls}',
1044
+ hmCalls: ',{count} 次调用',
1045
+ wd0: '周日',
1046
+ wd1: '周一',
1047
+ wd2: '周二',
1048
+ wd3: '周三',
1049
+ wd4: '周四',
1050
+ wd5: '周五',
1051
+ wd6: '周六',
1052
+ }
1053
+ const EN = {
1054
+ nav: 'Usage Stats',
1055
+ subtitle: 'View AI model usage and cost statistics',
1056
+ refresh: 'Refresh',
1057
+ today: 'Today',
1058
+ last7d: 'Last 7 days',
1059
+ last30d: 'Last 30 days',
1060
+ custom: 'Custom',
1061
+ allModels: 'All models',
1062
+ selectedModels: '{count} models selected',
1063
+ selectAll: 'Select all',
1064
+ clear: 'Clear',
1065
+ searchModels: 'Search models',
1066
+ noMatchModels: 'No matching models',
1067
+ modelFilterHint: 'Applies instantly; lists only models with data in the range',
1068
+ customRangeTitle: 'Custom time range',
1069
+ customRangeAria: 'Custom date range',
1070
+ startTime: 'Start',
1071
+ endTime: 'End',
1072
+ rangeInvalid: 'Start must not be later than end',
1073
+ rangeHint: 'Date only, up to 30 days',
1074
+ cancel: 'Cancel',
1075
+ confirm: 'OK',
1076
+ dateRangeGroup: 'Date range',
1077
+ modelFilterGroup: 'Model filter',
1078
+ chartTypeGroup: 'Chart type',
1079
+ totalTokens: 'Total Tokens',
1080
+ inputTokens: 'Input Tokens',
1081
+ outputTokens: 'Output Tokens',
1082
+ calls: 'Calls',
1083
+ cost: 'Cost',
1084
+ approxIO: '≈ input+output',
1085
+ unpricedNote: '{count} calls unpriced',
1086
+ dailyTrend: 'Daily usage trend',
1087
+ barView: 'Bars',
1088
+ lineView: 'Line',
1089
+ byHour: 'Hourly',
1090
+ byDay: 'Daily',
1091
+ everyNDays: 'Every {n} days',
1092
+ legendInput: 'Input',
1093
+ legendOutput: 'Output',
1094
+ legendTotal: 'Total',
1095
+ tooltipTotal: 'Total',
1096
+ startNDays: 'for {n} days',
1097
+ lineChartAria: 'Token usage trend line chart',
1098
+ yearlyHeatmap: 'Yearly heatmap',
1099
+ yearDaily: 'Daily tokens in {year}',
1100
+ monthLabel: '{n}',
1101
+ weekdayMon: 'Mon',
1102
+ weekdayWed: 'Wed',
1103
+ weekdayFri: 'Fri',
1104
+ less: 'Less',
1105
+ more: 'More',
1106
+ byModelTitle: 'By model',
1107
+ modelCount: '{n} models',
1108
+ modelTableCaption: 'Token usage by model',
1109
+ colModel: 'Model',
1110
+ colCalls: 'Calls',
1111
+ colInput: 'Input Tokens',
1112
+ colOutput: 'Output Tokens',
1113
+ colTotal: 'Total Tokens',
1114
+ colCost: 'Cost',
1115
+ colShare: 'Share',
1116
+ unpriced: 'Unpriced',
1117
+ unpricedRowHint: 'No pricing configured; counted as 0',
1118
+ noData: 'No data',
1119
+ emptyDesc: 'No calls in this period',
1120
+ emptyFiltered: 'No data for the current filters, try adjusting the range or models',
1121
+ resetFilters: 'Reset filters',
1122
+ loadFailed: 'Failed to load data: {message}',
1123
+ loadFailedTitle: 'Failed to load data',
1124
+ retry: 'Retry',
1125
+ barAria: '{key}, input {in} tokens, output {out} tokens, total {tot} tokens',
1126
+ hmTitle: '{key}: {tokens} tokens{calls}',
1127
+ hmCalls: ', {count} calls',
1128
+ wd0: 'Sun',
1129
+ wd1: 'Mon',
1130
+ wd2: 'Tue',
1131
+ wd3: 'Wed',
1132
+ wd4: 'Thu',
1133
+ wd5: 'Fri',
1134
+ wd6: 'Sat',
1135
+ }
1136
+ const T = React.createContext(null)
1137
+ function useT() {
1138
+ return React.useContext(T)
1139
+ }
1140
+ function fallbackT(key, params) {
1141
+ let s = ZH[key]
1142
+ if (s === undefined) return key
1143
+ if (params) {
1144
+ s = s.replace(/\{(\w+)\}/g, (m, n) => (params[n] !== undefined ? String(params[n]) : m))
1145
+ }
1146
+ return s
1147
+ }
1148
+
1149
+ /* ---------------------------------------------------------------------------
1150
+ * 数字格式化(规格第 5 节)
1151
+ * ------------------------------------------------------------------------- */
1152
+ function formatTokensShort(n) {
1153
+ if (!Number.isFinite(n)) return '--'
1154
+ if (n >= 1e6) return Math.round(n / 1e4) / 100 + 'M' // 1.23M
1155
+ if (n >= 1e3) return Math.round(n / 1e2) / 10 + 'K' // 12.3K
1156
+ return String(Math.round(n))
1157
+ }
1158
+ function formatInt(n) {
1159
+ return Number.isFinite(n) ? Math.round(n).toLocaleString() : '--'
1160
+ }
1161
+ function formatCost(n, cur) {
1162
+ if (!Number.isFinite(n)) return '--'
1163
+ return (cur || '¥') + n.toFixed(2)
1164
+ }
1165
+ function dayLabel(key) {
1166
+ const [, m, d] = String(key || '').split('-')
1167
+ if (m === undefined || d === undefined) return '--'
1168
+ return m + '-' + d
1169
+ }
1170
+ function fmtPct(ratio) {
1171
+ if (!Number.isFinite(ratio)) return '0%'
1172
+ const p = ratio * 100
1173
+ if (p >= 99.95) return '100%'
1174
+ return p.toFixed(1) + '%'
1175
+ }
1176
+
1177
+ /* ---------------------------------------------------------------------------
1178
+ * 日期工具
1179
+ * ------------------------------------------------------------------------- */
1180
+ function startOfDay(d) {
1181
+ const x = new Date(d)
1182
+ x.setHours(0, 0, 0, 0)
1183
+ return x
1184
+ }
1185
+ function addDays(d, n) {
1186
+ const x = new Date(d)
1187
+ x.setDate(x.getDate() + n)
1188
+ return x
1189
+ }
1190
+ function fmtDateInput(d) {
1191
+ const y = d.getFullYear()
1192
+ const m = String(d.getMonth() + 1).padStart(2, '0')
1193
+ const day = String(d.getDate()).padStart(2, '0')
1194
+ return y + '-' + m + '-' + day
1195
+ }
1196
+ function fmtDateSlash(s) {
1197
+ const m = String(s || '').match(/^(\d{4})-(\d{2})-(\d{2})$/)
1198
+ if (!m) return '--'
1199
+ return m[1] + '/' + m[2] + '/' + m[3]
1200
+ }
1201
+ // 预设 / 自定义 → { from, to }(Unix 毫秒)
1202
+ // 今日 = 当天 00:00 至今;近7/30天 = 含今天往前 N 个自然日 00:00 起;自定义 = 起日 00:00 ~ 止日 23:59:59.999
1203
+ function rangeFor(preset, custom) {
1204
+ const now = new Date()
1205
+ const today = startOfDay(now)
1206
+ if (preset === 'today') return { from: today.getTime(), to: now.getTime() }
1207
+ if (preset === '7d') return { from: addDays(today, -6).getTime(), to: now.getTime() }
1208
+ if (preset === '30d') return { from: addDays(today, -29).getTime(), to: now.getTime() }
1209
+ const from = new Date(custom.from + 'T00:00:00').getTime()
1210
+ const to = new Date(custom.to + 'T23:59:59.999').getTime()
1211
+ if (!Number.isFinite(from) || !Number.isFinite(to)) {
1212
+ return { from: today.getTime(), to: now.getTime() }
1213
+ }
1214
+ return { from, to }
1215
+ }
1216
+ function weekdayOf(key) {
1217
+ try {
1218
+ return new Date(key + 'T00:00:00').toLocaleDateString('zh-CN', { weekday: 'short' })
1219
+ } catch (e) {
1220
+ return ''
1221
+ }
1222
+ }
1223
+ /** Weekday for a day/hour/minute bucket key (`YYYY-MM-DD` or `YYYY-MM-DD HH:mm`). */
1224
+ function weekdayName(key, t) {
1225
+ try {
1226
+ const d = new Date(String(key).replace(' ', 'T'))
1227
+ return t(['wd0', 'wd1', 'wd2', 'wd3', 'wd4', 'wd5', 'wd6'][d.getDay()] || 'wd0')
1228
+ } catch (e) {
1229
+ return ''
1230
+ }
1231
+ }
1232
+ /** X-axis label for a bucket key under a granularity. */
1233
+ function xLabel(key, g) {
1234
+ const s = String(key)
1235
+ const sp = s.split(' ')
1236
+ if (g === 'minute') return sp.length > 1 ? sp[1].slice(0, 5) : s.slice(5)
1237
+ if (g === 'hour') return sp.length > 1 ? sp[1].slice(0, 2) + ':00' : s.slice(5)
1238
+ return dayLabel(key)
1239
+ }
1240
+ /** Fixed granularity policy: today is hourly, everything else is daily. */
1241
+ function effGranularity(range) {
1242
+ return range === 'today' ? 'hour' : 'day';
1243
+ }
1244
+ /** Stable step strategy for custom ranges: target ~15-25 buckets. */
1245
+ function pickStepDays(spanMs) {
1246
+ const days = Math.max(1, Math.round((spanMs || 0) / 86400000))
1247
+ if (days <= 31) return 1
1248
+ if (days <= 62) return 3
1249
+ if (days <= 150) return 5
1250
+ return 7
1251
+ }
1252
+ /** Human label for the current step policy. */
1253
+ function stepLabel(t, range, stepDays) {
1254
+ if (range === 'today') return t('byHour')
1255
+ return stepDays > 1 ? t('everyNDays', { n: stepDays }) : t('byDay')
1256
+ }
1257
+ /** Heatmap cell color by relative token volume. */
1258
+ function heatColor(total, max) {
1259
+ if (!total) return 'var(--dsw-alias-interactive-bg-hover)'
1260
+ const r = total / max
1261
+ const pct = r < 0.25 ? 22 : r < 0.5 ? 45 : r < 0.75 ? 70 : 100
1262
+ return 'color-mix(in srgb, var(--dsw-static-deepseek-500) ' + pct + '%, var(--dsw-alias-bg-layer-2))'
1263
+ }
1264
+ function niceCeil(v) {
1265
+ if (!Number.isFinite(v) || v <= 0) return 1
1266
+ const mag = Math.pow(10, Math.floor(Math.log10(v)))
1267
+ const norm = v / mag
1268
+ const nice = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10
1269
+ return nice * mag
1270
+ }
1271
+ function defaultCustom() {
1272
+ const now = new Date()
1273
+ return { from: fmtDateInput(addDays(now, -6)), to: fmtDateInput(now) }
1274
+ }
1275
+
1276
+ /* ---------------------------------------------------------------------------
1277
+ * 内联 SVG 图标(无图标库,手写 feather 风格 stroke 图标)
1278
+ * ------------------------------------------------------------------------- */
1279
+ const ICON_PATHS = {
1280
+ layers: 'M12 2l8 4.5-8 4.5-8-4.5L12 2zm0 8l8 4.5-8 4.5-8-4.5L12 10zm0 8l8 4.5-8 4.5-8-4.5L12 18z',
1281
+ down: 'M12 3v12m0 0l-4.5-4.5M12 15l4.5-4.5M4 21h16',
1282
+ up: 'M12 21V9m0 0l-4.5 4.5M12 9l4.5 4.5M4 3h16',
1283
+ activity: 'M2 12h4l2.5-8 5 16 2.5-8h6',
1284
+ coins: 'M3 9a2 2 0 012-2h14a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9zm0 5h18M8 5.5V4a2 2 0 012-2h4a2 2 0 012 2v1.5',
1285
+ refresh: 'M21 12a9 9 0 11-2.64-6.36M21 4v6h-6',
1286
+ chevron: 'M6 9l6 6 6-6',
1287
+ inbox: 'M22 12h-6l-2 3h-4l-2-3H2m3.5-6.5L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.5-6.5A2 2 0 0016.76 5H7.24a2 2 0 00-1.74 1.5z',
1288
+ alert: 'M12 9v4m0 4h.01M10.3 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.7 3.86a2 2 0 00-3.4 0z',
1289
+ search: 'M21 21l-4.35-4.35M17 10.5a6.5 6.5 0 11-13 0 6.5 6.5 0 0113 0z',
1290
+ }
1291
+ function SvgIcon(props) {
1292
+ const { name, size, className } = props
1293
+ return h('svg', {
1294
+ className: className || 'tsd-icon',
1295
+ width: size || 16,
1296
+ height: size || 16,
1297
+ viewBox: '0 0 24 24',
1298
+ fill: 'none',
1299
+ stroke: 'currentColor',
1300
+ strokeWidth: 2,
1301
+ strokeLinecap: 'round',
1302
+ strokeLinejoin: 'round',
1303
+ 'aria-hidden': true,
1304
+ }, h('path', { d: ICON_PATHS[name] || ICON_PATHS.activity }))
1305
+ }
1306
+
1307
+ /* ---------------------------------------------------------------------------
1308
+ * 通用 Hook:外部点击关闭
1309
+ * ------------------------------------------------------------------------- */
1310
+ function useOutsideClick(open, ref, onClose) {
1311
+ React.useEffect(() => {
1312
+ if (!open) return undefined
1313
+ function handler(e) {
1314
+ if (ref.current && !ref.current.contains(e.target)) onClose()
1315
+ }
1316
+ document.addEventListener('click', handler)
1317
+ return () => document.removeEventListener('click', handler)
1318
+ }, [open, onClose, ref])
1319
+ }
1320
+
1321
+ /* ---------------------------------------------------------------------------
1322
+ * Header:标题 + 副标题 + 刷新按钮
1323
+ * ------------------------------------------------------------------------- */
1324
+ function StatsHeader(props) {
1325
+ const { busy, onRefresh } = props
1326
+ const t = useT()
1327
+ return h('div', { className: 'tsd-header' },
1328
+ h('div', { className: 'tsd-header-text' },
1329
+ h('h2', { className: 'tsd-title' }, t('nav')),
1330
+ h('p', { className: 'tsd-subtitle' }, t('subtitle'))),
1331
+ h('button', {
1332
+ type: 'button',
1333
+ className: 'tsd-icon-btn' + (busy ? ' tsd-spinning' : ''),
1334
+ onClick: onRefresh,
1335
+ title: t('refresh'),
1336
+ 'aria-label': t('refresh'),
1337
+ }, h(SvgIcon, { name: 'refresh' })))
1338
+ }
1339
+
1340
+ /* ---------------------------------------------------------------------------
1341
+ * 自定义日期范围浮层
1342
+ * ------------------------------------------------------------------------- */
1343
+ function CustomRangePopover(props) {
1344
+ const { custom, onApply, onClose } = props
1345
+ const t = useT()
1346
+ const [from, setFrom] = React.useState(custom.from)
1347
+ const [to, setTo] = React.useState(custom.to)
1348
+ const invalid = !from || !to || from > to
1349
+ return h('div', {
1350
+ className: 'tsd-popover tsd-range-popover',
1351
+ role: 'dialog',
1352
+ 'aria-label': t('customRangeAria'),
1353
+ },
1354
+ h('div', { className: 'tsd-popover-title' }, t('customRangeTitle')),
1355
+ h('div', { className: 'tsd-range-fields' },
1356
+ h('label', { className: 'tsd-field' },
1357
+ h('span', { className: 'tsd-field-label' }, t('startTime')),
1358
+ h('input', {
1359
+ type: 'date', value: from, 'aria-label': t('startTime'),
1360
+ onChange: (e) => setFrom(e.target.value),
1361
+ })),
1362
+ h('label', { className: 'tsd-field' },
1363
+ h('span', { className: 'tsd-field-label' }, t('endTime')),
1364
+ h('input', {
1365
+ type: 'date', value: to, 'aria-label': t('endTime'),
1366
+ onChange: (e) => setTo(e.target.value),
1367
+ }))),
1368
+ invalid
1369
+ ? h('p', { className: 'tsd-field-error', role: 'alert' }, t('rangeInvalid'))
1370
+ : null,
1371
+ h('p', { className: 'tsd-field-hint' }, t('rangeHint')),
1372
+ h('div', { className: 'tsd-popover-actions' },
1373
+ h('button', { type: 'button', className: 'tsd-btn tsd-btn-ghost', onClick: onClose }, t('cancel')),
1374
+ h('button', {
1375
+ type: 'button', className: 'tsd-btn tsd-btn-primary',
1376
+ disabled: invalid,
1377
+ onClick: () => { if (!invalid) onApply({ from, to }) },
1378
+ }, t('confirm'))))
1379
+ }
1380
+
1381
+ /* ---------------------------------------------------------------------------
1382
+ * 模型多选下拉
1383
+ * ------------------------------------------------------------------------- */
1384
+ function ModelDropdown(props) {
1385
+ const { options, selected, onChange } = props
1386
+ const t = useT()
1387
+ const [open, setOpen] = React.useState(false)
1388
+ const [query, setQuery] = React.useState('')
1389
+ const anchorRef = React.useRef(null)
1390
+
1391
+ React.useEffect(() => {
1392
+ if (!open) return undefined
1393
+ function handler(e) {
1394
+ if (anchorRef.current && !anchorRef.current.contains(e.target)) setOpen(false)
1395
+ }
1396
+ document.addEventListener('click', handler)
1397
+ return () => document.removeEventListener('click', handler)
1398
+ }, [open])
1399
+
1400
+ // 只列出当前范围有数据的模型;已选但掉出候选列表的「补回」,保证可渲染、可清除
1401
+ const candidates = React.useMemo(() => {
1402
+ const set = new Set((options || []).map((m) => m.provider + '/' + m.model))
1403
+ ;(selected || []).forEach((m) => set.add(m))
1404
+ const list = Array.from(set)
1405
+ list.sort()
1406
+ return list
1407
+ }, [options, selected])
1408
+
1409
+ const q = query.trim().toLocaleLowerCase()
1410
+ const filtered = q === '' ? candidates : candidates.filter((c) => c.toLocaleLowerCase().indexOf(q) >= 0)
1411
+ const label = selected.length === 0 ? t('allModels') : t('selectedModels', { count: selected.length })
1412
+
1413
+ return h('div', { className: 'tsd-popover-anchor', ref: anchorRef },
1414
+ h('button', {
1415
+ type: 'button',
1416
+ className: 'tsd-select-btn' + (selected.length > 0 ? ' tsd-select-has' : ''),
1417
+ onClick: () => { setOpen((v) => !v); setQuery('') },
1418
+ 'aria-expanded': open,
1419
+ 'aria-haspopup': 'listbox',
1420
+ title: label,
1421
+ },
1422
+ h('span', { className: 'tsd-select-label' }, label),
1423
+ h(SvgIcon, { name: 'chevron', size: 14, className: 'tsd-chevron' + (open ? ' tsd-chevron-open' : '') })),
1424
+ open
1425
+ ? h('div', {
1426
+ className: 'tsd-popover tsd-model-popover',
1427
+ role: 'listbox',
1428
+ 'aria-label': t('modelFilterGroup'),
1429
+ 'aria-multiselectable': true,
1430
+ },
1431
+ h('div', { className: 'tsd-popover-tools' },
1432
+ h('button', { type: 'button', className: 'tsd-link-btn', onClick: () => onChange(candidates.slice()) }, t('selectAll')),
1433
+ h('button', { type: 'button', className: 'tsd-link-btn', onClick: () => onChange([]) }, t('clear'))),
1434
+ h('div', { className: 'tsd-search' },
1435
+ h(SvgIcon, { name: 'search', size: 14, className: 'tsd-search-icon' }),
1436
+ h('input', {
1437
+ type: 'search', value: query, placeholder: t('searchModels'),
1438
+ 'aria-label': t('searchModels'),
1439
+ onChange: (e) => setQuery(e.target.value),
1440
+ })),
1441
+ h('div', { className: 'tsd-model-list' },
1442
+ filtered.length === 0
1443
+ ? h('p', { className: 'tsd-popover-empty' }, t('noMatchModels'))
1444
+ : null,
1445
+ filtered.map((key) =>
1446
+ h('label', { key, className: 'tsd-model-item' },
1447
+ h('input', {
1448
+ type: 'checkbox',
1449
+ checked: selected.indexOf(key) >= 0,
1450
+ onChange: (e) => {
1451
+ const next = selected.slice()
1452
+ if (e.target.checked) {
1453
+ if (next.indexOf(key) < 0) next.push(key)
1454
+ } else {
1455
+ const i = next.indexOf(key)
1456
+ if (i >= 0) next.splice(i, 1)
1457
+ }
1458
+ onChange(next) // 勾选即时生效
1459
+ },
1460
+ }),
1461
+ h('span', { className: 'tsd-model-name', title: key }, key)))),
1462
+ h('p', { className: 'tsd-popover-note' }, t('modelFilterHint')))
1463
+ : null)
1464
+ }
1465
+
1466
+ /* ---------------------------------------------------------------------------
1467
+ * 筛选栏:日期预设(分段按钮)+ 自定义浮层 + 模型多选
1468
+ * ------------------------------------------------------------------------- */
1469
+ function FilterBar(props) {
1470
+ const {
1471
+ range, custom, models, customOpen, modelOptions,
1472
+ onPreset, onCustomOpen, onCustomApply, onCustomClose, onModelsChange,
1473
+ } = props
1474
+ const t = useT()
1475
+ const segRef = React.useRef(null)
1476
+ useOutsideClick(customOpen, segRef, onCustomClose)
1477
+
1478
+ const customLabel = fmtDateSlash(custom.from) + ' - ' + fmtDateSlash(custom.to)
1479
+ const presets = [
1480
+ { key: 'today', label: t('today') },
1481
+ { key: '7d', label: t('last7d') },
1482
+ { key: '30d', label: t('last30d') },
1483
+ { key: 'custom', label: t('custom') },
1484
+ ]
1485
+
1486
+ return h('div', { className: 'tsd-filterbar' },
1487
+ h('div', { className: 'tsd-seg-wrap', ref: segRef },
1488
+ h('div', { className: 'tsd-seg', role: 'group', 'aria-label': t('dateRangeGroup') },
1489
+ presets.map((p) => {
1490
+ const active = range === p.key
1491
+ const isCustom = p.key === 'custom'
1492
+ const label = isCustom && active ? customLabel : p.label
1493
+ return h('button', {
1494
+ key: p.key,
1495
+ type: 'button',
1496
+ className: 'tsd-seg-btn' + (active ? ' tsd-seg-active' : '') + (isCustom ? ' tsd-seg-custom' : ''),
1497
+ 'aria-pressed': active,
1498
+ title: isCustom && active ? customLabel : undefined,
1499
+ onClick: () => { if (isCustom) onCustomOpen(!customOpen); else onPreset(p.key) },
1500
+ },
1501
+ h('span', { className: 'tsd-seg-label' }, label))
1502
+ })),
1503
+ customOpen ? h(CustomRangePopover, { custom, onApply: onCustomApply, onClose: onCustomClose }) : null),
1504
+ h(ModelDropdown, { options: modelOptions, selected: models, onChange: onModelsChange }))
1505
+ }
1506
+
1507
+ /* ---------------------------------------------------------------------------
1508
+ * 汇总指标卡 ×5
1509
+ * ------------------------------------------------------------------------- */
1510
+ function SummaryCards(props) {
1511
+ const { total, currency, unpricedCalls } = props
1512
+ const tt = useT()
1513
+ const d = total || {}
1514
+ const inputTotal = (d.inputTokens || 0) + (d.cacheReadTokens || 0) + (d.cacheWriteTokens || 0)
1515
+ const cards = [
1516
+ {
1517
+ key: 'total', icon: 'layers', label: tt('totalTokens'),
1518
+ value: formatTokensShort(d.totalTokens), title: formatInt(d.totalTokens),
1519
+ sub: tt('approxIO'), color: 'var(--dsw-alias-brand-primary)',
1520
+ },
1521
+ {
1522
+ key: 'input', icon: 'down', label: tt('inputTokens'),
1523
+ value: formatTokensShort(inputTotal), title: formatInt(inputTotal),
1524
+ color: 'var(--dsw-static-deepseek-500)',
1525
+ },
1526
+ {
1527
+ key: 'output', icon: 'up', label: tt('outputTokens'),
1528
+ value: formatTokensShort(d.outputTokens), title: formatInt(d.outputTokens),
1529
+ color: 'var(--dsw-static-green-500)',
1530
+ },
1531
+ {
1532
+ key: 'calls', icon: 'activity', label: tt('calls'),
1533
+ value: formatInt(d.calls), title: formatInt(d.calls),
1534
+ color: 'var(--dsw-static-deepseek-500)',
1535
+ },
1536
+ {
1537
+ key: 'cost', icon: 'coins', label: tt('cost'),
1538
+ value: formatCost(d.cost, currency), title: formatCost(d.cost, currency),
1539
+ color: 'var(--dsw-static-green-500)',
1540
+ },
1541
+ ]
1542
+ return h('div', { className: 'tsd-cards' },
1543
+ cards.map((c) =>
1544
+ h('div', { key: c.key, className: 'tsd-card' },
1545
+ h('div', { className: 'tsd-card-head' },
1546
+ h('span', { className: 'tsd-card-icon', style: { color: c.color } },
1547
+ h(SvgIcon, { name: c.icon, size: 14 })),
1548
+ h('span', { className: 'tsd-card-label' }, c.label)),
1549
+ h('div', { className: 'tsd-card-value', title: c.title }, c.value),
1550
+ c.sub ? h('div', { className: 'tsd-card-sub' }, c.sub) : null,
1551
+ c.key === 'cost' && unpricedCalls > 0
1552
+ ? h('div', { className: 'tsd-card-note' }, tt('unpricedNote', { count: formatInt(unpricedCalls) }))
1553
+ : null)))
1554
+ }
1555
+
1556
+ /* ---------------------------------------------------------------------------
1557
+ * 每日用量柱状图(手写 div 堆叠柱:下段输入蓝 / 上段输出绿)
1558
+ * ------------------------------------------------------------------------- */
1559
+ function DailyChart(props) {
1560
+ const { daily, gran, stepDays, granLabel } = props
1561
+ const t = useT()
1562
+ const chartRef = React.useRef(null)
1563
+ const [tip, setTip] = React.useState(null) // { row, x }
1564
+ const [view, setView] = React.useState('bar') // bar | line
1565
+ const rows = daily || []
1566
+
1567
+ const inOf = (r) => (r.inputTokens || 0) + (r.cacheReadTokens || 0) + (r.cacheWriteTokens || 0)
1568
+ const outOf = (r) => r.outputTokens || 0
1569
+ const totOf = (r) => r.totalTokens || 0
1570
+
1571
+ if (rows.length === 0) {
1572
+ return h('div', { className: 'tsd-card tsd-chart-card' },
1573
+ h('div', { className: 'tsd-card-header' },
1574
+ h('h3', { className: 'tsd-card-title' }, t('dailyTrend')),
1575
+ h('span', { className: 'tsd-card-tag' }, t('noData'))),
1576
+ h('div', { className: 'tsd-empty-block' }, t('noData')))
1577
+ }
1578
+
1579
+ const max = Math.max(1, ...rows.map((d) => d.totalTokens || 0))
1580
+ const labelEvery = rows.length > 28 ? 4 : rows.length > 14 ? 2 : 1
1581
+ const firstLabel = xLabel(rows[0].key, gran)
1582
+ const lastLabel = xLabel(rows[rows.length - 1].key, gran)
1583
+ const rangeLabel = firstLabel + ' - ' + lastLabel
1584
+
1585
+ function showTip(e, row) {
1586
+ const chartRect = chartRef.current ? chartRef.current.getBoundingClientRect() : null
1587
+ if (!chartRect) return
1588
+ const elRect = e.currentTarget.getBoundingClientRect()
1589
+ const x = Math.min(Math.max(elRect.left + elRect.width / 2 - chartRect.left, 72), chartRect.width - 72)
1590
+ setTip({ row, x })
1591
+ }
1592
+
1593
+ const viewSwitcher = h('div', { className: 'tsd-gran', role: 'group', 'aria-label': t('chartTypeGroup') },
1594
+ h('button', {
1595
+ type: 'button',
1596
+ className: 'tsd-gran-btn',
1597
+ 'data-active': view === 'bar' ? 'true' : undefined,
1598
+ onClick: () => { setTip(null); setView('bar') },
1599
+ }, t('barView')),
1600
+ h('button', {
1601
+ type: 'button',
1602
+ className: 'tsd-gran-btn',
1603
+ 'data-active': view === 'line' ? 'true' : undefined,
1604
+ onClick: () => { setTip(null); setView('line') },
1605
+ }, t('lineView')))
1606
+
1607
+ const barTip = tip
1608
+ ? h('div', { className: 'tsd-tooltip', style: { left: tip.x + 'px' } },
1609
+ h('div', { className: 'tsd-tooltip-date' }, tip.row.key + ' ' + weekdayName(tip.row.key, t)),
1610
+ h('div', { className: 'tsd-tooltip-row' },
1611
+ h('span', { className: 'tsd-tooltip-dot tsd-legend-in' }),
1612
+ h('span', null, t('legendInput')),
1613
+ h('span', { className: 'tsd-tooltip-val' }, formatInt(inOf(tip.row)))),
1614
+ h('div', { className: 'tsd-tooltip-row' },
1615
+ h('span', { className: 'tsd-tooltip-dot tsd-legend-out' }),
1616
+ h('span', null, t('legendOutput')),
1617
+ h('span', { className: 'tsd-tooltip-val' }, formatInt(outOf(tip.row)))),
1618
+ h('div', { className: 'tsd-tooltip-total' },
1619
+ h('span', null, t('tooltipTotal')),
1620
+ h('span', null, formatInt(totOf(tip.row)))))
1621
+ : null
1622
+
1623
+ const body = view === 'line'
1624
+ ? h(LineChart, { rows, gran, stepDays, showTip, tip, setTip, chartRef })
1625
+ : h('div', { className: 'tsd-chart', ref: chartRef },
1626
+ rows.map((row, i) => {
1627
+ const inT = inOf(row)
1628
+ const outT = outOf(row)
1629
+ const totT = totOf(row)
1630
+ const pctIn = (inT / max) * 100
1631
+ const pctOut = (outT / max) * 100
1632
+ const showLabel = i % labelEvery === 0 || i === rows.length - 1
1633
+ return h('div', {
1634
+ key: row.key,
1635
+ className: 'tsd-chart-col',
1636
+ tabIndex: 0,
1637
+ onMouseEnter: (e) => showTip(e, row),
1638
+ onMouseLeave: () => setTip(null),
1639
+ onFocus: (e) => showTip(e, row),
1640
+ onBlur: () => setTip(null),
1641
+ 'aria-label': t('barAria', { key: row.key, in: formatInt(inT), out: formatInt(outT), tot: formatInt(totT) }),
1642
+ },
1643
+ h('div', { className: 'tsd-chart-plot' },
1644
+ h('div', { className: 'tsd-chart-stack' },
1645
+ h('div', { className: 'tsd-chart-seg tsd-seg-out', style: { height: pctOut + '%' } }),
1646
+ h('div', { className: 'tsd-chart-seg tsd-seg-in', style: { height: pctIn + '%' } }))),
1647
+ h('span', {
1648
+ className: 'tsd-chart-label' + (showLabel ? '' : ' tsd-chart-label-hidden'),
1649
+ }, xLabel(row.key, gran)))
1650
+ }),
1651
+ barTip)
1652
+
1653
+ return h('div', { className: 'tsd-card tsd-chart-card' },
1654
+ h('div', { className: 'tsd-card-header' },
1655
+ h('h3', { className: 'tsd-card-title' }, t('dailyTrend')),
1656
+ h('span', { className: 'tsd-card-actions' },
1657
+ h('span', { className: 'tsd-card-tag' }, granLabel + ' · ' + rangeLabel),
1658
+ viewSwitcher)),
1659
+ h('div', { className: 'tsd-legend' },
1660
+ h('span', { className: 'tsd-legend-item' },
1661
+ h('span', { className: 'tsd-legend-dot tsd-legend-in' }), t('legendInput')),
1662
+ h('span', { className: 'tsd-legend-item' },
1663
+ h('span', { className: 'tsd-legend-dot tsd-legend-out' }), t('legendOutput')),
1664
+ h('span', { className: 'tsd-legend-item' },
1665
+ h('span', { className: 'tsd-legend-dot tsd-legend-total' }), t('legendTotal'))),
1666
+ body)
1667
+ }
1668
+
1669
+ /* ---------------------------------------------------------------------------
1670
+ * 折线图(SVG:输入 / 输出 / 总 Token)
1671
+ * ------------------------------------------------------------------------- */
1672
+ /** Catmull-Rom -> cubic bezier smooth path through the given points. */
1673
+ function smoothPath(pts) {
1674
+ if (!pts || pts.length < 2) return ''
1675
+ const f = (v) => Math.round(v * 100) / 100
1676
+ let d = 'M ' + f(pts[0].x) + ' ' + f(pts[0].y)
1677
+ for (let i = 0; i < pts.length - 1; i++) {
1678
+ const p0 = pts[i - 1] || pts[i]
1679
+ const p1 = pts[i]
1680
+ const p2 = pts[i + 1]
1681
+ const p3 = pts[i + 2] || p2
1682
+ const c1x = p1.x + (p2.x - p0.x) / 6
1683
+ const c1y = p1.y + (p2.y - p0.y) / 6
1684
+ const c2x = p2.x - (p3.x - p1.x) / 6
1685
+ const c2y = p2.y - (p3.y - p1.y) / 6
1686
+ d += ' C ' + f(c1x) + ' ' + f(c1y) + ', ' + f(c2x) + ' ' + f(c2y) + ', ' + f(p2.x) + ' ' + f(p2.y)
1687
+ }
1688
+ return d
1689
+ }
1690
+
1691
+ function LineChart(props) {
1692
+ const { rows, gran, stepDays, showTip, tip, setTip, chartRef } = props
1693
+ const t = useT()
1694
+ const W = 620
1695
+ const H = 240
1696
+ const PAD = { t: 14, r: 14, b: 26, l: 48 }
1697
+ const innerW = W - PAD.l - PAD.r
1698
+ const innerH = H - PAD.t - PAD.b
1699
+ const n = rows.length
1700
+
1701
+ const inOf = (r) => (r.inputTokens || 0) + (r.cacheReadTokens || 0) + (r.cacheWriteTokens || 0)
1702
+ const outOf = (r) => r.outputTokens || 0
1703
+ const totOf = (r) => r.totalTokens || 0
1704
+ const max = Math.max(1, ...rows.map((d) => d.totalTokens || 0))
1705
+ const niceMax = niceCeil(max)
1706
+
1707
+ const x = (i) => PAD.l + (n === 1 ? innerW / 2 : (i / (n - 1)) * innerW)
1708
+ const y = (v) => PAD.t + innerH - (v / niceMax) * innerH
1709
+ const colW = innerW / n
1710
+
1711
+ // Hover state: pixel x for the tooltip, viewBox x for the cursor + dots.
1712
+ function showTipLine(e, row) {
1713
+ const rect = chartRef.current ? chartRef.current.getBoundingClientRect() : null
1714
+ if (!rect || rect.width === 0) return
1715
+ const elRect = e.currentTarget.getBoundingClientRect()
1716
+ const xPx = elRect.left + elRect.width / 2 - rect.left
1717
+ const svgX = (xPx / rect.width) * W
1718
+ const x = Math.min(Math.max(xPx, 72), rect.width - 72)
1719
+ setTip({ row, x, svgX })
1720
+ }
1721
+ const trow = tip ? tip.row : null
1722
+ const cursor = tip
1723
+ ? h('line', {
1724
+ key: 'cursor',
1725
+ className: 'tsd-line-cursor',
1726
+ x1: tip.svgX, y1: PAD.t, x2: tip.svgX, y2: H - PAD.b,
1727
+ })
1728
+ : null
1729
+ const dots = tip
1730
+ ? [
1731
+ h('circle', { key: 'hl-in', className: 'tsd-line-hl tsd-line-hl-in', cx: tip.svgX, cy: y(inOf(trow)), r: 3.5 }),
1732
+ h('circle', { key: 'hl-out', className: 'tsd-line-hl tsd-line-hl-out', cx: tip.svgX, cy: y(outOf(trow)), r: 3.5 }),
1733
+ h('circle', { key: 'hl-tot', className: 'tsd-line-hl tsd-line-hl-total', cx: tip.svgX, cy: y(totOf(trow)), r: 3.5 }),
1734
+ ]
1735
+ : null
1736
+
1737
+ const inPts = smoothPath(rows.map((r, i) => ({ x: x(i), y: y(inOf(r)) })))
1738
+ const outPts = smoothPath(rows.map((r, i) => ({ x: x(i), y: y(outOf(r)) })))
1739
+ const totPts = smoothPath(rows.map((r, i) => ({ x: x(i), y: y(totOf(r)) })))
1740
+ const gridF = [0, 0.25, 0.5, 0.75, 1]
1741
+ const labelEvery = rows.length > 28 ? 4 : rows.length > 14 ? 2 : 1
1742
+ const showLabel = (i) => i % labelEvery === 0 || i === rows.length - 1
1743
+
1744
+ return h('div', { className: 'tsd-linechart', ref: chartRef },
1745
+ h('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img', 'aria-label': t('lineChartAria') },
1746
+ // grid + axis
1747
+ gridF.map((f) =>
1748
+ h('line', {
1749
+ key: 'g' + f,
1750
+ className: 'tsd-line-grid',
1751
+ x1: PAD.l, y1: y(f * niceMax), x2: W - PAD.r, y2: y(f * niceMax),
1752
+ })),
1753
+ h('line', { className: 'tsd-line-axis', x1: PAD.l, y1: PAD.t, x2: PAD.l, y2: H - PAD.b }),
1754
+ h('line', { className: 'tsd-line-axis', x1: PAD.l, y1: H - PAD.b, x2: W - PAD.r, y2: H - PAD.b }),
1755
+ // y labels
1756
+ gridF.map((f) =>
1757
+ h('text', {
1758
+ key: 'yt' + f,
1759
+ className: 'tsd-line-text',
1760
+ x: PAD.l - 6,
1761
+ y: y(f * niceMax) + 3,
1762
+ textAnchor: 'end',
1763
+ }, formatTokensShort(f * niceMax))),
1764
+ // series (smooth curves)
1765
+ h('path', { className: 'tsd-line-total', d: totPts }),
1766
+ h('path', { className: 'tsd-line-in', d: inPts }),
1767
+ h('path', { className: 'tsd-line-out', d: outPts }),
1768
+ cursor,
1769
+ dots,
1770
+ // x labels
1771
+ rows.map((r, i) =>
1772
+ showLabel(i)
1773
+ ? h('text', {
1774
+ key: 'xl' + i,
1775
+ className: 'tsd-line-text',
1776
+ x: x(i),
1777
+ y: H - 8,
1778
+ textAnchor: i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle',
1779
+ }, xLabel(r.key, gran))
1780
+ : null),
1781
+ // hover targets: a full-height column per bucket, matching the bar chart feel
1782
+ rows.map((r, i) =>
1783
+ h('rect', {
1784
+ key: 'h' + i,
1785
+ className: 'tsd-line-hover',
1786
+ x: x(i) - colW / 2,
1787
+ y: PAD.t,
1788
+ width: colW,
1789
+ height: innerH,
1790
+ tabIndex: 0,
1791
+ onMouseEnter: (e) => showTipLine(e, r),
1792
+ onMouseLeave: () => setTip(null),
1793
+ onFocus: (e) => showTipLine(e, r),
1794
+ onBlur: () => setTip(null),
1795
+ })),
1796
+ // visible dots on sparse points
1797
+ rows.map((r, i) =>
1798
+ i % labelEvery === 0 || i === n - 1
1799
+ ? h('circle', {
1800
+ key: 'd' + i,
1801
+ className: 'tsd-line-dot',
1802
+ cx: x(i),
1803
+ cy: y(totOf(r)),
1804
+ r: 2.5,
1805
+ })
1806
+ : null)),
1807
+ tip
1808
+ ? h('div', { className: 'tsd-tooltip', style: { left: tip.x + 'px' } },
1809
+ h('div', { className: 'tsd-tooltip-date' }, tip.row.key + (stepDays > 1 ? ' ' + t('startNDays', { n: stepDays }) : '') + ' ' + weekdayName(tip.row.key, t)),
1810
+ h('div', { className: 'tsd-tooltip-row' },
1811
+ h('span', { className: 'tsd-tooltip-dot tsd-legend-in' }),
1812
+ h('span', null, t('legendInput')),
1813
+ h('span', { className: 'tsd-tooltip-val' }, formatInt(inOf(tip.row)))),
1814
+ h('div', { className: 'tsd-tooltip-row' },
1815
+ h('span', { className: 'tsd-tooltip-dot tsd-legend-out' }),
1816
+ h('span', null, t('legendOutput')),
1817
+ h('span', { className: 'tsd-tooltip-val' }, formatInt(outOf(tip.row)))),
1818
+ h('div', { className: 'tsd-tooltip-total' },
1819
+ h('span', null, t('tooltipTotal')),
1820
+ h('span', null, formatInt(totOf(tip.row)))))
1821
+ : null)
1822
+ }
1823
+
1824
+ /* ---------------------------------------------------------------------------
1825
+ * 年度用量热力图(GitHub 风格)
1826
+ * ------------------------------------------------------------------------- */
1827
+ function HeatmapCard(props) {
1828
+ const { days } = props
1829
+ const t = useT()
1830
+ const [htip, setHtip] = React.useState(null) // { cell, left, top } in viewport coords
1831
+ const grid = React.useMemo(() => {
1832
+ const list = days || []
1833
+ if (list.length === 0) return null
1834
+ const max = Math.max(1, ...list.map((d) => d.totalTokens || 0))
1835
+ const first = new Date(list[0].key + 'T00:00:00')
1836
+ const offset = first.getDay() // 0 = Sunday
1837
+ const weeks = []
1838
+ let cur = []
1839
+ for (let i = 0; i < offset; i++) cur.push(null)
1840
+ for (const d of list) {
1841
+ cur.push(d)
1842
+ if (cur.length === 7) {
1843
+ weeks.push(cur)
1844
+ cur = []
1845
+ }
1846
+ }
1847
+ if (cur.length > 0) {
1848
+ while (cur.length < 7) cur.push(null)
1849
+ weeks.push(cur)
1850
+ }
1851
+ // 月份标记:某月 1 日所在的列
1852
+ const monthMarks = []
1853
+ weeks.forEach((wk, col) => {
1854
+ for (const c of wk) {
1855
+ if (c && c.key.endsWith('-01')) {
1856
+ monthMarks.push({ col, name: t('monthLabel', { n: Number(c.key.slice(5, 7)) }) })
1857
+ break
1858
+ }
1859
+ }
1860
+ })
1861
+ return { weeks, max, monthMarks }
1862
+ }, [days])
1863
+
1864
+ if (grid === null) return null
1865
+
1866
+ function showCellTip(e, c) {
1867
+ const elRect = e.currentTarget.getBoundingClientRect()
1868
+ setHtip({ cell: c, left: elRect.left + elRect.width / 2, top: elRect.top })
1869
+ }
1870
+
1871
+ const heatTip = htip
1872
+ ? h('div', {
1873
+ className: 'tsd-tooltip tsd-heat-tip',
1874
+ style: { position: 'fixed', left: htip.left + 'px', top: htip.top + 'px', transform: 'translate(-50%, -110%)' },
1875
+ },
1876
+ h('div', { className: 'tsd-tooltip-date' }, htip.cell.key + ' ' + weekdayName(htip.cell.key, t)),
1877
+ h('div', { className: 'tsd-tooltip-row' },
1878
+ h('span', { className: 'tsd-tooltip-dot', style: { background: 'var(--dsw-static-deepseek-500)' } }),
1879
+ h('span', null, t('totalTokens')),
1880
+ h('span', { className: 'tsd-tooltip-val' }, formatInt(htip.cell.totalTokens || 0))),
1881
+ htip.cell.calls > 0
1882
+ ? h('div', { className: 'tsd-tooltip-row' },
1883
+ h('span', { className: 'tsd-tooltip-dot', style: { background: 'var(--dsw-alias-label-tertiary)' } }),
1884
+ h('span', null, t('calls')),
1885
+ h('span', { className: 'tsd-tooltip-val' }, formatInt(htip.cell.calls)))
1886
+ : null)
1887
+ : null
1888
+
1889
+ const dayNames = ['', t('weekdayMon'), '', t('weekdayWed'), '', t('weekdayFri'), '']
1890
+ const legendSteps = [0, 0.25, 0.5, 0.75, 1]
1891
+
1892
+ return h('div', { className: 'tsd-card tsd-heatmap-card' },
1893
+ h('div', { className: 'tsd-card-header' },
1894
+ h('h3', { className: 'tsd-card-title' }, t('yearlyHeatmap')),
1895
+ h('span', { className: 'tsd-card-tag' }, t('yearDaily', { year: new Date().getFullYear() }))),
1896
+ h('div', { className: 'tsd-heatmap' },
1897
+ h('div', { className: 'tsd-heatmap-inner' },
1898
+ h('div', { className: 'tsd-heatmap-months' },
1899
+ grid.monthMarks.map((m) =>
1900
+ h('span', {
1901
+ key: m.col,
1902
+ className: 'tsd-heatmap-month',
1903
+ style: { left: (m.col * 12) + 'px' },
1904
+ }, m.name))),
1905
+ h('div', { className: 'tsd-heatmap-body' },
1906
+ h('div', { className: 'tsd-heatmap-days' },
1907
+ dayNames.map((n, i) =>
1908
+ h('span', { key: i, className: 'tsd-heatmap-day' }, n))),
1909
+ h('div', { className: 'tsd-heatmap-grid' },
1910
+ grid.weeks.map((wk, col) =>
1911
+ h('div', { key: col, className: 'tsd-heatmap-col' },
1912
+ wk.map((c, row) =>
1913
+ c
1914
+ ? h('span', {
1915
+ key: c.key,
1916
+ className: 'tsd-heatmap-cell',
1917
+ style: { background: heatColor(c.totalTokens || 0, grid.max) },
1918
+ title: t('hmTitle', { key: c.key, tokens: formatInt(c.totalTokens || 0), calls: c.calls ? t('hmCalls', { count: formatInt(c.calls) }) : '' }),
1919
+ onMouseEnter: (e) => showCellTip(e, c),
1920
+ onMouseLeave: () => setHtip(null),
1921
+ onFocus: (e) => showCellTip(e, c),
1922
+ onBlur: () => setHtip(null),
1923
+ tabIndex: 0,
1924
+ })
1925
+ : h('span', { key: 'e' + col + '-' + row, className: 'tsd-heatmap-cell', style: { visibility: 'hidden' } }))))))),
1926
+ heatTip,
1927
+ h('div', { className: 'tsd-heatmap-legend' },
1928
+ h('span', null, t('less')),
1929
+ legendSteps.map((s) =>
1930
+ h('span', {
1931
+ key: s,
1932
+ className: 'tsd-heatmap-cell',
1933
+ style: { background: s === 0 ? 'var(--dsw-alias-interactive-bg-hover)' : heatColor(Math.max(1, s * 100), 100) },
1934
+ })),
1935
+ h('span', null, t('more')))))
1936
+ }
1937
+
1938
+ /* ---------------------------------------------------------------------------
1939
+ * 按模型明细表
1940
+ * ------------------------------------------------------------------------- */
1941
+ function ModelTable(props) {
1942
+ const { rows, currency } = props
1943
+ const t = useT()
1944
+ const all = rows || []
1945
+ const [sortKey, setSortKey] = React.useState('total')
1946
+ const [sortDir, setSortDir] = React.useState('desc')
1947
+
1948
+ const view = React.useMemo(() => all.map((r) => ({
1949
+ key: r.key,
1950
+ calls: r.calls || 0,
1951
+ input: (r.inputTokens || 0) + (r.cacheReadTokens || 0) + (r.cacheWriteTokens || 0),
1952
+ output: r.outputTokens || 0,
1953
+ total: r.totalTokens || 0,
1954
+ cost: r.cost || 0,
1955
+ unpriced: (r.totalTokens || 0) > 0 && !(r.cost > 0),
1956
+ })), [all])
1957
+
1958
+ const sumTotal = React.useMemo(() => view.reduce((s, r) => s + r.total, 0), [view])
1959
+
1960
+ const sorted = React.useMemo(() => {
1961
+ const list = view.slice()
1962
+ const dir = sortDir === 'asc' ? 1 : -1
1963
+ list.sort((a, b) => {
1964
+ const av = a[sortKey]
1965
+ const bv = b[sortKey]
1966
+ if (av === bv) return 0
1967
+ return (av > bv ? 1 : -1) * dir
1968
+ })
1969
+ return list
1970
+ }, [view, sortKey, sortDir])
1971
+
1972
+ function toggleSort(key) {
1973
+ if (sortKey === key) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
1974
+ else { setSortKey(key); setSortDir('desc') }
1975
+ }
1976
+
1977
+ const cols = [
1978
+ { key: 'model', label: t('colModel'), sortable: false },
1979
+ { key: 'calls', label: t('colCalls'), sortable: true },
1980
+ { key: 'input', label: t('colInput'), sortable: true },
1981
+ { key: 'output', label: t('colOutput'), sortable: true },
1982
+ { key: 'total', label: t('colTotal'), sortable: true },
1983
+ { key: 'cost', label: t('colCost'), sortable: true },
1984
+ { key: 'ratio', label: t('colShare'), sortable: false },
1985
+ ]
1986
+
1987
+ return h('div', { className: 'tsd-card tsd-table-card' },
1988
+ h('div', { className: 'tsd-card-header' },
1989
+ h('h3', { className: 'tsd-card-title' }, t('byModelTitle')),
1990
+ h('span', { className: 'tsd-card-tag' },
1991
+ t('modelCount', { n: view.length }) + ' · ' + t('colTotal') + ' ' + formatInt(sumTotal))),
1992
+ view.length === 0
1993
+ ? h('div', { className: 'tsd-empty-block' }, t('noData'))
1994
+ : h('div', { className: 'tsd-table-wrap' },
1995
+ h('table', { className: 'tsd-table' },
1996
+ h('caption', { className: 'tsd-visually-hidden' }, t('modelTableCaption')),
1997
+ h('thead', null,
1998
+ h('tr', null, cols.map((c) =>
1999
+ h('th', {
2000
+ key: c.key,
2001
+ className: 'tsd-th' + (c.key === 'model' ? '' : ' tsd-th-num'),
2002
+ },
2003
+ c.sortable
2004
+ ? h('button', {
2005
+ type: 'button',
2006
+ className: 'tsd-sort-btn' + (sortKey === c.key ? ' tsd-sort-active' : ''),
2007
+ onClick: () => toggleSort(c.key),
2008
+ },
2009
+ c.label,
2010
+ sortKey === c.key
2011
+ ? h('span', { className: 'tsd-sort-arrow' }, sortDir === 'asc' ? '↑' : '↓')
2012
+ : null)
2013
+ : c.label)))),
2014
+ h('tbody', null, sorted.map((r) => {
2015
+ const ratio = sumTotal > 0 ? r.total / sumTotal : 0
2016
+ return h('tr', { key: r.key, className: 'tsd-tr' },
2017
+ h('td', { className: 'tsd-td tsd-td-model' },
2018
+ h('span', { className: 'tsd-model-mono', title: r.key }, r.key)),
2019
+ h('td', { className: 'tsd-td tsd-num' }, formatInt(r.calls)),
2020
+ h('td', { className: 'tsd-td tsd-num' }, formatInt(r.input)),
2021
+ h('td', { className: 'tsd-td tsd-num' }, formatInt(r.output)),
2022
+ h('td', { className: 'tsd-td tsd-num tsd-total' }, formatInt(r.total)),
2023
+ h('td', { className: 'tsd-td tsd-num' },
2024
+ r.unpriced
2025
+ ? h('span', { className: 'tsd-unpriced', title: t('unpricedRowHint') }, t('unpriced'))
2026
+ : formatCost(r.cost, currency)),
2027
+ h('td', { className: 'tsd-td tsd-td-ratio' },
2028
+ h('div', { className: 'tsd-ratio' },
2029
+ h('div', { className: 'tsd-ratio-bar' },
2030
+ h('div', { className: 'tsd-ratio-fill', style: { width: ratio * 100 + '%' } })),
2031
+ h('span', { className: 'tsd-ratio-pct' }, fmtPct(ratio)))))
2032
+ })))))
2033
+ }
2034
+
2035
+ /* ---------------------------------------------------------------------------
2036
+ * 三态:加载 / 错误 / 空
2037
+ * ------------------------------------------------------------------------- */
2038
+ function LoadingState() {
2039
+ const t = useT()
2040
+ return h('div', { className: 'tsd-loading' },
2041
+ h('div', { className: 'tsd-cards' },
2042
+ [0, 1, 2, 3, 4].map((i) =>
2043
+ h('div', { key: i, className: 'tsd-card tsd-card-skeleton' },
2044
+ h('div', { className: 'tsd-skeleton', style: { width: '40%', height: 12 } }),
2045
+ h('div', { className: 'tsd-skeleton', style: { width: '70%', height: 22, marginTop: 10 } })))),
2046
+ h('div', { className: 'tsd-card' },
2047
+ h('div', { className: 'tsd-skeleton', style: { width: '100%', height: 300 } })),
2048
+ h('div', { className: 'tsd-card' },
2049
+ [0, 1, 2, 3, 4, 5].map((i) =>
2050
+ h('div', { key: i, className: 'tsd-skeleton-row', style: { width: 96 - i * 8 + '%' } }))))
2051
+ }
2052
+
2053
+ function ErrorState(props) {
2054
+ const { error, onRetry } = props
2055
+ const t = useT()
2056
+ return h('div', { className: 'tsd-card tsd-error', role: 'alert' },
2057
+ h(SvgIcon, { name: 'alert', size: 22, className: 'tsd-error-icon' }),
2058
+ h('div', { className: 'tsd-error-text' },
2059
+ h('p', { className: 'tsd-error-title' }, t('loadFailedTitle')),
2060
+ h('p', { className: 'tsd-error-detail' }, String((error && error.message) || error))),
2061
+ h('button', { type: 'button', className: 'tsd-btn tsd-btn-primary', onClick: onRetry }, t('retry')))
2062
+ }
2063
+
2064
+ function EmptyState(props) {
2065
+ const { filtered, onReset } = props
2066
+ const t = useT()
2067
+ return h('div', { className: 'tsd-card tsd-empty' },
2068
+ h(SvgIcon, { name: 'inbox', size: 28, className: 'tsd-empty-icon' }),
2069
+ h('p', { className: 'tsd-empty-title' }, t('noData')),
2070
+ h('p', { className: 'tsd-empty-desc' },
2071
+ filtered ? t('emptyFiltered') : t('emptyDesc')),
2072
+ h('button', { type: 'button', className: 'tsd-btn tsd-btn-ghost', onClick: onReset }, t('resetFilters')))
2073
+ }
2074
+
2075
+ /* ---------------------------------------------------------------------------
2076
+ * 页面级状态机 + 布局
2077
+ * ------------------------------------------------------------------------- */
2078
+ function StatsDashboard(props) {
2079
+ const t = props.t || fallbackT
2080
+ const [range, setRange] = React.useState('today') // today | 7d | 30d | custom
2081
+ const [custom, setCustom] = React.useState(defaultCustom)
2082
+ const [models, setModels] = React.useState([]) // [] = 全部模型
2083
+ const [customOpen, setCustomOpen] = React.useState(false)
2084
+ const [retry, setRetry] = React.useState(0)
2085
+ const [status, setStatus] = React.useState('loading') // loading | refreshing | ready | error
2086
+ const [data, setData] = React.useState(null)
2087
+ const [error, setError] = React.useState(null)
2088
+ const [prices, setPrices] = React.useState({ prices: {}, currency: '¥' })
2089
+
2090
+ const eff = React.useMemo(() => rangeFor(range, custom), [range, custom])
2091
+ // Fixed step policy: today = hourly; 7d/30d/custom = daily, custom uses a
2092
+ // stable step (1/3/5/7 days) chosen by the range length.
2093
+ const effGran = effGranularity(range)
2094
+ const stepDays = range === 'custom' ? pickStepDays((eff.to || 0) - (eff.from || 0)) : 1
2095
+ const granLabel = stepLabel(t, range, stepDays)
2096
+
2097
+ // 查询统计(首载骨架;刷新保留旧数据转 refreshing;fill 0 填充 + 年度热力图)
2098
+ React.useEffect(() => {
2099
+ let current = true
2100
+ setStatus((s) => (s === 'ready' && data !== null ? 'refreshing' : 'loading'))
2101
+ host.call('usage/query', {
2102
+ from: eff.from,
2103
+ to: eff.to,
2104
+ models: models.slice(),
2105
+ fill: true,
2106
+ granularity: effGran,
2107
+ stepDays,
2108
+ heatmap: true,
2109
+ })
2110
+ .then((r) => {
2111
+ if (!current) return
2112
+ setData(r)
2113
+ setStatus('ready')
2114
+ setError(null)
2115
+ }, (e) => {
2116
+ if (!current) return
2117
+ setError(e)
2118
+ setStatus('error')
2119
+ })
2120
+ return () => { current = false }
2121
+ }, [eff.from, eff.to, models, effGran, stepDays, retry])
2122
+
2123
+ // 价格表:进入页面拉一次 + 每次刷新同步
2124
+ React.useEffect(() => {
2125
+ let current = true
2126
+ host.call('usage/prices').then((p) => {
2127
+ if (current && p && p.prices) setPrices(p)
2128
+ }, () => {})
2129
+ return () => { current = false }
2130
+ }, [retry])
2131
+
2132
+ const reload = React.useCallback(() => setRetry((v) => v + 1), [])
2133
+
2134
+ const resetFilters = React.useCallback(() => {
2135
+ setModels([])
2136
+ setCustom(defaultCustom())
2137
+ setRange('today')
2138
+ }, [])
2139
+
2140
+ const busy = status === 'loading' || status === 'refreshing'
2141
+ const ready = data !== null
2142
+ const isError = status === 'error'
2143
+ const isEmpty = ready && data.daily.length === 0 && data.byModel.length === 0
2144
+
2145
+ return h(T.Provider, { value: t },
2146
+ h('div', { className: 'tsd-section', 'aria-busy': busy || undefined },
2147
+ h(StatsHeader, { busy, onRefresh: reload }),
2148
+ h(FilterBar, {
2149
+ range,
2150
+ custom,
2151
+ models,
2152
+ customOpen,
2153
+ modelOptions: data ? data.models : [],
2154
+ onPreset: (p) => { setRange(p); setCustomOpen(false) },
2155
+ onCustomOpen: (open) => { setRange('custom'); setCustomOpen(open) },
2156
+ onCustomApply: (c) => { setCustom(c); setCustomOpen(false) },
2157
+ onCustomClose: () => setCustomOpen(false),
2158
+ onModelsChange: setModels,
2159
+ }),
2160
+ isError && !ready
2161
+ ? h(ErrorState, { error, onRetry: reload })
2162
+ : null,
2163
+ !ready && !isError
2164
+ ? h(LoadingState, null)
2165
+ : null,
2166
+ ready && isError
2167
+ ? h('div', { className: 'tsd-alert-banner', role: 'alert' },
2168
+ h('span', null, t('loadFailed', { message: String((error && error.message) || error) })),
2169
+ h('button', { type: 'button', className: 'tsd-btn tsd-btn-ghost', onClick: reload }, t('retry')))
2170
+ : null,
2171
+ ready && !isError && isEmpty
2172
+ ? h(EmptyState, { filtered: models.length > 0, onReset: resetFilters })
2173
+ : null,
2174
+ ready && !isError && !isEmpty
2175
+ ? h('div', { className: 'tsd-content', 'data-busy': busy ? 'true' : undefined },
2176
+ h(SummaryCards, {
2177
+ total: data.total,
2178
+ currency: prices.currency || '¥',
2179
+ unpricedCalls: data.unpricedCalls || 0,
2180
+ }),
2181
+ h(DailyChart, {
2182
+ daily: data.daily,
2183
+ gran: effGran,
2184
+ stepDays,
2185
+ granLabel,
2186
+ }),
2187
+ h(HeatmapCard, { days: data.heatmap || [] }),
2188
+ h(ModelTable, { rows: data.byModel, currency: prices.currency || '¥' }))
2189
+ : null))
2190
+ }
2191
+
2192
+ /* ---------------------------------------------------------------------------
2193
+ * 注册:settings.section 页面
2194
+ * ------------------------------------------------------------------------- */
2195
+ function apply(ctx) {
2196
+ ctx.effect(() => styles.insert(CSS), 'usage: styles')
2197
+ ctx.effect(() => ctx.locale.register(NS, { zh: ZH, en: EN }), 'usage: dictionaries')
2198
+ const t0 = ctx.locale ? ctx.locale.bind(NS) : fallbackT
2199
+ const slots = ctx.get('slots')
2200
+ if (slots === undefined) return
2201
+ slots.inject('settings.section', () => slots.register(
2202
+ { name: 'settings.section', id: 'token-stats', order: 20, label: () => t0('nav'), locale: NS },
2203
+ (props) => h(StatsDashboard, props),
2204
+ ))
2205
+ }
2206
+
2207
+ return { inject: ['locale'], apply }
2208
+
2209
+ }