creditdaddy 0.9.2 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -204
- package/bin/creditdaddy.js +7 -3
- package/package.json +28 -28
- package/src/checkin.js +2 -1
- package/src/constants.js +4 -2
- package/src/daemon.js +76 -13
- package/src/localDetect.js +2 -2
- package/src/logger.js +4 -1
- package/src/mirasimClient.js +189 -0
- package/src/mirasimLocal.js +307 -0
- package/src/panel.html +589 -70
- package/src/providers.js +25 -1
- package/src/qoderApp.js +6 -4
- package/src/qoderUmid.js +25 -4
- package/src/store.js +10 -0
- package/src/zcodeClient.js +50 -36
- package/src/zcodeLocal.js +103 -1
package/src/panel.html
CHANGED
|
@@ -43,6 +43,29 @@
|
|
|
43
43
|
.title { font-family: "Avenir Next", Avenir, "Segoe UI Variable Display", "Segoe UI", sans-serif; font-size: 17px; font-weight: 750; letter-spacing: -.5px; line-height: 1.15; }
|
|
44
44
|
.ver { color: var(--text-3); font-size: 11px; line-height: 1.3; font-variant-numeric: tabular-nums; }
|
|
45
45
|
.head-right { display: flex; align-items: center; gap: 10px; color: var(--text-3); font-size: 12px; }
|
|
46
|
+
/* 组件开关:右上角弹出菜单 */
|
|
47
|
+
.comp-wrap { position: relative; display: inline-flex; }
|
|
48
|
+
.comp-menu { position: absolute; right: 0; top: calc(100% + 8px); z-index: 80; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--toast-shadow); padding: 10px 12px; min-width: 132px; display: flex; flex-direction: column; gap: 8px; }
|
|
49
|
+
.comp-menu[hidden] { display: none; }
|
|
50
|
+
.comp-menu-title { font-size: 11px; color: var(--text-3); }
|
|
51
|
+
.comp-menu .check { margin-right: 0; font-size: 12.5px; color: var(--text); }
|
|
52
|
+
#comp-menu { max-height: 72vh; overflow: auto; min-width: 236px; }
|
|
53
|
+
.comp-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
|
54
|
+
.comp-row.off .check { opacity: .45; }
|
|
55
|
+
.comp-ops { display: inline-flex; gap: 2px; flex: none; }
|
|
56
|
+
.icon-btn.mini { width: 20px; height: 20px; border-radius: 6px; font-size: 11px; line-height: 1; padding: 0; }
|
|
57
|
+
.icon-btn.mini svg { width: 12px; height: 12px; }
|
|
58
|
+
.sort-sec { border-top: 1px dashed var(--border-subtle); padding-top: 6px; font-size: 12px; }
|
|
59
|
+
.sort-sec summary { cursor: pointer; color: var(--text-2); user-select: none; display: flex; align-items: center; gap: 6px; }
|
|
60
|
+
.sort-sec summary::-webkit-details-marker { display: none; }
|
|
61
|
+
.sort-hint { font-size: 11px; color: var(--text-3); margin: 5px 0 3px; }
|
|
62
|
+
.sort-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 2px 0; }
|
|
63
|
+
.sort-row .sort-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
64
|
+
.card.dragging, .tr-card.dragging { opacity: .45; }
|
|
65
|
+
.card.drop-before { box-shadow: inset 0 3px 0 0 rgb(var(--accent-rgb)); }
|
|
66
|
+
.card.drop-after { box-shadow: inset 0 -3px 0 0 rgb(var(--accent-rgb)); }
|
|
67
|
+
.tr-card.drop-before { box-shadow: inset 0 3px 0 0 rgb(var(--accent-rgb)); }
|
|
68
|
+
.tr-card.drop-after { box-shadow: inset 0 -3px 0 0 rgb(var(--accent-rgb)); }
|
|
46
69
|
|
|
47
70
|
/* shell panel */
|
|
48
71
|
.panel { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 18px; box-shadow: 0 1px 2px rgba(0,0,0,.03); overflow: hidden; }
|
|
@@ -193,8 +216,10 @@
|
|
|
193
216
|
.prod-name { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
|
|
194
217
|
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
195
218
|
.dot.qoder { background: #7c3aed; } .dot.workbuddy { background: #14b8a6; } .dot.zcode { background: #2563eb; }
|
|
196
|
-
.prod-go { color: var(--text-3); font-size: 12px; white-space: nowrap; }
|
|
219
|
+
.prod-go { color: var(--text-3); font-size: 12px; white-space: nowrap; display: inline-flex; align-items: center; gap: 2px; }
|
|
220
|
+
.prod-go svg { width: 12px; height: 12px; transition: transform .15s; }
|
|
197
221
|
.prodcard:hover .prod-go { color: var(--text); }
|
|
222
|
+
.prodcard:hover .prod-go svg { transform: translateX(2px); }
|
|
198
223
|
.prod-rows { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 7px 12px; font-size: 12px; align-items: center; }
|
|
199
224
|
.prod-rows .val { text-align: right; }
|
|
200
225
|
.meter { height: 5px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); border-radius: 3px; overflow: hidden; }
|
|
@@ -209,6 +234,8 @@
|
|
|
209
234
|
.item-side { display: flex; align-items: center; gap: 8px; flex-shrink: 0; min-width: 0; }
|
|
210
235
|
.item-time { color: var(--text-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
211
236
|
.muted-empty { color: var(--text-3); font-size: 12px; padding: 16px 0 6px; text-align: center; }
|
|
237
|
+
.muted-empty .ok-glyph { display: inline-flex; vertical-align: -2px; margin-right: 4px; color: var(--ok); }
|
|
238
|
+
.muted-empty .ok-glyph svg { width: 13px; height: 13px; }
|
|
212
239
|
@media (max-width: 900px) { .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } .prods, .dash-cols { grid-template-columns: 1fr; } }
|
|
213
240
|
|
|
214
241
|
/* 10Router */
|
|
@@ -251,6 +278,9 @@
|
|
|
251
278
|
.tr-p-val.low b { color: var(--err); }
|
|
252
279
|
.tr-meter.big { height: 7px; }
|
|
253
280
|
.tr-line { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px; margin-top: 6px; }
|
|
281
|
+
.card .tr-line { margin-top: 6px; font-size: 11.5px; }
|
|
282
|
+
.card .tr-meter { margin-top: 2px; }
|
|
283
|
+
.card .mirasim-windows { margin-top: 4px; padding-top: 4px; border-top: 1px dashed var(--border-subtle); }
|
|
254
284
|
.tr-summary { display: flex; align-items: baseline; gap: 6px; font-size: 12px; margin-top: 6px; }
|
|
255
285
|
.tr-sum-txt { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
256
286
|
.low-text { color: var(--err); font-weight: 600; }
|
|
@@ -287,6 +317,10 @@
|
|
|
287
317
|
</div>
|
|
288
318
|
<div class="head-right">
|
|
289
319
|
<span id="sched" class="num"></span>
|
|
320
|
+
<div class="comp-wrap">
|
|
321
|
+
<button class="icon-btn" id="btn-comp" title="面板设置:组件显隐与排序、账号卡片排序"></button>
|
|
322
|
+
<div class="comp-menu" id="comp-menu" hidden></div>
|
|
323
|
+
</div>
|
|
290
324
|
<a class="icon-btn" id="btn-home" href="https://github.com/techysy/CreditDaddy" target="_blank" rel="noopener" title="项目主页(GitHub)"></a>
|
|
291
325
|
<button class="icon-btn" id="btn-theme" title="切换亮色 / 暗色"></button>
|
|
292
326
|
</div>
|
|
@@ -297,6 +331,7 @@
|
|
|
297
331
|
<button data-view="qoder">Qoder<span class="cnt" id="cnt-qoder">0</span></button>
|
|
298
332
|
<button data-view="workbuddy">WorkBuddy<span class="cnt" id="cnt-workbuddy">0</span></button>
|
|
299
333
|
<button data-view="zcode">ZCode<span class="cnt" id="cnt-zcode">0</span></button>
|
|
334
|
+
<button data-view="mirasim">mirasim<span class="cnt" id="cnt-mirasim">0</span></button>
|
|
300
335
|
<button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
|
|
301
336
|
</nav>
|
|
302
337
|
|
|
@@ -498,31 +533,160 @@ const I = {
|
|
|
498
533
|
github: svg('<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65S8.93 17.38 9 18v4"/><path d="M9 18c-4.51 2-5-2-7-2"/>'),
|
|
499
534
|
credit: svg('<path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/><path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8z"/>'),
|
|
500
535
|
gift: svg('<rect x="3" y="8" width="18" height="4"/><path d="M12 8v13"/><path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7"/><path d="M7.5 8a2.5 2.5 0 0 1 0-5C11 3 12 8 12 8s1-5 4.5-5a2.5 2.5 0 0 1 0 5"/>'),
|
|
536
|
+
tune: svg('<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/>'),
|
|
537
|
+
chevUp: svg('<path d="M18 15l-6-6-6 6"/>'),
|
|
538
|
+
chevDown: svg('<path d="M6 9l6 6 6-6"/>'),
|
|
539
|
+
chevRight: svg('<path d="M9 18l6-6-6-6"/>'),
|
|
540
|
+
check: svg('<path d="M20 6L9 17l-5-5"/>'),
|
|
501
541
|
};
|
|
502
542
|
|
|
503
543
|
const $ = (id) => document.getElementById(id);
|
|
504
544
|
const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '' };
|
|
505
|
-
const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'tenrouter'];
|
|
545
|
+
const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'tenrouter'];
|
|
506
546
|
// 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
|
|
507
547
|
const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
|
|
508
548
|
const PRODUCTS = [
|
|
509
549
|
{ id: 'qoder', label: 'Qoder' },
|
|
510
550
|
{ id: 'workbuddy', label: 'WorkBuddy' },
|
|
511
551
|
{ id: 'zcode', label: 'ZCode' },
|
|
552
|
+
{ id: 'mirasim', label: 'mirasim' },
|
|
512
553
|
];
|
|
513
554
|
const prodLabel = (id) => (PRODUCTS.find((x) => x.id === id) || { label: id }).label;
|
|
555
|
+
|
|
556
|
+
// ── 全局组件开关:不用某个产品 / 10Router 时整块收起(偏好存浏览器本地) ──
|
|
557
|
+
const COMPONENTS = [
|
|
558
|
+
{ id: 'qoder', label: 'Qoder' },
|
|
559
|
+
{ id: 'workbuddy', label: 'WorkBuddy' },
|
|
560
|
+
{ id: 'zcode', label: 'ZCode' },
|
|
561
|
+
{ id: 'mirasim', label: 'mirasim' },
|
|
562
|
+
{ id: 'tenrouter', label: '10Router' },
|
|
563
|
+
];
|
|
564
|
+
const HIDDEN_COMP_KEY = 'qd_hidden_components';
|
|
565
|
+
state.hidden = (() => {
|
|
566
|
+
const set = new Set();
|
|
567
|
+
try {
|
|
568
|
+
for (const v of JSON.parse(localStorage.getItem(HIDDEN_COMP_KEY) || '[]')) {
|
|
569
|
+
if (COMPONENTS.some((c) => c.id === v)) set.add(v);
|
|
570
|
+
}
|
|
571
|
+
} catch {}
|
|
572
|
+
return set;
|
|
573
|
+
})();
|
|
574
|
+
const compVisible = (id) => !state.hidden.has(id);
|
|
575
|
+
function saveHiddenComps() {
|
|
576
|
+
try { localStorage.setItem(HIDDEN_COMP_KEY, JSON.stringify([...state.hidden])); } catch {}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── 排序体系 ──
|
|
580
|
+
// 组件顺序:仪表盘产品卡与顶部标签栏按此顺序同步渲染(↑↓ 在右上角设置里调整)
|
|
581
|
+
const COMP_ORDER_KEY = 'qd_comp_order';
|
|
582
|
+
state.compOrder = (() => {
|
|
583
|
+
let arr = [];
|
|
584
|
+
try { arr = JSON.parse(localStorage.getItem(COMP_ORDER_KEY) || '[]'); } catch {}
|
|
585
|
+
const ids = COMPONENTS.map((c) => c.id);
|
|
586
|
+
arr = arr.filter((id) => ids.includes(id));
|
|
587
|
+
for (const id of ids) if (!arr.includes(id)) arr.push(id);
|
|
588
|
+
return arr;
|
|
589
|
+
})();
|
|
590
|
+
function saveCompOrder() {
|
|
591
|
+
try { localStorage.setItem(COMP_ORDER_KEY, JSON.stringify(state.compOrder)); } catch {}
|
|
592
|
+
}
|
|
593
|
+
const orderedVisibleComps = () => state.compOrder.filter(compVisible);
|
|
594
|
+
|
|
595
|
+
// 产品面板账号卡:默认「剩余积分多者靠前」;手动拖拽 / 设置里上下调整的顺序优先级最高(每产品独立持久化)
|
|
596
|
+
const CARD_ORDER_KEY = 'qd_card_order';
|
|
597
|
+
state.cardOrder = (() => {
|
|
598
|
+
try { const o = JSON.parse(localStorage.getItem(CARD_ORDER_KEY) || '{}'); return o && typeof o === 'object' && !Array.isArray(o) ? o : {}; } catch { return {}; }
|
|
599
|
+
})();
|
|
600
|
+
function saveCardOrder() {
|
|
601
|
+
try { localStorage.setItem(CARD_ORDER_KEY, JSON.stringify(state.cardOrder)); } catch {}
|
|
602
|
+
}
|
|
603
|
+
// 参与排序的剩余量:无额度数据 / 查询失败的账号排最后(稳定排序保持原有次序)
|
|
604
|
+
function quotaRemainForSort(a) {
|
|
605
|
+
const q = state.quotas[a.id];
|
|
606
|
+
if (!q || q.error) return -1;
|
|
607
|
+
const r = Number(q.remaining);
|
|
608
|
+
return Number.isFinite(r) ? r : -1;
|
|
609
|
+
}
|
|
610
|
+
function defaultCardCmp(a, b) {
|
|
611
|
+
return quotaRemainForSort(b) - quotaRemainForSort(a);
|
|
612
|
+
}
|
|
613
|
+
function sortedAccounts(prod, list) {
|
|
614
|
+
const manual = state.cardOrder[prod];
|
|
615
|
+
const arr = list.slice();
|
|
616
|
+
if (manual && manual.length) {
|
|
617
|
+
const idx = new Map(manual.map((id, i) => [id, i]));
|
|
618
|
+
arr.sort((x, y) => {
|
|
619
|
+
const ix = idx.has(x.id) ? idx.get(x.id) : Infinity;
|
|
620
|
+
const iy = idx.has(y.id) ? idx.get(y.id) : Infinity;
|
|
621
|
+
return ix !== iy ? ix - iy : defaultCardCmp(x, y);
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
arr.sort(defaultCardCmp);
|
|
625
|
+
}
|
|
626
|
+
return arr;
|
|
627
|
+
}
|
|
628
|
+
// 10Router 连接卡:默认启用在前、剩余额度 % 多者在前;手动拖拽(每供应商独立)优先级最高
|
|
629
|
+
const TR_ORDER_KEY = 'qd_tr_order';
|
|
630
|
+
state.trOrder = (() => {
|
|
631
|
+
try { const o = JSON.parse(localStorage.getItem(TR_ORDER_KEY) || '{}'); return o && typeof o === 'object' && !Array.isArray(o) ? o : {}; } catch { return {}; }
|
|
632
|
+
})();
|
|
633
|
+
function saveTrOrder() {
|
|
634
|
+
try { localStorage.setItem(TR_ORDER_KEY, JSON.stringify(state.trOrder)); } catch {}
|
|
635
|
+
}
|
|
636
|
+
function connPctForSort(c) {
|
|
637
|
+
if (c.error) return -1;
|
|
638
|
+
const s = splitQuotas(c);
|
|
639
|
+
const cands = [s.primary, ...s.windows].filter(Boolean).map((q) => quotaPct(q)).filter((v) => v !== null);
|
|
640
|
+
return cands.length ? Math.max(...cands) : -1;
|
|
641
|
+
}
|
|
642
|
+
function trBaseCmp(a, b) {
|
|
643
|
+
return ((b.isActive ? 1 : 0) - (a.isActive ? 1 : 0)) || (connPctForSort(b) - connPctForSort(a));
|
|
644
|
+
}
|
|
645
|
+
function sortedByManual(items, manual, baseCmp) {
|
|
646
|
+
const arr = items.slice();
|
|
647
|
+
if (manual && manual.length) {
|
|
648
|
+
const idx = new Map(manual.map((id, i) => [id, i]));
|
|
649
|
+
arr.sort((x, y) => {
|
|
650
|
+
const ix = idx.has(x.id) ? idx.get(x.id) : Infinity;
|
|
651
|
+
const iy = idx.has(y.id) ? idx.get(y.id) : Infinity;
|
|
652
|
+
return ix !== iy ? ix - iy : baseCmp(x, y);
|
|
653
|
+
});
|
|
654
|
+
} else {
|
|
655
|
+
arr.sort(baseCmp);
|
|
656
|
+
}
|
|
657
|
+
return arr;
|
|
658
|
+
}
|
|
659
|
+
const sortedConnections = (vendor, items) => sortedByManual(items, state.trOrder[vendor], trBaseCmp);
|
|
660
|
+
// 「全部」视角的供应商汇总卡顺序:同名供应商聚合后按主额度 % 排序,手动拖拽(__vendors)优先
|
|
661
|
+
function sortedVendorGroups(groups) {
|
|
662
|
+
const keyOf = (g) => g.name;
|
|
663
|
+
const withId = groups.map((g) => ({ ...g, id: keyOf(g) }));
|
|
664
|
+
const base = (a, b) => connPctForSort(b.items[0]) - connPctForSort(a.items[0]) || String(a.name).localeCompare(String(b.name));
|
|
665
|
+
return sortedByManual(withId, state.trOrder.__vendors, base);
|
|
666
|
+
}
|
|
514
667
|
const PROV = {
|
|
515
668
|
qoder: { prod: 'qoder', prodLabel: 'Qoder', region: '国际版', cls: 'intl' },
|
|
516
669
|
'qoder-cn': { prod: 'qoder', prodLabel: 'Qoder', region: '国内版', cls: 'cn' },
|
|
517
670
|
workbuddy: { prod: 'workbuddy', prodLabel: 'WorkBuddy', region: '国内版', cls: 'cn' },
|
|
518
671
|
'workbuddy-intl': { prod: 'workbuddy', prodLabel: 'WorkBuddy', region: '国际版', cls: 'intl' },
|
|
519
672
|
zcode: { prod: 'zcode', prodLabel: 'ZCode', region: '', cls: '' },
|
|
673
|
+
mirasim: { prod: 'mirasim', prodLabel: 'mirasim', region: '', cls: '' },
|
|
520
674
|
};
|
|
521
|
-
// 支持每日签到的产品(ZCode
|
|
675
|
+
// 支持每日签到的产品(ZCode / mirasim 额度靠活动或滚动窗口,不做签到)
|
|
522
676
|
const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy']);
|
|
523
|
-
const CLIENT_LABEL = { workbuddy: 'WorkBuddy', zcode: 'ZCode' };
|
|
677
|
+
const CLIENT_LABEL = { workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim' };
|
|
524
678
|
const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
|
|
525
|
-
const provBadges = (p, regionOnly) => {
|
|
679
|
+
const provBadges = (p, regionOnly, flavor) => {
|
|
680
|
+
const v = provOf(p);
|
|
681
|
+
const zBadge = p === 'zcode' && flavor
|
|
682
|
+
? (flavor === 'both' ? '<span class="badge intl">Z.ai</span><span class="badge cn">BigModel</span>'
|
|
683
|
+
: flavor === 'zai' ? '<span class="badge intl">Z.ai</span>'
|
|
684
|
+
: '<span class="badge cn">BigModel</span>')
|
|
685
|
+
: '';
|
|
686
|
+
return (regionOnly ? '' : '<span class="badge prod ' + v.prod + '">' + esc(v.prodLabel) + '</span>')
|
|
687
|
+
+ (v.region ? '<span class="badge ' + v.cls + '">' + v.region + '</span>' : '')
|
|
688
|
+
+ zBadge;
|
|
689
|
+
};
|
|
526
690
|
|
|
527
691
|
function esc(s) {
|
|
528
692
|
return String(s === null || s === undefined ? '' : s)
|
|
@@ -545,6 +709,7 @@ function mask(s) {
|
|
|
545
709
|
}
|
|
546
710
|
|
|
547
711
|
let toastTimer = null;
|
|
712
|
+
const armed = {};
|
|
548
713
|
function toast(msg, ok) {
|
|
549
714
|
const t = $('toast');
|
|
550
715
|
t.textContent = msg;
|
|
@@ -641,10 +806,62 @@ function statusOf(a) {
|
|
|
641
806
|
}
|
|
642
807
|
|
|
643
808
|
function expiryCell(a) {
|
|
809
|
+
const q = state.quotas[a.id];
|
|
810
|
+
// 优先展示真实的套餐/订阅到期时间(而不是几个小时就会自动刷新的 JWT 临时 token 时间)
|
|
811
|
+
if (q && q.planExpiresAt) {
|
|
812
|
+
const msPlan = new Date(q.planExpiresAt).getTime() - Date.now();
|
|
813
|
+
const planName = q.plan ? esc(q.plan) + ' ' : '';
|
|
814
|
+
if (msPlan <= 0) return { label: planName + '已到期', warn: true, title: '套餐已到期' };
|
|
815
|
+
return {
|
|
816
|
+
label: planName + fmtTime(q.planExpiresAt).slice(0, 10),
|
|
817
|
+
warn: msPlan < 3 * 86400e3,
|
|
818
|
+
title: '套餐有效期至 ' + fmtTime(q.planExpiresAt),
|
|
819
|
+
isPlan: true,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 具有 refreshToken 的账号会自动在后台续期,短期 exp 属于正常轮换,不应显示刺眼红字误导用户
|
|
824
|
+
if (a.hasRefreshToken) {
|
|
825
|
+
return {
|
|
826
|
+
label: '凭据有效 · 自动续期',
|
|
827
|
+
warn: false,
|
|
828
|
+
title: a.expiresAt ? '本次登录凭据有效期至 ' + fmtTime(a.expiresAt) + '(过期将自动通过 refreshToken 刷新续期,无需手动干预)' : '支持自动刷新续期',
|
|
829
|
+
isAuto: true,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
644
833
|
if (!a.expiresAt) return { label: a.isPat ? 'PAT 长期有效' : '—', warn: false };
|
|
645
834
|
const ms = new Date(a.expiresAt).getTime() - Date.now();
|
|
646
|
-
if (ms <= 0) return { label: '已过期', warn: true };
|
|
647
|
-
return { label: fmtTime(a.expiresAt), warn: ms <
|
|
835
|
+
if (ms <= 0) return { label: '已过期', warn: true, title: '登录凭据已过期' };
|
|
836
|
+
return { label: fmtTime(a.expiresAt), warn: ms < 48 * 3600e3, title: '有效期至 ' + fmtTime(a.expiresAt) };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// 通用相对重置标签:「3 天 后重置」「45 分钟 后到期」;一小时内标红,悬停看绝对时间
|
|
840
|
+
function relReset(q) {
|
|
841
|
+
const at = q.resetAt || q.expiresAt;
|
|
842
|
+
if (!at) return null;
|
|
843
|
+
const ms = new Date(at).getTime() - Date.now();
|
|
844
|
+
if (!Number.isFinite(ms)) return null;
|
|
845
|
+
if (ms <= 0) return { text: '即将' + (q.recurring === false ? '到期' : '重置'), soon: true };
|
|
846
|
+
const m = Math.ceil(ms / 60000);
|
|
847
|
+
const d = Math.floor(m / 1440), h = Math.floor((m % 1440) / 60), mm = m % 60;
|
|
848
|
+
const dur = d > 0 ? d + ' 天' + (h ? h + ' 小时' : '') : h > 0 ? h + ' 小时' + (mm ? mm + ' 分' : '') : Math.max(1, mm) + ' 分钟';
|
|
849
|
+
return { text: dur + ' 后' + (q.recurring === false ? '到期' : '重置'), soon: ms < 3600e3 };
|
|
850
|
+
}
|
|
851
|
+
function relResetHtml(q) {
|
|
852
|
+
const r = relReset(q);
|
|
853
|
+
const at = q.resetAt || q.expiresAt;
|
|
854
|
+
return r ? '<span class="tr-reset' + (r.soon ? ' soon' : '') + '" title="' + esc(fmtTime(at)) + '">' + esc(r.text) + '</span>' : '';
|
|
855
|
+
}
|
|
856
|
+
// 三档着色水位条(≥70 绿 / ≥30 琥珀 / <30 红)
|
|
857
|
+
const meterCls = (pct) => (pct >= 70 ? 'hi' : pct >= 30 ? 'mid' : 'low');
|
|
858
|
+
function trMeter(pct) {
|
|
859
|
+
if (pct === null || pct === undefined) return '';
|
|
860
|
+
const w = Math.min(100, Math.max(0, pct)).toFixed(1);
|
|
861
|
+
return '<div class="tr-meter"><span class="' + meterCls(pct) + '" style="width:' + w + '%"></span></div>';
|
|
862
|
+
}
|
|
863
|
+
function trLine(name, valHtml, title) {
|
|
864
|
+
return '<div class="tr-line"><span class="tr-q-name"' + (title ? ' title="' + esc(title) + '"' : '') + '>' + esc(name) + '</span><span class="tr-q-val">' + valHtml + '</span></div>';
|
|
648
865
|
}
|
|
649
866
|
|
|
650
867
|
function creditHtml(a) {
|
|
@@ -652,6 +869,35 @@ function creditHtml(a) {
|
|
|
652
869
|
if (!q) return '<div class="credit-line"><span class="dim">读取积分中…</span></div><div class="bar"></div>';
|
|
653
870
|
if (q.error) return '<div class="credit-line"><span class="dim" title="' + esc(q.error) + '">积分读取失败:' + esc(q.error.slice(0, 40)) + '</span></div><div class="bar"></div>';
|
|
654
871
|
if (q.empty) return '<div class="credit-line"><span class="dim">无有效套餐(仅免费额度)</span></div><div class="bar"></div>';
|
|
872
|
+
|
|
873
|
+
// 多窗口配额体系(mirasim 等):每个窗口平等等权,独立一行带分散对齐的名称、数值与三档细水位条
|
|
874
|
+
if (a.product === 'mirasim' && q.parts && q.parts.length) {
|
|
875
|
+
const isExhausted = q.exceeded || (q.remaining <= 0);
|
|
876
|
+
const planBadge = q.plan ? '<span class="badge">' + esc(q.plan) + ' 套餐</span>' : '';
|
|
877
|
+
const expLabel = q.planExpiresAt ? '<span class="dim">' + fmtTime(q.planExpiresAt).slice(0, 10) + ' 到期</span>' : '';
|
|
878
|
+
// 状态采用温和的徽章,避免整行大红字制造恐慌
|
|
879
|
+
const topStatus = isExhausted
|
|
880
|
+
? '<span class="pill pending">' + esc(q.bottleneckLabel || '额度已用尽') + '</span>'
|
|
881
|
+
: '<span class="lbl">可用额度</span><b>' + fmtNum(q.remaining) + '%</b>';
|
|
882
|
+
|
|
883
|
+
const fmtUnits = (n) => (n >= 10000 ? (n / 10000).toFixed(2) + '万' : fmtNum(Math.round(n)));
|
|
884
|
+
const rows = q.parts.map((p) => {
|
|
885
|
+
const pct = p.remainingPercentage ?? (p.total > 0 ? Math.max(0, Math.min(100, (p.remaining / p.total) * 100)) : 0);
|
|
886
|
+
const isZero = pct <= 0;
|
|
887
|
+
const numTxt = p.total > 0 ? fmtUnits(p.remaining) + ' / ' + fmtUnits(p.total) : '';
|
|
888
|
+
// 数值保持中性温和排版,不用鲜红大字恐吓用户
|
|
889
|
+
const pctTxt = isZero
|
|
890
|
+
? '<span class="dim">剩 0%</span>'
|
|
891
|
+
: '<span>剩 ' + fmtNum(pct) + '%</span>';
|
|
892
|
+
const valHtml = pctTxt + (numTxt ? ' <small class="dim">(' + numTxt + ')</small>' : '') + ' ' + relResetHtml(p);
|
|
893
|
+
return trLine(p.name, valHtml) + trMeter(pct);
|
|
894
|
+
}).join('');
|
|
895
|
+
|
|
896
|
+
return '<div class="credit-line"><span class="credit">' + topStatus + '</span>'
|
|
897
|
+
+ '<span class="credit dim">' + planBadge + (planBadge && expLabel ? ' · ' : '') + expLabel + '</span></div>'
|
|
898
|
+
+ '<div class="mirasim-windows">' + rows + '</div>';
|
|
899
|
+
}
|
|
900
|
+
|
|
655
901
|
const unit = q.unit ? ' ' + q.unit : '';
|
|
656
902
|
if (q.unit) {
|
|
657
903
|
const items = q.parts.map((x) => {
|
|
@@ -660,9 +906,12 @@ function creditHtml(a) {
|
|
|
660
906
|
return base + (x.expiresAt ? '(' + fmtTime(x.expiresAt).slice(5, 16) + (x.recurring ? ' 重置)' : ' 到期)') : '');
|
|
661
907
|
}).join(' · ');
|
|
662
908
|
const pct = q.total > 0 ? Math.min(1, q.remaining / q.total) : 0;
|
|
909
|
+
// 满额(100%)不再渲染灰色「已使用」尾段——它只占 0 宽也会造成几像素的灰边
|
|
663
910
|
return '<div class="credit-line"><span class="credit"><span class="lbl">剩余</span><b>' + fmtNum(q.remaining) + '</b>' + esc(unit) + '</span>'
|
|
664
911
|
+ '<span class="credit dim" title="' + items + '">' + (q.plan ? esc(q.plan) + ' · ' : '') + items + '</span></div>'
|
|
665
|
-
+ '<div class="bar"><span class="seg" style="flex:' + pct.toFixed(4) + ' 1 0"></span
|
|
912
|
+
+ '<div class="bar"><span class="seg" style="flex:' + pct.toFixed(4) + ' 1 0"></span>'
|
|
913
|
+
+ (pct < 0.9999 ? '<span class="seg used" style="flex:' + (1 - pct).toFixed(4) + ' 1 0"></span>' : '')
|
|
914
|
+
+ '</div>';
|
|
666
915
|
}
|
|
667
916
|
const total = q.total || 0;
|
|
668
917
|
let bar = '';
|
|
@@ -686,10 +935,10 @@ function cardHtml(a) {
|
|
|
686
935
|
const ident = a.email || a.phone || (a.uid ? 'UID ' + a.uid.slice(0, 8) : a.tokenMasked);
|
|
687
936
|
const isCurrent = isClientCurrent(a);
|
|
688
937
|
const canCheckin = CHECKIN_PRODUCTS.has(a.product);
|
|
689
|
-
return '<div class="card" data-id="' + esc(a.id) + '">'
|
|
938
|
+
return '<div class="card" draggable="true" data-id="' + esc(a.id) + '">'
|
|
690
939
|
+ '<div class="row1"><div class="name-group">'
|
|
691
940
|
+ '<span class="name" data-act="rename" title="点击修改备注名">' + esc(a.name || '(未命名)') + '</span>'
|
|
692
|
-
+ provBadges(a.provider, true)
|
|
941
|
+
+ provBadges(a.provider, true, a.flavor)
|
|
693
942
|
+ (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
|
|
694
943
|
+ (a.isPat ? '<span class="badge">PAT</span>' : '')
|
|
695
944
|
+ (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
|
|
@@ -703,7 +952,8 @@ function cardHtml(a) {
|
|
|
703
952
|
+ '</div></div>'
|
|
704
953
|
+ '<div class="meta">'
|
|
705
954
|
+ '<div class="mi"><span class="lbl">账号</span><span class="val" title="' + esc(a.email || a.phone || a.uid || '') + '">' + esc(a.phone && !a.email ? ident : mask(ident)) + '</span></div>'
|
|
706
|
-
+ '<div class="mi right"><span class="lbl"
|
|
955
|
+
+ '<div class="mi right"><span class="lbl">' + (ex.isPlan ? '套餐到期' : ex.isAuto ? '凭据状态' : '有效期至') + '</span>'
|
|
956
|
+
+ '<span class="val' + (ex.warn ? ' warn' : '') + '" title="' + esc(ex.title || '') + '">' + esc(ex.label) + '</span></div>'
|
|
707
957
|
+ '</div>'
|
|
708
958
|
+ creditHtml(a)
|
|
709
959
|
+ (st.msg && !(state.quotas[a.id] && state.quotas[a.id].error && state.quotas[a.id].error.includes(st.msg.slice(-24)))
|
|
@@ -714,11 +964,12 @@ function cardHtml(a) {
|
|
|
714
964
|
const isDone = (a) => Boolean(state.status && state.status.todayDone && state.status.todayDone[a.id] === state.status.today);
|
|
715
965
|
const ofProduct = (prod) => state.accounts.filter((a) => a.product === prod);
|
|
716
966
|
const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : null);
|
|
717
|
-
// 客户端当前登录判定:WorkBuddy 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
|
|
967
|
+
// 客户端当前登录判定:WorkBuddy / mirasim 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
|
|
718
968
|
function isClientCurrent(a) {
|
|
719
969
|
const s = state.status;
|
|
720
970
|
if (!s) return false;
|
|
721
971
|
if (a.product === 'workbuddy') return Boolean(a.uid && s.workbuddyCurrentUid && s.workbuddyCurrentUid === a.uid);
|
|
972
|
+
if (a.product === 'mirasim') return Boolean(a.uid && s.mirasimCurrentUid && s.mirasimCurrentUid === a.uid);
|
|
722
973
|
if (a.product === 'zcode') {
|
|
723
974
|
const id = s.zcodeCurrentIdentity;
|
|
724
975
|
return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
|
|
@@ -750,6 +1001,7 @@ const EMPTY_TIP = {
|
|
|
750
1001
|
qoder: '还没有 Qoder 账号。<br>点「添加账号」可一键读取本机 Qoder / Qoder CN 客户端已登录的账号,或用浏览器登录。',
|
|
751
1002
|
workbuddy: '还没有 WorkBuddy 账号。<br>点「添加账号」可读取本机 WorkBuddy 客户端(当前 + 历史会话),或用浏览器登录 WorkBuddy / CodeBuddy 账号。',
|
|
752
1003
|
zcode: '还没有 ZCode 账号。<br>点「添加账号」可读取本机 ZCode 客户端已登录的账号,或用浏览器登录(BigModel / Z.ai),多个账号可一键切换。',
|
|
1004
|
+
mirasim: '还没有 mirasim 账号。<br>点「添加账号」可读取本机 mirasim 客户端当前登录的账号(本地解密,token 不出本机)。',
|
|
753
1005
|
};
|
|
754
1006
|
|
|
755
1007
|
function renderProd() {
|
|
@@ -758,7 +1010,7 @@ function renderProd() {
|
|
|
758
1010
|
const acc = all.filter((a) => !state.region || provOf(a.provider).cls === state.region);
|
|
759
1011
|
const list = $('list');
|
|
760
1012
|
if (!acc.length) list.innerHTML = '<div class="empty">' + (all.length ? '该地区下没有账号。' : EMPTY_TIP[prod]) + '</div>';
|
|
761
|
-
else list.innerHTML = acc.map(cardHtml).join('');
|
|
1013
|
+
else list.innerHTML = sortedAccounts(prod, acc).map(cardHtml).join('');
|
|
762
1014
|
const canCheckin = CHECKIN_PRODUCTS.has(prod);
|
|
763
1015
|
$('s-count').textContent = acc.length;
|
|
764
1016
|
$('s-done-wrap').style.display = canCheckin ? 'contents' : 'none';
|
|
@@ -774,11 +1026,11 @@ function renderProd() {
|
|
|
774
1026
|
$('s-done').textContent = acc.filter(isDone).length + ' / ' + acc.length;
|
|
775
1027
|
}
|
|
776
1028
|
const c = creditSum(acc);
|
|
777
|
-
$('s-credits-label').textContent = prod === 'zcode' ? '
|
|
778
|
-
$('s-credits').textContent = prod === 'zcode'
|
|
1029
|
+
$('s-credits-label').textContent = prod === 'zcode' || prod === 'mirasim' ? '有效套餐' : '剩余积分';
|
|
1030
|
+
$('s-credits').textContent = (prod === 'zcode' || prod === 'mirasim')
|
|
779
1031
|
? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
|
|
780
1032
|
: c.known ? fmtNum(c.sum) : '-';
|
|
781
|
-
const hasRegions = prod !== 'zcode';
|
|
1033
|
+
const hasRegions = prod !== 'zcode' && prod !== 'mirasim';
|
|
782
1034
|
$('region').style.display = hasRegions ? '' : 'none';
|
|
783
1035
|
$('region-div').style.display = hasRegions ? '' : 'none';
|
|
784
1036
|
document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
|
|
@@ -797,12 +1049,13 @@ const meter = (done, total) => '<div class="meter"><span style="width:' + (total
|
|
|
797
1049
|
function renderDash() {
|
|
798
1050
|
const s = state.status || {};
|
|
799
1051
|
const sch = s.scheduler || {};
|
|
1052
|
+
const visibleProducts = PRODUCTS.filter((p) => compVisible(p.id));
|
|
800
1053
|
const signable = state.accounts.filter((a) => CHECKIN_PRODUCTS.has(a.product));
|
|
801
1054
|
const done = signable.filter(isDone).length;
|
|
802
1055
|
const failed = signable.filter((a) => statusOf(a).cls === 'fail').length;
|
|
803
1056
|
const credit = creditSum(state.accounts.filter((a) => a.product !== 'zcode'));
|
|
804
|
-
const byProd =
|
|
805
|
-
const creditByProd = ['qoder', 'workbuddy'].map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
|
|
1057
|
+
const byProd = orderedVisibleComps().filter((id) => id !== 'tenrouter').map((id) => prodLabel(id) + ' ' + ofProduct(id).length).join(' · ');
|
|
1058
|
+
const creditByProd = ['qoder', 'workbuddy'].filter(compVisible).map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
|
|
806
1059
|
// 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
|
|
807
1060
|
const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl');
|
|
808
1061
|
const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
|
|
@@ -820,7 +1073,8 @@ function renderDash() {
|
|
|
820
1073
|
+ kpi('下次自动领取', sch.ticking ? '领取中…' : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
|
|
821
1074
|
sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : '每 ~2 小时一轮');
|
|
822
1075
|
|
|
823
|
-
|
|
1076
|
+
// 仪表盘产品卡按组件设置里的顺序渲染(与标签栏同步),10Router 汇总卡按其顺序插入
|
|
1077
|
+
const prodCardHtml = (p) => {
|
|
824
1078
|
const list = ofProduct(p.id);
|
|
825
1079
|
let rows = '';
|
|
826
1080
|
if (!list.length) {
|
|
@@ -845,6 +1099,10 @@ function renderDash() {
|
|
|
845
1099
|
}
|
|
846
1100
|
rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
|
|
847
1101
|
+ '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
|
|
1102
|
+
} else if (p.id === 'mirasim') {
|
|
1103
|
+
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
1104
|
+
rows = '<span class="lbl">额度体系</span><span class="val dim">5h / 7d 滚动窗口</span>'
|
|
1105
|
+
+ '<span class="lbl">有效套餐</span><span class="val">' + planned + ' / ' + list.length + '</span>';
|
|
848
1106
|
} else {
|
|
849
1107
|
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
850
1108
|
// ZCode 没有每日签到:不展示签到语义的行
|
|
@@ -859,12 +1117,18 @@ function renderDash() {
|
|
|
859
1117
|
const zid = p.id === 'zcode' ? s.zcodeCurrentIdentity : null;
|
|
860
1118
|
const live = cur ? esc(cur.name || cur.email || cur.uid)
|
|
861
1119
|
: zid && (zid.email || zid.uid || zid.username) ? '未导入的账号(' + esc(zid.email || zid.username || zid.uid) + ')'
|
|
1120
|
+
: p.id === 'mirasim' && s.mirasimCurrentUid ? '未导入的账号'
|
|
862
1121
|
: (p.id === 'workbuddy' && s.workbuddyCurrentUid ? '未导入的账号' : '—');
|
|
863
1122
|
rows += '<span class="lbl">客户端当前</span><span class="val">' + live + '</span>';
|
|
864
1123
|
}
|
|
865
1124
|
return '<div class="prodcard" data-view="' + p.id + '"><div class="prod-head"><span class="prod-name"><span class="dot ' + p.id + '"></span>' + p.label
|
|
866
|
-
+ '<span class="badge">' + list.length + ' 个账号</span></span><span class="prod-go">查看
|
|
867
|
-
}
|
|
1125
|
+
+ '<span class="badge">' + list.length + ' 个账号</span></span><span class="prod-go">查看' + I.chevRight + '</span></div><div class="prod-rows">' + rows + '</div></div>';
|
|
1126
|
+
};
|
|
1127
|
+
$('prods').innerHTML = orderedVisibleComps().map((id) => {
|
|
1128
|
+
if (id === 'tenrouter') return trDashCard();
|
|
1129
|
+
const p = PRODUCTS.find((x) => x.id === id);
|
|
1130
|
+
return p ? prodCardHtml(p) : '';
|
|
1131
|
+
}).join('');
|
|
868
1132
|
|
|
869
1133
|
// 需要处理:token 失效 / 即将过期、领取失败、国际版缺风控身份、10Router 额度告急 / 查询失败
|
|
870
1134
|
const att = [];
|
|
@@ -881,20 +1145,24 @@ function renderDash() {
|
|
|
881
1145
|
if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: a.provider === 'workbuddy-intl' ? '活跃失败' : '领取失败', title: st.msg }, base));
|
|
882
1146
|
}
|
|
883
1147
|
for (const c of (tr.quotas && tr.quotas.connections) || []) {
|
|
1148
|
+
if (!compVisible('tenrouter')) break;
|
|
884
1149
|
const base = { view: 'tenrouter', name: c.name || c.email || c.providerName, badges: '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>' };
|
|
885
1150
|
if (c.error) att.push(Object.assign({ pill: 'fail', text: '额度查询失败', title: c.error }, base));
|
|
886
1151
|
else if (c.isActive && quotaLow(c)) att.push(Object.assign({ pill: 'limited', text: '额度告急' }, base));
|
|
887
1152
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1153
|
+
// 隐藏掉的组件不进「需要处理」(如收起 10Router 后不再展示其额度告急)
|
|
1154
|
+
const attVisible = att.filter((x) => compVisible(x.view));
|
|
1155
|
+
$('att-count').textContent = attVisible.length ? attVisible.length + ' 项' : '';
|
|
1156
|
+
$('attention').innerHTML = attVisible.length
|
|
1157
|
+
? attVisible.slice(0, 8).map((x) => '<div class="item" data-view="' + x.view + '" title="' + esc(x.title || '') + '"><span class="item-main"><span class="item-name">' + esc(x.name) + '</span>' + x.badges + '</span>'
|
|
891
1158
|
+ '<span class="item-side"><span class="pill ' + x.pill + '">' + esc(x.text) + '</span></span></div>').join('')
|
|
892
|
-
+ (
|
|
893
|
-
: '<div class="muted-empty">' + (state.accounts.length ? '
|
|
1159
|
+
+ (attVisible.length > 8 ? '<div class="muted-empty">还有 ' + (attVisible.length - 8) + ' 项</div>' : '')
|
|
1160
|
+
: '<div class="muted-empty">' + (state.accounts.length ? '<span class="ok-glyph">' + I.check + '</span>一切正常' : '还没有账号') + '</div>';
|
|
894
1161
|
|
|
895
1162
|
// 最近领取记录
|
|
896
1163
|
const RES = { 'checked-in': ['ok', '领取成功'], already: ['ok', '已领过'], limited: ['limited', '本机已领'], 'no-activity': ['pending', '无可领活动'], failed: ['fail', '失败'] };
|
|
897
|
-
const recent = state.accounts.filter((a) => a.lastResult && a.lastResult.at
|
|
1164
|
+
const recent = state.accounts.filter((a) => a.lastResult && a.lastResult.at && compVisible(a.product))
|
|
1165
|
+
.sort((x, y) => String(y.lastResult.at).localeCompare(String(x.lastResult.at))).slice(0, 8);
|
|
898
1166
|
$('last-tick').textContent = sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) : '';
|
|
899
1167
|
$('recent').innerHTML = recent.length
|
|
900
1168
|
? recent.map((a) => {
|
|
@@ -922,8 +1190,13 @@ function renderStatus() {
|
|
|
922
1190
|
// 产品相关提示只在对应标签页出现
|
|
923
1191
|
const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
|
|
924
1192
|
const n = $('notice');
|
|
925
|
-
n.classList.toggle('info', state.view === 'zcode' || (hasIntl && Boolean(s.riskIdentity)));
|
|
926
|
-
if (state.view === '
|
|
1193
|
+
n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || (hasIntl && Boolean(s.riskIdentity)));
|
|
1194
|
+
if (state.view === 'mirasim') {
|
|
1195
|
+
n.style.display = '';
|
|
1196
|
+
n.innerHTML = 'mirasim(原生 AI 编程开发环境)额度按「平台额度」(5h 滚动窗口、7d 全局上限与分模型上限)分配。Token 约 1 小时过期,读取额度时会自动用 refreshToken 静默续期并同步回客户端,无需手动处理。'
|
|
1197
|
+
+ '切换账号需要至少两个已导入的账号(当前登录 + 目标账号):先在「添加账号 → 本机导入」把各账号分别导入,切换时当前登录会自动保存、目标账号写入客户端,桌面版切完自动重启 mirasim。'
|
|
1198
|
+
+ mirasimClientChip(s);
|
|
1199
|
+
} else if (state.view === 'zcode') {
|
|
927
1200
|
n.style.display = '';
|
|
928
1201
|
const pf = state.zProxyFirst === true;
|
|
929
1202
|
const pu = state.zProxyUrl;
|
|
@@ -931,7 +1204,8 @@ function renderStatus() {
|
|
|
931
1204
|
n.innerHTML = 'ZCode 没有每日签到,积分靠「活动领取」。打开「自动领取」后,每轮签到结束会轮询可领活动并尝试领取(桌面版静默过验证码,需要人工时弹验证码窗口);平时保持关闭即可,活动期前再开。也可点卡片上的礼物图标手动领取。'
|
|
932
1205
|
+ ' <button class="btn" id="btn-zac" style="margin-left:6px" title="ZCode 活动自动轮询领取(每轮签到后执行一次)">' + (ac ? '自动领取:开' : '自动领取:关') + '</button>'
|
|
933
1206
|
+ ' <button class="btn" id="btn-znet" style="margin-left:4px" title="ZCode 上游接口的访问顺序:失败 / 被拦时自动回退另一条">出口:' + (pf ? '代理优先' : '直连优先') + '</button>'
|
|
934
|
-
+ ' <button class="btn" id="btn-zproxy" style="margin-left:4px" title="设置 ZCode 出口用的 http 代理(留空则清除;不设置时使用 HTTPS_PROXY 环境变量)">代理:' + esc(pu || (state.zHasEnvProxy ? '环境变量' : '未设置')) + '</button>'
|
|
1207
|
+
+ ' <button class="btn" id="btn-zproxy" style="margin-left:4px" title="设置 ZCode 出口用的 http 代理(留空则清除;不设置时使用 HTTPS_PROXY 环境变量)">代理:' + esc(pu || (state.zHasEnvProxy ? '环境变量' : '未设置')) + '</button>'
|
|
1208
|
+
+ zcodeClientChip(s);
|
|
935
1209
|
} else if (hasIntl && !s.riskIdentity && umidOffer(s)) {
|
|
936
1210
|
n.style.display = '';
|
|
937
1211
|
const busy = s.umid.installing || umidBusy;
|
|
@@ -950,6 +1224,24 @@ function renderStatus() {
|
|
|
950
1224
|
}
|
|
951
1225
|
}
|
|
952
1226
|
|
|
1227
|
+
// ZCode 客户端状态芯片 + 「打开客户端」按钮(仅桌面版:面板里有 Electron 桥才显示按钮)
|
|
1228
|
+
function zcodeClientChip(s) {
|
|
1229
|
+
const c = s.zcodeClient || {};
|
|
1230
|
+
if (!c.installed) return ' <span class="dim">客户端:未安装</span>';
|
|
1231
|
+
const canOpen = window.creditdaddy && typeof window.creditdaddy.openZcodeClient === 'function';
|
|
1232
|
+
return ' <span class="dim">客户端:' + (c.running ? '运行中' : '未运行') + '</span>'
|
|
1233
|
+
+ (canOpen ? ' <button class="btn" id="btn-zopen" style="margin-left:4px" title="打开本机 ZCode 客户端">打开客户端</button>' : '');
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// mirasim 客户端状态芯片 + 「打开客户端」按钮
|
|
1237
|
+
function mirasimClientChip(s) {
|
|
1238
|
+
const c = s.mirasimClient || {};
|
|
1239
|
+
if (!c.installed) return ' <span class="dim">客户端:未安装</span>';
|
|
1240
|
+
const canOpen = window.creditdaddy && typeof window.creditdaddy.openMirasimClient === 'function';
|
|
1241
|
+
return ' <span class="dim">客户端:' + (c.running ? '运行中' : '未运行') + '</span>'
|
|
1242
|
+
+ (canOpen ? ' <button class="btn" id="btn-miraopen" style="margin-left:4px" title="打开本机 Mirasim 客户端">打开客户端</button>' : '');
|
|
1243
|
+
}
|
|
1244
|
+
|
|
953
1245
|
// Qoder 设备身份组件(Linux / fnOS)
|
|
954
1246
|
const umidOffer = (s) => Boolean(s && s.umid && s.umid.supported && !s.umid.installed);
|
|
955
1247
|
let umidBusy = false;
|
|
@@ -964,6 +1256,22 @@ async function installUmid() {
|
|
|
964
1256
|
}
|
|
965
1257
|
$('notice').addEventListener('click', (e) => { if (e.target.closest('#btn-umid')) installUmid(); });
|
|
966
1258
|
$('notice').addEventListener('click', async (e) => {
|
|
1259
|
+
if (e.target.closest('#btn-zopen')) {
|
|
1260
|
+
try {
|
|
1261
|
+
const r = await window.creditdaddy.openZcodeClient();
|
|
1262
|
+
if (r && r.ok) toast('已启动 ZCode 客户端');
|
|
1263
|
+
else toast((r && r.error) || '无法打开 ZCode 客户端', false);
|
|
1264
|
+
} catch (err) { toast(String(err && err.message || err), false); }
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (e.target.closest('#btn-miraopen')) {
|
|
1268
|
+
try {
|
|
1269
|
+
const r = await window.creditdaddy.openMirasimClient();
|
|
1270
|
+
if (r && r.ok) toast('已启动 Mirasim 客户端');
|
|
1271
|
+
else toast((r && r.error) || '无法打开 Mirasim 客户端', false);
|
|
1272
|
+
} catch (err) { toast(String(err && err.message || err), false); }
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
967
1275
|
if (e.target.closest('#btn-zac')) {
|
|
968
1276
|
try {
|
|
969
1277
|
const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: state.zAutoClaim !== true } });
|
|
@@ -1040,37 +1348,11 @@ function quotaValue(q) {
|
|
|
1040
1348
|
}
|
|
1041
1349
|
const resetLabel = (q) => (q.resetAt ? fmtTime(q.resetAt).slice(5) + (q.recurring === false ? ' 到期' : ' 重置') : '');
|
|
1042
1350
|
|
|
1043
|
-
// 相对重置标签(CLIProxyAPI 管理中心风格):「3 天 后重置」「45 分钟 后到期」;一小时内标红,悬停看绝对时间
|
|
1044
|
-
function relReset(q) {
|
|
1045
|
-
if (!q.resetAt) return null;
|
|
1046
|
-
const ms = new Date(q.resetAt).getTime() - Date.now();
|
|
1047
|
-
if (!Number.isFinite(ms)) return null;
|
|
1048
|
-
if (ms <= 0) return { text: '即将' + (q.recurring === false ? '到期' : '重置'), soon: true };
|
|
1049
|
-
const m = Math.ceil(ms / 60000);
|
|
1050
|
-
const d = Math.floor(m / 1440), h = Math.floor((m % 1440) / 60), mm = m % 60;
|
|
1051
|
-
const dur = d > 0 ? d + ' 天' + (h ? h + ' 小时' : '') : h > 0 ? h + ' 小时' + (mm ? mm + ' 分' : '') : Math.max(1, mm) + ' 分钟';
|
|
1052
|
-
return { text: dur + ' 后' + (q.recurring === false ? '到期' : '重置'), soon: ms < 3600e3 };
|
|
1053
|
-
}
|
|
1054
|
-
function relResetHtml(q) {
|
|
1055
|
-
const r = relReset(q);
|
|
1056
|
-
return r ? '<span class="tr-reset' + (r.soon ? ' soon' : '') + '" title="' + esc(fmtTime(q.resetAt)) + '">' + esc(r.text) + '</span>' : '';
|
|
1057
|
-
}
|
|
1058
|
-
// 三档着色水位条(≥70 绿 / ≥30 琥珀 / <30 红)
|
|
1059
|
-
const meterCls = (pct) => (pct >= 70 ? 'hi' : pct >= 30 ? 'mid' : 'low');
|
|
1060
|
-
function trMeter(pct) {
|
|
1061
|
-
if (pct === null) return '';
|
|
1062
|
-
const w = Math.min(100, Math.max(0, pct)).toFixed(1);
|
|
1063
|
-
return '<div class="tr-meter"><span class="' + meterCls(pct) + '" style="width:' + w + '%"></span></div>';
|
|
1064
|
-
}
|
|
1065
1351
|
// 明细整行:名称 + 数值/相对重置 + 水位条(CLIProxyAPI quotaRow 版式)
|
|
1066
1352
|
function trRow(q) {
|
|
1067
1353
|
return trLine(q.name, lineVal(q)) + trMeter(quotaPct(q));
|
|
1068
1354
|
}
|
|
1069
1355
|
|
|
1070
|
-
// 紧凑行(周期额度 / 聚合积分包):名称在左、数值在右,不带进度条
|
|
1071
|
-
function trLine(name, valHtml, title) {
|
|
1072
|
-
return '<div class="tr-line"><span class="tr-q-name"' + (title ? ' title="' + esc(title) + '"' : '') + '>' + esc(name) + '</span><span class="tr-q-val">' + valHtml + '</span></div>';
|
|
1073
|
-
}
|
|
1074
1356
|
function lineVal(q) {
|
|
1075
1357
|
const pct = quotaPct(q);
|
|
1076
1358
|
// 百分比口径(限时限额额度):一律显示 %,节约空间;积分类保持实际数字
|
|
@@ -1159,7 +1441,7 @@ function trCardHtml(c) {
|
|
|
1159
1441
|
+ trPacksHtml(c, packs);
|
|
1160
1442
|
}
|
|
1161
1443
|
}
|
|
1162
|
-
return '<div class="tr-card' + (c.isActive ? '' : ' off') + '"><div class="row1"><div class="name-group">'
|
|
1444
|
+
return '<div class="tr-card' + (c.isActive ? '' : ' off') + '" draggable="true" data-id="' + esc(c.id) + '" data-vendor="' + esc(c.providerName || c.provider || '') + '"><div class="row1"><div class="name-group">'
|
|
1163
1445
|
+ '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>'
|
|
1164
1446
|
+ (TR_REGION[c.provider] ? '<span class="badge ' + TR_REGION[c.provider][1] + '">' + TR_REGION[c.provider][0] + '</span>' : '')
|
|
1165
1447
|
+ '<span class="tr-name" title="' + esc([c.name, c.email].filter(Boolean).join(' · ')) + '">' + esc(who) + '</span>'
|
|
@@ -1285,7 +1567,7 @@ function trVendorCardHtml(g) {
|
|
|
1285
1567
|
? '<div class="tr-summary dim"><span class="tr-sum-txt">' + briefs.join(' · ') + '</span></div>'
|
|
1286
1568
|
: '');
|
|
1287
1569
|
}
|
|
1288
|
-
return '<div class="tr-card"><div class="row1"><div class="name-group">'
|
|
1570
|
+
return '<div class="tr-card" draggable="true" data-vendor="' + esc(g.name) + '"><div class="row1"><div class="name-group">'
|
|
1289
1571
|
+ '<span class="badge prod tenrouter">' + esc(g.name) + '</span>'
|
|
1290
1572
|
+ (TR_REGION[prov] ? '<span class="badge ' + TR_REGION[prov][1] + '">' + TR_REGION[prov][0] + '</span>' : '')
|
|
1291
1573
|
+ '<span class="badge">' + items.length + ' 个账号</span>'
|
|
@@ -1345,10 +1627,10 @@ function renderTenrouter() {
|
|
|
1345
1627
|
else if (!list.length) grid = '<div class="empty">10Router 里还没有可查额度的供应商连接(OAuth 或支持额度查询的 API Key)。</div>';
|
|
1346
1628
|
else if (activeVendor) {
|
|
1347
1629
|
const g = groups.find((x) => x.name === activeVendor) || { items: [] };
|
|
1348
|
-
grid = '<div class="tr-vcards">' + g.items.map(trCardHtml).join('') + '</div>';
|
|
1630
|
+
grid = '<div class="tr-vcards">' + sortedConnections(activeVendor, g.items).map(trCardHtml).join('') + '</div>';
|
|
1349
1631
|
} else {
|
|
1350
1632
|
grid = shown.length
|
|
1351
|
-
? '<div class="tr-vcards">' + shown.map(trVendorCardHtml).join('') + '</div>'
|
|
1633
|
+
? '<div class="tr-vcards">' + sortedVendorGroups(shown).map(trVendorCardHtml).join('') + '</div>'
|
|
1352
1634
|
: '<div class="empty">没有可显示的供应商(可在「设置」里关掉隐藏)</div>';
|
|
1353
1635
|
}
|
|
1354
1636
|
const h = tr.health;
|
|
@@ -1385,7 +1667,7 @@ function trDashCard() {
|
|
|
1385
1667
|
+ '<span class="lbl">额度告急</span><span class="val' + (low ? ' warn' : '') + '">' + (tr.quotas ? low : '-') + '</span>'
|
|
1386
1668
|
+ '<span class="lbl">用量同步</span><span class="val">' + (ls ? esc(fmtTime(ls.at).slice(5)) : c.sync.enabled ? '等待首次同步' : '手动') + '</span>';
|
|
1387
1669
|
return '<div class="prodcard" data-view="tenrouter"><div class="prod-head"><span class="prod-name"><span class="dot tenrouter"></span>10Router</span>'
|
|
1388
|
-
+ '<span class="prod-go">查看
|
|
1670
|
+
+ '<span class="prod-go">查看' + I.chevRight + '</span></div><div class="prod-rows">' + rows + '</div></div>';
|
|
1389
1671
|
}
|
|
1390
1672
|
|
|
1391
1673
|
async function loadTrConfig() {
|
|
@@ -1555,19 +1837,40 @@ async function onCardClick(e) {
|
|
|
1555
1837
|
const client = CLIENT_LABEL[a && a.product] || '客户端';
|
|
1556
1838
|
const tip = a && a.product === 'zcode'
|
|
1557
1839
|
? '\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
|
|
1558
|
-
:
|
|
1840
|
+
: a && a.product === 'mirasim'
|
|
1841
|
+
? '\n需先退出 Mirasim;切换后重新打开即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
|
|
1842
|
+
: '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
|
|
1559
1843
|
if (!(await uiConfirm('把 ' + client + ' 切换到「' + (a && a.name || id) + '」?' + tip, '切换账号', '切换'))) return;
|
|
1560
1844
|
const doSwitch = (force) => api('/api/accounts/' + encodeURIComponent(id) + '/switch', { json: { force } });
|
|
1561
1845
|
try {
|
|
1562
1846
|
let r;
|
|
1563
1847
|
try { r = await doSwitch(false); }
|
|
1564
1848
|
catch (err) {
|
|
1565
|
-
if (err.code !== 'ZCODE_RUNNING') throw err;
|
|
1566
|
-
if (!(await uiConfirm(err.message + '\n\n
|
|
1849
|
+
if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING') throw err;
|
|
1850
|
+
if (!(await uiConfirm(err.message + '\n\n仍要强制切换吗?强制切换会先关闭客户端再写入登录,重新打开客户端即为新账号。', client + ' 正在运行', '关闭并强制切换'))) return;
|
|
1567
1851
|
r = await doSwitch(true);
|
|
1568
1852
|
}
|
|
1569
|
-
|
|
1853
|
+
const msg = r && r.alreadyActive ? client + ' 当前已是该账号'
|
|
1854
|
+
: (r && r.closedClient ? '已关闭并切换 ' : '') + client + ' 已切换到 ' + (a && a.name || id);
|
|
1855
|
+
toast(msg);
|
|
1570
1856
|
await loadStatus(); await loadAccounts();
|
|
1857
|
+
// 桌面版切号后自动唤醒打开客户端,免去手动查找客户端快捷方式
|
|
1858
|
+
if (a && a.product === 'zcode' && window.creditdaddy && typeof window.creditdaddy.openZcodeClient === 'function') {
|
|
1859
|
+
setTimeout(async () => {
|
|
1860
|
+
try {
|
|
1861
|
+
const re = await window.creditdaddy.openZcodeClient();
|
|
1862
|
+
if (re && re.ok) toast('已重新启动 ZCode 客户端');
|
|
1863
|
+
} catch (e) {}
|
|
1864
|
+
}, 800);
|
|
1865
|
+
}
|
|
1866
|
+
if (a && a.product === 'mirasim' && window.creditdaddy && typeof window.creditdaddy.openMirasimClient === 'function') {
|
|
1867
|
+
setTimeout(async () => {
|
|
1868
|
+
try {
|
|
1869
|
+
const re = await window.creditdaddy.openMirasimClient();
|
|
1870
|
+
if (re && re.ok) toast('已重新启动 Mirasim 客户端');
|
|
1871
|
+
} catch (e) {}
|
|
1872
|
+
}, 800);
|
|
1873
|
+
}
|
|
1571
1874
|
} catch (err) { toast(err.message, false); }
|
|
1572
1875
|
return;
|
|
1573
1876
|
}
|
|
@@ -1778,14 +2081,15 @@ function switchTab(name) {
|
|
|
1778
2081
|
|
|
1779
2082
|
// 添加账号:在产品页打开时只显示该产品可用的方式
|
|
1780
2083
|
const LOCAL_HINT = {
|
|
1781
|
-
'': '读取本机 Qoder 客户端(本地解密)、WorkBuddy 客户端(当前 + 历史会话)、ZCode 客户端(本地解密)已登录的账号,token 不出本机;同时扫描旧版 Qoder IDE / CLI 文件。',
|
|
2084
|
+
'': '读取本机 Qoder 客户端(本地解密)、WorkBuddy 客户端(当前 + 历史会话)、ZCode 客户端(本地解密)、mirasim 客户端(本地解密)已登录的账号,token 不出本机;同时扫描旧版 Qoder IDE / CLI 文件。',
|
|
1782
2085
|
qoder: '读取本机 Qoder / Qoder CN 客户端已登录的账号(本地解密 auth.v1.dat),token 不出本机;同时扫描旧版 Qoder IDE / CLI 文件。',
|
|
1783
2086
|
workbuddy: '读取本机 WorkBuddy 客户端的当前会话与历史会话(CodeBuddyExtension 凭据目录),token 不出本机。',
|
|
1784
2087
|
zcode: '读取本机 ZCode 客户端当前登录的账号(本地解密 ~/.zcode/v2/credentials.json),token 不出本机。',
|
|
2088
|
+
mirasim: '读取本机 mirasim 客户端当前登录的账号(本地解密 ~/.mirasim/setting.json),token 不出本机。',
|
|
1785
2089
|
};
|
|
1786
2090
|
function openAdd(product) {
|
|
1787
2091
|
state.addProduct = product || '';
|
|
1788
|
-
const tabShown = { local: true, device:
|
|
2092
|
+
const tabShown = { local: true, device: product !== 'mirasim', token: product !== 'zcode' && product !== 'mirasim' };
|
|
1789
2093
|
document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
|
|
1790
2094
|
$('local-hint').textContent = LOCAL_HINT[state.addProduct];
|
|
1791
2095
|
let first = null;
|
|
@@ -1833,9 +2137,9 @@ async function localScan() {
|
|
|
1833
2137
|
}
|
|
1834
2138
|
for (const e of r.errors || []) html += '<div class="hint" style="color:var(--err)">读取失败:' + esc(e.file) + ':' + esc(e.error) + '</div>';
|
|
1835
2139
|
if (!cands.length) {
|
|
1836
|
-
const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode';
|
|
1837
|
-
const alt = state.addProduct === 'zcode' ? '
|
|
1838
|
-
html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录'
|
|
2140
|
+
const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim';
|
|
2141
|
+
const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' ? '' : ',或改用「浏览器登录」「粘贴 Token」';
|
|
2142
|
+
html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录'
|
|
1839
2143
|
+ (r.scannedDirs && r.scannedDirs.length ? '、' + r.scannedDirs.map(esc).join('、') : '') + '</div>';
|
|
1840
2144
|
}
|
|
1841
2145
|
box.innerHTML = html;
|
|
@@ -1977,6 +2281,219 @@ async function submitImport(password) {
|
|
|
1977
2281
|
}
|
|
1978
2282
|
|
|
1979
2283
|
// ── 绑定 ──
|
|
2284
|
+
// 全局组件开关:收起 Qoder / WorkBuddy / ZCode / 10Router 的标签页与仪表盘对应卡片
|
|
2285
|
+
$('btn-comp').innerHTML = I.tune;
|
|
2286
|
+
|
|
2287
|
+
// ── 卡片拖拽排序(产品面板 + 10Router):手动顺序优先级最高,落库 localStorage ──
|
|
2288
|
+
const dragState = { kind: null, id: null, vendor: null };
|
|
2289
|
+
function clearDropMarkers(root) {
|
|
2290
|
+
root.querySelectorAll('.drop-before, .drop-after').forEach((el) => el.classList.remove('drop-before', 'drop-after'));
|
|
2291
|
+
}
|
|
2292
|
+
/** 把 dragId 移到 targetId 的 before/after 位置;manual 为空时以当前默认顺序为底表 */
|
|
2293
|
+
function moveInManual(orderMap, key, baseIds, dragId, targetId, before) {
|
|
2294
|
+
const arr = (orderMap[key] && orderMap[key].length ? orderMap[key].slice() : baseIds.slice()).filter((id) => id !== dragId);
|
|
2295
|
+
const ti = arr.indexOf(targetId);
|
|
2296
|
+
arr.splice(ti < 0 ? arr.length : (before ? ti : ti + 1), 0, dragId);
|
|
2297
|
+
orderMap[key] = arr;
|
|
2298
|
+
}
|
|
2299
|
+
function bindCardDrag(container, { kind, onReorder }) {
|
|
2300
|
+
if (!container) return;
|
|
2301
|
+
container.addEventListener('dragstart', (e) => {
|
|
2302
|
+
const card = e.target.closest && e.target.closest('.card, .tr-card');
|
|
2303
|
+
if (!card) return;
|
|
2304
|
+
dragState.kind = kind;
|
|
2305
|
+
dragState.id = card.dataset.id || null;
|
|
2306
|
+
dragState.vendor = card.dataset.vendor || null;
|
|
2307
|
+
card.classList.add('dragging');
|
|
2308
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
2309
|
+
try { e.dataTransfer.setData('text/plain', dragState.id || dragState.vendor || ''); } catch {}
|
|
2310
|
+
});
|
|
2311
|
+
container.addEventListener('dragend', (e) => {
|
|
2312
|
+
const card = e.target.closest && e.target.closest('.card, .tr-card');
|
|
2313
|
+
if (card) card.classList.remove('dragging');
|
|
2314
|
+
clearDropMarkers(container);
|
|
2315
|
+
dragState.kind = null; dragState.id = null; dragState.vendor = null;
|
|
2316
|
+
});
|
|
2317
|
+
container.addEventListener('dragover', (e) => {
|
|
2318
|
+
if (dragState.kind !== kind) return;
|
|
2319
|
+
const over = e.target.closest && e.target.closest('.card, .tr-card');
|
|
2320
|
+
if (!over) return;
|
|
2321
|
+
e.preventDefault();
|
|
2322
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2323
|
+
clearDropMarkers(container);
|
|
2324
|
+
if ((over.dataset.id || null) === dragState.id) return;
|
|
2325
|
+
const rect = over.getBoundingClientRect();
|
|
2326
|
+
over.classList.add(e.clientY < rect.top + rect.height / 2 ? 'drop-before' : 'drop-after');
|
|
2327
|
+
});
|
|
2328
|
+
container.addEventListener('drop', (e) => {
|
|
2329
|
+
if (dragState.kind !== kind) return;
|
|
2330
|
+
const over = e.target.closest && e.target.closest('.card, .tr-card');
|
|
2331
|
+
if (!over) return;
|
|
2332
|
+
e.preventDefault();
|
|
2333
|
+
const before = over.classList.contains('drop-before');
|
|
2334
|
+
clearDropMarkers(container);
|
|
2335
|
+
const targetId = over.dataset.id || null;
|
|
2336
|
+
if (!targetId || targetId === dragState.id) return;
|
|
2337
|
+
onReorder(dragState, targetId, before);
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
// 产品面板:拖拽账号卡 → state.cardOrder[prod]
|
|
2341
|
+
bindCardDrag($('list'), {
|
|
2342
|
+
kind: 'card',
|
|
2343
|
+
onReorder: (d, targetId, before) => {
|
|
2344
|
+
const prod = state.view;
|
|
2345
|
+
if (!PRODUCTS.some((p) => p.id === prod)) return;
|
|
2346
|
+
const baseIds = sortedAccounts(prod, ofProduct(prod)).map((a) => a.id);
|
|
2347
|
+
moveInManual(state.cardOrder, prod, baseIds, d.id, targetId, before);
|
|
2348
|
+
saveCardOrder();
|
|
2349
|
+
render();
|
|
2350
|
+
renderCompMenu();
|
|
2351
|
+
},
|
|
2352
|
+
});
|
|
2353
|
+
// 10Router:拖拽连接卡(单供应商视角按该供应商存序)与供应商汇总卡(全部视角存 __vendors)
|
|
2354
|
+
bindCardDrag($('tr-panel'), {
|
|
2355
|
+
kind: 'tr',
|
|
2356
|
+
onReorder: (d, targetId, before) => {
|
|
2357
|
+
if (!tr.quotas) return;
|
|
2358
|
+
const list = (tr.quotas.connections || []);
|
|
2359
|
+
const groups = trGroups(list);
|
|
2360
|
+
if (d.id) {
|
|
2361
|
+
// 连接卡:vendor 取自卡片或当前视图
|
|
2362
|
+
const vendor = d.vendor || state.trVendor || (groups.find((g) => g.items.some((x) => x.id === d.id)) || {}).name;
|
|
2363
|
+
if (!vendor) return;
|
|
2364
|
+
const g = groups.find((x) => x.name === vendor);
|
|
2365
|
+
if (!g) return;
|
|
2366
|
+
const baseIds = sortedConnections(vendor, g.items).map((c) => c.id);
|
|
2367
|
+
moveInManual(state.trOrder, vendor, baseIds, d.id, targetId, before);
|
|
2368
|
+
} else if (d.vendor) {
|
|
2369
|
+
// 供应商汇总卡
|
|
2370
|
+
const baseIds = sortedVendorGroups(groups).map((g) => g.name);
|
|
2371
|
+
moveInManual(state.trOrder, '__vendors', baseIds, d.vendor, targetId, before);
|
|
2372
|
+
} else return;
|
|
2373
|
+
saveTrOrder();
|
|
2374
|
+
renderTenrouter();
|
|
2375
|
+
},
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
function syncCompMenu() { renderCompMenu(); }
|
|
2379
|
+
function applyComponents() {
|
|
2380
|
+
// 标签栏按组件设置里的顺序重排(仪表盘按钮始终最前)
|
|
2381
|
+
const nav = $('nav');
|
|
2382
|
+
for (const id of state.compOrder) {
|
|
2383
|
+
const btn = nav.querySelector('button[data-view="' + id + '"]');
|
|
2384
|
+
if (btn) nav.appendChild(btn);
|
|
2385
|
+
}
|
|
2386
|
+
for (const c of COMPONENTS) {
|
|
2387
|
+
const btn = document.querySelector('#nav button[data-view="' + c.id + '"]');
|
|
2388
|
+
if (btn) btn.style.display = compVisible(c.id) ? '' : 'none';
|
|
2389
|
+
}
|
|
2390
|
+
if (state.view !== 'dash' && !compVisible(state.view)) {
|
|
2391
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
2392
|
+
applyView();
|
|
2393
|
+
return; // applyView 内部已 render + renderStatus
|
|
2394
|
+
}
|
|
2395
|
+
render();
|
|
2396
|
+
renderStatus();
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// 右上角面板设置:组件显隐 + 组件顺序(↑↓,仪表盘与标签栏同步)+ 每产品账号排序(默认积分多者靠前,可手动固定)
|
|
2400
|
+
function renderCompMenu() {
|
|
2401
|
+
const compRows = state.compOrder.map((id) => {
|
|
2402
|
+
const c = COMPONENTS.find((x) => x.id === id);
|
|
2403
|
+
if (!c) return '';
|
|
2404
|
+
const vis = compVisible(id);
|
|
2405
|
+
return '<div class="comp-row' + (vis ? '' : ' off') + '">'
|
|
2406
|
+
+ '<label class="check"><input type="checkbox" data-comp="' + id + '"' + (vis ? ' checked' : '') + '>' + esc(c.label) + '</label>'
|
|
2407
|
+
+ '<span class="comp-ops">'
|
|
2408
|
+
+ '<button class="icon-btn mini" data-move="up" data-comp="' + id + '" title="上移(仪表盘与标签栏同步)">' + I.chevUp + '</button>'
|
|
2409
|
+
+ '<button class="icon-btn mini" data-move="down" data-comp="' + id + '" title="下移(仪表盘与标签栏同步)">' + I.chevDown + '</button>'
|
|
2410
|
+
+ '</span></div>';
|
|
2411
|
+
}).join('');
|
|
2412
|
+
const sortSections = PRODUCTS.filter((p) => compVisible(p.id) && ofProduct(p.id).length).map((p) => {
|
|
2413
|
+
const manual = state.cardOrder[p.id];
|
|
2414
|
+
const rows = sortedAccounts(p.id, ofProduct(p.id)).map((a) => '<div class="sort-row" data-aid="' + esc(a.id) + '">'
|
|
2415
|
+
+ '<span class="sort-name" title="' + esc(a.name || a.email || a.uid || a.id) + '">' + esc(a.name || a.email || ('#' + a.id.slice(0, 6))) + '</span>'
|
|
2416
|
+
+ '<span class="comp-ops">'
|
|
2417
|
+
+ '<button class="icon-btn mini" data-acc-move="up" data-prod="' + p.id + '" data-aid="' + esc(a.id) + '" title="上移">' + I.chevUp + '</button>'
|
|
2418
|
+
+ '<button class="icon-btn mini" data-acc-move="down" data-prod="' + p.id + '" data-aid="' + esc(a.id) + '" title="下移">' + I.chevDown + '</button>'
|
|
2419
|
+
+ '</span></div>').join('');
|
|
2420
|
+
return '<details class="sort-sec"' + (manual && manual.length ? ' open' : '') + '><summary>' + esc(p.label)
|
|
2421
|
+
+ (manual && manual.length ? ' <span class="badge">手动排序</span>' : '')
|
|
2422
|
+
+ '<span class="dim" style="font-size:11px">' + ofProduct(p.id).length + '</span></summary>'
|
|
2423
|
+
+ '<div class="sort-hint">默认按剩余积分从多到少;拖拽卡片或用上下按钮固定顺序。</div>'
|
|
2424
|
+
+ rows
|
|
2425
|
+
+ (manual && manual.length ? '<button class="btn" data-acc-reset="' + p.id + '" style="margin-top:6px;width:100%">恢复默认排序</button>' : '')
|
|
2426
|
+
+ '</details>';
|
|
2427
|
+
}).join('');
|
|
2428
|
+
$('comp-menu').innerHTML = '<div class="comp-menu-title">组件顺序(仪表盘与标签栏同步)</div>' + compRows
|
|
2429
|
+
+ '<div class="comp-menu-title" style="margin-top:2px">账号卡片排序</div>'
|
|
2430
|
+
+ (sortSections || '<div class="sort-hint">暂无账号</div>');
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/** 在手动排序列表里移动一个 id;列表不存在时先按当前生效顺序物化一份 */
|
|
2434
|
+
function moveAccountId(prod, aid, dir) {
|
|
2435
|
+
const baseIds = sortedAccounts(prod, ofProduct(prod)).map((a) => a.id);
|
|
2436
|
+
const arr = (state.cardOrder[prod] && state.cardOrder[prod].length ? state.cardOrder[prod].slice() : baseIds);
|
|
2437
|
+
const i = arr.indexOf(aid);
|
|
2438
|
+
if (i < 0) return;
|
|
2439
|
+
const j = dir === 'up' ? i - 1 : i + 1;
|
|
2440
|
+
if (j < 0 || j >= arr.length) return;
|
|
2441
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
2442
|
+
state.cardOrder[prod] = arr;
|
|
2443
|
+
saveCardOrder();
|
|
2444
|
+
}
|
|
2445
|
+
$('btn-comp').addEventListener('click', (e) => {
|
|
2446
|
+
e.stopPropagation();
|
|
2447
|
+
const m = $('comp-menu');
|
|
2448
|
+
if (m.hidden) renderCompMenu();
|
|
2449
|
+
m.hidden = !m.hidden;
|
|
2450
|
+
});
|
|
2451
|
+
document.addEventListener('click', (e) => {
|
|
2452
|
+
const m = $('comp-menu');
|
|
2453
|
+
if (!m.hidden && !e.target.closest('.comp-wrap')) m.hidden = true;
|
|
2454
|
+
});
|
|
2455
|
+
$('comp-menu').addEventListener('change', (e) => {
|
|
2456
|
+
const c = e.target.dataset && e.target.dataset.comp;
|
|
2457
|
+
if (!c) return;
|
|
2458
|
+
if (e.target.checked) state.hidden.delete(c); else state.hidden.add(c);
|
|
2459
|
+
saveHiddenComps();
|
|
2460
|
+
applyComponents();
|
|
2461
|
+
renderCompMenu();
|
|
2462
|
+
});
|
|
2463
|
+
$('comp-menu').addEventListener('click', (e) => {
|
|
2464
|
+
const move = e.target.closest && e.target.closest('[data-move]');
|
|
2465
|
+
if (move) {
|
|
2466
|
+
e.stopPropagation();
|
|
2467
|
+
const id = move.dataset.comp;
|
|
2468
|
+
const i = state.compOrder.indexOf(id);
|
|
2469
|
+
const j = move.dataset.move === 'up' ? i - 1 : i + 1;
|
|
2470
|
+
if (i < 0 || j < 0 || j >= state.compOrder.length) return;
|
|
2471
|
+
[state.compOrder[i], state.compOrder[j]] = [state.compOrder[j], state.compOrder[i]];
|
|
2472
|
+
saveCompOrder();
|
|
2473
|
+
applyComponents();
|
|
2474
|
+
renderCompMenu();
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
const accMove = e.target.closest && e.target.closest('[data-acc-move]');
|
|
2478
|
+
if (accMove) {
|
|
2479
|
+
e.stopPropagation();
|
|
2480
|
+
moveAccountId(accMove.dataset.prod, accMove.dataset.aid, accMove.dataset.accMove);
|
|
2481
|
+
render();
|
|
2482
|
+
renderCompMenu();
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const reset = e.target.closest && e.target.closest('[data-acc-reset]');
|
|
2486
|
+
if (reset) {
|
|
2487
|
+
e.stopPropagation();
|
|
2488
|
+
delete state.cardOrder[reset.dataset.accReset];
|
|
2489
|
+
saveCardOrder();
|
|
2490
|
+
render();
|
|
2491
|
+
renderCompMenu();
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
applyComponents();
|
|
2495
|
+
renderCompMenu();
|
|
2496
|
+
|
|
1980
2497
|
$('btn-checkin-all').innerHTML = I.play + '全部领取';
|
|
1981
2498
|
for (const id of ['btn-add', 'btn-add-dash']) $(id).innerHTML = I.plus + '添加账号';
|
|
1982
2499
|
for (const id of ['btn-import', 'btn-import-dash']) { $(id).innerHTML = I.upload + '导入'; $(id).onclick = pickImport; }
|
|
@@ -2002,6 +2519,8 @@ function applyView() {
|
|
|
2002
2519
|
const raw = location.hash.slice(1);
|
|
2003
2520
|
const [v, sub] = raw.split('/');
|
|
2004
2521
|
state.view = VIEWS.includes(v) ? v : 'dash';
|
|
2522
|
+
// 被组件开关收起的页面回仪表盘
|
|
2523
|
+
if (state.view !== 'dash' && !compVisible(state.view)) state.view = 'dash';
|
|
2005
2524
|
state.region = '';
|
|
2006
2525
|
// 10Router 供应商标签可用 hash 直达:#tenrouter/<供应商名>
|
|
2007
2526
|
state.trVendor = state.view === 'tenrouter' && sub ? decodeURIComponent(sub) : '';
|