dsh-desktop-statusbar 2.0.0 → 2.0.2
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.en.md +4 -4
- package/README.md +4 -4
- package/docs/settings.png +0 -0
- package/lib/balance-api.js +224 -0
- package/lib/client.js +481 -146
- package/lib/index.js +117 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -40,6 +40,8 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
* v6 起:`segments` 存全部段的有序列表(顺序对整份列表生效),`hidden` 存未勾选的段。
|
|
41
41
|
*/
|
|
42
42
|
const CONFIG_VERSION = 6;
|
|
43
|
+
/** 插件版本:显示在设置页底部,用来确认页面加载的是哪一版客户端代码。 */
|
|
44
|
+
const VERSION = "2.0.2";
|
|
43
45
|
/** 官方参考价的版本:官方调价时改这个数字,老价格库会自动并入新参考价。 */
|
|
44
46
|
const PRICE_VERSION = 2;
|
|
45
47
|
|
|
@@ -346,7 +348,10 @@ window.__ModuleLoader__.load({
|
|
|
346
348
|
* 累加用 costOf 的原始值,只在这里做一次舍入。
|
|
347
349
|
*/
|
|
348
350
|
function costText(amount) {
|
|
349
|
-
|
|
351
|
+
const n = Number(amount);
|
|
352
|
+
/* 兜住 NaN / -0 / 负数:它们会显示成 "NaN" 或 "-0.00",看着像坏掉了 */
|
|
353
|
+
if (Number.isFinite(n) !== true || n <= 0) return "0.00";
|
|
354
|
+
return n.toFixed(2);
|
|
350
355
|
}
|
|
351
356
|
|
|
352
357
|
/** 逐字段相减,得到两次全量用量之间的增量 —— 即本轮消耗。 */
|
|
@@ -447,12 +452,16 @@ window.__ModuleLoader__.load({
|
|
|
447
452
|
f_tokensTurn: "本轮用量",
|
|
448
453
|
tipModelTime: "模型用时",
|
|
449
454
|
tipToolTime: "工具调用",
|
|
455
|
+
tipTtftTurn: "本轮平均首字",
|
|
450
456
|
tipTtftFast: "最快首字延迟",
|
|
451
457
|
tipTtftSlow: "最慢首字延迟",
|
|
458
|
+
tipTpsTurn: "本轮平均速度",
|
|
452
459
|
tipTpsFast: "最快输出速度",
|
|
453
460
|
tipTpsSlow: "最慢输出速度",
|
|
454
461
|
tipTopUp: "充值余额",
|
|
455
462
|
tipGranted: "赠金余额",
|
|
463
|
+
tipSpent: "累计消费",
|
|
464
|
+
tipHitTurn: "本轮缓存命中率",
|
|
456
465
|
tipHitHigh: "最高缓存命中率",
|
|
457
466
|
tipHitLow: "最低缓存命中率",
|
|
458
467
|
contextAria: "上下文已用 {percent}",
|
|
@@ -464,7 +473,7 @@ window.__ModuleLoader__.load({
|
|
|
464
473
|
f_cost: "总计 {symbol}{cost}",
|
|
465
474
|
priceTierPeak: "高峰时段",
|
|
466
475
|
priceTierOffPeak: "空闲时段",
|
|
467
|
-
f_balanceDash: "余额 -",
|
|
476
|
+
f_balanceDash: "余额 {symbol}-",
|
|
468
477
|
f_lastCost: "本轮 {symbol}{cost}",
|
|
469
478
|
f_balance: "余额 {symbol}{amount}",
|
|
470
479
|
f_balanceSubCent: "余额 <{symbol}0.01",
|
|
@@ -533,12 +542,16 @@ window.__ModuleLoader__.load({
|
|
|
533
542
|
f_tokensTurn: "This turn usage",
|
|
534
543
|
tipModelTime: "Model time",
|
|
535
544
|
tipToolTime: "Tool calls",
|
|
545
|
+
tipTtftTurn: "Average TTFT this turn",
|
|
536
546
|
tipTtftFast: "Fastest TTFT",
|
|
537
547
|
tipTtftSlow: "Slowest TTFT",
|
|
548
|
+
tipTpsTurn: "Average TPS this turn",
|
|
538
549
|
tipTpsFast: "Fastest TPS",
|
|
539
550
|
tipTpsSlow: "Slowest TPS",
|
|
540
551
|
tipTopUp: "Top-up balance",
|
|
541
552
|
tipGranted: "Granted balance",
|
|
553
|
+
tipSpent: "Total spent",
|
|
554
|
+
tipHitTurn: "Cache hit rate this turn",
|
|
542
555
|
tipHitHigh: "Highest cache hit rate",
|
|
543
556
|
tipHitLow: "Lowest cache hit rate",
|
|
544
557
|
contextAria: "{percent} of context used",
|
|
@@ -573,7 +586,7 @@ window.__ModuleLoader__.load({
|
|
|
573
586
|
unitCountSteps: " steps",
|
|
574
587
|
unitSpeed: "t/s",
|
|
575
588
|
priceSuggestedHint: "The price book uses the DeepSeek reference prices.",
|
|
576
|
-
f_balanceDash: "Balance -",
|
|
589
|
+
f_balanceDash: "Balance {symbol}-",
|
|
577
590
|
priceNewModel: "Custom model",
|
|
578
591
|
priceAdd: "Add",
|
|
579
592
|
priceEmpty: "Price book is empty — type a model name (e.g. deepseek-flash) and click Add.",
|
|
@@ -762,18 +775,34 @@ window.__ModuleLoader__.load({
|
|
|
762
775
|
return !(Array.isArray(cfg.hidden) && cfg.hidden.indexOf(id) !== -1);
|
|
763
776
|
}
|
|
764
777
|
/**
|
|
765
|
-
*
|
|
766
|
-
*
|
|
778
|
+
* 拖动中算落点:浮层的下边界压过哪一行的中线,影子就往下滑到那一行;
|
|
779
|
+
* 上边界举过哪一行的中线,就往上滑。
|
|
780
|
+
* slots 是按下那一刻按原始顺序量下的各行矩形,shift 是这期间页面滚动的位移
|
|
781
|
+
* (每次移动都重新量锚点得到)—— 直接用真实矩形,行距里有没有 gap、
|
|
782
|
+
* 某行是不是比别人高,都不影响判定。
|
|
767
783
|
*/
|
|
768
|
-
function
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
784
|
+
function dropIndexAt(slots, shift, from, top, bottom) {
|
|
785
|
+
let to = from;
|
|
786
|
+
for (let i = from + 1; i < slots.length; i += 1) {
|
|
787
|
+
const rect = slots[i];
|
|
788
|
+
if (rect === null || rect === undefined) continue;
|
|
789
|
+
if (bottom <= rect.top + shift + rect.height / 2) break;
|
|
790
|
+
to = i;
|
|
791
|
+
}
|
|
792
|
+
for (let i = from - 1; i >= 0; i -= 1) {
|
|
793
|
+
const rect = slots[i];
|
|
794
|
+
if (rect === null || rect === undefined) continue;
|
|
795
|
+
if (top >= rect.top + shift + rect.height / 2) break;
|
|
796
|
+
to = i;
|
|
797
|
+
}
|
|
798
|
+
return to;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** 把 from 位置的元素挪到 to 位置(不动原数组)。 */
|
|
802
|
+
function moveItem(list, from, to) {
|
|
803
|
+
const next = list.slice();
|
|
804
|
+
next.splice(to, 0, next.splice(from, 1)[0]);
|
|
805
|
+
return next;
|
|
777
806
|
}
|
|
778
807
|
|
|
779
808
|
/* ---------------------------------------------------------------- 工具 */
|
|
@@ -915,6 +944,19 @@ window.__ModuleLoader__.load({
|
|
|
915
944
|
* 令牌面板数据:第一行是总用量,第二行三段占比(顶满 100%),下面三行明细。
|
|
916
945
|
* 数字一律千分位精确值,不带单位(面板标题已经说明是 token 用量)。
|
|
917
946
|
*/
|
|
947
|
+
/** 没有 token 数据时的面板:不画三段条,三行都给横杠 —— 保证气泡在任何情况下都点得开。 */
|
|
948
|
+
function dashTokenPanel(t) {
|
|
949
|
+
return {
|
|
950
|
+
totalText: "-",
|
|
951
|
+
segments: [],
|
|
952
|
+
rows: [
|
|
953
|
+
{ label: t("f_tokensHit"), color: "var(--dsw-static-neutral-bluish-400)", value: "-" },
|
|
954
|
+
{ label: t("f_tokensMiss"), color: "#a78bfa", value: "-" },
|
|
955
|
+
{ label: t("f_tokensOut"), color: "var(--dsw-static-blue-450)", value: "-" }
|
|
956
|
+
]
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
918
960
|
function tokenPanelView(cached, missInput, output, t) {
|
|
919
961
|
const total = cached + missInput + output;
|
|
920
962
|
if (total <= 0) return null;
|
|
@@ -993,18 +1035,58 @@ window.__ModuleLoader__.load({
|
|
|
993
1035
|
});
|
|
994
1036
|
}
|
|
995
1037
|
|
|
996
|
-
/** 令牌面板的 tip
|
|
1038
|
+
/** 令牌面板的 tip。没有数据也给面板(三行横杠)—— 气泡必须任何情况下都点得开。 */
|
|
997
1039
|
function tokensTip(parts, title, t) {
|
|
998
1040
|
const panel = parts === null ? null : tokenPanelView(parts.cached, parts.missInput, parts.output, t);
|
|
999
|
-
return
|
|
1041
|
+
return { kind: "tokens", title: title, panel: panel === null ? dashTokenPanel(t) : panel };
|
|
1000
1042
|
}
|
|
1001
1043
|
|
|
1002
|
-
/** 毫秒 →
|
|
1044
|
+
/** 毫秒 → 面板里的时长文本(精确到秒);0 或缺数据给横杠。 */
|
|
1003
1045
|
function tipDuration(ms) {
|
|
1004
|
-
const text =
|
|
1046
|
+
const text = formatDurationFine(ms);
|
|
1005
1047
|
return text === null ? "-" : text;
|
|
1006
1048
|
}
|
|
1007
1049
|
|
|
1050
|
+
/** 时长精确到秒:1h23m45s / 23m45s / 45s。底栏那条用 formatDuration(省掉秒,宽度有限)。 */
|
|
1051
|
+
function formatDurationFine(ms) {
|
|
1052
|
+
const n = Number(ms) || 0;
|
|
1053
|
+
if (n <= 0) return null;
|
|
1054
|
+
const whole = Math.round(n / 1000);
|
|
1055
|
+
if (whole < 60) return whole + "s";
|
|
1056
|
+
const minutes = Math.floor(whole / 60);
|
|
1057
|
+
if (minutes < 60) return minutes + "m" + (whole % 60) + "s";
|
|
1058
|
+
return Math.floor(minutes / 60) + "h" + (minutes % 60) + "m" + (whole % 60) + "s";
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** 本轮缓存命中率:这一轮的输入 token 里命中缓存的比例;本轮还没有调用给横杠。
|
|
1062
|
+
读插件自己的 desktopStatusbarUsage 投影 —— 官方 tokenUsage 只有会话累计值。
|
|
1063
|
+
注意字段是短名(input / cacheRead / cacheWrite),与官方投影的 *Tokens 不同名。 */
|
|
1064
|
+
function turnHitRate(sessionUsage) {
|
|
1065
|
+
const current = sessionUsage === undefined || sessionUsage === null ? null : sessionUsage.current;
|
|
1066
|
+
if (current === null || current === undefined) return "-";
|
|
1067
|
+
const denominator = (current.input || 0) + (current.cacheRead || 0) + (current.cacheWrite || 0);
|
|
1068
|
+
if (denominator <= 0) return "-";
|
|
1069
|
+
return hitRateText(((current.cacheRead || 0) / denominator) * 100);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** 本轮平均首字;本轮还没有样本给横杠。 */
|
|
1073
|
+
function turnAverageTtft(timing) {
|
|
1074
|
+
if (timing === null || timing === undefined) return "-";
|
|
1075
|
+
const samples = timing.turnTtftSamples || 0;
|
|
1076
|
+
if (samples <= 0) return "-";
|
|
1077
|
+
const text = formatSeconds(timing.turnTtftMs / samples);
|
|
1078
|
+
return text === null ? "-" : text + "s";
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** 本轮平均输出速度;本轮还没有可测样本给横杠。 */
|
|
1082
|
+
function turnAverageTps(timing, t) {
|
|
1083
|
+
if (timing === null || timing === undefined) return "-";
|
|
1084
|
+
const decodeMs = timing.turnDecodeMs || 0;
|
|
1085
|
+
const tokens = timing.turnDecodeTokens || 0;
|
|
1086
|
+
if (decodeMs <= 0 || tokens <= 0) return "-";
|
|
1087
|
+
return formatThroughput(tokens / (decodeMs / 1000)) + t("unitSpeed");
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1008
1090
|
/** 首字极值(毫秒)→ "9.3s";没有样本给横杠。 */
|
|
1009
1091
|
function tipSeconds(mark) {
|
|
1010
1092
|
if (mark === null || mark === undefined) return "-";
|
|
@@ -1126,6 +1208,7 @@ window.__ModuleLoader__.load({
|
|
|
1126
1208
|
kind: "rows",
|
|
1127
1209
|
title: t("segTtft"),
|
|
1128
1210
|
rows: [
|
|
1211
|
+
{ label: t("tipTtftTurn"), value: turnAverageTtft(timing) },
|
|
1129
1212
|
{ label: t("tipTtftFast"), value: tipSeconds(timing === null ? null : timing.fastestTtft) },
|
|
1130
1213
|
{ label: t("tipTtftSlow"), value: tipSeconds(timing === null ? null : timing.slowestTtft) }
|
|
1131
1214
|
]
|
|
@@ -1147,6 +1230,7 @@ window.__ModuleLoader__.load({
|
|
|
1147
1230
|
kind: "rows",
|
|
1148
1231
|
title: t("segCacheHit"),
|
|
1149
1232
|
rows: [
|
|
1233
|
+
{ label: t("tipHitTurn"), value: turnHitRate(src.sessionUsage) },
|
|
1150
1234
|
{ label: t("tipHitHigh"), value: extremes === null ? "-" : extremes.high },
|
|
1151
1235
|
{ label: t("tipHitLow"), value: extremes === null ? "-" : extremes.low }
|
|
1152
1236
|
]
|
|
@@ -1165,6 +1249,7 @@ window.__ModuleLoader__.load({
|
|
|
1165
1249
|
kind: "rows",
|
|
1166
1250
|
title: t("segTps"),
|
|
1167
1251
|
rows: [
|
|
1252
|
+
{ label: t("tipTpsTurn"), value: turnAverageTps(timing, t) },
|
|
1168
1253
|
{ label: t("tipTpsFast"), value: tipSpeed(timing === null ? null : timing.fastestTps, t) },
|
|
1169
1254
|
{ label: t("tipTpsSlow"), value: tipSpeed(timing === null ? null : timing.slowestTps, t) }
|
|
1170
1255
|
]
|
|
@@ -1207,9 +1292,9 @@ window.__ModuleLoader__.load({
|
|
|
1207
1292
|
|
|
1208
1293
|
if (id === "cost") {
|
|
1209
1294
|
const sessionUsage = src.sessionUsage;
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1295
|
+
const calls = sessionUsage === undefined || sessionUsage === null || Array.isArray(sessionUsage.calls) !== true
|
|
1296
|
+
? []
|
|
1297
|
+
: sessionUsage.calls;
|
|
1213
1298
|
/* 每条调用按自己发生的那一刻、自己的模型定峰谷:跨时段、跨模型都不会串价。
|
|
1214
1299
|
累加的是未舍入金额,最后一次性舍入 —— 逐条舍入会累积出几毛钱的误差。 */
|
|
1215
1300
|
const fallbackModel = src.sessionModel !== undefined && src.sessionModel !== null ? src.sessionModel.model : null;
|
|
@@ -1222,10 +1307,10 @@ window.__ModuleLoader__.load({
|
|
|
1222
1307
|
priced = true;
|
|
1223
1308
|
total += one;
|
|
1224
1309
|
}
|
|
1225
|
-
if (!priced) return null;
|
|
1226
1310
|
return {
|
|
1227
1311
|
id: id,
|
|
1228
|
-
|
|
1312
|
+
/* 算不出价(价格库里没这个模型)时给 ¥-:与"这笔真的是 0"区分开 */
|
|
1313
|
+
text: t("f_cost", { symbol: symbol, cost: priced ? costText(total) : "-" }),
|
|
1229
1314
|
/* token 计数并进总计:点这一段看会话的 token 明细 */
|
|
1230
1315
|
tip: tokensTip(sessionTokenParts(src.usage), t("f_tokensTotal"), t)
|
|
1231
1316
|
};
|
|
@@ -1240,29 +1325,31 @@ window.__ModuleLoader__.load({
|
|
|
1240
1325
|
&& Array.isArray(sessionUsageNow.current.calls)
|
|
1241
1326
|
? sessionUsageNow.current.calls
|
|
1242
1327
|
: null;
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1328
|
+
/* 本轮的 token 汇总与定价一起算:即使价格库里没有这个模型(算不出钱),
|
|
1329
|
+
也要留住 token 汇总 —— 本轮气泡照样能点开看明细。 */
|
|
1330
|
+
const turnParts = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
|
|
1331
|
+
let turnTotal = 0;
|
|
1332
|
+
let turnPriced = false;
|
|
1333
|
+
const turnList = turnCalls === null ? [] : turnCalls;
|
|
1334
|
+
for (let i = 0; i < turnList.length; i += 1) {
|
|
1335
|
+
const callModel = typeof turnList[i].model === "string" ? turnList[i].model : fallbackModel;
|
|
1336
|
+
const one = costOf(turnList[i], callModel, turnList[i].at);
|
|
1337
|
+
turnParts.input += turnList[i].input || 0;
|
|
1338
|
+
turnParts.cacheRead += turnList[i].cacheRead || 0;
|
|
1339
|
+
turnParts.cacheWrite += turnList[i].cacheWrite || 0;
|
|
1340
|
+
turnParts.output += turnList[i].output || 0;
|
|
1341
|
+
if (one === null) continue;
|
|
1342
|
+
turnPriced = true;
|
|
1343
|
+
turnTotal += Number(one);
|
|
1344
|
+
}
|
|
1345
|
+
const turnTip = tokensTip(billableParts(turnParts), t("f_tokensTurn"), t);
|
|
1346
|
+
if (turnPriced) {
|
|
1347
|
+
return {
|
|
1348
|
+
id: id,
|
|
1349
|
+
text: t("f_lastCost", { symbol: symbol, cost: costText(turnTotal) }),
|
|
1350
|
+
/* 本轮点开的是本轮 token 明细,读法与总计那个面板同一套 */
|
|
1351
|
+
tip: turnTip
|
|
1352
|
+
};
|
|
1266
1353
|
}
|
|
1267
1354
|
/* 本轮是不是正在跑(与状态段同一套判断) */
|
|
1268
1355
|
const active = src.running === true
|
|
@@ -1279,37 +1366,77 @@ window.__ModuleLoader__.load({
|
|
|
1279
1366
|
if (active === true && bucket !== null && sameTurn === true) {
|
|
1280
1367
|
const runningCost = costOf(bucket, fallbackModel, src.now);
|
|
1281
1368
|
if (runningCost !== null) {
|
|
1282
|
-
return { id: id, text: t("f_lastCost", { symbol: symbol, cost: costText(runningCost) }), tip:
|
|
1369
|
+
return { id: id, text: t("f_lastCost", { symbol: symbol, cost: costText(runningCost) }), tip: turnTip };
|
|
1283
1370
|
}
|
|
1284
1371
|
}
|
|
1285
|
-
/*
|
|
1286
|
-
|
|
1287
|
-
|
|
1372
|
+
/* 新一轮已经开跑、但还没有属于它的调用:必须归零,不能退回上一轮
|
|
1373
|
+
—— 节点折叠曾错位一轮,底栏显示成上上轮的金额,这条就是为堵它留的。
|
|
1374
|
+
一次模型调用都还没完成过(第一轮)时没有"归零"可言,跟总计一样给横杠。
|
|
1375
|
+
等这一轮真的产生了调用,上面那条精确口径就接管了。 */
|
|
1288
1376
|
if (active === true) {
|
|
1289
|
-
|
|
1377
|
+
const settled = src.sessionUsage !== undefined && src.sessionUsage !== null
|
|
1378
|
+
&& Array.isArray(src.sessionUsage.calls) === true
|
|
1379
|
+
&& src.sessionUsage.calls.length > 0;
|
|
1380
|
+
return {
|
|
1381
|
+
id: id,
|
|
1382
|
+
text: t("f_lastCost", { symbol: symbol, cost: settled ? costText(0) : "-" }),
|
|
1383
|
+
tip: turnTip
|
|
1384
|
+
};
|
|
1290
1385
|
}
|
|
1291
|
-
/* 空闲:显示最近完成的一轮(节点折叠 →
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
return {
|
|
1386
|
+
/* 空闲:显示最近完成的一轮(节点折叠 → 官方差分兜底),同样只出金额。
|
|
1387
|
+
没有可用数据时也给 ¥0.00:本轮不整段消失,底栏长度稳定,
|
|
1388
|
+
也不会退回上一轮(节点折叠曾错位一轮,显示成上上轮)的旧金额。 */
|
|
1389
|
+
const cost = bucket === null ? null : costOf(bucket, fallbackModel, src.now);
|
|
1390
|
+
return {
|
|
1391
|
+
id: id,
|
|
1392
|
+
text: t("f_lastCost", { symbol: symbol, cost: cost === null ? "-" : costText(cost) }),
|
|
1393
|
+
tip: turnTip
|
|
1394
|
+
};
|
|
1296
1395
|
}
|
|
1297
1396
|
|
|
1298
1397
|
if (id === "balance") {
|
|
1398
|
+
/* 查不到余额(非 DeepSeek 会话 / 还没查到 / 查询失败)时用同一套两行结构,值都是横杠:
|
|
1399
|
+
气泡里仍然是「充值余额 / 赠金余额」两行,只是没有数字。 */
|
|
1400
|
+
const dashBalance = () => {
|
|
1401
|
+
const dashSymbol = currencySymbol(src.currency);
|
|
1402
|
+
return {
|
|
1403
|
+
id: id,
|
|
1404
|
+
text: t("f_balanceDash", { symbol: dashSymbol }),
|
|
1405
|
+
tip: {
|
|
1406
|
+
kind: "rows",
|
|
1407
|
+
title: t("segBalance"),
|
|
1408
|
+
rows: [
|
|
1409
|
+
{ label: t("tipTopUp"), value: dashSymbol + "-" },
|
|
1410
|
+
{ label: t("tipGranted"), value: dashSymbol + "-" }
|
|
1411
|
+
]
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
};
|
|
1415
|
+
const sessionModelNow = src.sessionModel;
|
|
1416
|
+
const modelText = sessionModelNow === undefined || sessionModelNow === null
|
|
1417
|
+
? ""
|
|
1418
|
+
: String(sessionModelNow.provider === undefined ? "" : sessionModelNow.provider)
|
|
1419
|
+
+ " " + String(sessionModelNow.model === undefined ? "" : sessionModelNow.model);
|
|
1420
|
+
if (modelText.toLowerCase().includes("deepseek") !== true) return dashBalance();
|
|
1299
1421
|
const balance = src.balance;
|
|
1300
|
-
if (balance === null || balance === undefined) return
|
|
1301
|
-
if (balance.ok !== true) return
|
|
1302
|
-
if (typeof balance.total !== "string") return
|
|
1422
|
+
if (balance === null || balance === undefined) return dashBalance();
|
|
1423
|
+
if (balance.ok !== true) return dashBalance();
|
|
1424
|
+
if (typeof balance.total !== "string") return dashBalance();
|
|
1303
1425
|
/* 余额用接口自己报的币种:账户里是人民币就显示 ¥,不跟着上面的计价单位走 */
|
|
1304
|
-
|
|
1426
|
+
/* 百分比口径(GLM 配额)不带货币符号,否则会显示成 ¥37.50% */
|
|
1427
|
+
const balSymbol = balance.isPercent === true ? "" : currencySymbol(balance.currency !== undefined && balance.currency !== null ? balance.currency : src.currency);
|
|
1305
1428
|
/* 点开看构成:total = 充值余额 + 赠金余额;没有赠金时接口不给这个字段,按 0.00 显示 */
|
|
1306
1429
|
const tip = {
|
|
1307
1430
|
kind: "rows",
|
|
1308
1431
|
title: t("segBalance"),
|
|
1309
1432
|
rows: [
|
|
1310
1433
|
{ label: t("tipTopUp"), value: balSymbol + balanceAmount(balance.toppedUp) },
|
|
1311
|
-
{ label: t("tipGranted"), value: balSymbol + balanceAmount(balance.granted) }
|
|
1312
|
-
|
|
1434
|
+
{ label: t("tipGranted"), value: balSymbol + balanceAmount(balance.granted) },
|
|
1435
|
+
/* 累计消费只有部分平台能给(智谱有,DeepSeek 的接口没有),没有就不显示这一行 */
|
|
1436
|
+
balance.spent === undefined || balance.spent === null
|
|
1437
|
+
? null
|
|
1438
|
+
: { label: t("tipSpent"), value: balSymbol + balanceAmount(balance.spent) }
|
|
1439
|
+
].filter((one) => one !== null)
|
|
1313
1440
|
};
|
|
1314
1441
|
/* 不足一分钱时官方显示 <¥0.01,这里跟着来 */
|
|
1315
1442
|
if (balance.subCent === true) return { id: id, text: t("f_balanceSubCent", { symbol: balSymbol }), tip: tip };
|
|
@@ -1339,8 +1466,8 @@ window.__ModuleLoader__.load({
|
|
|
1339
1466
|
/* 速度占位不带单位:英文那边标签已经是 TPS,再跟 t/s 就重复了 */
|
|
1340
1467
|
if (id === "tps") return label + " " + dash;
|
|
1341
1468
|
if (id === "sessionTime") return label + " " + dash + "m " + dash + "s";
|
|
1342
|
-
if (id === "cost" || id === "lastCost") return label + " " +
|
|
1343
|
-
if (id === "balance") return t("f_balanceDash");
|
|
1469
|
+
if (id === "cost" || id === "lastCost") return label + " " + currencySymbol(config.currency) + dash;
|
|
1470
|
+
if (id === "balance") return t("f_balanceDash", { symbol: currencySymbol(config.currency) });
|
|
1344
1471
|
return dash;
|
|
1345
1472
|
}
|
|
1346
1473
|
/* ---------------------------------------------------------------- css */
|
|
@@ -1416,21 +1543,29 @@ window.__ModuleLoader__.load({
|
|
|
1416
1543
|
".dsb-settings .dsb-section{display:flex;flex-direction:column;gap:8px}",
|
|
1417
1544
|
".dsb-settings .dsb-sectitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;letter-spacing:.02em}",
|
|
1418
1545
|
".dsb-settings .dsb-hint{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.6}",
|
|
1419
|
-
".dsb-settings .dsb-row{display:flex;align-items:center;justify-content:space-between;gap:12px;border-radius:8px;padding:6px 8px;background:var(--dsw-alias-bg-layer-1,transparent)}",
|
|
1546
|
+
".dsb-settings .dsb-row{display:flex;align-items:center;justify-content:space-between;gap:12px;border-radius:8px;padding:6px 8px;background:var(--dsw-alias-bg-layer-1,transparent);user-select:none;-webkit-user-select:none}",
|
|
1420
1547
|
".dsb-settings .dsb-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}",
|
|
1548
|
+
/* 拖动中不给悬停反馈:浮层不吃指针事件,鼠标其实悬在下面那行上,会莫名发灰 */
|
|
1549
|
+
".dsb-settings:has(.dsb-dragging) .dsb-row:hover{background:var(--dsw-alias-bg-layer-1,transparent)}",
|
|
1550
|
+
/* 拖动中光标一律抓手:光标由鼠标底下那个元素决定,而浮层不吃指针事件,
|
|
1551
|
+
底下既可能是握把(grab)、文字(pointer)、也可能是行间空隙(默认箭头)。
|
|
1552
|
+
!important 是因为这些元素各自都写了 cursor,靠继承压不住握把那条。 */
|
|
1553
|
+
".dsb-settings:has(.dsb-dragging) *{cursor:grabbing !important}",
|
|
1421
1554
|
".dsb-settings .dsb-check{display:flex;align-items:flex-start;gap:8px;cursor:pointer;flex:1;min-width:0}",
|
|
1422
1555
|
".dsb-settings .dsb-check input{flex:none;margin:2px 0 0}",
|
|
1423
1556
|
".dsb-settings input[type=checkbox]{accent-color:var(--dsw-alias-label-primary,#202020)}",
|
|
1424
1557
|
".dsb-settings .dsb-labels{display:flex;flex-direction:column;gap:2px;min-width:0}",
|
|
1425
1558
|
".dsb-settings .dsb-name{color:var(--dsw-alias-label-primary)}",
|
|
1426
1559
|
".dsb-settings .dsb-desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}",
|
|
1427
|
-
|
|
1428
|
-
".dsb-settings .dsb-row.dsb-
|
|
1429
|
-
|
|
1560
|
+
/* 被拿起来的那一行:原位留个淡影,跟手的是下面那张卡 */
|
|
1561
|
+
".dsb-settings .dsb-row.dsb-dragging{opacity:.32}",
|
|
1562
|
+
/* 跟手的浮层:气泡同款材质(只有 .5px 描边,没有柔光),不吃指针事件 */
|
|
1563
|
+
".dsb-settings .dsb-drag-card{position:fixed;z-index:40;pointer-events:none;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:6px 8px;border-radius:var(--dsw-radius-md,12px);background:rgb(from var(--dsw-specific-menu,var(--dsw-alias-bg-layer-1,#1f1f1f)) r g b / .382);backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%));-webkit-backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%));box-shadow:0 0 0 .5px var(--dsw-alias-border-l1,rgba(127,127,127,.3))}",
|
|
1430
1564
|
".dsb-settings .dsb-handle{position:relative;flex:none;width:16px;height:16px;color:var(--dsw-alias-label-tertiary);cursor:grab}",
|
|
1431
1565
|
'.dsb-settings .dsb-handle::before{content:"";position:absolute;left:4px;top:1.5px;width:3px;height:3px;border-radius:50%;background:currentColor;box-shadow:5px 0 0 currentColor,0 5px 0 currentColor,5px 5px 0 currentColor,0 10px 0 currentColor,5px 10px 0 currentColor}',
|
|
1432
1566
|
".dsb-settings .dsb-handle:active{cursor:grabbing}",
|
|
1433
|
-
|
|
1567
|
+
/* 未勾选的段也能调位置,所以光标跟勾选时一样是抓手,只是点阵淡一些 */
|
|
1568
|
+
".dsb-settings .dsb-handle-off{opacity:.25}",
|
|
1434
1569
|
/* 价格库 */
|
|
1435
1570
|
".dsb-settings .dsb-card{border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.25));border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:10px}",
|
|
1436
1571
|
".dsb-settings .dsb-current{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.6}",
|
|
@@ -1475,6 +1610,8 @@ window.__ModuleLoader__.load({
|
|
|
1475
1610
|
".dsb-settings .dsb-action:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1))}",
|
|
1476
1611
|
".dsb-settings .dsb-action-sm{height:24px;padding:0 8px;font-size:12px}",
|
|
1477
1612
|
".dsb-settings .dsb-foot{display:flex;align-items:center;gap:12px;flex-wrap:wrap}",
|
|
1613
|
+
/* 版本号贴右侧、与按钮底边对齐 */
|
|
1614
|
+
".dsb-settings .dsb-version{margin-left:auto;align-self:flex-end;font-size:11px;line-height:1;color:var(--dsw-alias-label-tertiary);opacity:.75}",
|
|
1478
1615
|
".dsb-settings .dsb-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}"
|
|
1479
1616
|
].join("");
|
|
1480
1617
|
|
|
@@ -1646,19 +1783,34 @@ window.__ModuleLoader__.load({
|
|
|
1646
1783
|
}, []);
|
|
1647
1784
|
|
|
1648
1785
|
const wantsBalance = isSegmentOn(cfg, "balance");
|
|
1786
|
+
/* 当前会话用的 provider / model:host 按它决定查哪个平台的余额。
|
|
1787
|
+
这两个值变了要重新查,所以下面 effect 的依赖里带着它们。 */
|
|
1788
|
+
const balanceProvider = sessionModel !== undefined && sessionModel !== null ? sessionModel.provider : null;
|
|
1789
|
+
const balanceModel = sessionModel !== undefined && sessionModel !== null ? sessionModel.model : null;
|
|
1790
|
+
const balanceQuery = "?provider=" + encodeURIComponent(String(balanceProvider === undefined || balanceProvider === null ? "" : balanceProvider))
|
|
1791
|
+
+ "&model=" + encodeURIComponent(String(balanceModel === undefined || balanceModel === null ? "" : balanceModel));
|
|
1792
|
+
/* 点开余额气泡时先强制重查一次:host 端余额不缓存,带上 force 就不用等
|
|
1793
|
+
下一次轮询(最多 60 秒),气泡里直接是此刻的数字。轮询本身不带 force。 */
|
|
1794
|
+
const balanceReloadRef = react.useRef(null);
|
|
1649
1795
|
react.useEffect(() => {
|
|
1650
1796
|
if (!wantsBalance) return undefined;
|
|
1651
1797
|
let alive = true;
|
|
1652
|
-
const load = () => {
|
|
1653
|
-
|
|
1798
|
+
const load = (force) => {
|
|
1799
|
+
const url = BALANCE_URL + balanceQuery + (force === true ? "&force=1" : "");
|
|
1800
|
+
window.fetch(url, { cache: "no-store" })
|
|
1654
1801
|
.then((response) => response.json())
|
|
1655
1802
|
.then((data) => { if (alive) setBalance(data); })
|
|
1656
1803
|
.catch(() => { if (alive) setBalance({ ok: false, reason: "fetch-failed" }); });
|
|
1657
1804
|
};
|
|
1658
|
-
load();
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1805
|
+
balanceReloadRef.current = () => load(true);
|
|
1806
|
+
load(false);
|
|
1807
|
+
const timer = window.setInterval(() => load(false), BALANCE_POLL_MS);
|
|
1808
|
+
return () => {
|
|
1809
|
+
alive = false;
|
|
1810
|
+
balanceReloadRef.current = null;
|
|
1811
|
+
window.clearInterval(timer);
|
|
1812
|
+
};
|
|
1813
|
+
}, [wantsBalance, balanceProvider, balanceModel]);
|
|
1662
1814
|
|
|
1663
1815
|
/* 只在"点别处"时收起。这里刻意不监听 scroll:底栏固定在视口底部,对话区滚动
|
|
1664
1816
|
不会移动触发项,但每来一次工具调用/新一轮,对话区都会自动滚到底 ——
|
|
@@ -1798,7 +1950,9 @@ window.__ModuleLoader__.load({
|
|
|
1798
1950
|
const at = tipKeys.indexOf(key);
|
|
1799
1951
|
if (at < 0) return "center";
|
|
1800
1952
|
if (at === 0) return "left";
|
|
1801
|
-
|
|
1953
|
+
/* 只有余额贴右边(它固定在栏尾)。其余一律居中、装不下时再夹 ——
|
|
1954
|
+
否则某一段没有气泡时会被当成"最右项"改成右对齐,看起来就不居中了。 */
|
|
1955
|
+
return key === "balance" ? "right" : "center";
|
|
1802
1956
|
};
|
|
1803
1957
|
/* 打开时保存触发项的位置并收起上一次的气泡;真正的坐标等面板挂上、量到宽度后再算 */
|
|
1804
1958
|
const toggleTip = (id, event, mode) => {
|
|
@@ -1807,6 +1961,8 @@ window.__ModuleLoader__.load({
|
|
|
1807
1961
|
setTipPos(null);
|
|
1808
1962
|
return;
|
|
1809
1963
|
}
|
|
1964
|
+
/* 点开余额:顺手重查一次(收起那一下不查),数据回来气泡自己更新 */
|
|
1965
|
+
if (id === "balance" && balanceReloadRef.current !== null) balanceReloadRef.current();
|
|
1810
1966
|
const target = event !== undefined && event !== null ? event.currentTarget : null;
|
|
1811
1967
|
/* 存节点而不是当次的矩形:窗口缩放后要重新量 */
|
|
1812
1968
|
anchorRef.current = target !== null && typeof target.getBoundingClientRect === "function" ? target : null;
|
|
@@ -1865,9 +2021,9 @@ window.__ModuleLoader__.load({
|
|
|
1865
2021
|
const t = props.t;
|
|
1866
2022
|
const cfg = useConfig();
|
|
1867
2023
|
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
const [
|
|
2024
|
+
/* 自绘拖拽:全程用 pointer 事件自己跟手。不用原生 HTML5 拖拽 ——
|
|
2025
|
+
原生拖拽会话期间浏览器不给页面 wheel 事件(滚轮失灵),而且只有松手才换位。 */
|
|
2026
|
+
const [drag, setDrag] = react.useState(null); /* { id, from, to, dy, startY, height, rects } */
|
|
1871
2027
|
const [newModel, setNewModel] = react.useState("");
|
|
1872
2028
|
const [hostModel, setHostModel] = react.useState(null);
|
|
1873
2029
|
const [editing, setEditing] = react.useState(null);
|
|
@@ -1878,21 +2034,38 @@ window.__ModuleLoader__.load({
|
|
|
1878
2034
|
const currencyRef = react.useRef(null);
|
|
1879
2035
|
const editPopRef = react.useRef(null);
|
|
1880
2036
|
|
|
1881
|
-
/*
|
|
1882
|
-
|
|
2037
|
+
/* 编辑浮层打开后按需滚动:block:"nearest" 只补上被遮住的那一段,
|
|
2038
|
+
本来就在视野里就一动不动(旧版用 "end" 会平白往上滚,明明下面有地方)。 */
|
|
1883
2039
|
react.useEffect(() => {
|
|
1884
2040
|
if (editing === null) return;
|
|
1885
2041
|
const node = editPopRef.current;
|
|
1886
2042
|
if (node === null || node === undefined || typeof node.scrollIntoView !== "function") return;
|
|
1887
|
-
node.scrollIntoView({ block: "
|
|
2043
|
+
node.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
1888
2044
|
}, [editing]);
|
|
1889
2045
|
|
|
1890
|
-
/*
|
|
2046
|
+
/* 编辑浮层:点别处收起并丢弃草稿。判断"里面"上三重保险 —— 浮层节点、价格卡节点,
|
|
2047
|
+
以及鼠标坐标是否落在卡片或币种下拉的矩形内。切币种时点在卡片内的控件上,
|
|
2048
|
+
无论 DOM 结构怎么变都不该把编辑区收掉。 */
|
|
1891
2049
|
react.useEffect(() => {
|
|
1892
2050
|
if (editing === null) return undefined;
|
|
1893
|
-
const
|
|
2051
|
+
const inside = (event) => {
|
|
2052
|
+
const target = event.target;
|
|
2053
|
+
if (target !== null && target !== undefined && typeof target.closest === "function"
|
|
2054
|
+
&& target.closest(".dsb-model") !== null) return true;
|
|
1894
2055
|
const node = editPopRef.current;
|
|
1895
|
-
if (node !== null && node !== undefined && typeof node.contains === "function" && node.contains(
|
|
2056
|
+
if (node !== null && node !== undefined && typeof node.contains === "function" && node.contains(target)) return true;
|
|
2057
|
+
const x = event.clientX;
|
|
2058
|
+
const y = event.clientY;
|
|
2059
|
+
if (typeof x !== "number" || typeof y !== "number") return false;
|
|
2060
|
+
return [node, currencyRef.current].some((one) => {
|
|
2061
|
+
if (one === null || one === undefined || typeof one.getBoundingClientRect !== "function") return false;
|
|
2062
|
+
const box = one.getBoundingClientRect();
|
|
2063
|
+
return box.width > 0 && box.height > 0
|
|
2064
|
+
&& x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
|
|
2065
|
+
});
|
|
2066
|
+
};
|
|
2067
|
+
const onDocClick = (event) => {
|
|
2068
|
+
if (inside(event)) return;
|
|
1896
2069
|
cancelEdit();
|
|
1897
2070
|
};
|
|
1898
2071
|
document.addEventListener("click", onDocClick);
|
|
@@ -2027,7 +2200,168 @@ window.__ModuleLoader__.load({
|
|
|
2027
2200
|
.map((id) => byId[id])
|
|
2028
2201
|
.filter((segment) => segment !== undefined && segment !== null);
|
|
2029
2202
|
|
|
2030
|
-
|
|
2203
|
+
/* 行节点 + 拖前位置:前者用来量落点,后者给 FLIP 补位移动画 */
|
|
2204
|
+
const rowNodes = react.useRef(new Map());
|
|
2205
|
+
const rowTops = react.useRef(null);
|
|
2206
|
+
/* 拖动是否还活着:pointerup 会被行和 window 各收到一次,靠它兜住重复落盘 */
|
|
2207
|
+
const draggingRef = react.useRef(false);
|
|
2208
|
+
const captureTops = () => {
|
|
2209
|
+
const tops = new Map();
|
|
2210
|
+
rowNodes.current.forEach((node, id) => {
|
|
2211
|
+
if (node !== null && node !== undefined && typeof node.getBoundingClientRect === "function") {
|
|
2212
|
+
tops.set(id, node.getBoundingClientRect().top);
|
|
2213
|
+
}
|
|
2214
|
+
});
|
|
2215
|
+
rowTops.current = tops;
|
|
2216
|
+
};
|
|
2217
|
+
|
|
2218
|
+
const measure = (segmentId) => {
|
|
2219
|
+
const node = rowNodes.current.get(segmentId);
|
|
2220
|
+
return node !== null && node !== undefined && typeof node.getBoundingClientRect === "function"
|
|
2221
|
+
? node.getBoundingClientRect()
|
|
2222
|
+
: null;
|
|
2223
|
+
};
|
|
2224
|
+
|
|
2225
|
+
const startDrag = (id, index, event) => {
|
|
2226
|
+
const at = measure(id);
|
|
2227
|
+
const anchor = measure("__context");
|
|
2228
|
+
draggingRef.current = true;
|
|
2229
|
+
setDrag({
|
|
2230
|
+
id: id, from: index, to: index, at: at,
|
|
2231
|
+
/* 指针在行内的抓取位置:浮层位置和落点都由它推出来,不依赖按下时的绝对坐标 */
|
|
2232
|
+
grab: at === null || at === undefined ? 0 : event.clientY - at.top,
|
|
2233
|
+
cardTop: at === null || at === undefined ? event.clientY : at.top,
|
|
2234
|
+
pointerY: event.clientY,
|
|
2235
|
+
/* 各槽位按下那一刻的真实矩形;滚动多少由锚点差值补回来 */
|
|
2236
|
+
slots: orderedSegments.map((one) => measure(one.id)),
|
|
2237
|
+
anchorTop: anchor === null || anchor === undefined ? 0 : anchor.top,
|
|
2238
|
+
/* base 是按下那一刻的顺序:每次换位都从它重算,不会累积误差 */
|
|
2239
|
+
base: cfg.segments.slice(),
|
|
2240
|
+
order: cfg.segments.slice()
|
|
2241
|
+
});
|
|
2242
|
+
};
|
|
2243
|
+
|
|
2244
|
+
/* 按给定指针位置重算落点。单独抽出来是因为滚轮滚动不产生 pointermove,
|
|
2245
|
+
滚动时得靠 scroll 事件带着"上一次的指针位置"再算一遍,影子才不会落在后面。 */
|
|
2246
|
+
const applyDrop = (current, pointerY) => {
|
|
2247
|
+
const cardTop = pointerY - current.grab;
|
|
2248
|
+
const height = current.at === null || current.at === undefined ? 0 : current.at.height;
|
|
2249
|
+
/* 锚点每次实时量:中途滚了滚轮,按下时的坐标早就失效了 */
|
|
2250
|
+
const anchor = measure("__context");
|
|
2251
|
+
const shift = anchor === null || anchor === undefined ? 0 : anchor.top - current.anchorTop;
|
|
2252
|
+
const to = dropIndexAt(current.slots, shift, current.from, cardTop, cardTop + height);
|
|
2253
|
+
/* 落点和指针都没动就别重渲染:滚动事件很密,大多数帧其实什么都没变 */
|
|
2254
|
+
if (to === current.to && cardTop === current.cardTop) return;
|
|
2255
|
+
if (to !== current.to) captureTops(); /* 顺序要变了,先记下当前位置 */
|
|
2256
|
+
setDrag({
|
|
2257
|
+
id: current.id, from: current.from, to: to, cardTop: cardTop, pointerY: pointerY,
|
|
2258
|
+
grab: current.grab, at: current.at, slots: current.slots, anchorTop: current.anchorTop,
|
|
2259
|
+
base: current.base,
|
|
2260
|
+
order: moveItem(current.base, current.from, to)
|
|
2261
|
+
});
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
const moveDrag = (event) => {
|
|
2265
|
+
if (drag === null) return;
|
|
2266
|
+
/* 指针已经松开(截屏工具之类把 pointerup 吃掉了):按取消处理,回原位 */
|
|
2267
|
+
if (event.buttons === 0) {
|
|
2268
|
+
cancelDrag();
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
applyDrop(drag, event.clientY);
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
/* 松手才落盘:拖动全程只在内存里重排,config 一动不动 */
|
|
2275
|
+
const endDrag = () => {
|
|
2276
|
+
if (draggingRef.current !== true || drag === null) return;
|
|
2277
|
+
draggingRef.current = false;
|
|
2278
|
+
if (drag.to !== drag.from) setConfig({ segments: drag.order });
|
|
2279
|
+
setDrag(null);
|
|
2280
|
+
};
|
|
2281
|
+
|
|
2282
|
+
/* 拖动被系统打断(截屏、切窗口、指针事件丢失):回原位,什么账都不记 */
|
|
2283
|
+
const cancelDrag = () => {
|
|
2284
|
+
if (draggingRef.current !== true) return;
|
|
2285
|
+
draggingRef.current = false;
|
|
2286
|
+
setDrag(null);
|
|
2287
|
+
};
|
|
2288
|
+
|
|
2289
|
+
/* 拖动中的重排没有过渡可言:DOM 顺序一变,行就是瞬移。
|
|
2290
|
+
这里把移动补回来 —— 变之前记下位置,变之后先反推回原处,再过渡回去。 */
|
|
2291
|
+
useTipLayout(() => {
|
|
2292
|
+
const before = rowTops.current;
|
|
2293
|
+
if (before === null) return;
|
|
2294
|
+
rowTops.current = null;
|
|
2295
|
+
rowNodes.current.forEach((node, id) => {
|
|
2296
|
+
if (node === null || node === undefined || node.style === undefined) return;
|
|
2297
|
+
const was = before.get(id);
|
|
2298
|
+
if (was === undefined) return;
|
|
2299
|
+
const delta = was - node.getBoundingClientRect().top;
|
|
2300
|
+
if (delta === 0) return;
|
|
2301
|
+
node.style.transition = "none";
|
|
2302
|
+
node.style.transform = "translateY(" + delta + "px)";
|
|
2303
|
+
void node.offsetHeight; /* 强制回流,否则紧接着的过渡不会触发 */
|
|
2304
|
+
node.style.transition = "transform .18s ease";
|
|
2305
|
+
node.style.transform = "";
|
|
2306
|
+
});
|
|
2307
|
+
}, [drag === null ? "" : drag.order.join(",")]);
|
|
2308
|
+
|
|
2309
|
+
/* 指针事件再挂一份到 window 兜底:被拖的那一行在拖动中会重排,指针捕获一旦丢了,
|
|
2310
|
+
pointerup 就永远收不到,浮层会一直粘着鼠标。挂在 window 上不受重排影响。
|
|
2311
|
+
顺带在拖动期间禁掉文本选中 —— 长按拖动会把整页文字刷成蓝底。 */
|
|
2312
|
+
const dragMoveRef = react.useRef(null);
|
|
2313
|
+
const dragEndRef = react.useRef(null);
|
|
2314
|
+
const dragCancelRef = react.useRef(null);
|
|
2315
|
+
const dragRef = react.useRef(null);
|
|
2316
|
+
const dragDropRef = react.useRef(null);
|
|
2317
|
+
dragMoveRef.current = moveDrag;
|
|
2318
|
+
dragEndRef.current = endDrag;
|
|
2319
|
+
dragCancelRef.current = cancelDrag;
|
|
2320
|
+
dragRef.current = drag;
|
|
2321
|
+
dragDropRef.current = applyDrop;
|
|
2322
|
+
react.useEffect(() => {
|
|
2323
|
+
if (drag === null) return undefined;
|
|
2324
|
+
const onMove = (event) => { if (dragMoveRef.current !== null) dragMoveRef.current(event); };
|
|
2325
|
+
const onUp = () => { if (dragEndRef.current !== null) dragEndRef.current(); };
|
|
2326
|
+
/* 打断 = 取消(回原位),绝不能当成落点:截屏工具常把 pointerup 整个吃掉 */
|
|
2327
|
+
const onCancel = () => { if (dragCancelRef.current !== null) dragCancelRef.current(); };
|
|
2328
|
+
window.addEventListener("pointermove", onMove);
|
|
2329
|
+
window.addEventListener("pointerup", onUp);
|
|
2330
|
+
window.addEventListener("pointercancel", onCancel);
|
|
2331
|
+
window.addEventListener("blur", onCancel);
|
|
2332
|
+
/* 滚轮滚动不产生 pointermove:不补这一手,影子会落在后面,要再动一下鼠标才跟上 */
|
|
2333
|
+
const onScroll = () => {
|
|
2334
|
+
const current = dragRef.current;
|
|
2335
|
+
if (current !== null && dragDropRef.current !== null) dragDropRef.current(current, current.pointerY);
|
|
2336
|
+
};
|
|
2337
|
+
window.addEventListener("scroll", onScroll, true);
|
|
2338
|
+
const body = document === undefined || document === null ? undefined : document.body;
|
|
2339
|
+
const style = body === undefined || body === null ? undefined : body.style;
|
|
2340
|
+
/* 禁选:长按拖动会把整页文字刷成蓝底;抓手:光标统一,不再忽抓忽指 */
|
|
2341
|
+
const hadSelect = style === undefined ? null : style.userSelect;
|
|
2342
|
+
const hadCursor = style === undefined ? null : style.cursor;
|
|
2343
|
+
if (hadSelect !== null) style.userSelect = "none";
|
|
2344
|
+
if (hadCursor !== null) style.cursor = "grabbing";
|
|
2345
|
+
return () => {
|
|
2346
|
+
window.removeEventListener("pointermove", onMove);
|
|
2347
|
+
window.removeEventListener("pointerup", onUp);
|
|
2348
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
2349
|
+
window.removeEventListener("blur", onCancel);
|
|
2350
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
2351
|
+
if (hadSelect !== null) style.userSelect = hadSelect;
|
|
2352
|
+
if (hadCursor !== null) style.cursor = hadCursor;
|
|
2353
|
+
};
|
|
2354
|
+
}, [drag === null]);
|
|
2355
|
+
|
|
2356
|
+
const contextRow = h("div", {
|
|
2357
|
+
className: "dsb-row",
|
|
2358
|
+
key: "__context",
|
|
2359
|
+
/* 落点靠它算槽位几何:它在栏首、不参与排序、也没有过渡动画,位置最稳 */
|
|
2360
|
+
ref: (node) => {
|
|
2361
|
+
if (node === null || node === undefined) rowNodes.current.delete("__context");
|
|
2362
|
+
else rowNodes.current.set("__context", node);
|
|
2363
|
+
}
|
|
2364
|
+
},
|
|
2031
2365
|
h("label", { className: "dsb-check" },
|
|
2032
2366
|
h("input", {
|
|
2033
2367
|
type: "checkbox",
|
|
@@ -2041,75 +2375,74 @@ window.__ModuleLoader__.load({
|
|
|
2041
2375
|
)
|
|
2042
2376
|
);
|
|
2043
2377
|
|
|
2044
|
-
|
|
2378
|
+
/* 行内容抽出来:列表里的行和跟手的浮层共用同一份 */
|
|
2379
|
+
const rowParts = (segment, on) => [
|
|
2380
|
+
h("label", { className: "dsb-check", key: "__check" },
|
|
2381
|
+
h("input", {
|
|
2382
|
+
type: "checkbox",
|
|
2383
|
+
checked: on,
|
|
2384
|
+
onChange: (event) => toggleSegment(segment.id, event.target.checked)
|
|
2385
|
+
}),
|
|
2386
|
+
h("span", { className: "dsb-labels" },
|
|
2387
|
+
h("span", { className: "dsb-name" }, t(segment.label)),
|
|
2388
|
+
h("span", { className: "dsb-desc" }, t(segment.hint))
|
|
2389
|
+
)
|
|
2390
|
+
),
|
|
2391
|
+
h("span", {
|
|
2392
|
+
className: on === true ? "dsb-handle" : "dsb-handle dsb-handle-off",
|
|
2393
|
+
title: on === true ? t("dragHint") : "",
|
|
2394
|
+
key: "__handle"
|
|
2395
|
+
})
|
|
2396
|
+
];
|
|
2397
|
+
|
|
2398
|
+
/* 拖动中就地按落点排好:列表里被拖那一格留淡影,跟手的是上面的浮层 */
|
|
2399
|
+
const visibleOrder = drag === null ? cfg.segments : drag.order;
|
|
2400
|
+
const visibleSegments = visibleOrder
|
|
2401
|
+
.map((id) => byId[id])
|
|
2402
|
+
.filter((segment) => segment !== undefined && segment !== null);
|
|
2403
|
+
|
|
2404
|
+
const segmentRows = visibleSegments.map((segment, index) => {
|
|
2045
2405
|
const on = isSegmentOn(cfg, segment.id);
|
|
2046
|
-
const
|
|
2047
|
-
if (dragId === segment.id) classes.push("dsb-dragging");
|
|
2048
|
-
if (overId === segment.id && dragId !== segment.id) {
|
|
2049
|
-
classes.push(overAfter === true ? "dsb-over-after" : "dsb-over-before");
|
|
2050
|
-
}
|
|
2406
|
+
const dragged = drag !== null && drag.id === segment.id;
|
|
2051
2407
|
return h("div", {
|
|
2052
|
-
className:
|
|
2408
|
+
className: dragged === true ? "dsb-row dsb-dragging" : "dsb-row",
|
|
2053
2409
|
key: segment.id,
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
try {
|
|
2058
|
-
event.dataTransfer.effectAllowed = "move";
|
|
2059
|
-
event.dataTransfer.setData("text/plain", segment.id);
|
|
2060
|
-
} catch (error) { /* 某些环境 dataTransfer 受限 */ }
|
|
2061
|
-
},
|
|
2062
|
-
onDragOver: (event) => {
|
|
2063
|
-
if (dragId === null || dragId === segment.id) return;
|
|
2064
|
-
event.preventDefault();
|
|
2065
|
-
try { event.dataTransfer.dropEffect = "move"; } catch (error) { /* ignore */ }
|
|
2066
|
-
/* 以目标行中线分前后;中线附近 4px 内保持原状态,避免来回闪 */
|
|
2067
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
2068
|
-
const middle = rect.top + rect.height / 2;
|
|
2069
|
-
if (Math.abs(event.clientY - middle) < 4) return;
|
|
2070
|
-
const after = event.clientY > middle;
|
|
2071
|
-
if (overId !== segment.id || overAfter !== after) {
|
|
2072
|
-
setOverId(segment.id);
|
|
2073
|
-
setOverAfter(after);
|
|
2074
|
-
}
|
|
2075
|
-
},
|
|
2076
|
-
onDragLeave: () => {
|
|
2077
|
-
if (overId === segment.id) {
|
|
2078
|
-
setOverId(null);
|
|
2079
|
-
setOverAfter(false);
|
|
2080
|
-
}
|
|
2410
|
+
ref: (node) => {
|
|
2411
|
+
if (node === null || node === undefined) rowNodes.current.delete(segment.id);
|
|
2412
|
+
else rowNodes.current.set(segment.id, node);
|
|
2081
2413
|
},
|
|
2082
|
-
|
|
2083
|
-
event.
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2414
|
+
onPointerDown: (event) => {
|
|
2415
|
+
if (event.button !== 0) return;
|
|
2416
|
+
/* 只有右侧那六个点能抓:整行可抓会跟勾选、选中文字打架 */
|
|
2417
|
+
const target = event.target;
|
|
2418
|
+
if (target === null || target === undefined) return;
|
|
2419
|
+
if (String(target.className === undefined ? "" : target.className).indexOf("dsb-handle") === -1) return;
|
|
2420
|
+
startDrag(segment.id, index, event);
|
|
2088
2421
|
},
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
type: "checkbox",
|
|
2094
|
-
checked: on,
|
|
2095
|
-
onChange: (event) => toggleSegment(segment.id, event.target.checked)
|
|
2096
|
-
}),
|
|
2097
|
-
h("span", { className: "dsb-labels" },
|
|
2098
|
-
h("span", { className: "dsb-name" }, t(segment.label)),
|
|
2099
|
-
h("span", { className: "dsb-desc" }, t(segment.hint))
|
|
2100
|
-
)
|
|
2101
|
-
),
|
|
2102
|
-
h("span", {
|
|
2103
|
-
className: on === true ? "dsb-handle" : "dsb-handle dsb-handle-off",
|
|
2104
|
-
title: on === true ? t("dragHint") : ""
|
|
2105
|
-
})
|
|
2106
|
-
);
|
|
2422
|
+
onPointerMove: dragged === true ? moveDrag : undefined,
|
|
2423
|
+
onPointerUp: dragged === true ? endDrag : undefined,
|
|
2424
|
+
onPointerCancel: dragged === true ? cancelDrag : undefined
|
|
2425
|
+
}, rowParts(segment, on));
|
|
2107
2426
|
});
|
|
2427
|
+
|
|
2428
|
+
/* 跟着鼠标的那张卡:位置锁在按下那一行的矩形上,纵向随 dy 走 */
|
|
2429
|
+
const dragCard = drag === null || drag.at === null || drag.at === undefined ? null : h("div", {
|
|
2430
|
+
className: "dsb-drag-card",
|
|
2431
|
+
key: "__dragcard",
|
|
2432
|
+
style: {
|
|
2433
|
+
left: drag.at.left + "px",
|
|
2434
|
+
top: drag.cardTop + "px",
|
|
2435
|
+
width: drag.at.width + "px",
|
|
2436
|
+
height: drag.at.height + "px"
|
|
2437
|
+
}
|
|
2438
|
+
}, rowParts(byId[drag.id], isSegmentOn(cfg, drag.id)));
|
|
2439
|
+
|
|
2108
2440
|
sections.push(h("div", { className: "dsb-section", key: "__segments" },
|
|
2109
2441
|
h("div", { className: "dsb-sectitle" }, t("secSegments")),
|
|
2110
2442
|
h("p", { className: "dsb-hint" }, t("secSegmentsHint")),
|
|
2111
2443
|
contextRow,
|
|
2112
|
-
segmentRows
|
|
2444
|
+
segmentRows,
|
|
2445
|
+
dragCard
|
|
2113
2446
|
));
|
|
2114
2447
|
|
|
2115
2448
|
/* 自定义模型价格 */
|
|
@@ -2280,7 +2613,9 @@ window.__ModuleLoader__.load({
|
|
|
2280
2613
|
modelOrder: Object.keys(DEFAULT_PRICES)
|
|
2281
2614
|
})
|
|
2282
2615
|
}, t("reset")),
|
|
2283
|
-
h("div", { className: "dsb-actions", key: "__actions" }, holidayActions)
|
|
2616
|
+
h("div", { className: "dsb-actions", key: "__actions" }, holidayActions),
|
|
2617
|
+
/* 版本标记贴右、与行内底边对齐:改了客户端代码要刷新页面才生效,这行字让"跑的是哪版"一眼可见 */
|
|
2618
|
+
h("span", { className: "dsb-version", key: "__version" }, "dsh-desktop-statusbar " + VERSION)
|
|
2284
2619
|
]));
|
|
2285
2620
|
|
|
2286
2621
|
return h("div", { className: "dsb-settings" }, sections);
|