creditdaddy 0.9.3 → 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 +74 -12
- 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 +566 -68
- 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) => {
|
|
@@ -689,10 +935,10 @@ function cardHtml(a) {
|
|
|
689
935
|
const ident = a.email || a.phone || (a.uid ? 'UID ' + a.uid.slice(0, 8) : a.tokenMasked);
|
|
690
936
|
const isCurrent = isClientCurrent(a);
|
|
691
937
|
const canCheckin = CHECKIN_PRODUCTS.has(a.product);
|
|
692
|
-
return '<div class="card" data-id="' + esc(a.id) + '">'
|
|
938
|
+
return '<div class="card" draggable="true" data-id="' + esc(a.id) + '">'
|
|
693
939
|
+ '<div class="row1"><div class="name-group">'
|
|
694
940
|
+ '<span class="name" data-act="rename" title="点击修改备注名">' + esc(a.name || '(未命名)') + '</span>'
|
|
695
|
-
+ provBadges(a.provider, true)
|
|
941
|
+
+ provBadges(a.provider, true, a.flavor)
|
|
696
942
|
+ (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
|
|
697
943
|
+ (a.isPat ? '<span class="badge">PAT</span>' : '')
|
|
698
944
|
+ (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
|
|
@@ -706,7 +952,8 @@ function cardHtml(a) {
|
|
|
706
952
|
+ '</div></div>'
|
|
707
953
|
+ '<div class="meta">'
|
|
708
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>'
|
|
709
|
-
+ '<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>'
|
|
710
957
|
+ '</div>'
|
|
711
958
|
+ creditHtml(a)
|
|
712
959
|
+ (st.msg && !(state.quotas[a.id] && state.quotas[a.id].error && state.quotas[a.id].error.includes(st.msg.slice(-24)))
|
|
@@ -717,11 +964,12 @@ function cardHtml(a) {
|
|
|
717
964
|
const isDone = (a) => Boolean(state.status && state.status.todayDone && state.status.todayDone[a.id] === state.status.today);
|
|
718
965
|
const ofProduct = (prod) => state.accounts.filter((a) => a.product === prod);
|
|
719
966
|
const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : null);
|
|
720
|
-
// 客户端当前登录判定:WorkBuddy 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
|
|
967
|
+
// 客户端当前登录判定:WorkBuddy / mirasim 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
|
|
721
968
|
function isClientCurrent(a) {
|
|
722
969
|
const s = state.status;
|
|
723
970
|
if (!s) return false;
|
|
724
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);
|
|
725
973
|
if (a.product === 'zcode') {
|
|
726
974
|
const id = s.zcodeCurrentIdentity;
|
|
727
975
|
return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
|
|
@@ -753,6 +1001,7 @@ const EMPTY_TIP = {
|
|
|
753
1001
|
qoder: '还没有 Qoder 账号。<br>点「添加账号」可一键读取本机 Qoder / Qoder CN 客户端已登录的账号,或用浏览器登录。',
|
|
754
1002
|
workbuddy: '还没有 WorkBuddy 账号。<br>点「添加账号」可读取本机 WorkBuddy 客户端(当前 + 历史会话),或用浏览器登录 WorkBuddy / CodeBuddy 账号。',
|
|
755
1003
|
zcode: '还没有 ZCode 账号。<br>点「添加账号」可读取本机 ZCode 客户端已登录的账号,或用浏览器登录(BigModel / Z.ai),多个账号可一键切换。',
|
|
1004
|
+
mirasim: '还没有 mirasim 账号。<br>点「添加账号」可读取本机 mirasim 客户端当前登录的账号(本地解密,token 不出本机)。',
|
|
756
1005
|
};
|
|
757
1006
|
|
|
758
1007
|
function renderProd() {
|
|
@@ -761,7 +1010,7 @@ function renderProd() {
|
|
|
761
1010
|
const acc = all.filter((a) => !state.region || provOf(a.provider).cls === state.region);
|
|
762
1011
|
const list = $('list');
|
|
763
1012
|
if (!acc.length) list.innerHTML = '<div class="empty">' + (all.length ? '该地区下没有账号。' : EMPTY_TIP[prod]) + '</div>';
|
|
764
|
-
else list.innerHTML = acc.map(cardHtml).join('');
|
|
1013
|
+
else list.innerHTML = sortedAccounts(prod, acc).map(cardHtml).join('');
|
|
765
1014
|
const canCheckin = CHECKIN_PRODUCTS.has(prod);
|
|
766
1015
|
$('s-count').textContent = acc.length;
|
|
767
1016
|
$('s-done-wrap').style.display = canCheckin ? 'contents' : 'none';
|
|
@@ -777,11 +1026,11 @@ function renderProd() {
|
|
|
777
1026
|
$('s-done').textContent = acc.filter(isDone).length + ' / ' + acc.length;
|
|
778
1027
|
}
|
|
779
1028
|
const c = creditSum(acc);
|
|
780
|
-
$('s-credits-label').textContent = prod === 'zcode' ? '
|
|
781
|
-
$('s-credits').textContent = prod === 'zcode'
|
|
1029
|
+
$('s-credits-label').textContent = prod === 'zcode' || prod === 'mirasim' ? '有效套餐' : '剩余积分';
|
|
1030
|
+
$('s-credits').textContent = (prod === 'zcode' || prod === 'mirasim')
|
|
782
1031
|
? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
|
|
783
1032
|
: c.known ? fmtNum(c.sum) : '-';
|
|
784
|
-
const hasRegions = prod !== 'zcode';
|
|
1033
|
+
const hasRegions = prod !== 'zcode' && prod !== 'mirasim';
|
|
785
1034
|
$('region').style.display = hasRegions ? '' : 'none';
|
|
786
1035
|
$('region-div').style.display = hasRegions ? '' : 'none';
|
|
787
1036
|
document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
|
|
@@ -800,12 +1049,13 @@ const meter = (done, total) => '<div class="meter"><span style="width:' + (total
|
|
|
800
1049
|
function renderDash() {
|
|
801
1050
|
const s = state.status || {};
|
|
802
1051
|
const sch = s.scheduler || {};
|
|
1052
|
+
const visibleProducts = PRODUCTS.filter((p) => compVisible(p.id));
|
|
803
1053
|
const signable = state.accounts.filter((a) => CHECKIN_PRODUCTS.has(a.product));
|
|
804
1054
|
const done = signable.filter(isDone).length;
|
|
805
1055
|
const failed = signable.filter((a) => statusOf(a).cls === 'fail').length;
|
|
806
1056
|
const credit = creditSum(state.accounts.filter((a) => a.product !== 'zcode'));
|
|
807
|
-
const byProd =
|
|
808
|
-
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(' · ');
|
|
809
1059
|
// 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
|
|
810
1060
|
const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl');
|
|
811
1061
|
const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
|
|
@@ -823,7 +1073,8 @@ function renderDash() {
|
|
|
823
1073
|
+ kpi('下次自动领取', sch.ticking ? '领取中…' : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
|
|
824
1074
|
sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : '每 ~2 小时一轮');
|
|
825
1075
|
|
|
826
|
-
|
|
1076
|
+
// 仪表盘产品卡按组件设置里的顺序渲染(与标签栏同步),10Router 汇总卡按其顺序插入
|
|
1077
|
+
const prodCardHtml = (p) => {
|
|
827
1078
|
const list = ofProduct(p.id);
|
|
828
1079
|
let rows = '';
|
|
829
1080
|
if (!list.length) {
|
|
@@ -848,6 +1099,10 @@ function renderDash() {
|
|
|
848
1099
|
}
|
|
849
1100
|
rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
|
|
850
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>';
|
|
851
1106
|
} else {
|
|
852
1107
|
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
853
1108
|
// ZCode 没有每日签到:不展示签到语义的行
|
|
@@ -862,12 +1117,18 @@ function renderDash() {
|
|
|
862
1117
|
const zid = p.id === 'zcode' ? s.zcodeCurrentIdentity : null;
|
|
863
1118
|
const live = cur ? esc(cur.name || cur.email || cur.uid)
|
|
864
1119
|
: zid && (zid.email || zid.uid || zid.username) ? '未导入的账号(' + esc(zid.email || zid.username || zid.uid) + ')'
|
|
1120
|
+
: p.id === 'mirasim' && s.mirasimCurrentUid ? '未导入的账号'
|
|
865
1121
|
: (p.id === 'workbuddy' && s.workbuddyCurrentUid ? '未导入的账号' : '—');
|
|
866
1122
|
rows += '<span class="lbl">客户端当前</span><span class="val">' + live + '</span>';
|
|
867
1123
|
}
|
|
868
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
|
|
869
|
-
+ '<span class="badge">' + list.length + ' 个账号</span></span><span class="prod-go">查看
|
|
870
|
-
}
|
|
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('');
|
|
871
1132
|
|
|
872
1133
|
// 需要处理:token 失效 / 即将过期、领取失败、国际版缺风控身份、10Router 额度告急 / 查询失败
|
|
873
1134
|
const att = [];
|
|
@@ -884,20 +1145,24 @@ function renderDash() {
|
|
|
884
1145
|
if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: a.provider === 'workbuddy-intl' ? '活跃失败' : '领取失败', title: st.msg }, base));
|
|
885
1146
|
}
|
|
886
1147
|
for (const c of (tr.quotas && tr.quotas.connections) || []) {
|
|
1148
|
+
if (!compVisible('tenrouter')) break;
|
|
887
1149
|
const base = { view: 'tenrouter', name: c.name || c.email || c.providerName, badges: '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>' };
|
|
888
1150
|
if (c.error) att.push(Object.assign({ pill: 'fail', text: '额度查询失败', title: c.error }, base));
|
|
889
1151
|
else if (c.isActive && quotaLow(c)) att.push(Object.assign({ pill: 'limited', text: '额度告急' }, base));
|
|
890
1152
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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>'
|
|
894
1158
|
+ '<span class="item-side"><span class="pill ' + x.pill + '">' + esc(x.text) + '</span></span></div>').join('')
|
|
895
|
-
+ (
|
|
896
|
-
: '<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>';
|
|
897
1161
|
|
|
898
1162
|
// 最近领取记录
|
|
899
1163
|
const RES = { 'checked-in': ['ok', '领取成功'], already: ['ok', '已领过'], limited: ['limited', '本机已领'], 'no-activity': ['pending', '无可领活动'], failed: ['fail', '失败'] };
|
|
900
|
-
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);
|
|
901
1166
|
$('last-tick').textContent = sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) : '';
|
|
902
1167
|
$('recent').innerHTML = recent.length
|
|
903
1168
|
? recent.map((a) => {
|
|
@@ -925,8 +1190,13 @@ function renderStatus() {
|
|
|
925
1190
|
// 产品相关提示只在对应标签页出现
|
|
926
1191
|
const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
|
|
927
1192
|
const n = $('notice');
|
|
928
|
-
n.classList.toggle('info', state.view === 'zcode' || (hasIntl && Boolean(s.riskIdentity)));
|
|
929
|
-
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') {
|
|
930
1200
|
n.style.display = '';
|
|
931
1201
|
const pf = state.zProxyFirst === true;
|
|
932
1202
|
const pu = state.zProxyUrl;
|
|
@@ -963,6 +1233,15 @@ function zcodeClientChip(s) {
|
|
|
963
1233
|
+ (canOpen ? ' <button class="btn" id="btn-zopen" style="margin-left:4px" title="打开本机 ZCode 客户端">打开客户端</button>' : '');
|
|
964
1234
|
}
|
|
965
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
|
+
|
|
966
1245
|
// Qoder 设备身份组件(Linux / fnOS)
|
|
967
1246
|
const umidOffer = (s) => Boolean(s && s.umid && s.umid.supported && !s.umid.installed);
|
|
968
1247
|
let umidBusy = false;
|
|
@@ -985,6 +1264,14 @@ $('notice').addEventListener('click', async (e) => {
|
|
|
985
1264
|
} catch (err) { toast(String(err && err.message || err), false); }
|
|
986
1265
|
return;
|
|
987
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
|
+
}
|
|
988
1275
|
if (e.target.closest('#btn-zac')) {
|
|
989
1276
|
try {
|
|
990
1277
|
const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: state.zAutoClaim !== true } });
|
|
@@ -1061,37 +1348,11 @@ function quotaValue(q) {
|
|
|
1061
1348
|
}
|
|
1062
1349
|
const resetLabel = (q) => (q.resetAt ? fmtTime(q.resetAt).slice(5) + (q.recurring === false ? ' 到期' : ' 重置') : '');
|
|
1063
1350
|
|
|
1064
|
-
// 相对重置标签(CLIProxyAPI 管理中心风格):「3 天 后重置」「45 分钟 后到期」;一小时内标红,悬停看绝对时间
|
|
1065
|
-
function relReset(q) {
|
|
1066
|
-
if (!q.resetAt) return null;
|
|
1067
|
-
const ms = new Date(q.resetAt).getTime() - Date.now();
|
|
1068
|
-
if (!Number.isFinite(ms)) return null;
|
|
1069
|
-
if (ms <= 0) return { text: '即将' + (q.recurring === false ? '到期' : '重置'), soon: true };
|
|
1070
|
-
const m = Math.ceil(ms / 60000);
|
|
1071
|
-
const d = Math.floor(m / 1440), h = Math.floor((m % 1440) / 60), mm = m % 60;
|
|
1072
|
-
const dur = d > 0 ? d + ' 天' + (h ? h + ' 小时' : '') : h > 0 ? h + ' 小时' + (mm ? mm + ' 分' : '') : Math.max(1, mm) + ' 分钟';
|
|
1073
|
-
return { text: dur + ' 后' + (q.recurring === false ? '到期' : '重置'), soon: ms < 3600e3 };
|
|
1074
|
-
}
|
|
1075
|
-
function relResetHtml(q) {
|
|
1076
|
-
const r = relReset(q);
|
|
1077
|
-
return r ? '<span class="tr-reset' + (r.soon ? ' soon' : '') + '" title="' + esc(fmtTime(q.resetAt)) + '">' + esc(r.text) + '</span>' : '';
|
|
1078
|
-
}
|
|
1079
|
-
// 三档着色水位条(≥70 绿 / ≥30 琥珀 / <30 红)
|
|
1080
|
-
const meterCls = (pct) => (pct >= 70 ? 'hi' : pct >= 30 ? 'mid' : 'low');
|
|
1081
|
-
function trMeter(pct) {
|
|
1082
|
-
if (pct === null) return '';
|
|
1083
|
-
const w = Math.min(100, Math.max(0, pct)).toFixed(1);
|
|
1084
|
-
return '<div class="tr-meter"><span class="' + meterCls(pct) + '" style="width:' + w + '%"></span></div>';
|
|
1085
|
-
}
|
|
1086
1351
|
// 明细整行:名称 + 数值/相对重置 + 水位条(CLIProxyAPI quotaRow 版式)
|
|
1087
1352
|
function trRow(q) {
|
|
1088
1353
|
return trLine(q.name, lineVal(q)) + trMeter(quotaPct(q));
|
|
1089
1354
|
}
|
|
1090
1355
|
|
|
1091
|
-
// 紧凑行(周期额度 / 聚合积分包):名称在左、数值在右,不带进度条
|
|
1092
|
-
function trLine(name, valHtml, title) {
|
|
1093
|
-
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>';
|
|
1094
|
-
}
|
|
1095
1356
|
function lineVal(q) {
|
|
1096
1357
|
const pct = quotaPct(q);
|
|
1097
1358
|
// 百分比口径(限时限额额度):一律显示 %,节约空间;积分类保持实际数字
|
|
@@ -1180,7 +1441,7 @@ function trCardHtml(c) {
|
|
|
1180
1441
|
+ trPacksHtml(c, packs);
|
|
1181
1442
|
}
|
|
1182
1443
|
}
|
|
1183
|
-
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">'
|
|
1184
1445
|
+ '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>'
|
|
1185
1446
|
+ (TR_REGION[c.provider] ? '<span class="badge ' + TR_REGION[c.provider][1] + '">' + TR_REGION[c.provider][0] + '</span>' : '')
|
|
1186
1447
|
+ '<span class="tr-name" title="' + esc([c.name, c.email].filter(Boolean).join(' · ')) + '">' + esc(who) + '</span>'
|
|
@@ -1306,7 +1567,7 @@ function trVendorCardHtml(g) {
|
|
|
1306
1567
|
? '<div class="tr-summary dim"><span class="tr-sum-txt">' + briefs.join(' · ') + '</span></div>'
|
|
1307
1568
|
: '');
|
|
1308
1569
|
}
|
|
1309
|
-
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">'
|
|
1310
1571
|
+ '<span class="badge prod tenrouter">' + esc(g.name) + '</span>'
|
|
1311
1572
|
+ (TR_REGION[prov] ? '<span class="badge ' + TR_REGION[prov][1] + '">' + TR_REGION[prov][0] + '</span>' : '')
|
|
1312
1573
|
+ '<span class="badge">' + items.length + ' 个账号</span>'
|
|
@@ -1366,10 +1627,10 @@ function renderTenrouter() {
|
|
|
1366
1627
|
else if (!list.length) grid = '<div class="empty">10Router 里还没有可查额度的供应商连接(OAuth 或支持额度查询的 API Key)。</div>';
|
|
1367
1628
|
else if (activeVendor) {
|
|
1368
1629
|
const g = groups.find((x) => x.name === activeVendor) || { items: [] };
|
|
1369
|
-
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>';
|
|
1370
1631
|
} else {
|
|
1371
1632
|
grid = shown.length
|
|
1372
|
-
? '<div class="tr-vcards">' + shown.map(trVendorCardHtml).join('') + '</div>'
|
|
1633
|
+
? '<div class="tr-vcards">' + sortedVendorGroups(shown).map(trVendorCardHtml).join('') + '</div>'
|
|
1373
1634
|
: '<div class="empty">没有可显示的供应商(可在「设置」里关掉隐藏)</div>';
|
|
1374
1635
|
}
|
|
1375
1636
|
const h = tr.health;
|
|
@@ -1406,7 +1667,7 @@ function trDashCard() {
|
|
|
1406
1667
|
+ '<span class="lbl">额度告急</span><span class="val' + (low ? ' warn' : '') + '">' + (tr.quotas ? low : '-') + '</span>'
|
|
1407
1668
|
+ '<span class="lbl">用量同步</span><span class="val">' + (ls ? esc(fmtTime(ls.at).slice(5)) : c.sync.enabled ? '等待首次同步' : '手动') + '</span>';
|
|
1408
1669
|
return '<div class="prodcard" data-view="tenrouter"><div class="prod-head"><span class="prod-name"><span class="dot tenrouter"></span>10Router</span>'
|
|
1409
|
-
+ '<span class="prod-go">查看
|
|
1670
|
+
+ '<span class="prod-go">查看' + I.chevRight + '</span></div><div class="prod-rows">' + rows + '</div></div>';
|
|
1410
1671
|
}
|
|
1411
1672
|
|
|
1412
1673
|
async function loadTrConfig() {
|
|
@@ -1576,19 +1837,40 @@ async function onCardClick(e) {
|
|
|
1576
1837
|
const client = CLIENT_LABEL[a && a.product] || '客户端';
|
|
1577
1838
|
const tip = a && a.product === 'zcode'
|
|
1578
1839
|
? '\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
|
|
1579
|
-
:
|
|
1840
|
+
: a && a.product === 'mirasim'
|
|
1841
|
+
? '\n需先退出 Mirasim;切换后重新打开即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
|
|
1842
|
+
: '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
|
|
1580
1843
|
if (!(await uiConfirm('把 ' + client + ' 切换到「' + (a && a.name || id) + '」?' + tip, '切换账号', '切换'))) return;
|
|
1581
1844
|
const doSwitch = (force) => api('/api/accounts/' + encodeURIComponent(id) + '/switch', { json: { force } });
|
|
1582
1845
|
try {
|
|
1583
1846
|
let r;
|
|
1584
1847
|
try { r = await doSwitch(false); }
|
|
1585
1848
|
catch (err) {
|
|
1586
|
-
if (err.code !== 'ZCODE_RUNNING') throw err;
|
|
1587
|
-
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;
|
|
1588
1851
|
r = await doSwitch(true);
|
|
1589
1852
|
}
|
|
1590
|
-
|
|
1853
|
+
const msg = r && r.alreadyActive ? client + ' 当前已是该账号'
|
|
1854
|
+
: (r && r.closedClient ? '已关闭并切换 ' : '') + client + ' 已切换到 ' + (a && a.name || id);
|
|
1855
|
+
toast(msg);
|
|
1591
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
|
+
}
|
|
1592
1874
|
} catch (err) { toast(err.message, false); }
|
|
1593
1875
|
return;
|
|
1594
1876
|
}
|
|
@@ -1799,14 +2081,15 @@ function switchTab(name) {
|
|
|
1799
2081
|
|
|
1800
2082
|
// 添加账号:在产品页打开时只显示该产品可用的方式
|
|
1801
2083
|
const LOCAL_HINT = {
|
|
1802
|
-
'': '读取本机 Qoder 客户端(本地解密)、WorkBuddy 客户端(当前 + 历史会话)、ZCode 客户端(本地解密)已登录的账号,token 不出本机;同时扫描旧版 Qoder IDE / CLI 文件。',
|
|
2084
|
+
'': '读取本机 Qoder 客户端(本地解密)、WorkBuddy 客户端(当前 + 历史会话)、ZCode 客户端(本地解密)、mirasim 客户端(本地解密)已登录的账号,token 不出本机;同时扫描旧版 Qoder IDE / CLI 文件。',
|
|
1803
2085
|
qoder: '读取本机 Qoder / Qoder CN 客户端已登录的账号(本地解密 auth.v1.dat),token 不出本机;同时扫描旧版 Qoder IDE / CLI 文件。',
|
|
1804
2086
|
workbuddy: '读取本机 WorkBuddy 客户端的当前会话与历史会话(CodeBuddyExtension 凭据目录),token 不出本机。',
|
|
1805
2087
|
zcode: '读取本机 ZCode 客户端当前登录的账号(本地解密 ~/.zcode/v2/credentials.json),token 不出本机。',
|
|
2088
|
+
mirasim: '读取本机 mirasim 客户端当前登录的账号(本地解密 ~/.mirasim/setting.json),token 不出本机。',
|
|
1806
2089
|
};
|
|
1807
2090
|
function openAdd(product) {
|
|
1808
2091
|
state.addProduct = product || '';
|
|
1809
|
-
const tabShown = { local: true, device:
|
|
2092
|
+
const tabShown = { local: true, device: product !== 'mirasim', token: product !== 'zcode' && product !== 'mirasim' };
|
|
1810
2093
|
document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
|
|
1811
2094
|
$('local-hint').textContent = LOCAL_HINT[state.addProduct];
|
|
1812
2095
|
let first = null;
|
|
@@ -1854,9 +2137,9 @@ async function localScan() {
|
|
|
1854
2137
|
}
|
|
1855
2138
|
for (const e of r.errors || []) html += '<div class="hint" style="color:var(--err)">读取失败:' + esc(e.file) + ':' + esc(e.error) + '</div>';
|
|
1856
2139
|
if (!cands.length) {
|
|
1857
|
-
const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode';
|
|
1858
|
-
const alt = state.addProduct === 'zcode' ? '
|
|
1859
|
-
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 数据目录'
|
|
1860
2143
|
+ (r.scannedDirs && r.scannedDirs.length ? '、' + r.scannedDirs.map(esc).join('、') : '') + '</div>';
|
|
1861
2144
|
}
|
|
1862
2145
|
box.innerHTML = html;
|
|
@@ -1998,6 +2281,219 @@ async function submitImport(password) {
|
|
|
1998
2281
|
}
|
|
1999
2282
|
|
|
2000
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
|
+
|
|
2001
2497
|
$('btn-checkin-all').innerHTML = I.play + '全部领取';
|
|
2002
2498
|
for (const id of ['btn-add', 'btn-add-dash']) $(id).innerHTML = I.plus + '添加账号';
|
|
2003
2499
|
for (const id of ['btn-import', 'btn-import-dash']) { $(id).innerHTML = I.upload + '导入'; $(id).onclick = pickImport; }
|
|
@@ -2023,6 +2519,8 @@ function applyView() {
|
|
|
2023
2519
|
const raw = location.hash.slice(1);
|
|
2024
2520
|
const [v, sub] = raw.split('/');
|
|
2025
2521
|
state.view = VIEWS.includes(v) ? v : 'dash';
|
|
2522
|
+
// 被组件开关收起的页面回仪表盘
|
|
2523
|
+
if (state.view !== 'dash' && !compVisible(state.view)) state.view = 'dash';
|
|
2026
2524
|
state.region = '';
|
|
2027
2525
|
// 10Router 供应商标签可用 hash 直达:#tenrouter/<供应商名>
|
|
2028
2526
|
state.trVendor = state.view === 'tenrouter' && sub ? decodeURIComponent(sub) : '';
|