creditdaddy 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -225
- package/bin/creditdaddy.js +9 -9
- package/package.json +1 -1
- package/src/panel.html +2065 -2048
package/src/panel.html
CHANGED
|
@@ -1,2048 +1,2065 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>CreditDaddy</title>
|
|
7
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDI0IiBoZWlnaHQ9IjEwMjQiPjxwYXRoIGQ9Ik0zNzIuNDggMTM4LjY2NjY2N0ExMjggMTI4IDAgMCAwIDI5OC42NjY2NjcgMTAxLjU0NjY2N2ExMTcuNzYgMTE3Ljc2IDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzNsNzEuMjUzMzMzIDkxLjUyLTgxLjI4IDI4LjM3MzMzM2ExMC42NjY2NjcgMTAuNjY2NjY3IDAgMCAwLTIuMzQ2NjY3IDE4Ljk4NjY2NyAyNDIuNTYgMjQyLjU2IDAgMCAwIDY5Ljc2IDMyLjg1MzMzMyAyNzMuNzA2NjY3IDI3My43MDY2NjcgMCAwIDAgNTYuOTYgOC4zMnoiIGZpbGw9IiNGRjhBMTQiPjwvcGF0aD48cGF0aCBkPSJNMjQ1Ljc2IDEzOC4wMjY2NjdhMTMwLjk4NjY2NyAxMzAuOTg2NjY3IDAgMCAxIDUyLjA1MzMzMyAyLjEzMzMzM0ExNDguMDUzMzMzIDE0OC4wNTMzMzMgMCAwIDEgMzY2LjcyIDE3MC42NjY2NjdsNS43Ni0zMi44NTMzMzRBMTI4IDEyOCAwIDAgMCAyOTguNjY2NjY3IDEwMS41NDY2NjdhMTE3Ljk3MzMzMyAxMTcuOTczMzMzIDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzN6IiBmaWxsPSIjRkZBQTU0Ij48L3BhdGg+PHBhdGggZD0iTTIzNC42NjY2NjcgOTcwLjY2NjY2N2EyNzcuMzMzMzMzIDMyIDAgMSAwIDU1NC42NjY2NjYgMCAyNzcuMzMzMzMzIDMyIDAgMSAwLTU1NC42NjY2NjYgMFoiIGZpbGw9IiM0NTQxM0MiIG9wYWNpdHk9Ii4xNSI+PC9wYXRoPjxwYXRoIGQ9Ik02MjAuMzczMzMzIDQ2MC41ODY2NjdzMTUxLjA0LTkwLjY2NjY2NyAxODEuMzMzMzM0LTc0LjY2NjY2Ny0xMi41ODY2NjcgMTc0LjI5MzMzMy0xMDYuNjY2NjY3IDIxMy4zMzMzMzMtNzQuNjY2NjY3LTEzOC42NjY2NjctNzQuNjY2NjY3LTEzOC42NjY2NjZ6IiBmaWxsPSIjRkZFNTAwIj48L3BhdGg+PHBhdGggZD0iTTYyMC4zNzMzMzMgNTEzLjkyczE1MS4wNC05MC42NjY2NjcgMTgxLjMzMzMzNC03NC42NjY2NjdhMTQuNzIgMTQuNzIgMCAwIDEgNS4zMzMzMzMgNS45NzMzMzRjNi44MjY2NjctMjkuODY2NjY3IDUuMzMzMzMzLTUzLjU0NjY2Ny01LjMzMzMzMy01OS4zMDY2NjctMzAuMjkzMzMzLTE2LTE4MS4zMzMzMzMgNzQuNjY2NjY3LTE4MS4zMzMzMzQgNzQuNjY2NjY3YTM4Ni45ODY2NjcgMzg2Ljk4NjY2NyAwIDAgMCAwIDYxLjg2NjY2NmMtMC40MjY2NjctNS4zMzMzMzMgMC04LjUzMzMzMyAwLTguNTMzMzMzeiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik00MDMuNjI2NjY3IDQ2MC41ODY2NjdzLTE1MS4wNC05MC42NjY2NjctMTgxLjMzMzMzNC03NC42NjY2NjcgMTIuMzczMzMzIDE3NC4yOTMzMzMgMTA2LjY2NjY2NyAyMTMuMzMzMzMzIDc0LjY2NjY2Ny0xMzguNjY2NjY3IDc0LjY2NjY2Ny0xMzguNjY2NjY2eiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik0yMjIuMjkzMzMzIDQzOS4yNTMzMzNjMzAuMjkzMzMzLTE2IDE4MS4zMzMzMzMgNzQuNjY2NjY3IDE4MS4zMzMzMzQgNzQuNjY2NjY3djguNTMzMzMzYTM3My41NDY2NjcgMzczLjU0NjY2NyAwIDAgMCAwLTYxLjg2NjY2NnMtMTUxLjA0LTkwLjY2NjY2Ny0xODEuMzMzMzM0LTc0LjY2NjY2N2MtMTAuNjY2NjY3IDUuNzYtMTIuMTYgMjkuNDQtNS4zMzMzMzMgNTkuMzA2NjY3YTEzLjQ0IDEzLjQ0IDAgMCAxIDUuMzMzMzMzLTUuOTczMzM0eiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik0yODggNjUwLjY2NjY2N2EyNjYuNjY2NjY3IDIyNCA5MCAxIDAgNDQ4IDAgMjY2LjY2NjY2NyAyMjQgOTAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik02ODAuMTA2NjY3IDQ3NC4yNGEyMjQgMjI0IDAgMCAxLTMzNi4yMTMzMzQgMEEyMTAuNTYgMjEwLjU2IDAgMCAxIDUxMiAzODRhMjEwLjU2IDIxMC41NiAwIDAgMSAxNjguMTA2NjY3IDkwLjI0eiIgZmlsbD0iI0VCQ0IwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMjY2LjY2NjY2N20tMjI0IDBhMjI0IDIyNCAwIDEgMCA0NDggMCAyMjQgMjI0IDAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMTAxLjk3MzMzM2EyMjQuMjEzMzMzIDIyNC4yMTMzMzMgMCAwIDEgMjIxLjg2NjY2NyAxOTQuMzQ2NjY3IDIyNCAyMjQgMCAxIDAtNDQzLjczMzMzNCAwQTIyMy43ODY2NjcgMjIzLjc4NjY2NyAwIDAgMSA1MTIgMTAxLjk3MzMzM3oiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48cGF0aCBkPSJNODMzLjQ5MzMzMyA1OTguODI2NjY3bC03MC40IDcwLjRhMTguMzQ2NjY3IDE4LjM0NjY2NyAwIDAgMS0yNS44MTMzMzMgMEw2NDQuMDUzMzMzIDU3NkExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAwIDYxOC42NjY2NjcgNTc2bC05My4wMTMzMzQgOTMuMjI2NjY3YTE4Ljc3MzMzMyAxOC43NzMzMzMgMCAwIDEtMjYuMDI2NjY2IDBMNDA1LjMzMzMzMyA1NzZhMTguMTMzMzMzIDE4LjEzMzMzMyAwIDAgMC0yNS44MTMzMzMgMGwtOTMuMjI2NjY3IDkzLjIyNjY2N2ExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwbC03MC40LTcwLjRhOS4xNzMzMzMgOS4xNzMzMzMgMCAwIDAtMTUuNzg2NjY3IDYuODI2NjY2QzE4OS44NjY2NjcgODQxLjgxMzMzMyAzMzQuOTMzMzMzIDk2MCA1MTIgOTYwczMyMi4xMzMzMzMtMTE4LjE4NjY2NyAzMzcuMjgtMzU0LjM0NjY2N2E5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZ6IiBmaWxsPSIjRjBGMEYwIj48L3BhdGg+PHBhdGggZD0iTTE5MC41MDY2NjcgNjY1LjgxMzMzM2w3MC40IDYxLjIyNjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAwIDI1LjgxMzMzMyAwbDkzLjIyNjY2Ny04MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMSAyNS44MTMzMzMgMGw5My4yMjY2NjcgODEuMDY2NjY3YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMjYuMDI2NjY2IDBsOTMuNjUzMzM0LTgxLjA2NjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAxIDI2LjAyNjY2NiAwbDkzLjIyNjY2NyA4MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMCAyNS44MTMzMzMgMGw3MC40LTYxLjIyNjY2N2E5LjgxMzMzMyA5LjgxMzMzMyAwIDAgMSA4LjUzMzMzNC0xLjkyIDUzNi4xMDY2NjcgNTM2LjEwNjY2NyAwIDAgMCA3LjI1MzMzMy01OC4yNCA5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZsLTcwLjQgNzAuNGExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwTDY0NC4wNTMzMzMgNTc2QTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDAgNjE4LjY2NjY2NyA1NzZsLTkzLjAxMzMzNCA5My4yMjY2NjdhMTguNzczMzMzIDE4Ljc3MzMzMyAwIDAgMS0yNi4wMjY2NjYgMEw0MDUuMzMzMzMzIDU3NmExOC4xMzMzMzMgMTguMTMzMzMzIDAgMCAwLTI1LjgxMzMzMyAwbC05My4yMjY2NjcgOTMuMjI2NjY3YTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDEtMjUuODEzMzMzIDBsLTcwLjQtNzAuNGE5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjcgNi44MjY2NjYgNTM3LjE3MzMzMyA1MzcuMTczMzMzIDAgMCAwIDcuMDQgNTguMjQgMTAuMDI2NjY3IDEwLjAyNjY2NyAwIDAgMSA5LjE3MzMzNCAxLjkyeiIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPjxwYXRoIGQ9Ik00OTguMTMzMzMzIDE4NC41MzMzMzNtLTI5LjAxMzMzMyAwYTI5LjAxMzMzMyAyOS4wMTMzMzMgMCAxIDAgNTguMDI2NjY3IDAgMjkuMDEzMzMzIDI5LjAxMzMzMyAwIDEgMC01OC4wMjY2NjcgMFoiIGZpbGw9IiM0NTQxM0MiPjwvcGF0aD48cGF0aCBkPSJNNTc2IDI5MC45ODY2NjdjMCAxMS43MzMzMzMtMTUuNzg2NjY3IDIxLjMzMzMzMy0zNC45ODY2NjcgMjEuMzMzMzMzcy0zNS4yLTkuNi0zNS4yLTIxLjMzMzMzMyAxNS43ODY2NjctMjEuMzMzMzMzIDM1LjItMjEuMzMzMzM0IDM0Ljk4NjY2NyA5LjYgMzQuOTg2NjY3IDIxLjMzMzMzNHoiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48L3N2Zz4=">
|
|
8
|
-
<style>
|
|
9
|
-
:root {
|
|
10
|
-
--bg: #f5f6f8; --surface: #ffffff; --surface-2: #fafafa; --tertiary: #f0f0f0; --hover: #f5f6f8; --active: #eef0f3;
|
|
11
|
-
--border-subtle: #eeeeee; --border: #e5e5e5; --border-strong: #cccccc;
|
|
12
|
-
--text: #1f1f1f; --text-2: #5f6368; --text-3: #999999;
|
|
13
|
-
--btn-bg: #1f1f1f; --btn-fg: #ffffff;
|
|
14
|
-
--accent-rgb: 34, 197, 94; --accent: #22c55e;
|
|
15
|
-
--ok: #16a34a; --warn: #d97706; --err: #dc2626; --err-soft: rgba(239, 68, 68, .1); --warn-soft: rgba(245, 158, 11, .12);
|
|
16
|
-
--shadow: 0 10px 40px rgba(0, 0, 0, .12); --toast-shadow: 0 6px 24px rgb(0 0 0 / 14%);
|
|
17
|
-
--mask: rgba(0, 0, 0, .4);
|
|
18
|
-
}
|
|
19
|
-
html[data-theme="dark"] {
|
|
20
|
-
--bg: #0a0a0c; --surface: #131316; --surface-2: #17171b; --tertiary: #1b1b20; --hover: rgba(255,255,255,.06); --active: rgba(255,255,255,.1);
|
|
21
|
-
--border-subtle: #202025; --border: rgba(255,255,255,.13); --border-strong: #3a3a42;
|
|
22
|
-
--text: #e6e6e9; --text-2: rgba(230,230,233,.72); --text-3: rgba(230,230,233,.5);
|
|
23
|
-
--btn-bg: rgba(255,255,255,.92); --btn-fg: #0a0a0c;
|
|
24
|
-
--accent-rgb: 127, 119, 221; --accent: #7f77dd;
|
|
25
|
-
--ok: #2ee59d; --warn: #ffb03a; --err: #ff6b6b; --err-soft: rgba(255,107,107,.12); --warn-soft: rgba(255,176,58,.12);
|
|
26
|
-
--shadow: 0 10px 40px rgba(0,0,0,.5); --toast-shadow: 0 6px 24px rgb(0 0 0 / 45%);
|
|
27
|
-
--mask: rgba(0, 0, 0, .55);
|
|
28
|
-
}
|
|
29
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
30
|
-
body {
|
|
31
|
-
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
32
|
-
font-size: 13px; color: var(--text); background: var(--bg); min-height: 100vh;
|
|
33
|
-
-webkit-font-smoothing: antialiased; transition: background .2s;
|
|
34
|
-
}
|
|
35
|
-
button, input, select { font-family: inherit; font-size: inherit; color: inherit; }
|
|
36
|
-
.wrap { max-width: 1120px; margin: 0 auto; padding: 18px 22px 40px; }
|
|
37
|
-
.num { font-variant-numeric: tabular-nums; }
|
|
38
|
-
|
|
39
|
-
/* header */
|
|
40
|
-
.head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 4px 2px 16px; }
|
|
41
|
-
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
42
|
-
.logo { width: 36px; height: 36px; flex-shrink: 0; display: block; }
|
|
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
|
-
.ver { color: var(--text-3); font-size: 11px; line-height: 1.3; font-variant-numeric: tabular-nums; }
|
|
45
|
-
.head-right { display: flex; align-items: center; gap: 10px; color: var(--text-3); font-size: 12px; }
|
|
46
|
-
|
|
47
|
-
/* shell panel */
|
|
48
|
-
.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; }
|
|
49
|
-
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border-subtle); flex-wrap: wrap; }
|
|
50
|
-
.summary { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
51
|
-
.stat { display: flex; align-items: baseline; gap: 5px; white-space: nowrap; }
|
|
52
|
-
.stat span { color: var(--text-3); font-weight: 500; }
|
|
53
|
-
.stat strong { font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
54
|
-
.divider { width: 1px; height: 16px; background: var(--border-subtle); }
|
|
55
|
-
.actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
56
|
-
|
|
57
|
-
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 11px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2); color: var(--text-2); font-size: 12px; font-weight: 600; cursor: pointer; transition: all .15s; line-height: 1.2; white-space: nowrap; }
|
|
58
|
-
.btn:hover { background: var(--hover); border-color: var(--border-strong); color: var(--text); }
|
|
59
|
-
.btn.primary { background: var(--btn-bg); color: var(--btn-fg); border-color: var(--btn-bg); }
|
|
60
|
-
.btn.primary:hover { opacity: .88; }
|
|
61
|
-
.btn.danger { background: #c62828; border-color: #c62828; color: #fff; }
|
|
62
|
-
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
63
|
-
.btn svg, .icon-btn svg { width: 15px; height: 15px; flex-shrink: 0; }
|
|
64
|
-
.icon-btn { width: 28px; height: 28px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid transparent; background: var(--hover); color: var(--text-2); cursor: pointer; transition: all .15s; padding: 0; }
|
|
65
|
-
.icon-btn:hover { background: var(--active); color: var(--text); }
|
|
66
|
-
.icon-btn.del:hover { background: #ffecec; color: #f53f3f; }
|
|
67
|
-
html[data-theme="dark"] .icon-btn.del:hover { background: rgba(245,63,63,.15); }
|
|
68
|
-
.icon-btn.del.armed { background: #f53f3f; color: #fff; }
|
|
69
|
-
.icon-btn.busy svg { animation: spin 1s linear infinite; }
|
|
70
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
71
|
-
|
|
72
|
-
.notice { margin: 12px 16px 0; padding: 9px 12px; border-radius: 10px; background: var(--warn-soft); color: var(--warn); font-size: 12px; line-height: 1.6; }
|
|
73
|
-
|
|
74
|
-
/* cards */
|
|
75
|
-
.list { display: grid; grid-template-columns: repeat(auto-fill, minmax(440px, 1fr)); gap: 4px 10px; padding: 10px; }
|
|
76
|
-
.card { position: relative; padding: 11px 12px 12px; border-radius: 12px; transition: background .12s; min-width: 0; }
|
|
77
|
-
.card:hover { background: var(--hover); }
|
|
78
|
-
.row1 { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 28px; margin-bottom: 7px; }
|
|
79
|
-
.name-group { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; }
|
|
80
|
-
.name { font-size: 14px; font-weight: 600; cursor: text; border-radius: 5px; padding: 0 2px; margin: 0 -2px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
81
|
-
.name:hover { background: var(--active); }
|
|
82
|
-
.badge { display: inline-flex; align-items: center; font-size: 10px; line-height: 1; padding: 3px 7px; border-radius: 999px; font-weight: 500; background: var(--tertiary); color: var(--text-2); white-space: nowrap; }
|
|
83
|
-
.badge.intl { color: #3b82f6; background: rgba(59,130,246,.1); }
|
|
84
|
-
.badge.cn { color: #d97706; background: rgba(245,158,11,.12); }
|
|
85
|
-
.badge.prod { font-weight: 600; }
|
|
86
|
-
.badge.prod.qoder { color: #6d28d9; background: rgba(124,58,237,.1); }
|
|
87
|
-
.badge.prod.workbuddy { color: #0f766e; background: rgba(20,184,166,.12); }
|
|
88
|
-
html[data-theme="dark"] .badge.prod.qoder { color: #b8a4ff; }
|
|
89
|
-
html[data-theme="dark"] .badge.prod.workbuddy { color: #5eead4; }
|
|
90
|
-
.badge.prod.zcode { color: #1d4ed8; background: rgba(37,99,235,.1); }
|
|
91
|
-
html[data-theme="dark"] .badge.prod.zcode { color: #93c5fd; }
|
|
92
|
-
.seg-ctl { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
|
|
93
|
-
.seg-ctl button { border: none; background: transparent; color: var(--text-2); font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 7px; cursor: pointer; }
|
|
94
|
-
.seg-ctl button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
|
95
|
-
html[data-theme="dark"] .seg-ctl button.active { background: rgba(255,255,255,.1); }
|
|
96
|
-
/* 胶囊徽章:不要类型背景色,细边框 + 彩色文字即可 */
|
|
97
|
-
.pill { display: inline-flex; align-items: center; height: 22px; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; border: 1px solid var(--border); background: transparent; color: var(--text-2); }
|
|
98
|
-
.pill.ok { color: var(--text); border-color: var(--text); }
|
|
99
|
-
.pill.pending { color: var(--text-3); }
|
|
100
|
-
.pill.limited { color: var(--warn); border-color: var(--warn-soft); }
|
|
101
|
-
.pill.fail { color: var(--err); border-color: var(--err-soft); }
|
|
102
|
-
.pill.cur { color: #0f766e; border-color: rgba(20,184,166,.5); }
|
|
103
|
-
html[data-theme="dark"] .pill.cur { color: #5eead4; }
|
|
104
|
-
.ops { display: flex; gap: 6px; flex-shrink: 0; }
|
|
105
|
-
|
|
106
|
-
.meta { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; font-size: 12px; }
|
|
107
|
-
.mi { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
108
|
-
.mi.right { justify-content: flex-end; flex-shrink: 0; }
|
|
109
|
-
.lbl { color: var(--text-3); flex-shrink: 0; white-space: nowrap; }
|
|
110
|
-
.val { color: var(--text); font-variant-numeric: tabular-nums; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
111
|
-
.val.warn { color: #f53f3f; font-weight: 600; }
|
|
112
|
-
|
|
113
|
-
.credit-line { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; font-size: 12px; }
|
|
114
|
-
.credit { display: inline-flex; align-items: center; gap: 4px; font-variant-numeric: tabular-nums; }
|
|
115
|
-
.credit b { font-weight: 700; }
|
|
116
|
-
.credit svg { width: 13px; height: 13px; color: rgb(var(--accent-rgb)); }
|
|
117
|
-
.dim { color: var(--text-3); }
|
|
118
|
-
.claim-plan { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 2px; border-bottom: 1px solid var(--border); }
|
|
119
|
-
.claim-plan:last-of-type { border-bottom: 0; }
|
|
120
|
-
.claim-plan .grants { font-size: 12px; margin-top: 2px; }
|
|
121
|
-
#cap-holder { margin-top: 10px; min-height: 0; }
|
|
122
|
-
#cap-btn { margin-top: 10px; }
|
|
123
|
-
.bar { display: flex; gap: 2px; width: 100%; height: 5px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); border-radius: 3px; overflow: hidden; }
|
|
124
|
-
.seg { display: block; height: 5px; min-width: 3px; background: rgba(var(--accent-rgb), var(--a, 1)); transition: filter .15s; }
|
|
125
|
-
.seg.used { background: color-mix(in srgb, var(--text-3) 35%, transparent); }
|
|
126
|
-
.seg:hover { filter: brightness(1.12); }
|
|
127
|
-
.msg { margin-top: 7px; font-size: 11px; color: var(--text-3); line-height: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
128
|
-
.msg.err { color: var(--err); }
|
|
129
|
-
.empty { grid-column: 1 / -1; text-align: center; color: var(--text-3); padding: 42px 12px; font-size: 13px; line-height: 1.9; }
|
|
130
|
-
|
|
131
|
-
/* section card */
|
|
132
|
-
.pcard { margin-top: 14px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 12px 14px; }
|
|
133
|
-
.pcard-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; font-weight: 700; }
|
|
134
|
-
.logs { margin-top: 8px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; line-height: 1.65; max-height: 240px; overflow-y: auto; background: var(--tertiary); border-radius: 9px; padding: 8px 10px; }
|
|
135
|
-
.logs .info { color: var(--text-2); } .logs .warn { color: var(--warn); } .logs .error { color: var(--err); } .logs .debug { color: var(--text-3); }
|
|
136
|
-
.collapsed .logs { display: none; }
|
|
137
|
-
|
|
138
|
-
/* modal */
|
|
139
|
-
.mask { position: fixed; inset: 0; z-index: 50; background: var(--mask); display: none; align-items: center; justify-content: center; padding: 16px; }
|
|
140
|
-
.mask.show { display: flex; }
|
|
141
|
-
.modal { width: 560px; max-width: 100%; max-height: 90vh; overflow-y: auto; background: var(--surface); border-radius: 14px; padding: 16px 18px; box-shadow: var(--shadow); }
|
|
142
|
-
.modal.sm { width: 360px; }
|
|
143
|
-
.modal-title { display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight: 700; margin-bottom: 12px; }
|
|
144
|
-
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
|
|
145
|
-
.close { border: none; background: none; color: var(--text-3); font-size: 16px; cursor: pointer; padding: 2px 6px; border-radius: 6px; }
|
|
146
|
-
.close:hover { background: var(--hover); color: var(--text); }
|
|
147
|
-
.tabs { display: flex; gap: 6px; margin-bottom: 14px; }
|
|
148
|
-
.tab { flex: 1; padding: 7px 4px; border: none; border-radius: 10px; background: var(--hover); color: var(--text-2); font-size: 12px; font-weight: 600; cursor: pointer; transition: all .18s; }
|
|
149
|
-
.tab:hover { color: var(--text); }
|
|
150
|
-
.tab.active { background: var(--btn-bg); color: var(--btn-fg); }
|
|
151
|
-
.pane { display: none; } .pane.active { display: block; }
|
|
152
|
-
.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }
|
|
153
|
-
.field label { font-size: 12px; color: var(--text-2); font-weight: 600; }
|
|
154
|
-
.input { width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface-2); outline: none; transition: border-color .15s; }
|
|
155
|
-
.input:focus { border-color: #4f86ff; }
|
|
156
|
-
.input.mono { font-family: ui-monospace, Consolas, monospace; }
|
|
157
|
-
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
158
|
-
.hint { font-size: 12px; color: var(--text-3); line-height: 1.7; }
|
|
159
|
-
.hint a { color: #4f86ff; word-break: break-all; }
|
|
160
|
-
.box { border: 1px solid var(--border-subtle); border-radius: 10px; padding: 10px 12px; margin-top: 10px; background: var(--surface-2); }
|
|
161
|
-
.cand { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border-subtle); }
|
|
162
|
-
.cand:last-child { border-bottom: none; }
|
|
163
|
-
.cand-main { min-width: 0; }
|
|
164
|
-
.cand-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
165
|
-
.cand-sub { font-size: 11px; color: var(--text-3); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
166
|
-
|
|
167
|
-
.notice.info { background: var(--tertiary); color: var(--text-2); }
|
|
168
|
-
|
|
169
|
-
/* 顶部标签页 */
|
|
170
|
-
.nav { display: flex; gap: 4px; padding: 4px; margin-bottom: 14px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 12px; width: fit-content; max-width: 100%; overflow-x: auto; }
|
|
171
|
-
.nav button { display: inline-flex; align-items: center; gap: 6px; border: none; background: transparent; color: var(--text-2); font-size: 13px; font-weight: 600; padding: 7px 14px; border-radius: 9px; cursor: pointer; white-space: nowrap; transition: background .15s, color .15s; }
|
|
172
|
-
.nav button:hover { color: var(--text); background: var(--hover); }
|
|
173
|
-
.nav button.active { background: var(--btn-bg); color: var(--btn-fg); }
|
|
174
|
-
.nav .cnt { font-size: 11px; font-weight: 600; padding: 1px 6px; border-radius: 999px; background: var(--tertiary); color: var(--text-3); font-variant-numeric: tabular-nums; }
|
|
175
|
-
.nav button.active .cnt { background: rgba(255,255,255,.18); color: inherit; }
|
|
176
|
-
html[data-theme="dark"] .nav button.active .cnt { background: rgba(0,0,0,.12); }
|
|
177
|
-
.view { display: none; } .view.active { display: block; }
|
|
178
|
-
|
|
179
|
-
/* 仪表盘 */
|
|
180
|
-
.dash-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
181
|
-
.dash-title { font-size: 15px; font-weight: 700; }
|
|
182
|
-
.kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; }
|
|
183
|
-
.kpi { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 16px; min-width: 0; }
|
|
184
|
-
.kpi-label { color: var(--text-3); font-size: 12px; font-weight: 500; }
|
|
185
|
-
.kpi-value { font-size: 26px; font-weight: 700; letter-spacing: -.5px; margin-top: 4px; font-variant-numeric: tabular-nums; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
186
|
-
.kpi-value small { font-size: 14px; color: var(--text-3); font-weight: 600; letter-spacing: 0; }
|
|
187
|
-
.kpi-value .kpi-side { font-size: 13px; color: var(--text-2); font-weight: 600; letter-spacing: 0; }
|
|
188
|
-
.kpi-sub { color: var(--text-3); font-size: 11px; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
189
|
-
.prods { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; margin-bottom: 10px; }
|
|
190
|
-
.prodcard { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 16px; cursor: pointer; transition: border-color .15s, box-shadow .15s; display: flex; flex-direction: column; gap: 10px; min-width: 0; }
|
|
191
|
-
.prodcard:hover { border-color: var(--border-strong); box-shadow: 0 2px 10px rgba(0,0,0,.05); }
|
|
192
|
-
.prod-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
193
|
-
.prod-name { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
|
|
194
|
-
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
195
|
-
.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; }
|
|
197
|
-
.prodcard:hover .prod-go { color: var(--text); }
|
|
198
|
-
.prod-rows { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 7px 12px; font-size: 12px; align-items: center; }
|
|
199
|
-
.prod-rows .val { text-align: right; }
|
|
200
|
-
.meter { height: 5px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); border-radius: 3px; overflow: hidden; }
|
|
201
|
-
.meter span { display: block; height: 100%; background: rgb(var(--accent-rgb)); border-radius: 3px; }
|
|
202
|
-
.dash-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
203
|
-
.dash-cols .pcard { margin-top: 0; min-width: 0; }
|
|
204
|
-
.item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 4px; margin: 0 -4px; border-bottom: 1px solid var(--border-subtle); font-size: 12px; cursor: pointer; border-radius: 6px; }
|
|
205
|
-
.item:hover { background: var(--hover); }
|
|
206
|
-
.item:last-child { border-bottom: none; }
|
|
207
|
-
.item-main { min-width: 0; display: flex; align-items: center; gap: 6px; }
|
|
208
|
-
.item-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 150px; }
|
|
209
|
-
.item-side { display: flex; align-items: center; gap: 8px; flex-shrink: 0; min-width: 0; }
|
|
210
|
-
.item-time { color: var(--text-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
211
|
-
.muted-empty { color: var(--text-3); font-size: 12px; padding: 16px 0 6px; text-align: center; }
|
|
212
|
-
@media (max-width: 900px) { .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } .prods, .dash-cols { grid-template-columns: 1fr; } }
|
|
213
|
-
|
|
214
|
-
/* 10Router */
|
|
215
|
-
.dot.tenrouter { background: #f59e0b; }
|
|
216
|
-
.badge.prod.tenrouter { color: #b45309; background: rgba(245,158,11,.14); }
|
|
217
|
-
html[data-theme="dark"] .badge.prod.tenrouter { color: #fcd34d; }
|
|
218
|
-
/* 瀑布流:卡片高低差异大(积分包多的卡很长),按列填充避免大片空白 */
|
|
219
|
-
.tr-grid { padding: 12px; display: flex; flex-direction: column; gap: 14px; }
|
|
220
|
-
.tr-vrow { }
|
|
221
|
-
.tr-vhead { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; }
|
|
222
|
-
.tr-vhead .dim { font-size: 12px; }
|
|
223
|
-
.tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 12px 4px; }
|
|
224
|
-
.tr-vcards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; align-items: stretch; }
|
|
225
|
-
@media (max-width: 1180px) { .tr-vcards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
226
|
-
@media (max-width: 820px) { .tr-vcards { grid-template-columns: 1fr; } }
|
|
227
|
-
.tr-card { border: 1px solid var(--border-subtle); border-radius: 12px; padding: 11px 12px 12px; background: var(--surface); min-width: 0; }
|
|
228
|
-
.tr-card.off { opacity: .6; }
|
|
229
|
-
.tr-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
230
|
-
.tr-title { display: flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: wrap; }
|
|
231
|
-
.tr-name { font-weight: 600; font-size: 13px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
232
|
-
.tr-q { margin-top: 7px; }
|
|
233
|
-
.tr-q-line { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; margin-bottom: 4px; }
|
|
234
|
-
.tr-q-name { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
235
|
-
.tr-q-val { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
236
|
-
.tr-q-val.low { color: var(--err); font-weight: 600; }
|
|
237
|
-
.tr-meter { height: 5px; border-radius: 3px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); overflow: hidden; }
|
|
238
|
-
.tr-meter span { display: block; height: 100%; border-radius: 3px; background: rgb(var(--accent-rgb)); }
|
|
239
|
-
.tr-meter span.low { background: var(--err); }
|
|
240
|
-
.tr-meter span.mid { background: #d97706; }
|
|
241
|
-
.tr-reset { color: var(--text-3); font-weight: 400; }
|
|
242
|
-
.tr-reset.soon { color: var(--err); font-weight: 600; }
|
|
243
|
-
.tr-reset { font-size: 11px; color: var(--text-3); margin-top: 3px; }
|
|
244
|
-
.tr-primary { margin: 2px 0 6px; }
|
|
245
|
-
.tr-primary .
|
|
246
|
-
.tr-primary small { white-space: nowrap; }
|
|
247
|
-
.tr-p-line { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 5px; font-size: 12px; }
|
|
248
|
-
.tr-p-val { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
249
|
-
.tr-p-val b { font-size: 20px; font-weight: 700; letter-spacing: -.3px; }
|
|
250
|
-
.tr-p-val small { color: var(--text-3); font-size: 12px; }
|
|
251
|
-
.tr-p-val.low b { color: var(--err); }
|
|
252
|
-
.tr-meter.big { height: 7px; }
|
|
253
|
-
.tr-line { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px; margin-top: 6px; }
|
|
254
|
-
.tr-summary { display: flex; align-items: baseline; gap: 6px; font-size: 12px; margin-top: 6px; }
|
|
255
|
-
.tr-sum-txt { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
256
|
-
.low-text { color: var(--err); font-weight: 600; }
|
|
257
|
-
.tr-q-when { color: var(--text-3); font-weight: 400; }
|
|
258
|
-
.tr-q.spent .tr-q-name, .tr-q.spent .tr-q-val { color: var(--text-3); }
|
|
259
|
-
.tr-packs { margin-top: 9px; padding-top: 8px; border-top: 1px dashed var(--border-subtle); }
|
|
260
|
-
.tr-packs-head { font-size: 11px; color: var(--text-3); margin-bottom: 2px; }
|
|
261
|
-
.tr-packs-head b { color: var(--text-2); }
|
|
262
|
-
.tr-more { margin-top: 7px; border: none; background: none; color: #4f86ff; font-size: 12px; cursor: pointer; padding: 0; }
|
|
263
|
-
.tr-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); font-size: 12px; color: var(--text-2); }
|
|
264
|
-
.chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; font-size: 11px; background: var(--tertiary); color: var(--text-3); }
|
|
265
|
-
.chip.on { background: rgba(var(--accent-rgb), .14); color: var(--text); }
|
|
266
|
-
.setup { padding: 22px; max-width: 620px; }
|
|
267
|
-
.setup h3 { font-size: 15px; margin-bottom: 8px; }
|
|
268
|
-
.check { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-2); margin-right: 12px; cursor: pointer; }
|
|
269
|
-
|
|
270
|
-
/* toast */
|
|
271
|
-
#toast { position: fixed; bottom: 24px; left: 50%; transform: translate(-50%, 8px); z-index: 60; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 9px 14px; font-size: 12px; box-shadow: var(--toast-shadow); opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; max-width: 80vw; }
|
|
272
|
-
#toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
273
|
-
#toast.err { border-color: var(--err); color: var(--err); }
|
|
274
|
-
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
|
|
275
|
-
@media (max-width: 560px) { .list { grid-template-columns: 1fr; } .wrap { padding: 12px; } }
|
|
276
|
-
</style>
|
|
277
|
-
</head>
|
|
278
|
-
<body>
|
|
279
|
-
<div class="wrap">
|
|
280
|
-
<header class="head">
|
|
281
|
-
<div class="brand">
|
|
282
|
-
<img class="logo" alt="" src="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDI0IiBoZWlnaHQ9IjEwMjQiPjxwYXRoIGQ9Ik0zNzIuNDggMTM4LjY2NjY2N0ExMjggMTI4IDAgMCAwIDI5OC42NjY2NjcgMTAxLjU0NjY2N2ExMTcuNzYgMTE3Ljc2IDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzNsNzEuMjUzMzMzIDkxLjUyLTgxLjI4IDI4LjM3MzMzM2ExMC42NjY2NjcgMTAuNjY2NjY3IDAgMCAwLTIuMzQ2NjY3IDE4Ljk4NjY2NyAyNDIuNTYgMjQyLjU2IDAgMCAwIDY5Ljc2IDMyLjg1MzMzMyAyNzMuNzA2NjY3IDI3My43MDY2NjcgMCAwIDAgNTYuOTYgOC4zMnoiIGZpbGw9IiNGRjhBMTQiPjwvcGF0aD48cGF0aCBkPSJNMjQ1Ljc2IDEzOC4wMjY2NjdhMTMwLjk4NjY2NyAxMzAuOTg2NjY3IDAgMCAxIDUyLjA1MzMzMyAyLjEzMzMzM0ExNDguMDUzMzMzIDE0OC4wNTMzMzMgMCAwIDEgMzY2LjcyIDE3MC42NjY2NjdsNS43Ni0zMi44NTMzMzRBMTI4IDEyOCAwIDAgMCAyOTguNjY2NjY3IDEwMS41NDY2NjdhMTE3Ljk3MzMzMyAxMTcuOTczMzMzIDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzN6IiBmaWxsPSIjRkZBQTU0Ij48L3BhdGg+PHBhdGggZD0iTTIzNC42NjY2NjcgOTcwLjY2NjY2N2EyNzcuMzMzMzMzIDMyIDAgMSAwIDU1NC42NjY2NjYgMCAyNzcuMzMzMzMzIDMyIDAgMSAwLTU1NC42NjY2NjYgMFoiIGZpbGw9IiM0NTQxM0MiIG9wYWNpdHk9Ii4xNSI+PC9wYXRoPjxwYXRoIGQ9Ik02MjAuMzczMzMzIDQ2MC41ODY2NjdzMTUxLjA0LTkwLjY2NjY2NyAxODEuMzMzMzM0LTc0LjY2NjY2Ny0xMi41ODY2NjcgMTc0LjI5MzMzMy0xMDYuNjY2NjY3IDIxMy4zMzMzMzMtNzQuNjY2NjY3LTEzOC42NjY2NjctNzQuNjY2NjY3LTEzOC42NjY2NjZ6IiBmaWxsPSIjRkZFNTAwIj48L3BhdGg+PHBhdGggZD0iTTYyMC4zNzMzMzMgNTEzLjkyczE1MS4wNC05MC42NjY2NjcgMTgxLjMzMzMzNC03NC42NjY2NjdhMTQuNzIgMTQuNzIgMCAwIDEgNS4zMzMzMzMgNS45NzMzMzRjNi44MjY2NjctMjkuODY2NjY3IDUuMzMzMzMzLTUzLjU0NjY2Ny01LjMzMzMzMy01OS4zMDY2NjctMzAuMjkzMzMzLTE2LTE4MS4zMzMzMzMgNzQuNjY2NjY3LTE4MS4zMzMzMzQgNzQuNjY2NjY3YTM4Ni45ODY2NjcgMzg2Ljk4NjY2NyAwIDAgMCAwIDYxLjg2NjY2NmMtMC40MjY2NjctNS4zMzMzMzMgMC04LjUzMzMzMyAwLTguNTMzMzMzeiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik00MDMuNjI2NjY3IDQ2MC41ODY2NjdzLTE1MS4wNC05MC42NjY2NjctMTgxLjMzMzMzNC03NC42NjY2NjcgMTIuMzczMzMzIDE3NC4yOTMzMzMgMTA2LjY2NjY2NyAyMTMuMzMzMzMzIDc0LjY2NjY2Ny0xMzguNjY2NjY3IDc0LjY2NjY2Ny0xMzguNjY2NjY2eiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik0yMjIuMjkzMzMzIDQzOS4yNTMzMzNjMzAuMjkzMzMzLTE2IDE4MS4zMzMzMzMgNzQuNjY2NjY3IDE4MS4zMzMzMzQgNzQuNjY2NjY3djguNTMzMzMzYTM3My41NDY2NjcgMzczLjU0NjY2NyAwIDAgMCAwLTYxLjg2NjY2NnMtMTUxLjA0LTkwLjY2NjY2Ny0xODEuMzMzMzM0LTc0LjY2NjY2N2MtMTAuNjY2NjY3IDUuNzYtMTIuMTYgMjkuNDQtNS4zMzMzMzMgNTkuMzA2NjY3YTEzLjQ0IDEzLjQ0IDAgMCAxIDUuMzMzMzMzLTUuOTczMzM0eiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik0yODggNjUwLjY2NjY2N2EyNjYuNjY2NjY3IDIyNCA5MCAxIDAgNDQ4IDAgMjY2LjY2NjY2NyAyMjQgOTAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik02ODAuMTA2NjY3IDQ3NC4yNGEyMjQgMjI0IDAgMCAxLTMzNi4yMTMzMzQgMEEyMTAuNTYgMjEwLjU2IDAgMCAxIDUxMiAzODRhMjEwLjU2IDIxMC41NiAwIDAgMSAxNjguMTA2NjY3IDkwLjI0eiIgZmlsbD0iI0VCQ0IwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMjY2LjY2NjY2N20tMjI0IDBhMjI0IDIyNCAwIDEgMCA0NDggMCAyMjQgMjI0IDAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMTAxLjk3MzMzM2EyMjQuMjEzMzMzIDIyNC4yMTMzMzMgMCAwIDEgMjIxLjg2NjY2NyAxOTQuMzQ2NjY3IDIyNCAyMjQgMCAxIDAtNDQzLjczMzMzNCAwQTIyMy43ODY2NjcgMjIzLjc4NjY2NyAwIDAgMSA1MTIgMTAxLjk3MzMzM3oiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48cGF0aCBkPSJNODMzLjQ5MzMzMyA1OTguODI2NjY3bC03MC40IDcwLjRhMTguMzQ2NjY3IDE4LjM0NjY2NyAwIDAgMS0yNS44MTMzMzMgMEw2NDQuMDUzMzMzIDU3NkExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAwIDYxOC42NjY2NjcgNTc2bC05My4wMTMzMzQgOTMuMjI2NjY3YTE4Ljc3MzMzMyAxOC43NzMzMzMgMCAwIDEtMjYuMDI2NjY2IDBMNDA1LjMzMzMzMyA1NzZhMTguMTMzMzMzIDE4LjEzMzMzMyAwIDAgMC0yNS44MTMzMzMgMGwtOTMuMjI2NjY3IDkzLjIyNjY2N2ExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwbC03MC40LTcwLjRhOS4xNzMzMzMgOS4xNzMzMzMgMCAwIDAtMTUuNzg2NjY3IDYuODI2NjY2QzE4OS44NjY2NjcgODQxLjgxMzMzMyAzMzQuOTMzMzMzIDk2MCA1MTIgOTYwczMyMi4xMzMzMzMtMTE4LjE4NjY2NyAzMzcuMjgtMzU0LjM0NjY2N2E5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZ6IiBmaWxsPSIjRjBGMEYwIj48L3BhdGg+PHBhdGggZD0iTTE5MC41MDY2NjcgNjY1LjgxMzMzM2w3MC40IDYxLjIyNjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAwIDI1LjgxMzMzMyAwbDkzLjIyNjY2Ny04MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMSAyNS44MTMzMzMgMGw5My4yMjY2NjcgODEuMDY2NjY3YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMjYuMDI2NjY2IDBsOTMuNjUzMzM0LTgxLjA2NjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAxIDI2LjAyNjY2NiAwbDkzLjIyNjY2NyA4MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMCAyNS44MTMzMzMgMGw3MC40LTYxLjIyNjY2N2E5LjgxMzMzMyA5LjgxMzMzMyAwIDAgMSA4LjUzMzMzNC0xLjkyIDUzNi4xMDY2NjcgNTM2LjEwNjY2NyAwIDAgMCA3LjI1MzMzMy01OC4yNCA5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZsLTcwLjQgNzAuNGExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwTDY0NC4wNTMzMzMgNTc2QTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDAgNjE4LjY2NjY2NyA1NzZsLTkzLjAxMzMzNCA5My4yMjY2NjdhMTguNzczMzMzIDE4Ljc3MzMzMyAwIDAgMS0yNi4wMjY2NjYgMEw0MDUuMzMzMzMzIDU3NmExOC4xMzMzMzMgMTguMTMzMzMzIDAgMCAwLTI1LjgxMzMzMyAwbC05My4yMjY2NjcgOTMuMjI2NjY3YTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDEtMjUuODEzMzMzIDBsLTcwLjQtNzAuNGE5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjcgNi44MjY2NjYgNTM3LjE3MzMzMyA1MzcuMTczMzMzIDAgMCAwIDcuMDQgNTguMjQgMTAuMDI2NjY3IDEwLjAyNjY2NyAwIDAgMSA5LjE3MzMzNCAxLjkyeiIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPjxwYXRoIGQ9Ik00OTguMTMzMzMzIDE4NC41MzMzMzNtLTI5LjAxMzMzMyAwYTI5LjAxMzMzMyAyOS4wMTMzMzMgMCAxIDAgNTguMDI2NjY3IDAgMjkuMDEzMzMzIDI5LjAxMzMzMyAwIDEgMC01OC4wMjY2NjcgMFoiIGZpbGw9IiM0NTQxM0MiPjwvcGF0aD48cGF0aCBkPSJNNTc2IDI5MC45ODY2NjdjMCAxMS43MzMzMzMtMTUuNzg2NjY3IDIxLjMzMzMzMy0zNC45ODY2NjcgMjEuMzMzMzMzcy0zNS4yLTkuNi0zNS4yLTIxLjMzMzMzMyAxNS43ODY2NjctMjEuMzMzMzMzIDM1LjItMjEuMzMzMzM0IDM0Ljk4NjY2NyA5LjYgMzQuOTg2NjY3IDIxLjMzMzMzNHoiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48L3N2Zz4=">
|
|
283
|
-
<div>
|
|
284
|
-
<div class="title">CreditDaddy</div>
|
|
285
|
-
<div class="ver" id="ver">Qoder / WorkBuddy / ZCode 多账号管理 · 每日积分自动领取</div>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
<div class="head-right">
|
|
289
|
-
<span id="sched" class="num"></span>
|
|
290
|
-
<a class="icon-btn" id="btn-home" href="https://github.com/techysy/CreditDaddy" target="_blank" rel="noopener" title="项目主页(GitHub)"></a>
|
|
291
|
-
<button class="icon-btn" id="btn-theme" title="切换亮色 / 暗色"></button>
|
|
292
|
-
</div>
|
|
293
|
-
</header>
|
|
294
|
-
|
|
295
|
-
<nav class="nav" id="nav">
|
|
296
|
-
<button data-view="dash" class="active">仪表盘</button>
|
|
297
|
-
<button data-view="qoder">Qoder<span class="cnt" id="cnt-qoder">0</span></button>
|
|
298
|
-
<button data-view="workbuddy">WorkBuddy<span class="cnt" id="cnt-workbuddy">0</span></button>
|
|
299
|
-
<button data-view="zcode">ZCode<span class="cnt" id="cnt-zcode">0</span></button>
|
|
300
|
-
<button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
|
|
301
|
-
</nav>
|
|
302
|
-
|
|
303
|
-
<!-- 仪表盘 -->
|
|
304
|
-
<section class="view active" id="view-dash">
|
|
305
|
-
<div class="dash-head">
|
|
306
|
-
<div class="dash-title">总览</div>
|
|
307
|
-
<div class="actions">
|
|
308
|
-
<button class="btn primary" id="btn-checkin-all"></button>
|
|
309
|
-
<button class="btn" id="btn-add-dash"></button>
|
|
310
|
-
<button class="btn" id="btn-import-dash"></button>
|
|
311
|
-
<button class="btn" id="btn-export-dash"></button>
|
|
312
|
-
</div>
|
|
313
|
-
</div>
|
|
314
|
-
<div class="kpis" id="kpis"></div>
|
|
315
|
-
<div class="prods" id="prods"></div>
|
|
316
|
-
<div class="dash-cols">
|
|
317
|
-
<section class="pcard"><div class="pcard-title"><span>最近记录</span><span class="hint" id="last-tick"></span></div><div id="recent"></div></section>
|
|
318
|
-
<section class="pcard"><div class="pcard-title"><span>需要处理</span><span class="hint" id="att-count"></span></div><div id="attention"></div></section>
|
|
319
|
-
</div>
|
|
320
|
-
</section>
|
|
321
|
-
|
|
322
|
-
<!-- 产品页(Qoder / WorkBuddy / ZCode 共用) -->
|
|
323
|
-
<section class="view" id="view-prod">
|
|
324
|
-
<div class="panel">
|
|
325
|
-
<div class="toolbar">
|
|
326
|
-
<div class="summary">
|
|
327
|
-
<div class="stat"><span>账号</span><strong id="s-count">0</strong></div>
|
|
328
|
-
<div class="divider"></div>
|
|
329
|
-
<div id="s-done-wrap" style="display:contents">
|
|
330
|
-
<div class="stat"><span>今日已领</span><strong id="s-done">0</strong></div>
|
|
331
|
-
<div class="divider"></div>
|
|
332
|
-
</div>
|
|
333
|
-
<div class="stat"><span id="s-credits-label">剩余积分</span><strong id="s-credits">-</strong></div>
|
|
334
|
-
<div class="divider" id="region-div"></div>
|
|
335
|
-
<div class="seg-ctl" id="region">
|
|
336
|
-
<button data-r="" class="active">全部</button><button data-r="intl">国际版</button><button data-r="cn">国内版</button>
|
|
337
|
-
</div>
|
|
338
|
-
</div>
|
|
339
|
-
<div class="actions">
|
|
340
|
-
<button class="btn primary" id="btn-checkin"></button>
|
|
341
|
-
<button class="btn" id="btn-add"></button>
|
|
342
|
-
<button class="btn" id="btn-import"></button>
|
|
343
|
-
<button class="btn" id="btn-export"></button>
|
|
344
|
-
</div>
|
|
345
|
-
</div>
|
|
346
|
-
<div class="notice" id="notice" style="display:none"></div>
|
|
347
|
-
<div class="list" id="list"><div class="empty">加载中…</div></div>
|
|
348
|
-
</div>
|
|
349
|
-
</section>
|
|
350
|
-
|
|
351
|
-
<!-- 10Router:其他供应商额度 + 用量同步 -->
|
|
352
|
-
<section class="view" id="view-10r">
|
|
353
|
-
<div class="panel" id="tr-panel"><div class="empty">加载中…</div></div>
|
|
354
|
-
</section>
|
|
355
|
-
|
|
356
|
-
<section class="pcard collapsed" id="log-card">
|
|
357
|
-
<div class="pcard-title">
|
|
358
|
-
<span>运行日志</span>
|
|
359
|
-
<button class="btn" id="btn-logs">展开</button>
|
|
360
|
-
</div>
|
|
361
|
-
<div class="logs" id="logs">(暂无)</div>
|
|
362
|
-
</section>
|
|
363
|
-
</div>
|
|
364
|
-
|
|
365
|
-
<!-- 添加账号 -->
|
|
366
|
-
<div class="mask" id="m-add">
|
|
367
|
-
<div class="modal">
|
|
368
|
-
<div class="modal-title"><span>添加账号</span><button class="close" data-close>✕</button></div>
|
|
369
|
-
<div class="tabs">
|
|
370
|
-
<button class="tab active" data-tab="local">本机导入</button>
|
|
371
|
-
<button class="tab" data-tab="device">浏览器登录</button>
|
|
372
|
-
<button class="tab" data-tab="token">粘贴 Token</button>
|
|
373
|
-
</div>
|
|
374
|
-
|
|
375
|
-
<div class="pane active" data-pane="local">
|
|
376
|
-
<div class="hint" id="local-hint"></div>
|
|
377
|
-
<div class="row" style="margin-top:10px"><button class="btn primary" id="btn-scan">重新扫描</button><span class="hint" id="scan-status"></span></div>
|
|
378
|
-
<div class="box" id="scan-result"><div class="hint">点击「重新扫描」</div></div>
|
|
379
|
-
</div>
|
|
380
|
-
|
|
381
|
-
<div class="pane" data-pane="device">
|
|
382
|
-
<div class="hint" id="dev-hint" style="margin-bottom:10px"></div>
|
|
383
|
-
<div class="field">
|
|
384
|
-
<label>登录到</label>
|
|
385
|
-
<select class="input" id="dev-provider">
|
|
386
|
-
<option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option>
|
|
387
|
-
<option value="workbuddy">WorkBuddy / CodeBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy / CodeBuddy 国际版</option>
|
|
388
|
-
<option value="zcode-bigmodel">ZCode · BigModel(智谱)</option><option value="zcode-zai">ZCode · Z.ai(国际)</option>
|
|
389
|
-
</select>
|
|
390
|
-
</div>
|
|
391
|
-
<div class="row"><button class="btn primary" id="btn-device">生成授权链接</button><button class="btn" id="dev-cancel" style="display:none">取消</button></div>
|
|
392
|
-
<div class="box" id="device-box" style="display:none">
|
|
393
|
-
<div class="hint" id="dev-status"></div>
|
|
394
|
-
<div class="hint" style="margin-top:6px"><a id="dev-link" href="#" target="_blank" rel="noopener"></a></div>
|
|
395
|
-
</div>
|
|
396
|
-
</div>
|
|
397
|
-
|
|
398
|
-
<div class="pane" data-pane="token">
|
|
399
|
-
<div class="field">
|
|
400
|
-
<label>版本</label>
|
|
401
|
-
<select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy">WorkBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy 国际版</option></select>
|
|
402
|
-
</div>
|
|
403
|
-
<div class="field"><label>备注名(可选)</label><input class="input" id="f-name" placeholder="留空则自动读取昵称"></div>
|
|
404
|
-
<div class="field"><label>Token</label><input class="input mono" id="f-token" placeholder="dt-… / jt-… 或 PAT(pt-…,自动兑换)"></div>
|
|
405
|
-
<div class="modal-actions"><button class="btn primary" id="btn-add-token">添加</button></div>
|
|
406
|
-
</div>
|
|
407
|
-
</div>
|
|
408
|
-
</div>
|
|
409
|
-
|
|
410
|
-
<!-- 导出 -->
|
|
411
|
-
<div class="mask" id="m-export">
|
|
412
|
-
<div class="modal sm">
|
|
413
|
-
<div class="modal-title"><span>导出账号</span><button class="close" data-close>✕</button></div>
|
|
414
|
-
<div class="field">
|
|
415
|
-
<label>范围</label>
|
|
416
|
-
<select class="input" id="ex-provider"><option value="">全部账号</option><option value="p:qoder">Qoder(全部)</option><option value="qoder">仅 Qoder 国际版</option><option value="qoder-cn">仅 Qoder 国内版</option><option value="p:workbuddy">WorkBuddy(全部)</option><option value="workbuddy">仅 WorkBuddy 国内版</option><option value="workbuddy-intl">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option></select>
|
|
417
|
-
</div>
|
|
418
|
-
<div class="field">
|
|
419
|
-
<label>加密口令(可选)</label>
|
|
420
|
-
<input class="input" id="ex-password" type="password" placeholder="至少 4 位;留空则导出明文">
|
|
421
|
-
</div>
|
|
422
|
-
<div class="hint">设置口令后文件采用 10router 迁移格式加密(scrypt + AES-256-GCM),可直接在 10router 对应提供商页导入(WorkBuddy 对应 10router 的 CodeBuddy)。10router 按提供商导入,建议按单个版本导出。</div>
|
|
423
|
-
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-do-export">导出</button></div>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
|
|
427
|
-
<!-- 口令输入 -->
|
|
428
|
-
<div class="mask" id="m-pass">
|
|
429
|
-
<div class="modal sm">
|
|
430
|
-
<div class="modal-title"><span>输入口令</span><button class="close" data-close>✕</button></div>
|
|
431
|
-
<div class="hint" id="pass-hint">该文件已加密,请输入导出时设置的口令。</div>
|
|
432
|
-
<div class="field" style="margin-top:10px"><input class="input" id="pass-input" type="password"></div>
|
|
433
|
-
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-pass-ok">导入</button></div>
|
|
434
|
-
</div>
|
|
435
|
-
</div>
|
|
436
|
-
|
|
437
|
-
<!-- 10Router 设置 -->
|
|
438
|
-
<div class="mask" id="m-10r">
|
|
439
|
-
<div class="modal">
|
|
440
|
-
<div class="modal-title"><span>10Router 连接设置</span><button class="close" data-close>✕</button></div>
|
|
441
|
-
<div class="hint">填 10Router 的访问地址和仪表盘里创建的<b>虚拟 key(sk-…)</b>。key 只保存在 CreditDaddy 守护进程的数据目录(0600),面板里只显示脱敏值。</div>
|
|
442
|
-
<div class="field" style="margin-top:12px"><label>10Router 地址</label><input class="input mono" id="tr-endpoint" placeholder="http://127.0.0.1:20128 或 http://nas:20128"></div>
|
|
443
|
-
<div class="field"><label>虚拟 key</label><input class="input mono" id="tr-key" placeholder="sk-…" autocomplete="off"></div>
|
|
444
|
-
<div class="field"><label>用量同步(同 10router-sync 插件)</label>
|
|
445
|
-
<div><label class="check"><input type="checkbox" id="tr-auto"> 每小时自动同步本机用量到 10Router</label></div>
|
|
446
|
-
<div id="tr-sources" style="margin-top:6px"></div>
|
|
447
|
-
<div class="hint" style="margin-top:4px">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。10Router 按行签名去重,重复同步不会产生重复数据。</div>
|
|
448
|
-
</div>
|
|
449
|
-
<div class="field"><label>显示</label>
|
|
450
|
-
<div><label class="check"><input type="checkbox" id="tr-hide-empty"> 「全部」视图隐藏没有额度 / 余额为 0 的供应商汇总卡</label></div>
|
|
451
|
-
</div>
|
|
452
|
-
<div class="hint" id="tr-test-msg"></div>
|
|
453
|
-
<div class="modal-actions">
|
|
454
|
-
<button class="btn" id="tr-disconnect" style="margin-right:auto">断开</button>
|
|
455
|
-
<button class="btn" id="tr-test">测试连接</button>
|
|
456
|
-
<button class="btn primary" id="tr-save">保存</button>
|
|
457
|
-
</div>
|
|
458
|
-
</div>
|
|
459
|
-
</div>
|
|
460
|
-
|
|
461
|
-
<!-- ZCode 活动领取 -->
|
|
462
|
-
<div class="mask" id="m-claim">
|
|
463
|
-
<div class="modal">
|
|
464
|
-
<div class="modal-title"><span>活动领取</span><button class="close" data-close>✕</button></div>
|
|
465
|
-
<div class="hint">从 ZCode 官方活动接口领取(billing/preview → billing/claim)。领取时通常静默通过人机验证;触发风控时会弹出验证码,按提示完成即可。</div>
|
|
466
|
-
<div id="claim-body" style="margin-top:10px"></div>
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
469
|
-
|
|
470
|
-
<!-- 通用对话框 -->
|
|
471
|
-
<div class="mask" id="m-dialog">
|
|
472
|
-
<div class="modal sm">
|
|
473
|
-
<div class="modal-title"><span id="dlg-title"></span><button class="close" data-close>✕</button></div>
|
|
474
|
-
<div class="hint" id="dlg-msg" style="white-space:pre-wrap"></div>
|
|
475
|
-
<div class="field" id="dlg-field" style="margin-top:10px"><input class="input" id="dlg-input"></div>
|
|
476
|
-
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="dlg-ok">确定</button></div>
|
|
477
|
-
</div>
|
|
478
|
-
</div>
|
|
479
|
-
|
|
480
|
-
<input type="file" id="file-input" accept=".json,application/json" style="display:none">
|
|
481
|
-
<div id="toast"></div>
|
|
482
|
-
|
|
483
|
-
<script>
|
|
484
|
-
'use strict';
|
|
485
|
-
|
|
486
|
-
// ── 图标(Lucide 风格,currentColor) ──
|
|
487
|
-
const svg = (d) => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>';
|
|
488
|
-
const I = {
|
|
489
|
-
play: svg('<polygon points="6 4 20 12 6 20 6 4"/>'),
|
|
490
|
-
refresh: svg('<path d="M21 12a9 9 0 1 1-2.64-6.36L21 8"/><path d="M21 3v5h-5"/>'),
|
|
491
|
-
trash: svg('<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/>'),
|
|
492
|
-
plus: svg('<path d="M12 5v14M5 12h14"/>'),
|
|
493
|
-
upload: svg('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/>'),
|
|
494
|
-
download: svg('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/>'),
|
|
495
|
-
sun: svg('<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>'),
|
|
496
|
-
moon: svg('<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>'),
|
|
497
|
-
swap: svg('<path d="M7 7h13l-4-4"/><path d="M17 17H4l4 4"/>'),
|
|
498
|
-
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
|
-
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
|
-
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"/>'),
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const $ = (id) => document.getElementById(id);
|
|
504
|
-
const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '' };
|
|
505
|
-
const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'tenrouter'];
|
|
506
|
-
// 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
|
|
507
|
-
const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
|
|
508
|
-
const PRODUCTS = [
|
|
509
|
-
{ id: 'qoder', label: 'Qoder' },
|
|
510
|
-
{ id: 'workbuddy', label: 'WorkBuddy' },
|
|
511
|
-
{ id: 'zcode', label: 'ZCode' },
|
|
512
|
-
];
|
|
513
|
-
const prodLabel = (id) => (PRODUCTS.find((x) => x.id === id) || { label: id }).label;
|
|
514
|
-
const PROV = {
|
|
515
|
-
qoder: { prod: 'qoder', prodLabel: 'Qoder', region: '国际版', cls: 'intl' },
|
|
516
|
-
'qoder-cn': { prod: 'qoder', prodLabel: 'Qoder', region: '国内版', cls: 'cn' },
|
|
517
|
-
workbuddy: { prod: 'workbuddy', prodLabel: 'WorkBuddy', region: '国内版', cls: 'cn' },
|
|
518
|
-
'workbuddy-intl': { prod: 'workbuddy', prodLabel: 'WorkBuddy', region: '国际版', cls: 'intl' },
|
|
519
|
-
zcode: { prod: 'zcode', prodLabel: 'ZCode', region: '', cls: '' },
|
|
520
|
-
};
|
|
521
|
-
// 支持每日签到的产品(ZCode 积分靠需验证码的活动领取,不做签到)
|
|
522
|
-
const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy']);
|
|
523
|
-
const CLIENT_LABEL = { workbuddy: 'WorkBuddy', zcode: 'ZCode' };
|
|
524
|
-
const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
|
|
525
|
-
const provBadges = (p, regionOnly) => { const v = provOf(p); return (regionOnly ? '' : '<span class="badge prod ' + v.prod + '">' + esc(v.prodLabel) + '</span>') + (v.region ? '<span class="badge ' + v.cls + '">' + v.region + '</span>' : ''); };
|
|
526
|
-
|
|
527
|
-
function esc(s) {
|
|
528
|
-
return String(s === null || s === undefined ? '' : s)
|
|
529
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
530
|
-
}
|
|
531
|
-
function fmtNum(n) {
|
|
532
|
-
if (n === null || n === undefined || Number.isNaN(n)) return '-';
|
|
533
|
-
return Number(n).toLocaleString('en-US', { maximumFractionDigits: 2 });
|
|
534
|
-
}
|
|
535
|
-
function fmtTime(iso) {
|
|
536
|
-
if (!iso) return '';
|
|
537
|
-
const d = new Date(iso); if (Number.isNaN(d.getTime())) return '';
|
|
538
|
-
const p = (n) => String(n).padStart(2, '0');
|
|
539
|
-
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
|
|
540
|
-
}
|
|
541
|
-
function mask(s) {
|
|
542
|
-
if (!s) return '';
|
|
543
|
-
if (s.includes('@')) { const [u, d] = s.split('@'); return (u.length > 2 ? u.slice(0, 2) + '***' : u + '*') + '@' + d; }
|
|
544
|
-
return s.length > 10 ? s.slice(0, 6) + '…' + s.slice(-4) : s;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
let toastTimer = null;
|
|
548
|
-
function toast(msg, ok) {
|
|
549
|
-
const t = $('toast');
|
|
550
|
-
t.textContent = msg;
|
|
551
|
-
t.classList.toggle('err', ok === false);
|
|
552
|
-
t.classList.add('show');
|
|
553
|
-
clearTimeout(toastTimer);
|
|
554
|
-
toastTimer = setTimeout(() => t.classList.remove('show'), 4200);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// ── API ──
|
|
558
|
-
// 面板访问密码(fnOS / 设置了 CREDITDADDY_PASSWORD 时需要)。内存里的值优先:面板嵌在飞牛桌面窗口里是跨域 iframe,
|
|
559
|
-
// 浏览器拦截第三方存储时 localStorage 不可用,只靠它会每个请求都 401
|
|
560
|
-
let memKey = null;
|
|
561
|
-
function getKey() {
|
|
562
|
-
if (memKey !== null) return memKey;
|
|
563
|
-
try { return localStorage.getItem('qd_key') || ''; } catch (e) { return ''; }
|
|
564
|
-
}
|
|
565
|
-
function setKey(k) {
|
|
566
|
-
memKey = k;
|
|
567
|
-
try { if (k) localStorage.setItem('qd_key', k); else localStorage.removeItem('qd_key'); } catch (e) {}
|
|
568
|
-
}
|
|
569
|
-
let keyPrompt = null;
|
|
570
|
-
/** 弹一次密码输入框(并发的请求共用同一个);返回 true = 已输入 */
|
|
571
|
-
function promptKey(invalid) {
|
|
572
|
-
if (!keyPrompt) {
|
|
573
|
-
keyPrompt = uiPrompt('输入面板访问密码', '', {
|
|
574
|
-
type: 'password', okText: '进入',
|
|
575
|
-
message: (invalid ? '密码不正确,请重新输入。\n' : '')
|
|
576
|
-
+ 'fnOS:安装时设置的面板访问密码(忘了可以在 fnOS「应用设置」里重设)。\n命令行部署:环境变量 CREDITDADDY_PASSWORD 的值。',
|
|
577
|
-
}).then((k) => { keyPrompt = null; if (k === null || !k.trim()) return false; setKey(k.trim()); return true; });
|
|
578
|
-
}
|
|
579
|
-
return keyPrompt;
|
|
580
|
-
}
|
|
581
|
-
async function api(p, opts = {}, attempt = 0) {
|
|
582
|
-
const o = Object.assign({}, opts, { headers: Object.assign({}, opts.headers, { 'x-qd-key': getKey() }) });
|
|
583
|
-
if (opts.json !== undefined) {
|
|
584
|
-
o.method = opts.method || 'POST';
|
|
585
|
-
o.headers['Content-Type'] = 'application/json';
|
|
586
|
-
o.body = JSON.stringify(opts.json);
|
|
587
|
-
}
|
|
588
|
-
delete o.json;
|
|
589
|
-
const res = await fetch(p, o);
|
|
590
|
-
const data = await res.json().catch(() => ({}));
|
|
591
|
-
// 密码缺失 / 不正确:弹框要密码,输入后自动重试这次请求(不再只报一句错误)
|
|
592
|
-
if (res.status === 401 && data.code === 'PANEL_KEY' && attempt < 3) {
|
|
593
|
-
const hadKey = Boolean(getKey());
|
|
594
|
-
setKey('');
|
|
595
|
-
if (await promptKey(hadKey)) return api(p, opts, attempt + 1);
|
|
596
|
-
}
|
|
597
|
-
if (!res.ok) { const e = new Error(data.error || ('HTTP ' + res.status)); e.code = data.code; e.status = res.status; e.nextAt = data.nextAt; throw e; }
|
|
598
|
-
return data;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// 10Router 汇总卡「隐藏无额度」偏好(面板本地)
|
|
602
|
-
try { state.trHideEmpty = localStorage.getItem('qd_tr_hideempty') === '1'; } catch (e) {}
|
|
603
|
-
|
|
604
|
-
// ── 主题 ──
|
|
605
|
-
function applyTheme(t) {
|
|
606
|
-
document.documentElement.dataset.theme = t;
|
|
607
|
-
$('btn-theme').innerHTML = t === 'dark' ? I.sun : I.moon;
|
|
608
|
-
}
|
|
609
|
-
function initTheme() {
|
|
610
|
-
let t = null;
|
|
611
|
-
try { t = localStorage.getItem('qd_theme'); } catch (e) {}
|
|
612
|
-
if (!t) t = window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
613
|
-
applyTheme(t);
|
|
614
|
-
$('btn-theme').onclick = () => {
|
|
615
|
-
const n = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
616
|
-
applyTheme(n);
|
|
617
|
-
try { localStorage.setItem('qd_theme', n); } catch (e) {}
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// ── 状态 / 渲染 ──
|
|
622
|
-
// 统一「领鸡蛋」语义:Qoder / WorkBuddy 国内/国际 = 领取积分;workbuddy-intl = 保持活跃(不保证奖励)
|
|
623
|
-
function statusOf(a) {
|
|
624
|
-
if (!CHECKIN_PRODUCTS.has(a.product)) return { cls: '', text: '', msg: '' }; // ZCode 无每日领取,不显示徽标
|
|
625
|
-
const intl = a.provider === 'workbuddy-intl';
|
|
626
|
-
const today = state.status && state.status.today;
|
|
627
|
-
const done = state.status && state.status.todayDone && state.status.todayDone[a.id] === today;
|
|
628
|
-
const r = a.lastResult;
|
|
629
|
-
const fresh = r && r.at && state.status && (Date.now() - new Date(r.at).getTime() < 26 * 3600e3);
|
|
630
|
-
if (r && fresh && r.status === 'limited' && done) return { cls: 'limited', text: '本机已领', msg: r.message };
|
|
631
|
-
if (done) {
|
|
632
|
-
if (intl) return { cls: 'ok', text: '今日活跃', msg: r && r.message ? r.message : '' };
|
|
633
|
-
const amt = r && r.status === 'checked-in' && fresh && r.amount ? ' +' + fmtNum(r.amount) : '';
|
|
634
|
-
const streak = r && !intl && r.streakDays ? ' · 连续 ' + r.streakDays + ' 天' : '';
|
|
635
|
-
return { cls: 'ok', text: '今日已领' + amt + streak, msg: r && r.message && !['checked-in', 'already'].includes(r.status) ? r.message : '' };
|
|
636
|
-
}
|
|
637
|
-
const failText = intl ? '活跃失败' : '领取失败';
|
|
638
|
-
if (r && fresh && r.status === 'failed') return { cls: 'fail', text: failText, msg: r.message, err: true };
|
|
639
|
-
if (r && fresh && r.status === 'no-activity') return { cls: 'pending', text: intl ? '待活跃' : '待领取', msg: r.message };
|
|
640
|
-
return { cls: 'pending', text: intl ? '未活跃' : '未领取', msg: '' };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function expiryCell(a) {
|
|
644
|
-
if (!a.expiresAt) return { label: a.isPat ? 'PAT 长期有效' : '—', warn: false };
|
|
645
|
-
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 < 7 * 86400e3 };
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function creditHtml(a) {
|
|
651
|
-
const q = state.quotas[a.id];
|
|
652
|
-
if (!q) return '<div class="credit-line"><span class="dim">读取积分中…</span></div><div class="bar"></div>';
|
|
653
|
-
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
|
-
if (q.empty) return '<div class="credit-line"><span class="dim">无有效套餐(仅免费额度)</span></div><div class="bar"></div>';
|
|
655
|
-
const unit = q.unit ? ' ' + q.unit : '';
|
|
656
|
-
if (q.unit) {
|
|
657
|
-
const items = q.parts.map((x) => {
|
|
658
|
-
const base = esc(x.name) + ' ' + fmtNum(x.remaining) + '/' + fmtNum(x.total) + (x.unit ? ' ' + esc(x.unit) : '');
|
|
659
|
-
if (x.pending && x.startsAt) return base + '(' + fmtTime(x.startsAt).slice(5, 16) + ' 生效)';
|
|
660
|
-
return base + (x.expiresAt ? '(' + fmtTime(x.expiresAt).slice(5, 16) + (x.recurring ? ' 重置)' : ' 到期)') : '');
|
|
661
|
-
}).join(' · ');
|
|
662
|
-
const pct = q.total > 0 ? Math.min(1, q.remaining / q.total) : 0;
|
|
663
|
-
return '<div class="credit-line"><span class="credit"><span class="lbl">剩余</span><b>' + fmtNum(q.remaining) + '</b>' + esc(unit) + '</span>'
|
|
664
|
-
+ '<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><span class="seg used" style="flex:' + (1 - pct).toFixed(4) + ' 1 0"></span></div>';
|
|
666
|
-
}
|
|
667
|
-
const total = q.total || 0;
|
|
668
|
-
let bar = '';
|
|
669
|
-
if (total > 0) {
|
|
670
|
-
q.parts.filter((x) => x.remaining > 0).forEach((x, i) => {
|
|
671
|
-
bar += '<span class="seg" style="flex:' + (x.remaining / total).toFixed(4) + ' 1 0;--a:' + (x.recurring ? 1 : Math.max(.45, .85 - i * .05)) + '" title="'
|
|
672
|
-
+ esc(x.name) + ':剩余 ' + fmtNum(x.remaining) + ' / ' + fmtNum(x.total) + (x.expiresAt ? (x.recurring ? ',' + fmtTime(x.expiresAt).slice(0, 10) + ' 重置' : ',' + fmtTime(x.expiresAt).slice(0, 10) + ' 到期') : '') + '"></span>';
|
|
673
|
-
});
|
|
674
|
-
if (q.used > 0) bar += '<span class="seg used" style="flex:' + (q.used / total).toFixed(4) + ' 1 0" title="已使用 ' + fmtNum(q.used) + '"></span>';
|
|
675
|
-
}
|
|
676
|
-
const soon = q.parts.filter((x) => x.remaining > 0 && !x.recurring && x.expiresAt).sort((x, y) => x.expiresAt.localeCompare(y.expiresAt))[0];
|
|
677
|
-
return '<div class="credit-line">'
|
|
678
|
-
+ '<span class="credit"><span class="lbl">剩余</span>' + I.credit + '<b>' + fmtNum(q.remaining) + '</b></span>'
|
|
679
|
-
+ '<span class="credit dim">' + (soon ? fmtNum(soon.remaining) + ' 于 ' + fmtTime(soon.expiresAt).slice(5, 10) + ' 到期 · ' : '') + '已用 ' + fmtNum(q.used) + ' / 共 ' + fmtNum(total) + (q.exceeded ? ' · <span style="color:var(--err)">已超额</span>' : '') + '</span>'
|
|
680
|
-
+ '</div><div class="bar">' + bar + '</div>';
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function cardHtml(a) {
|
|
684
|
-
const st = statusOf(a);
|
|
685
|
-
const ex = expiryCell(a);
|
|
686
|
-
const ident = a.email || a.phone || (a.uid ? 'UID ' + a.uid.slice(0, 8) : a.tokenMasked);
|
|
687
|
-
const isCurrent = isClientCurrent(a);
|
|
688
|
-
const canCheckin = CHECKIN_PRODUCTS.has(a.product);
|
|
689
|
-
return '<div class="card" data-id="' + esc(a.id) + '">'
|
|
690
|
-
+ '<div class="row1"><div class="name-group">'
|
|
691
|
-
+ '<span class="name" data-act="rename" title="点击修改备注名">' + esc(a.name || '(未命名)') + '</span>'
|
|
692
|
-
+ provBadges(a.provider, true)
|
|
693
|
-
+ (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
|
|
694
|
-
+ (a.isPat ? '<span class="badge">PAT</span>' : '')
|
|
695
|
-
+ (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
|
|
696
|
-
+ (st.text ? '<span class="pill ' + st.cls + '" title="' + esc(a.lastResult ? fmtTime(a.lastResult.at) + ' ' + (a.lastResult.message || a.lastResult.status) : '') + '">' + esc(st.text) + '</span>' : '')
|
|
697
|
-
+ '</div><div class="ops">'
|
|
698
|
-
+ (a.product === 'zcode' ? '<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">' + I.gift + '</button>' : '')
|
|
699
|
-
+ (canCheckin ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="' + (a.provider === 'workbuddy-intl' ? '发送活跃请求' : '立即领取') + '">' + (state.busy[a.id] ? I.refresh : I.play) + '</button>' : '')
|
|
700
|
-
+ (CLIENT_LABEL[a.product] && a.canSwitch && !isCurrent && st.cls !== 'fail' ? '<button class="icon-btn" data-act="switch" title="切换 ' + CLIENT_LABEL[a.product] + ' 客户端到此账号">' + I.swap + '</button>' : '')
|
|
701
|
-
+ '<button class="icon-btn" data-act="quota" title="刷新积分">' + I.refresh + '</button>'
|
|
702
|
-
+ '<button class="icon-btn del" data-act="del" title="删除(点两次确认)">' + I.trash + '</button>'
|
|
703
|
-
+ '</div></div>'
|
|
704
|
-
+ '<div class="meta">'
|
|
705
|
-
+ '<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">有效期至</span><span class="val' + (ex.warn ? ' warn' : '') + '">' + esc(ex.label) + '</span></div>'
|
|
707
|
-
+ '</div>'
|
|
708
|
-
+ creditHtml(a)
|
|
709
|
-
+ (st.msg && !(state.quotas[a.id] && state.quotas[a.id].error && state.quotas[a.id].error.includes(st.msg.slice(-24)))
|
|
710
|
-
? '<div class="msg' + (st.err ? ' err' : '') + '" title="' + esc(st.msg) + '">' + esc(st.msg) + '</div>' : '')
|
|
711
|
-
+ '</div>';
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const isDone = (a) => Boolean(state.status && state.status.todayDone && state.status.todayDone[a.id] === state.status.today);
|
|
715
|
-
const ofProduct = (prod) => state.accounts.filter((a) => a.product === prod);
|
|
716
|
-
const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : null);
|
|
717
|
-
// 客户端当前登录判定:WorkBuddy 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
|
|
718
|
-
function isClientCurrent(a) {
|
|
719
|
-
const s = state.status;
|
|
720
|
-
if (!s) return false;
|
|
721
|
-
if (a.product === 'workbuddy') return Boolean(a.uid && s.workbuddyCurrentUid && s.workbuddyCurrentUid === a.uid);
|
|
722
|
-
if (a.product === 'zcode') {
|
|
723
|
-
const id = s.zcodeCurrentIdentity;
|
|
724
|
-
return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
|
|
725
|
-
}
|
|
726
|
-
return false;
|
|
727
|
-
}
|
|
728
|
-
// 积分合计:只统计「积分」口径(ZCode 是 token 额度,单位不同,不相加)
|
|
729
|
-
function creditSum(list) {
|
|
730
|
-
let sum = 0, known = 0, loading = 0;
|
|
731
|
-
for (const a of list) {
|
|
732
|
-
const q = state.quotas[a.id];
|
|
733
|
-
if (!q) { loading++; continue; }
|
|
734
|
-
if (q.error) continue;
|
|
735
|
-
known++;
|
|
736
|
-
if (!q.unit) sum += q.remaining || 0;
|
|
737
|
-
}
|
|
738
|
-
return { sum, known, loading };
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function render() {
|
|
742
|
-
for (const p of PRODUCTS) $('cnt-' + p.id).textContent = ofProduct(p.id).length;
|
|
743
|
-
$('cnt-tenrouter').textContent = tr.quotas ? tr.quotas.connections.length : tr.config && tr.config.configured ? '…' : '–';
|
|
744
|
-
if (state.view === 'dash') renderDash();
|
|
745
|
-
else if (state.view === 'tenrouter') renderTenrouter();
|
|
746
|
-
else renderProd();
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
const EMPTY_TIP = {
|
|
750
|
-
qoder: '还没有 Qoder 账号。<br>点「添加账号」可一键读取本机 Qoder / Qoder CN 客户端已登录的账号,或用浏览器登录。',
|
|
751
|
-
workbuddy: '还没有 WorkBuddy 账号。<br>点「添加账号」可读取本机 WorkBuddy 客户端(当前 + 历史会话),或用浏览器登录 WorkBuddy / CodeBuddy 账号。',
|
|
752
|
-
zcode: '还没有 ZCode 账号。<br>点「添加账号」可读取本机 ZCode 客户端已登录的账号,或用浏览器登录(BigModel / Z.ai),多个账号可一键切换。',
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
function renderProd() {
|
|
756
|
-
const prod = state.view;
|
|
757
|
-
const all = ofProduct(prod);
|
|
758
|
-
const acc = all.filter((a) => !state.region || provOf(a.provider).cls === state.region);
|
|
759
|
-
const list = $('list');
|
|
760
|
-
if (!acc.length) list.innerHTML = '<div class="empty">' + (all.length ? '该地区下没有账号。' : EMPTY_TIP[prod]) + '</div>';
|
|
761
|
-
else list.innerHTML = acc.map(cardHtml).join('');
|
|
762
|
-
const canCheckin = CHECKIN_PRODUCTS.has(prod);
|
|
763
|
-
$('s-count').textContent = acc.length;
|
|
764
|
-
$('s-done-wrap').style.display = canCheckin ? 'contents' : 'none';
|
|
765
|
-
if (prod === 'workbuddy') {
|
|
766
|
-
// 国内版 = 每日签到送积分;国际版 = 保持活跃(活跃不等于必有奖励)
|
|
767
|
-
const cnAcc = acc.filter((a) => a.provider === 'workbuddy');
|
|
768
|
-
const intlAcc = acc.filter((a) => a.provider === 'workbuddy-intl');
|
|
769
|
-
$('s-done-wrap').innerHTML = (cnAcc.length ? '<div class="stat"><span>领取(国内)</span><strong>' + cnAcc.filter(isDone).length + ' / ' + cnAcc.length + '</strong></div><div class="divider"></div>' : '')
|
|
770
|
-
+ (intlAcc.length ? '<div class="stat"><span>活跃(国际)</span><strong>' + intlAcc.filter(isDone).length + ' / ' + intlAcc.length + '</strong></div><div class="divider"></div>' : '');
|
|
771
|
-
} else {
|
|
772
|
-
// 从 WorkBuddy 页切回来时 s-done 可能被 innerHTML 换没了,先重建结构再写值
|
|
773
|
-
if (!$('s-done')) $('s-done-wrap').innerHTML = '<div class="stat"><span>今日已领</span><strong id="s-done"></strong></div><div class="divider"></div>';
|
|
774
|
-
$('s-done').textContent = acc.filter(isDone).length + ' / ' + acc.length;
|
|
775
|
-
}
|
|
776
|
-
const c = creditSum(acc);
|
|
777
|
-
$('s-credits-label').textContent = prod === 'zcode' ? '有套餐' : '剩余积分';
|
|
778
|
-
$('s-credits').textContent = prod === 'zcode'
|
|
779
|
-
? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
|
|
780
|
-
: c.known ? fmtNum(c.sum) : '-';
|
|
781
|
-
const hasRegions = prod !== 'zcode';
|
|
782
|
-
$('region').style.display = hasRegions ? '' : 'none';
|
|
783
|
-
$('region-div').style.display = hasRegions ? '' : 'none';
|
|
784
|
-
document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
|
|
785
|
-
$('btn-checkin').style.display = canCheckin ? '' : 'none';
|
|
786
|
-
if (!$('btn-checkin').disabled) {
|
|
787
|
-
const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl');
|
|
788
|
-
$('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? '领取 / 活跃全部' : '领取全部 ' + prodLabel(prod));
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function kpi(label, value, sub) {
|
|
793
|
-
return '<div class="kpi"><div class="kpi-label">' + label + '</div><div class="kpi-value">' + value + '</div><div class="kpi-sub" title="' + esc(sub.replace(/<[^>]+>/g, '')) + '">' + sub + '</div></div>';
|
|
794
|
-
}
|
|
795
|
-
const meter = (done, total) => '<div class="meter"><span style="width:' + (total ? Math.round(done / total * 100) : 0) + '%"></span></div>';
|
|
796
|
-
|
|
797
|
-
function renderDash() {
|
|
798
|
-
const s = state.status || {};
|
|
799
|
-
const sch = s.scheduler || {};
|
|
800
|
-
const signable = state.accounts.filter((a) => CHECKIN_PRODUCTS.has(a.product));
|
|
801
|
-
const done = signable.filter(isDone).length;
|
|
802
|
-
const failed = signable.filter((a) => statusOf(a).cls === 'fail').length;
|
|
803
|
-
const credit = creditSum(state.accounts.filter((a) => a.product !== 'zcode'));
|
|
804
|
-
const byProd = PRODUCTS.map((p) => p.label + ' ' + ofProduct(p.id).length).join(' · ');
|
|
805
|
-
const creditByProd = ['qoder', 'workbuddy'].map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
|
|
806
|
-
// 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
|
|
807
|
-
const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl');
|
|
808
|
-
const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
|
|
809
|
-
const doneDom = domAcc.filter(isDone).length;
|
|
810
|
-
const doneIntl = intlAcc.filter(isDone).length;
|
|
811
|
-
$('kpis').innerHTML =
|
|
812
|
-
kpi('账号', state.accounts.length, byProd)
|
|
813
|
-
+ kpi('今日领取' + (intlAcc.length ? ' / 活跃' : ''), intlAcc.length
|
|
814
|
-
? doneDom + ' <small>/ ' + domAcc.length + '</small><small class="kpi-side"> · 活跃 ' + doneIntl + ' / ' + intlAcc.length + '</small>'
|
|
815
|
-
: done + ' <small>/ ' + signable.length + '</small>',
|
|
816
|
-
!signable.length ? '暂无可领取的账号'
|
|
817
|
-
: (doneDom === domAcc.length && doneIntl === intlAcc.length ? (intlAcc.length ? '今天全部完成(国际为活跃请求,不保证奖励)' : '今天全部签完')
|
|
818
|
-
: '待办 ' + (signable.length - done) + (failed ? ' · 失败 ' + failed : '')))
|
|
819
|
-
+ kpi('剩余积分', credit.known ? fmtNum(credit.sum) : '-', credit.loading ? '读取中(' + credit.loading + ' 个账号)…' : creditByProd)
|
|
820
|
-
+ kpi('下次自动领取', sch.ticking ? '领取中…' : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
|
|
821
|
-
sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : '每 ~2 小时一轮');
|
|
822
|
-
|
|
823
|
-
$('prods').innerHTML = PRODUCTS.map((p) => {
|
|
824
|
-
const list = ofProduct(p.id);
|
|
825
|
-
let rows = '';
|
|
826
|
-
if (!list.length) {
|
|
827
|
-
rows = '<span class="lbl">账号</span><span class="val dim">还没有,点击进入添加</span>';
|
|
828
|
-
} else if (CHECKIN_PRODUCTS.has(p.id)) {
|
|
829
|
-
const d = list.filter(isDone).length;
|
|
830
|
-
const c = creditSum(list);
|
|
831
|
-
if (p.id === 'workbuddy') {
|
|
832
|
-
// 国内 =
|
|
833
|
-
const cnList = list.filter((a) => a.provider === 'workbuddy');
|
|
834
|
-
const intlList = list.filter((a) => a.provider === 'workbuddy-intl');
|
|
835
|
-
|
|
836
|
-
if (
|
|
837
|
-
|
|
838
|
-
rows
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
if (
|
|
880
|
-
else if (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
$('
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
+ '
|
|
934
|
-
+ ' <button class="btn
|
|
935
|
-
} else if (hasIntl && !s.riskIdentity) {
|
|
936
|
-
n.style.display = '';
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
n.
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
function
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
function
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
function
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
+ '
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const
|
|
1089
|
-
return '<div class="tr-
|
|
1090
|
-
+ '<span class="tr-q-val">' + esc(
|
|
1091
|
-
+ (
|
|
1092
|
-
+ '</div>';
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
)).
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
: ''
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
+
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
recurring: same[0].recurring,
|
|
1227
|
-
resetAt: same.map((p) => p.resetAt).filter(Boolean).sort()[0] || null,
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
const
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
+
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
+ '
|
|
1369
|
-
+ '<
|
|
1370
|
-
|
|
1371
|
-
+ '<span
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
async function
|
|
1392
|
-
tr.
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
try {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
toast('
|
|
1570
|
-
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
};
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
if (
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
$('
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
$('
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
$('btn-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
$('
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
(
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>CreditDaddy</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDI0IiBoZWlnaHQ9IjEwMjQiPjxwYXRoIGQ9Ik0zNzIuNDggMTM4LjY2NjY2N0ExMjggMTI4IDAgMCAwIDI5OC42NjY2NjcgMTAxLjU0NjY2N2ExMTcuNzYgMTE3Ljc2IDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzNsNzEuMjUzMzMzIDkxLjUyLTgxLjI4IDI4LjM3MzMzM2ExMC42NjY2NjcgMTAuNjY2NjY3IDAgMCAwLTIuMzQ2NjY3IDE4Ljk4NjY2NyAyNDIuNTYgMjQyLjU2IDAgMCAwIDY5Ljc2IDMyLjg1MzMzMyAyNzMuNzA2NjY3IDI3My43MDY2NjcgMCAwIDAgNTYuOTYgOC4zMnoiIGZpbGw9IiNGRjhBMTQiPjwvcGF0aD48cGF0aCBkPSJNMjQ1Ljc2IDEzOC4wMjY2NjdhMTMwLjk4NjY2NyAxMzAuOTg2NjY3IDAgMCAxIDUyLjA1MzMzMyAyLjEzMzMzM0ExNDguMDUzMzMzIDE0OC4wNTMzMzMgMCAwIDEgMzY2LjcyIDE3MC42NjY2NjdsNS43Ni0zMi44NTMzMzRBMTI4IDEyOCAwIDAgMCAyOTguNjY2NjY3IDEwMS41NDY2NjdhMTE3Ljk3MzMzMyAxMTcuOTczMzMzIDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzN6IiBmaWxsPSIjRkZBQTU0Ij48L3BhdGg+PHBhdGggZD0iTTIzNC42NjY2NjcgOTcwLjY2NjY2N2EyNzcuMzMzMzMzIDMyIDAgMSAwIDU1NC42NjY2NjYgMCAyNzcuMzMzMzMzIDMyIDAgMSAwLTU1NC42NjY2NjYgMFoiIGZpbGw9IiM0NTQxM0MiIG9wYWNpdHk9Ii4xNSI+PC9wYXRoPjxwYXRoIGQ9Ik02MjAuMzczMzMzIDQ2MC41ODY2NjdzMTUxLjA0LTkwLjY2NjY2NyAxODEuMzMzMzM0LTc0LjY2NjY2Ny0xMi41ODY2NjcgMTc0LjI5MzMzMy0xMDYuNjY2NjY3IDIxMy4zMzMzMzMtNzQuNjY2NjY3LTEzOC42NjY2NjctNzQuNjY2NjY3LTEzOC42NjY2NjZ6IiBmaWxsPSIjRkZFNTAwIj48L3BhdGg+PHBhdGggZD0iTTYyMC4zNzMzMzMgNTEzLjkyczE1MS4wNC05MC42NjY2NjcgMTgxLjMzMzMzNC03NC42NjY2NjdhMTQuNzIgMTQuNzIgMCAwIDEgNS4zMzMzMzMgNS45NzMzMzRjNi44MjY2NjctMjkuODY2NjY3IDUuMzMzMzMzLTUzLjU0NjY2Ny01LjMzMzMzMy01OS4zMDY2NjctMzAuMjkzMzMzLTE2LTE4MS4zMzMzMzMgNzQuNjY2NjY3LTE4MS4zMzMzMzQgNzQuNjY2NjY3YTM4Ni45ODY2NjcgMzg2Ljk4NjY2NyAwIDAgMCAwIDYxLjg2NjY2NmMtMC40MjY2NjctNS4zMzMzMzMgMC04LjUzMzMzMyAwLTguNTMzMzMzeiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik00MDMuNjI2NjY3IDQ2MC41ODY2NjdzLTE1MS4wNC05MC42NjY2NjctMTgxLjMzMzMzNC03NC42NjY2NjcgMTIuMzczMzMzIDE3NC4yOTMzMzMgMTA2LjY2NjY2NyAyMTMuMzMzMzMzIDc0LjY2NjY2Ny0xMzguNjY2NjY3IDc0LjY2NjY2Ny0xMzguNjY2NjY2eiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik0yMjIuMjkzMzMzIDQzOS4yNTMzMzNjMzAuMjkzMzMzLTE2IDE4MS4zMzMzMzMgNzQuNjY2NjY3IDE4MS4zMzMzMzQgNzQuNjY2NjY3djguNTMzMzMzYTM3My41NDY2NjcgMzczLjU0NjY2NyAwIDAgMCAwLTYxLjg2NjY2NnMtMTUxLjA0LTkwLjY2NjY2Ny0xODEuMzMzMzM0LTc0LjY2NjY2N2MtMTAuNjY2NjY3IDUuNzYtMTIuMTYgMjkuNDQtNS4zMzMzMzMgNTkuMzA2NjY3YTEzLjQ0IDEzLjQ0IDAgMCAxIDUuMzMzMzMzLTUuOTczMzM0eiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik0yODggNjUwLjY2NjY2N2EyNjYuNjY2NjY3IDIyNCA5MCAxIDAgNDQ4IDAgMjY2LjY2NjY2NyAyMjQgOTAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik02ODAuMTA2NjY3IDQ3NC4yNGEyMjQgMjI0IDAgMCAxLTMzNi4yMTMzMzQgMEEyMTAuNTYgMjEwLjU2IDAgMCAxIDUxMiAzODRhMjEwLjU2IDIxMC41NiAwIDAgMSAxNjguMTA2NjY3IDkwLjI0eiIgZmlsbD0iI0VCQ0IwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMjY2LjY2NjY2N20tMjI0IDBhMjI0IDIyNCAwIDEgMCA0NDggMCAyMjQgMjI0IDAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMTAxLjk3MzMzM2EyMjQuMjEzMzMzIDIyNC4yMTMzMzMgMCAwIDEgMjIxLjg2NjY2NyAxOTQuMzQ2NjY3IDIyNCAyMjQgMCAxIDAtNDQzLjczMzMzNCAwQTIyMy43ODY2NjcgMjIzLjc4NjY2NyAwIDAgMSA1MTIgMTAxLjk3MzMzM3oiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48cGF0aCBkPSJNODMzLjQ5MzMzMyA1OTguODI2NjY3bC03MC40IDcwLjRhMTguMzQ2NjY3IDE4LjM0NjY2NyAwIDAgMS0yNS44MTMzMzMgMEw2NDQuMDUzMzMzIDU3NkExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAwIDYxOC42NjY2NjcgNTc2bC05My4wMTMzMzQgOTMuMjI2NjY3YTE4Ljc3MzMzMyAxOC43NzMzMzMgMCAwIDEtMjYuMDI2NjY2IDBMNDA1LjMzMzMzMyA1NzZhMTguMTMzMzMzIDE4LjEzMzMzMyAwIDAgMC0yNS44MTMzMzMgMGwtOTMuMjI2NjY3IDkzLjIyNjY2N2ExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwbC03MC40LTcwLjRhOS4xNzMzMzMgOS4xNzMzMzMgMCAwIDAtMTUuNzg2NjY3IDYuODI2NjY2QzE4OS44NjY2NjcgODQxLjgxMzMzMyAzMzQuOTMzMzMzIDk2MCA1MTIgOTYwczMyMi4xMzMzMzMtMTE4LjE4NjY2NyAzMzcuMjgtMzU0LjM0NjY2N2E5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZ6IiBmaWxsPSIjRjBGMEYwIj48L3BhdGg+PHBhdGggZD0iTTE5MC41MDY2NjcgNjY1LjgxMzMzM2w3MC40IDYxLjIyNjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAwIDI1LjgxMzMzMyAwbDkzLjIyNjY2Ny04MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMSAyNS44MTMzMzMgMGw5My4yMjY2NjcgODEuMDY2NjY3YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMjYuMDI2NjY2IDBsOTMuNjUzMzM0LTgxLjA2NjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAxIDI2LjAyNjY2NiAwbDkzLjIyNjY2NyA4MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMCAyNS44MTMzMzMgMGw3MC40LTYxLjIyNjY2N2E5LjgxMzMzMyA5LjgxMzMzMyAwIDAgMSA4LjUzMzMzNC0xLjkyIDUzNi4xMDY2NjcgNTM2LjEwNjY2NyAwIDAgMCA3LjI1MzMzMy01OC4yNCA5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZsLTcwLjQgNzAuNGExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwTDY0NC4wNTMzMzMgNTc2QTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDAgNjE4LjY2NjY2NyA1NzZsLTkzLjAxMzMzNCA5My4yMjY2NjdhMTguNzczMzMzIDE4Ljc3MzMzMyAwIDAgMS0yNi4wMjY2NjYgMEw0MDUuMzMzMzMzIDU3NmExOC4xMzMzMzMgMTguMTMzMzMzIDAgMCAwLTI1LjgxMzMzMyAwbC05My4yMjY2NjcgOTMuMjI2NjY3YTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDEtMjUuODEzMzMzIDBsLTcwLjQtNzAuNGE5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjcgNi44MjY2NjYgNTM3LjE3MzMzMyA1MzcuMTczMzMzIDAgMCAwIDcuMDQgNTguMjQgMTAuMDI2NjY3IDEwLjAyNjY2NyAwIDAgMSA5LjE3MzMzNCAxLjkyeiIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPjxwYXRoIGQ9Ik00OTguMTMzMzMzIDE4NC41MzMzMzNtLTI5LjAxMzMzMyAwYTI5LjAxMzMzMyAyOS4wMTMzMzMgMCAxIDAgNTguMDI2NjY3IDAgMjkuMDEzMzMzIDI5LjAxMzMzMyAwIDEgMC01OC4wMjY2NjcgMFoiIGZpbGw9IiM0NTQxM0MiPjwvcGF0aD48cGF0aCBkPSJNNTc2IDI5MC45ODY2NjdjMCAxMS43MzMzMzMtMTUuNzg2NjY3IDIxLjMzMzMzMy0zNC45ODY2NjcgMjEuMzMzMzMzcy0zNS4yLTkuNi0zNS4yLTIxLjMzMzMzMyAxNS43ODY2NjctMjEuMzMzMzMzIDM1LjItMjEuMzMzMzM0IDM0Ljk4NjY2NyA5LjYgMzQuOTg2NjY3IDIxLjMzMzMzNHoiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48L3N2Zz4=">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #f5f6f8; --surface: #ffffff; --surface-2: #fafafa; --tertiary: #f0f0f0; --hover: #f5f6f8; --active: #eef0f3;
|
|
11
|
+
--border-subtle: #eeeeee; --border: #e5e5e5; --border-strong: #cccccc;
|
|
12
|
+
--text: #1f1f1f; --text-2: #5f6368; --text-3: #999999;
|
|
13
|
+
--btn-bg: #1f1f1f; --btn-fg: #ffffff;
|
|
14
|
+
--accent-rgb: 34, 197, 94; --accent: #22c55e;
|
|
15
|
+
--ok: #16a34a; --warn: #d97706; --err: #dc2626; --err-soft: rgba(239, 68, 68, .1); --warn-soft: rgba(245, 158, 11, .12);
|
|
16
|
+
--shadow: 0 10px 40px rgba(0, 0, 0, .12); --toast-shadow: 0 6px 24px rgb(0 0 0 / 14%);
|
|
17
|
+
--mask: rgba(0, 0, 0, .4);
|
|
18
|
+
}
|
|
19
|
+
html[data-theme="dark"] {
|
|
20
|
+
--bg: #0a0a0c; --surface: #131316; --surface-2: #17171b; --tertiary: #1b1b20; --hover: rgba(255,255,255,.06); --active: rgba(255,255,255,.1);
|
|
21
|
+
--border-subtle: #202025; --border: rgba(255,255,255,.13); --border-strong: #3a3a42;
|
|
22
|
+
--text: #e6e6e9; --text-2: rgba(230,230,233,.72); --text-3: rgba(230,230,233,.5);
|
|
23
|
+
--btn-bg: rgba(255,255,255,.92); --btn-fg: #0a0a0c;
|
|
24
|
+
--accent-rgb: 127, 119, 221; --accent: #7f77dd;
|
|
25
|
+
--ok: #2ee59d; --warn: #ffb03a; --err: #ff6b6b; --err-soft: rgba(255,107,107,.12); --warn-soft: rgba(255,176,58,.12);
|
|
26
|
+
--shadow: 0 10px 40px rgba(0,0,0,.5); --toast-shadow: 0 6px 24px rgb(0 0 0 / 45%);
|
|
27
|
+
--mask: rgba(0, 0, 0, .55);
|
|
28
|
+
}
|
|
29
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
30
|
+
body {
|
|
31
|
+
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
32
|
+
font-size: 13px; color: var(--text); background: var(--bg); min-height: 100vh;
|
|
33
|
+
-webkit-font-smoothing: antialiased; transition: background .2s;
|
|
34
|
+
}
|
|
35
|
+
button, input, select { font-family: inherit; font-size: inherit; color: inherit; }
|
|
36
|
+
.wrap { max-width: 1120px; margin: 0 auto; padding: 18px 22px 40px; }
|
|
37
|
+
.num { font-variant-numeric: tabular-nums; }
|
|
38
|
+
|
|
39
|
+
/* header */
|
|
40
|
+
.head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 4px 2px 16px; }
|
|
41
|
+
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
42
|
+
.logo { width: 36px; height: 36px; flex-shrink: 0; display: block; }
|
|
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
|
+
.ver { color: var(--text-3); font-size: 11px; line-height: 1.3; font-variant-numeric: tabular-nums; }
|
|
45
|
+
.head-right { display: flex; align-items: center; gap: 10px; color: var(--text-3); font-size: 12px; }
|
|
46
|
+
|
|
47
|
+
/* shell panel */
|
|
48
|
+
.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; }
|
|
49
|
+
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border-subtle); flex-wrap: wrap; }
|
|
50
|
+
.summary { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
51
|
+
.stat { display: flex; align-items: baseline; gap: 5px; white-space: nowrap; }
|
|
52
|
+
.stat span { color: var(--text-3); font-weight: 500; }
|
|
53
|
+
.stat strong { font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
54
|
+
.divider { width: 1px; height: 16px; background: var(--border-subtle); }
|
|
55
|
+
.actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
56
|
+
|
|
57
|
+
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 11px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2); color: var(--text-2); font-size: 12px; font-weight: 600; cursor: pointer; transition: all .15s; line-height: 1.2; white-space: nowrap; }
|
|
58
|
+
.btn:hover { background: var(--hover); border-color: var(--border-strong); color: var(--text); }
|
|
59
|
+
.btn.primary { background: var(--btn-bg); color: var(--btn-fg); border-color: var(--btn-bg); }
|
|
60
|
+
.btn.primary:hover { opacity: .88; }
|
|
61
|
+
.btn.danger { background: #c62828; border-color: #c62828; color: #fff; }
|
|
62
|
+
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
63
|
+
.btn svg, .icon-btn svg { width: 15px; height: 15px; flex-shrink: 0; }
|
|
64
|
+
.icon-btn { width: 28px; height: 28px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid transparent; background: var(--hover); color: var(--text-2); cursor: pointer; transition: all .15s; padding: 0; }
|
|
65
|
+
.icon-btn:hover { background: var(--active); color: var(--text); }
|
|
66
|
+
.icon-btn.del:hover { background: #ffecec; color: #f53f3f; }
|
|
67
|
+
html[data-theme="dark"] .icon-btn.del:hover { background: rgba(245,63,63,.15); }
|
|
68
|
+
.icon-btn.del.armed { background: #f53f3f; color: #fff; }
|
|
69
|
+
.icon-btn.busy svg { animation: spin 1s linear infinite; }
|
|
70
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
71
|
+
|
|
72
|
+
.notice { margin: 12px 16px 0; padding: 9px 12px; border-radius: 10px; background: var(--warn-soft); color: var(--warn); font-size: 12px; line-height: 1.6; }
|
|
73
|
+
|
|
74
|
+
/* cards */
|
|
75
|
+
.list { display: grid; grid-template-columns: repeat(auto-fill, minmax(440px, 1fr)); gap: 4px 10px; padding: 10px; }
|
|
76
|
+
.card { position: relative; padding: 11px 12px 12px; border-radius: 12px; transition: background .12s; min-width: 0; }
|
|
77
|
+
.card:hover { background: var(--hover); }
|
|
78
|
+
.row1 { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 28px; margin-bottom: 7px; }
|
|
79
|
+
.name-group { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; }
|
|
80
|
+
.name { font-size: 14px; font-weight: 600; cursor: text; border-radius: 5px; padding: 0 2px; margin: 0 -2px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
81
|
+
.name:hover { background: var(--active); }
|
|
82
|
+
.badge { display: inline-flex; align-items: center; font-size: 10px; line-height: 1; padding: 3px 7px; border-radius: 999px; font-weight: 500; background: var(--tertiary); color: var(--text-2); white-space: nowrap; }
|
|
83
|
+
.badge.intl { color: #3b82f6; background: rgba(59,130,246,.1); }
|
|
84
|
+
.badge.cn { color: #d97706; background: rgba(245,158,11,.12); }
|
|
85
|
+
.badge.prod { font-weight: 600; }
|
|
86
|
+
.badge.prod.qoder { color: #6d28d9; background: rgba(124,58,237,.1); }
|
|
87
|
+
.badge.prod.workbuddy { color: #0f766e; background: rgba(20,184,166,.12); }
|
|
88
|
+
html[data-theme="dark"] .badge.prod.qoder { color: #b8a4ff; }
|
|
89
|
+
html[data-theme="dark"] .badge.prod.workbuddy { color: #5eead4; }
|
|
90
|
+
.badge.prod.zcode { color: #1d4ed8; background: rgba(37,99,235,.1); }
|
|
91
|
+
html[data-theme="dark"] .badge.prod.zcode { color: #93c5fd; }
|
|
92
|
+
.seg-ctl { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
|
|
93
|
+
.seg-ctl button { border: none; background: transparent; color: var(--text-2); font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 7px; cursor: pointer; }
|
|
94
|
+
.seg-ctl button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
|
95
|
+
html[data-theme="dark"] .seg-ctl button.active { background: rgba(255,255,255,.1); }
|
|
96
|
+
/* 胶囊徽章:不要类型背景色,细边框 + 彩色文字即可 */
|
|
97
|
+
.pill { display: inline-flex; align-items: center; height: 22px; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; border: 1px solid var(--border); background: transparent; color: var(--text-2); }
|
|
98
|
+
.pill.ok { color: var(--text); border-color: var(--text); }
|
|
99
|
+
.pill.pending { color: var(--text-3); }
|
|
100
|
+
.pill.limited { color: var(--warn); border-color: var(--warn-soft); }
|
|
101
|
+
.pill.fail { color: var(--err); border-color: var(--err-soft); }
|
|
102
|
+
.pill.cur { color: #0f766e; border-color: rgba(20,184,166,.5); }
|
|
103
|
+
html[data-theme="dark"] .pill.cur { color: #5eead4; }
|
|
104
|
+
.ops { display: flex; gap: 6px; flex-shrink: 0; }
|
|
105
|
+
|
|
106
|
+
.meta { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; font-size: 12px; }
|
|
107
|
+
.mi { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
108
|
+
.mi.right { justify-content: flex-end; flex-shrink: 0; }
|
|
109
|
+
.lbl { color: var(--text-3); flex-shrink: 0; white-space: nowrap; }
|
|
110
|
+
.val { color: var(--text); font-variant-numeric: tabular-nums; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
111
|
+
.val.warn { color: #f53f3f; font-weight: 600; }
|
|
112
|
+
|
|
113
|
+
.credit-line { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; font-size: 12px; }
|
|
114
|
+
.credit { display: inline-flex; align-items: center; gap: 4px; font-variant-numeric: tabular-nums; }
|
|
115
|
+
.credit b { font-weight: 700; }
|
|
116
|
+
.credit svg { width: 13px; height: 13px; color: rgb(var(--accent-rgb)); }
|
|
117
|
+
.dim { color: var(--text-3); }
|
|
118
|
+
.claim-plan { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 2px; border-bottom: 1px solid var(--border); }
|
|
119
|
+
.claim-plan:last-of-type { border-bottom: 0; }
|
|
120
|
+
.claim-plan .grants { font-size: 12px; margin-top: 2px; }
|
|
121
|
+
#cap-holder { margin-top: 10px; min-height: 0; }
|
|
122
|
+
#cap-btn { margin-top: 10px; }
|
|
123
|
+
.bar { display: flex; gap: 2px; width: 100%; height: 5px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); border-radius: 3px; overflow: hidden; }
|
|
124
|
+
.seg { display: block; height: 5px; min-width: 3px; background: rgba(var(--accent-rgb), var(--a, 1)); transition: filter .15s; }
|
|
125
|
+
.seg.used { background: color-mix(in srgb, var(--text-3) 35%, transparent); }
|
|
126
|
+
.seg:hover { filter: brightness(1.12); }
|
|
127
|
+
.msg { margin-top: 7px; font-size: 11px; color: var(--text-3); line-height: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
128
|
+
.msg.err { color: var(--err); }
|
|
129
|
+
.empty { grid-column: 1 / -1; text-align: center; color: var(--text-3); padding: 42px 12px; font-size: 13px; line-height: 1.9; }
|
|
130
|
+
|
|
131
|
+
/* section card */
|
|
132
|
+
.pcard { margin-top: 14px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 12px 14px; }
|
|
133
|
+
.pcard-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; font-weight: 700; }
|
|
134
|
+
.logs { margin-top: 8px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; line-height: 1.65; max-height: 240px; overflow-y: auto; background: var(--tertiary); border-radius: 9px; padding: 8px 10px; }
|
|
135
|
+
.logs .info { color: var(--text-2); } .logs .warn { color: var(--warn); } .logs .error { color: var(--err); } .logs .debug { color: var(--text-3); }
|
|
136
|
+
.collapsed .logs { display: none; }
|
|
137
|
+
|
|
138
|
+
/* modal */
|
|
139
|
+
.mask { position: fixed; inset: 0; z-index: 50; background: var(--mask); display: none; align-items: center; justify-content: center; padding: 16px; }
|
|
140
|
+
.mask.show { display: flex; }
|
|
141
|
+
.modal { width: 560px; max-width: 100%; max-height: 90vh; overflow-y: auto; background: var(--surface); border-radius: 14px; padding: 16px 18px; box-shadow: var(--shadow); }
|
|
142
|
+
.modal.sm { width: 360px; }
|
|
143
|
+
.modal-title { display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight: 700; margin-bottom: 12px; }
|
|
144
|
+
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
|
|
145
|
+
.close { border: none; background: none; color: var(--text-3); font-size: 16px; cursor: pointer; padding: 2px 6px; border-radius: 6px; }
|
|
146
|
+
.close:hover { background: var(--hover); color: var(--text); }
|
|
147
|
+
.tabs { display: flex; gap: 6px; margin-bottom: 14px; }
|
|
148
|
+
.tab { flex: 1; padding: 7px 4px; border: none; border-radius: 10px; background: var(--hover); color: var(--text-2); font-size: 12px; font-weight: 600; cursor: pointer; transition: all .18s; }
|
|
149
|
+
.tab:hover { color: var(--text); }
|
|
150
|
+
.tab.active { background: var(--btn-bg); color: var(--btn-fg); }
|
|
151
|
+
.pane { display: none; } .pane.active { display: block; }
|
|
152
|
+
.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }
|
|
153
|
+
.field label { font-size: 12px; color: var(--text-2); font-weight: 600; }
|
|
154
|
+
.input { width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface-2); outline: none; transition: border-color .15s; }
|
|
155
|
+
.input:focus { border-color: #4f86ff; }
|
|
156
|
+
.input.mono { font-family: ui-monospace, Consolas, monospace; }
|
|
157
|
+
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
158
|
+
.hint { font-size: 12px; color: var(--text-3); line-height: 1.7; }
|
|
159
|
+
.hint a { color: #4f86ff; word-break: break-all; }
|
|
160
|
+
.box { border: 1px solid var(--border-subtle); border-radius: 10px; padding: 10px 12px; margin-top: 10px; background: var(--surface-2); }
|
|
161
|
+
.cand { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border-subtle); }
|
|
162
|
+
.cand:last-child { border-bottom: none; }
|
|
163
|
+
.cand-main { min-width: 0; }
|
|
164
|
+
.cand-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
165
|
+
.cand-sub { font-size: 11px; color: var(--text-3); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
166
|
+
|
|
167
|
+
.notice.info { background: var(--tertiary); color: var(--text-2); }
|
|
168
|
+
|
|
169
|
+
/* 顶部标签页 */
|
|
170
|
+
.nav { display: flex; gap: 4px; padding: 4px; margin-bottom: 14px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 12px; width: fit-content; max-width: 100%; overflow-x: auto; }
|
|
171
|
+
.nav button { display: inline-flex; align-items: center; gap: 6px; border: none; background: transparent; color: var(--text-2); font-size: 13px; font-weight: 600; padding: 7px 14px; border-radius: 9px; cursor: pointer; white-space: nowrap; transition: background .15s, color .15s; }
|
|
172
|
+
.nav button:hover { color: var(--text); background: var(--hover); }
|
|
173
|
+
.nav button.active { background: var(--btn-bg); color: var(--btn-fg); }
|
|
174
|
+
.nav .cnt { font-size: 11px; font-weight: 600; padding: 1px 6px; border-radius: 999px; background: var(--tertiary); color: var(--text-3); font-variant-numeric: tabular-nums; }
|
|
175
|
+
.nav button.active .cnt { background: rgba(255,255,255,.18); color: inherit; }
|
|
176
|
+
html[data-theme="dark"] .nav button.active .cnt { background: rgba(0,0,0,.12); }
|
|
177
|
+
.view { display: none; } .view.active { display: block; }
|
|
178
|
+
|
|
179
|
+
/* 仪表盘 */
|
|
180
|
+
.dash-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
181
|
+
.dash-title { font-size: 15px; font-weight: 700; }
|
|
182
|
+
.kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; }
|
|
183
|
+
.kpi { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 16px; min-width: 0; }
|
|
184
|
+
.kpi-label { color: var(--text-3); font-size: 12px; font-weight: 500; }
|
|
185
|
+
.kpi-value { font-size: 26px; font-weight: 700; letter-spacing: -.5px; margin-top: 4px; font-variant-numeric: tabular-nums; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
186
|
+
.kpi-value small { font-size: 14px; color: var(--text-3); font-weight: 600; letter-spacing: 0; }
|
|
187
|
+
.kpi-value .kpi-side { font-size: 13px; color: var(--text-2); font-weight: 600; letter-spacing: 0; }
|
|
188
|
+
.kpi-sub { color: var(--text-3); font-size: 11px; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
189
|
+
.prods { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; margin-bottom: 10px; }
|
|
190
|
+
.prodcard { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 16px; cursor: pointer; transition: border-color .15s, box-shadow .15s; display: flex; flex-direction: column; gap: 10px; min-width: 0; }
|
|
191
|
+
.prodcard:hover { border-color: var(--border-strong); box-shadow: 0 2px 10px rgba(0,0,0,.05); }
|
|
192
|
+
.prod-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
193
|
+
.prod-name { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
|
|
194
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
195
|
+
.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; }
|
|
197
|
+
.prodcard:hover .prod-go { color: var(--text); }
|
|
198
|
+
.prod-rows { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 7px 12px; font-size: 12px; align-items: center; }
|
|
199
|
+
.prod-rows .val { text-align: right; }
|
|
200
|
+
.meter { height: 5px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); border-radius: 3px; overflow: hidden; }
|
|
201
|
+
.meter span { display: block; height: 100%; background: rgb(var(--accent-rgb)); border-radius: 3px; }
|
|
202
|
+
.dash-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
203
|
+
.dash-cols .pcard { margin-top: 0; min-width: 0; }
|
|
204
|
+
.item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 4px; margin: 0 -4px; border-bottom: 1px solid var(--border-subtle); font-size: 12px; cursor: pointer; border-radius: 6px; }
|
|
205
|
+
.item:hover { background: var(--hover); }
|
|
206
|
+
.item:last-child { border-bottom: none; }
|
|
207
|
+
.item-main { min-width: 0; display: flex; align-items: center; gap: 6px; }
|
|
208
|
+
.item-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 150px; }
|
|
209
|
+
.item-side { display: flex; align-items: center; gap: 8px; flex-shrink: 0; min-width: 0; }
|
|
210
|
+
.item-time { color: var(--text-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
211
|
+
.muted-empty { color: var(--text-3); font-size: 12px; padding: 16px 0 6px; text-align: center; }
|
|
212
|
+
@media (max-width: 900px) { .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } .prods, .dash-cols { grid-template-columns: 1fr; } }
|
|
213
|
+
|
|
214
|
+
/* 10Router */
|
|
215
|
+
.dot.tenrouter { background: #f59e0b; }
|
|
216
|
+
.badge.prod.tenrouter { color: #b45309; background: rgba(245,158,11,.14); }
|
|
217
|
+
html[data-theme="dark"] .badge.prod.tenrouter { color: #fcd34d; }
|
|
218
|
+
/* 瀑布流:卡片高低差异大(积分包多的卡很长),按列填充避免大片空白 */
|
|
219
|
+
.tr-grid { padding: 12px; display: flex; flex-direction: column; gap: 14px; }
|
|
220
|
+
.tr-vrow { }
|
|
221
|
+
.tr-vhead { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; }
|
|
222
|
+
.tr-vhead .dim { font-size: 12px; }
|
|
223
|
+
.tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 12px 4px; }
|
|
224
|
+
.tr-vcards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; align-items: stretch; }
|
|
225
|
+
@media (max-width: 1180px) { .tr-vcards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
226
|
+
@media (max-width: 820px) { .tr-vcards { grid-template-columns: 1fr; } }
|
|
227
|
+
.tr-card { border: 1px solid var(--border-subtle); border-radius: 12px; padding: 11px 12px 12px; background: var(--surface); min-width: 0; }
|
|
228
|
+
.tr-card.off { opacity: .6; }
|
|
229
|
+
.tr-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
230
|
+
.tr-title { display: flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: wrap; }
|
|
231
|
+
.tr-name { font-weight: 600; font-size: 13px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
232
|
+
.tr-q { margin-top: 7px; }
|
|
233
|
+
.tr-q-line { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; margin-bottom: 4px; }
|
|
234
|
+
.tr-q-name { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
235
|
+
.tr-q-val { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
236
|
+
.tr-q-val.low { color: var(--err); font-weight: 600; }
|
|
237
|
+
.tr-meter { height: 5px; border-radius: 3px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); overflow: hidden; }
|
|
238
|
+
.tr-meter span { display: block; height: 100%; border-radius: 3px; background: rgb(var(--accent-rgb)); }
|
|
239
|
+
.tr-meter span.low { background: var(--err); }
|
|
240
|
+
.tr-meter span.mid { background: #d97706; }
|
|
241
|
+
.tr-reset { color: var(--text-3); font-weight: 400; }
|
|
242
|
+
.tr-reset.soon { color: var(--err); font-weight: 600; }
|
|
243
|
+
.tr-reset { font-size: 11px; color: var(--text-3); margin-top: 3px; }
|
|
244
|
+
.tr-primary { margin: 2px 0 6px; }
|
|
245
|
+
.tr-primary-line .tr-q-val b { font-size: 14px; font-weight: 700; }
|
|
246
|
+
.tr-primary-line .tr-q-val small { white-space: nowrap; }
|
|
247
|
+
.tr-p-line { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 5px; font-size: 12px; }
|
|
248
|
+
.tr-p-val { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
249
|
+
.tr-p-val b { font-size: 20px; font-weight: 700; letter-spacing: -.3px; }
|
|
250
|
+
.tr-p-val small { color: var(--text-3); font-size: 12px; }
|
|
251
|
+
.tr-p-val.low b { color: var(--err); }
|
|
252
|
+
.tr-meter.big { height: 7px; }
|
|
253
|
+
.tr-line { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 12px; margin-top: 6px; }
|
|
254
|
+
.tr-summary { display: flex; align-items: baseline; gap: 6px; font-size: 12px; margin-top: 6px; }
|
|
255
|
+
.tr-sum-txt { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
256
|
+
.low-text { color: var(--err); font-weight: 600; }
|
|
257
|
+
.tr-q-when { color: var(--text-3); font-weight: 400; }
|
|
258
|
+
.tr-q.spent .tr-q-name, .tr-q.spent .tr-q-val { color: var(--text-3); }
|
|
259
|
+
.tr-packs { margin-top: 9px; padding-top: 8px; border-top: 1px dashed var(--border-subtle); }
|
|
260
|
+
.tr-packs-head { font-size: 11px; color: var(--text-3); margin-bottom: 2px; }
|
|
261
|
+
.tr-packs-head b { color: var(--text-2); }
|
|
262
|
+
.tr-more { margin-top: 7px; border: none; background: none; color: #4f86ff; font-size: 12px; cursor: pointer; padding: 0; }
|
|
263
|
+
.tr-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); font-size: 12px; color: var(--text-2); }
|
|
264
|
+
.chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; font-size: 11px; background: var(--tertiary); color: var(--text-3); }
|
|
265
|
+
.chip.on { background: rgba(var(--accent-rgb), .14); color: var(--text); }
|
|
266
|
+
.setup { padding: 22px; max-width: 620px; }
|
|
267
|
+
.setup h3 { font-size: 15px; margin-bottom: 8px; }
|
|
268
|
+
.check { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-2); margin-right: 12px; cursor: pointer; }
|
|
269
|
+
|
|
270
|
+
/* toast */
|
|
271
|
+
#toast { position: fixed; bottom: 24px; left: 50%; transform: translate(-50%, 8px); z-index: 60; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 9px 14px; font-size: 12px; box-shadow: var(--toast-shadow); opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; max-width: 80vw; }
|
|
272
|
+
#toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
273
|
+
#toast.err { border-color: var(--err); color: var(--err); }
|
|
274
|
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
|
|
275
|
+
@media (max-width: 560px) { .list { grid-template-columns: 1fr; } .wrap { padding: 12px; } }
|
|
276
|
+
</style>
|
|
277
|
+
</head>
|
|
278
|
+
<body>
|
|
279
|
+
<div class="wrap">
|
|
280
|
+
<header class="head">
|
|
281
|
+
<div class="brand">
|
|
282
|
+
<img class="logo" alt="" src="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDI0IiBoZWlnaHQ9IjEwMjQiPjxwYXRoIGQ9Ik0zNzIuNDggMTM4LjY2NjY2N0ExMjggMTI4IDAgMCAwIDI5OC42NjY2NjcgMTAxLjU0NjY2N2ExMTcuNzYgMTE3Ljc2IDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzNsNzEuMjUzMzMzIDkxLjUyLTgxLjI4IDI4LjM3MzMzM2ExMC42NjY2NjcgMTAuNjY2NjY3IDAgMCAwLTIuMzQ2NjY3IDE4Ljk4NjY2NyAyNDIuNTYgMjQyLjU2IDAgMCAwIDY5Ljc2IDMyLjg1MzMzMyAyNzMuNzA2NjY3IDI3My43MDY2NjcgMCAwIDAgNTYuOTYgOC4zMnoiIGZpbGw9IiNGRjhBMTQiPjwvcGF0aD48cGF0aCBkPSJNMjQ1Ljc2IDEzOC4wMjY2NjdhMTMwLjk4NjY2NyAxMzAuOTg2NjY3IDAgMCAxIDUyLjA1MzMzMyAyLjEzMzMzM0ExNDguMDUzMzMzIDE0OC4wNTMzMzMgMCAwIDEgMzY2LjcyIDE3MC42NjY2NjdsNS43Ni0zMi44NTMzMzRBMTI4IDEyOCAwIDAgMCAyOTguNjY2NjY3IDEwMS41NDY2NjdhMTE3Ljk3MzMzMyAxMTcuOTczMzMzIDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzN6IiBmaWxsPSIjRkZBQTU0Ij48L3BhdGg+PHBhdGggZD0iTTIzNC42NjY2NjcgOTcwLjY2NjY2N2EyNzcuMzMzMzMzIDMyIDAgMSAwIDU1NC42NjY2NjYgMCAyNzcuMzMzMzMzIDMyIDAgMSAwLTU1NC42NjY2NjYgMFoiIGZpbGw9IiM0NTQxM0MiIG9wYWNpdHk9Ii4xNSI+PC9wYXRoPjxwYXRoIGQ9Ik02MjAuMzczMzMzIDQ2MC41ODY2NjdzMTUxLjA0LTkwLjY2NjY2NyAxODEuMzMzMzM0LTc0LjY2NjY2Ny0xMi41ODY2NjcgMTc0LjI5MzMzMy0xMDYuNjY2NjY3IDIxMy4zMzMzMzMtNzQuNjY2NjY3LTEzOC42NjY2NjctNzQuNjY2NjY3LTEzOC42NjY2NjZ6IiBmaWxsPSIjRkZFNTAwIj48L3BhdGg+PHBhdGggZD0iTTYyMC4zNzMzMzMgNTEzLjkyczE1MS4wNC05MC42NjY2NjcgMTgxLjMzMzMzNC03NC42NjY2NjdhMTQuNzIgMTQuNzIgMCAwIDEgNS4zMzMzMzMgNS45NzMzMzRjNi44MjY2NjctMjkuODY2NjY3IDUuMzMzMzMzLTUzLjU0NjY2Ny01LjMzMzMzMy01OS4zMDY2NjctMzAuMjkzMzMzLTE2LTE4MS4zMzMzMzMgNzQuNjY2NjY3LTE4MS4zMzMzMzQgNzQuNjY2NjY3YTM4Ni45ODY2NjcgMzg2Ljk4NjY2NyAwIDAgMCAwIDYxLjg2NjY2NmMtMC40MjY2NjctNS4zMzMzMzMgMC04LjUzMzMzMyAwLTguNTMzMzMzeiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik00MDMuNjI2NjY3IDQ2MC41ODY2NjdzLTE1MS4wNC05MC42NjY2NjctMTgxLjMzMzMzNC03NC42NjY2NjcgMTIuMzczMzMzIDE3NC4yOTMzMzMgMTA2LjY2NjY2NyAyMTMuMzMzMzMzIDc0LjY2NjY2Ny0xMzguNjY2NjY3IDc0LjY2NjY2Ny0xMzguNjY2NjY2eiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik0yMjIuMjkzMzMzIDQzOS4yNTMzMzNjMzAuMjkzMzMzLTE2IDE4MS4zMzMzMzMgNzQuNjY2NjY3IDE4MS4zMzMzMzQgNzQuNjY2NjY3djguNTMzMzMzYTM3My41NDY2NjcgMzczLjU0NjY2NyAwIDAgMCAwLTYxLjg2NjY2NnMtMTUxLjA0LTkwLjY2NjY2Ny0xODEuMzMzMzM0LTc0LjY2NjY2N2MtMTAuNjY2NjY3IDUuNzYtMTIuMTYgMjkuNDQtNS4zMzMzMzMgNTkuMzA2NjY3YTEzLjQ0IDEzLjQ0IDAgMCAxIDUuMzMzMzMzLTUuOTczMzM0eiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik0yODggNjUwLjY2NjY2N2EyNjYuNjY2NjY3IDIyNCA5MCAxIDAgNDQ4IDAgMjY2LjY2NjY2NyAyMjQgOTAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik02ODAuMTA2NjY3IDQ3NC4yNGEyMjQgMjI0IDAgMCAxLTMzNi4yMTMzMzQgMEEyMTAuNTYgMjEwLjU2IDAgMCAxIDUxMiAzODRhMjEwLjU2IDIxMC41NiAwIDAgMSAxNjguMTA2NjY3IDkwLjI0eiIgZmlsbD0iI0VCQ0IwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMjY2LjY2NjY2N20tMjI0IDBhMjI0IDIyNCAwIDEgMCA0NDggMCAyMjQgMjI0IDAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMTAxLjk3MzMzM2EyMjQuMjEzMzMzIDIyNC4yMTMzMzMgMCAwIDEgMjIxLjg2NjY2NyAxOTQuMzQ2NjY3IDIyNCAyMjQgMCAxIDAtNDQzLjczMzMzNCAwQTIyMy43ODY2NjcgMjIzLjc4NjY2NyAwIDAgMSA1MTIgMTAxLjk3MzMzM3oiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48cGF0aCBkPSJNODMzLjQ5MzMzMyA1OTguODI2NjY3bC03MC40IDcwLjRhMTguMzQ2NjY3IDE4LjM0NjY2NyAwIDAgMS0yNS44MTMzMzMgMEw2NDQuMDUzMzMzIDU3NkExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAwIDYxOC42NjY2NjcgNTc2bC05My4wMTMzMzQgOTMuMjI2NjY3YTE4Ljc3MzMzMyAxOC43NzMzMzMgMCAwIDEtMjYuMDI2NjY2IDBMNDA1LjMzMzMzMyA1NzZhMTguMTMzMzMzIDE4LjEzMzMzMyAwIDAgMC0yNS44MTMzMzMgMGwtOTMuMjI2NjY3IDkzLjIyNjY2N2ExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwbC03MC40LTcwLjRhOS4xNzMzMzMgOS4xNzMzMzMgMCAwIDAtMTUuNzg2NjY3IDYuODI2NjY2QzE4OS44NjY2NjcgODQxLjgxMzMzMyAzMzQuOTMzMzMzIDk2MCA1MTIgOTYwczMyMi4xMzMzMzMtMTE4LjE4NjY2NyAzMzcuMjgtMzU0LjM0NjY2N2E5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZ6IiBmaWxsPSIjRjBGMEYwIj48L3BhdGg+PHBhdGggZD0iTTE5MC41MDY2NjcgNjY1LjgxMzMzM2w3MC40IDYxLjIyNjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAwIDI1LjgxMzMzMyAwbDkzLjIyNjY2Ny04MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMSAyNS44MTMzMzMgMGw5My4yMjY2NjcgODEuMDY2NjY3YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMjYuMDI2NjY2IDBsOTMuNjUzMzM0LTgxLjA2NjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAxIDI2LjAyNjY2NiAwbDkzLjIyNjY2NyA4MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMCAyNS44MTMzMzMgMGw3MC40LTYxLjIyNjY2N2E5LjgxMzMzMyA5LjgxMzMzMyAwIDAgMSA4LjUzMzMzNC0xLjkyIDUzNi4xMDY2NjcgNTM2LjEwNjY2NyAwIDAgMCA3LjI1MzMzMy01OC4yNCA5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZsLTcwLjQgNzAuNGExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwTDY0NC4wNTMzMzMgNTc2QTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDAgNjE4LjY2NjY2NyA1NzZsLTkzLjAxMzMzNCA5My4yMjY2NjdhMTguNzczMzMzIDE4Ljc3MzMzMyAwIDAgMS0yNi4wMjY2NjYgMEw0MDUuMzMzMzMzIDU3NmExOC4xMzMzMzMgMTguMTMzMzMzIDAgMCAwLTI1LjgxMzMzMyAwbC05My4yMjY2NjcgOTMuMjI2NjY3YTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDEtMjUuODEzMzMzIDBsLTcwLjQtNzAuNGE5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjcgNi44MjY2NjYgNTM3LjE3MzMzMyA1MzcuMTczMzMzIDAgMCAwIDcuMDQgNTguMjQgMTAuMDI2NjY3IDEwLjAyNjY2NyAwIDAgMSA5LjE3MzMzNCAxLjkyeiIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPjxwYXRoIGQ9Ik00OTguMTMzMzMzIDE4NC41MzMzMzNtLTI5LjAxMzMzMyAwYTI5LjAxMzMzMyAyOS4wMTMzMzMgMCAxIDAgNTguMDI2NjY3IDAgMjkuMDEzMzMzIDI5LjAxMzMzMyAwIDEgMC01OC4wMjY2NjcgMFoiIGZpbGw9IiM0NTQxM0MiPjwvcGF0aD48cGF0aCBkPSJNNTc2IDI5MC45ODY2NjdjMCAxMS43MzMzMzMtMTUuNzg2NjY3IDIxLjMzMzMzMy0zNC45ODY2NjcgMjEuMzMzMzMzcy0zNS4yLTkuNi0zNS4yLTIxLjMzMzMzMyAxNS43ODY2NjctMjEuMzMzMzMzIDM1LjItMjEuMzMzMzM0IDM0Ljk4NjY2NyA5LjYgMzQuOTg2NjY3IDIxLjMzMzMzNHoiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48L3N2Zz4=">
|
|
283
|
+
<div>
|
|
284
|
+
<div class="title">CreditDaddy</div>
|
|
285
|
+
<div class="ver" id="ver">Qoder / WorkBuddy / ZCode 多账号管理 · 每日积分自动领取</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="head-right">
|
|
289
|
+
<span id="sched" class="num"></span>
|
|
290
|
+
<a class="icon-btn" id="btn-home" href="https://github.com/techysy/CreditDaddy" target="_blank" rel="noopener" title="项目主页(GitHub)"></a>
|
|
291
|
+
<button class="icon-btn" id="btn-theme" title="切换亮色 / 暗色"></button>
|
|
292
|
+
</div>
|
|
293
|
+
</header>
|
|
294
|
+
|
|
295
|
+
<nav class="nav" id="nav">
|
|
296
|
+
<button data-view="dash" class="active">仪表盘</button>
|
|
297
|
+
<button data-view="qoder">Qoder<span class="cnt" id="cnt-qoder">0</span></button>
|
|
298
|
+
<button data-view="workbuddy">WorkBuddy<span class="cnt" id="cnt-workbuddy">0</span></button>
|
|
299
|
+
<button data-view="zcode">ZCode<span class="cnt" id="cnt-zcode">0</span></button>
|
|
300
|
+
<button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
|
|
301
|
+
</nav>
|
|
302
|
+
|
|
303
|
+
<!-- 仪表盘 -->
|
|
304
|
+
<section class="view active" id="view-dash">
|
|
305
|
+
<div class="dash-head">
|
|
306
|
+
<div class="dash-title">总览</div>
|
|
307
|
+
<div class="actions">
|
|
308
|
+
<button class="btn primary" id="btn-checkin-all"></button>
|
|
309
|
+
<button class="btn" id="btn-add-dash"></button>
|
|
310
|
+
<button class="btn" id="btn-import-dash"></button>
|
|
311
|
+
<button class="btn" id="btn-export-dash"></button>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="kpis" id="kpis"></div>
|
|
315
|
+
<div class="prods" id="prods"></div>
|
|
316
|
+
<div class="dash-cols">
|
|
317
|
+
<section class="pcard"><div class="pcard-title"><span>最近记录</span><span class="hint" id="last-tick"></span></div><div id="recent"></div></section>
|
|
318
|
+
<section class="pcard"><div class="pcard-title"><span>需要处理</span><span class="hint" id="att-count"></span></div><div id="attention"></div></section>
|
|
319
|
+
</div>
|
|
320
|
+
</section>
|
|
321
|
+
|
|
322
|
+
<!-- 产品页(Qoder / WorkBuddy / ZCode 共用) -->
|
|
323
|
+
<section class="view" id="view-prod">
|
|
324
|
+
<div class="panel">
|
|
325
|
+
<div class="toolbar">
|
|
326
|
+
<div class="summary">
|
|
327
|
+
<div class="stat"><span>账号</span><strong id="s-count">0</strong></div>
|
|
328
|
+
<div class="divider"></div>
|
|
329
|
+
<div id="s-done-wrap" style="display:contents">
|
|
330
|
+
<div class="stat"><span>今日已领</span><strong id="s-done">0</strong></div>
|
|
331
|
+
<div class="divider"></div>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="stat"><span id="s-credits-label">剩余积分</span><strong id="s-credits">-</strong></div>
|
|
334
|
+
<div class="divider" id="region-div"></div>
|
|
335
|
+
<div class="seg-ctl" id="region">
|
|
336
|
+
<button data-r="" class="active">全部</button><button data-r="intl">国际版</button><button data-r="cn">国内版</button>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
<div class="actions">
|
|
340
|
+
<button class="btn primary" id="btn-checkin"></button>
|
|
341
|
+
<button class="btn" id="btn-add"></button>
|
|
342
|
+
<button class="btn" id="btn-import"></button>
|
|
343
|
+
<button class="btn" id="btn-export"></button>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="notice" id="notice" style="display:none"></div>
|
|
347
|
+
<div class="list" id="list"><div class="empty">加载中…</div></div>
|
|
348
|
+
</div>
|
|
349
|
+
</section>
|
|
350
|
+
|
|
351
|
+
<!-- 10Router:其他供应商额度 + 用量同步 -->
|
|
352
|
+
<section class="view" id="view-10r">
|
|
353
|
+
<div class="panel" id="tr-panel"><div class="empty">加载中…</div></div>
|
|
354
|
+
</section>
|
|
355
|
+
|
|
356
|
+
<section class="pcard collapsed" id="log-card">
|
|
357
|
+
<div class="pcard-title">
|
|
358
|
+
<span>运行日志</span>
|
|
359
|
+
<button class="btn" id="btn-logs">展开</button>
|
|
360
|
+
</div>
|
|
361
|
+
<div class="logs" id="logs">(暂无)</div>
|
|
362
|
+
</section>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<!-- 添加账号 -->
|
|
366
|
+
<div class="mask" id="m-add">
|
|
367
|
+
<div class="modal">
|
|
368
|
+
<div class="modal-title"><span>添加账号</span><button class="close" data-close>✕</button></div>
|
|
369
|
+
<div class="tabs">
|
|
370
|
+
<button class="tab active" data-tab="local">本机导入</button>
|
|
371
|
+
<button class="tab" data-tab="device">浏览器登录</button>
|
|
372
|
+
<button class="tab" data-tab="token">粘贴 Token</button>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div class="pane active" data-pane="local">
|
|
376
|
+
<div class="hint" id="local-hint"></div>
|
|
377
|
+
<div class="row" style="margin-top:10px"><button class="btn primary" id="btn-scan">重新扫描</button><span class="hint" id="scan-status"></span></div>
|
|
378
|
+
<div class="box" id="scan-result"><div class="hint">点击「重新扫描」</div></div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div class="pane" data-pane="device">
|
|
382
|
+
<div class="hint" id="dev-hint" style="margin-bottom:10px"></div>
|
|
383
|
+
<div class="field">
|
|
384
|
+
<label>登录到</label>
|
|
385
|
+
<select class="input" id="dev-provider">
|
|
386
|
+
<option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option>
|
|
387
|
+
<option value="workbuddy">WorkBuddy / CodeBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy / CodeBuddy 国际版</option>
|
|
388
|
+
<option value="zcode-bigmodel">ZCode · BigModel(智谱)</option><option value="zcode-zai">ZCode · Z.ai(国际)</option>
|
|
389
|
+
</select>
|
|
390
|
+
</div>
|
|
391
|
+
<div class="row"><button class="btn primary" id="btn-device">生成授权链接</button><button class="btn" id="dev-cancel" style="display:none">取消</button></div>
|
|
392
|
+
<div class="box" id="device-box" style="display:none">
|
|
393
|
+
<div class="hint" id="dev-status"></div>
|
|
394
|
+
<div class="hint" style="margin-top:6px"><a id="dev-link" href="#" target="_blank" rel="noopener"></a></div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<div class="pane" data-pane="token">
|
|
399
|
+
<div class="field">
|
|
400
|
+
<label>版本</label>
|
|
401
|
+
<select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy">WorkBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy 国际版</option></select>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="field"><label>备注名(可选)</label><input class="input" id="f-name" placeholder="留空则自动读取昵称"></div>
|
|
404
|
+
<div class="field"><label>Token</label><input class="input mono" id="f-token" placeholder="dt-… / jt-… 或 PAT(pt-…,自动兑换)"></div>
|
|
405
|
+
<div class="modal-actions"><button class="btn primary" id="btn-add-token">添加</button></div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<!-- 导出 -->
|
|
411
|
+
<div class="mask" id="m-export">
|
|
412
|
+
<div class="modal sm">
|
|
413
|
+
<div class="modal-title"><span>导出账号</span><button class="close" data-close>✕</button></div>
|
|
414
|
+
<div class="field">
|
|
415
|
+
<label>范围</label>
|
|
416
|
+
<select class="input" id="ex-provider"><option value="">全部账号</option><option value="p:qoder">Qoder(全部)</option><option value="qoder">仅 Qoder 国际版</option><option value="qoder-cn">仅 Qoder 国内版</option><option value="p:workbuddy">WorkBuddy(全部)</option><option value="workbuddy">仅 WorkBuddy 国内版</option><option value="workbuddy-intl">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option></select>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="field">
|
|
419
|
+
<label>加密口令(可选)</label>
|
|
420
|
+
<input class="input" id="ex-password" type="password" placeholder="至少 4 位;留空则导出明文">
|
|
421
|
+
</div>
|
|
422
|
+
<div class="hint">设置口令后文件采用 10router 迁移格式加密(scrypt + AES-256-GCM),可直接在 10router 对应提供商页导入(WorkBuddy 对应 10router 的 CodeBuddy)。10router 按提供商导入,建议按单个版本导出。</div>
|
|
423
|
+
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-do-export">导出</button></div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<!-- 口令输入 -->
|
|
428
|
+
<div class="mask" id="m-pass">
|
|
429
|
+
<div class="modal sm">
|
|
430
|
+
<div class="modal-title"><span>输入口令</span><button class="close" data-close>✕</button></div>
|
|
431
|
+
<div class="hint" id="pass-hint">该文件已加密,请输入导出时设置的口令。</div>
|
|
432
|
+
<div class="field" style="margin-top:10px"><input class="input" id="pass-input" type="password"></div>
|
|
433
|
+
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-pass-ok">导入</button></div>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<!-- 10Router 设置 -->
|
|
438
|
+
<div class="mask" id="m-10r">
|
|
439
|
+
<div class="modal">
|
|
440
|
+
<div class="modal-title"><span>10Router 连接设置</span><button class="close" data-close>✕</button></div>
|
|
441
|
+
<div class="hint">填 10Router 的访问地址和仪表盘里创建的<b>虚拟 key(sk-…)</b>。key 只保存在 CreditDaddy 守护进程的数据目录(0600),面板里只显示脱敏值。</div>
|
|
442
|
+
<div class="field" style="margin-top:12px"><label>10Router 地址</label><input class="input mono" id="tr-endpoint" placeholder="http://127.0.0.1:20128 或 http://nas:20128"></div>
|
|
443
|
+
<div class="field"><label>虚拟 key</label><input class="input mono" id="tr-key" placeholder="sk-…" autocomplete="off"></div>
|
|
444
|
+
<div class="field"><label>用量同步(同 10router-sync 插件)</label>
|
|
445
|
+
<div><label class="check"><input type="checkbox" id="tr-auto"> 每小时自动同步本机用量到 10Router</label></div>
|
|
446
|
+
<div id="tr-sources" style="margin-top:6px"></div>
|
|
447
|
+
<div class="hint" style="margin-top:4px">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。10Router 按行签名去重,重复同步不会产生重复数据。</div>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="field"><label>显示</label>
|
|
450
|
+
<div><label class="check"><input type="checkbox" id="tr-hide-empty"> 「全部」视图隐藏没有额度 / 余额为 0 的供应商汇总卡</label></div>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="hint" id="tr-test-msg"></div>
|
|
453
|
+
<div class="modal-actions">
|
|
454
|
+
<button class="btn" id="tr-disconnect" style="margin-right:auto">断开</button>
|
|
455
|
+
<button class="btn" id="tr-test">测试连接</button>
|
|
456
|
+
<button class="btn primary" id="tr-save">保存</button>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
<!-- ZCode 活动领取 -->
|
|
462
|
+
<div class="mask" id="m-claim">
|
|
463
|
+
<div class="modal">
|
|
464
|
+
<div class="modal-title"><span>活动领取</span><button class="close" data-close>✕</button></div>
|
|
465
|
+
<div class="hint">从 ZCode 官方活动接口领取(billing/preview → billing/claim)。领取时通常静默通过人机验证;触发风控时会弹出验证码,按提示完成即可。</div>
|
|
466
|
+
<div id="claim-body" style="margin-top:10px"></div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
<!-- 通用对话框 -->
|
|
471
|
+
<div class="mask" id="m-dialog">
|
|
472
|
+
<div class="modal sm">
|
|
473
|
+
<div class="modal-title"><span id="dlg-title"></span><button class="close" data-close>✕</button></div>
|
|
474
|
+
<div class="hint" id="dlg-msg" style="white-space:pre-wrap"></div>
|
|
475
|
+
<div class="field" id="dlg-field" style="margin-top:10px"><input class="input" id="dlg-input"></div>
|
|
476
|
+
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="dlg-ok">确定</button></div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<input type="file" id="file-input" accept=".json,application/json" style="display:none">
|
|
481
|
+
<div id="toast"></div>
|
|
482
|
+
|
|
483
|
+
<script>
|
|
484
|
+
'use strict';
|
|
485
|
+
|
|
486
|
+
// ── 图标(Lucide 风格,currentColor) ──
|
|
487
|
+
const svg = (d) => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>';
|
|
488
|
+
const I = {
|
|
489
|
+
play: svg('<polygon points="6 4 20 12 6 20 6 4"/>'),
|
|
490
|
+
refresh: svg('<path d="M21 12a9 9 0 1 1-2.64-6.36L21 8"/><path d="M21 3v5h-5"/>'),
|
|
491
|
+
trash: svg('<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/>'),
|
|
492
|
+
plus: svg('<path d="M12 5v14M5 12h14"/>'),
|
|
493
|
+
upload: svg('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/>'),
|
|
494
|
+
download: svg('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/>'),
|
|
495
|
+
sun: svg('<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>'),
|
|
496
|
+
moon: svg('<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>'),
|
|
497
|
+
swap: svg('<path d="M7 7h13l-4-4"/><path d="M17 17H4l4 4"/>'),
|
|
498
|
+
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
|
+
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
|
+
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"/>'),
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const $ = (id) => document.getElementById(id);
|
|
504
|
+
const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '' };
|
|
505
|
+
const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'tenrouter'];
|
|
506
|
+
// 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
|
|
507
|
+
const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
|
|
508
|
+
const PRODUCTS = [
|
|
509
|
+
{ id: 'qoder', label: 'Qoder' },
|
|
510
|
+
{ id: 'workbuddy', label: 'WorkBuddy' },
|
|
511
|
+
{ id: 'zcode', label: 'ZCode' },
|
|
512
|
+
];
|
|
513
|
+
const prodLabel = (id) => (PRODUCTS.find((x) => x.id === id) || { label: id }).label;
|
|
514
|
+
const PROV = {
|
|
515
|
+
qoder: { prod: 'qoder', prodLabel: 'Qoder', region: '国际版', cls: 'intl' },
|
|
516
|
+
'qoder-cn': { prod: 'qoder', prodLabel: 'Qoder', region: '国内版', cls: 'cn' },
|
|
517
|
+
workbuddy: { prod: 'workbuddy', prodLabel: 'WorkBuddy', region: '国内版', cls: 'cn' },
|
|
518
|
+
'workbuddy-intl': { prod: 'workbuddy', prodLabel: 'WorkBuddy', region: '国际版', cls: 'intl' },
|
|
519
|
+
zcode: { prod: 'zcode', prodLabel: 'ZCode', region: '', cls: '' },
|
|
520
|
+
};
|
|
521
|
+
// 支持每日签到的产品(ZCode 积分靠需验证码的活动领取,不做签到)
|
|
522
|
+
const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy']);
|
|
523
|
+
const CLIENT_LABEL = { workbuddy: 'WorkBuddy', zcode: 'ZCode' };
|
|
524
|
+
const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
|
|
525
|
+
const provBadges = (p, regionOnly) => { const v = provOf(p); return (regionOnly ? '' : '<span class="badge prod ' + v.prod + '">' + esc(v.prodLabel) + '</span>') + (v.region ? '<span class="badge ' + v.cls + '">' + v.region + '</span>' : ''); };
|
|
526
|
+
|
|
527
|
+
function esc(s) {
|
|
528
|
+
return String(s === null || s === undefined ? '' : s)
|
|
529
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
530
|
+
}
|
|
531
|
+
function fmtNum(n) {
|
|
532
|
+
if (n === null || n === undefined || Number.isNaN(n)) return '-';
|
|
533
|
+
return Number(n).toLocaleString('en-US', { maximumFractionDigits: 2 });
|
|
534
|
+
}
|
|
535
|
+
function fmtTime(iso) {
|
|
536
|
+
if (!iso) return '';
|
|
537
|
+
const d = new Date(iso); if (Number.isNaN(d.getTime())) return '';
|
|
538
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
539
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
|
|
540
|
+
}
|
|
541
|
+
function mask(s) {
|
|
542
|
+
if (!s) return '';
|
|
543
|
+
if (s.includes('@')) { const [u, d] = s.split('@'); return (u.length > 2 ? u.slice(0, 2) + '***' : u + '*') + '@' + d; }
|
|
544
|
+
return s.length > 10 ? s.slice(0, 6) + '…' + s.slice(-4) : s;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let toastTimer = null;
|
|
548
|
+
function toast(msg, ok) {
|
|
549
|
+
const t = $('toast');
|
|
550
|
+
t.textContent = msg;
|
|
551
|
+
t.classList.toggle('err', ok === false);
|
|
552
|
+
t.classList.add('show');
|
|
553
|
+
clearTimeout(toastTimer);
|
|
554
|
+
toastTimer = setTimeout(() => t.classList.remove('show'), 4200);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ── API ──
|
|
558
|
+
// 面板访问密码(fnOS / 设置了 CREDITDADDY_PASSWORD 时需要)。内存里的值优先:面板嵌在飞牛桌面窗口里是跨域 iframe,
|
|
559
|
+
// 浏览器拦截第三方存储时 localStorage 不可用,只靠它会每个请求都 401
|
|
560
|
+
let memKey = null;
|
|
561
|
+
function getKey() {
|
|
562
|
+
if (memKey !== null) return memKey;
|
|
563
|
+
try { return localStorage.getItem('qd_key') || ''; } catch (e) { return ''; }
|
|
564
|
+
}
|
|
565
|
+
function setKey(k) {
|
|
566
|
+
memKey = k;
|
|
567
|
+
try { if (k) localStorage.setItem('qd_key', k); else localStorage.removeItem('qd_key'); } catch (e) {}
|
|
568
|
+
}
|
|
569
|
+
let keyPrompt = null;
|
|
570
|
+
/** 弹一次密码输入框(并发的请求共用同一个);返回 true = 已输入 */
|
|
571
|
+
function promptKey(invalid) {
|
|
572
|
+
if (!keyPrompt) {
|
|
573
|
+
keyPrompt = uiPrompt('输入面板访问密码', '', {
|
|
574
|
+
type: 'password', okText: '进入',
|
|
575
|
+
message: (invalid ? '密码不正确,请重新输入。\n' : '')
|
|
576
|
+
+ 'fnOS:安装时设置的面板访问密码(忘了可以在 fnOS「应用设置」里重设)。\n命令行部署:环境变量 CREDITDADDY_PASSWORD 的值。',
|
|
577
|
+
}).then((k) => { keyPrompt = null; if (k === null || !k.trim()) return false; setKey(k.trim()); return true; });
|
|
578
|
+
}
|
|
579
|
+
return keyPrompt;
|
|
580
|
+
}
|
|
581
|
+
async function api(p, opts = {}, attempt = 0) {
|
|
582
|
+
const o = Object.assign({}, opts, { headers: Object.assign({}, opts.headers, { 'x-qd-key': getKey() }) });
|
|
583
|
+
if (opts.json !== undefined) {
|
|
584
|
+
o.method = opts.method || 'POST';
|
|
585
|
+
o.headers['Content-Type'] = 'application/json';
|
|
586
|
+
o.body = JSON.stringify(opts.json);
|
|
587
|
+
}
|
|
588
|
+
delete o.json;
|
|
589
|
+
const res = await fetch(p, o);
|
|
590
|
+
const data = await res.json().catch(() => ({}));
|
|
591
|
+
// 密码缺失 / 不正确:弹框要密码,输入后自动重试这次请求(不再只报一句错误)
|
|
592
|
+
if (res.status === 401 && data.code === 'PANEL_KEY' && attempt < 3) {
|
|
593
|
+
const hadKey = Boolean(getKey());
|
|
594
|
+
setKey('');
|
|
595
|
+
if (await promptKey(hadKey)) return api(p, opts, attempt + 1);
|
|
596
|
+
}
|
|
597
|
+
if (!res.ok) { const e = new Error(data.error || ('HTTP ' + res.status)); e.code = data.code; e.status = res.status; e.nextAt = data.nextAt; throw e; }
|
|
598
|
+
return data;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 10Router 汇总卡「隐藏无额度」偏好(面板本地)
|
|
602
|
+
try { state.trHideEmpty = localStorage.getItem('qd_tr_hideempty') === '1'; } catch (e) {}
|
|
603
|
+
|
|
604
|
+
// ── 主题 ──
|
|
605
|
+
function applyTheme(t) {
|
|
606
|
+
document.documentElement.dataset.theme = t;
|
|
607
|
+
$('btn-theme').innerHTML = t === 'dark' ? I.sun : I.moon;
|
|
608
|
+
}
|
|
609
|
+
function initTheme() {
|
|
610
|
+
let t = null;
|
|
611
|
+
try { t = localStorage.getItem('qd_theme'); } catch (e) {}
|
|
612
|
+
if (!t) t = window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
613
|
+
applyTheme(t);
|
|
614
|
+
$('btn-theme').onclick = () => {
|
|
615
|
+
const n = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
616
|
+
applyTheme(n);
|
|
617
|
+
try { localStorage.setItem('qd_theme', n); } catch (e) {}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ── 状态 / 渲染 ──
|
|
622
|
+
// 统一「领鸡蛋」语义:Qoder / WorkBuddy 国内/国际 = 领取积分;workbuddy-intl = 保持活跃(不保证奖励)
|
|
623
|
+
function statusOf(a) {
|
|
624
|
+
if (!CHECKIN_PRODUCTS.has(a.product)) return { cls: '', text: '', msg: '' }; // ZCode 无每日领取,不显示徽标
|
|
625
|
+
const intl = a.provider === 'workbuddy-intl';
|
|
626
|
+
const today = state.status && state.status.today;
|
|
627
|
+
const done = state.status && state.status.todayDone && state.status.todayDone[a.id] === today;
|
|
628
|
+
const r = a.lastResult;
|
|
629
|
+
const fresh = r && r.at && state.status && (Date.now() - new Date(r.at).getTime() < 26 * 3600e3);
|
|
630
|
+
if (r && fresh && r.status === 'limited' && done) return { cls: 'limited', text: '本机已领', msg: r.message };
|
|
631
|
+
if (done) {
|
|
632
|
+
if (intl) return { cls: 'ok', text: '今日活跃', msg: r && r.message ? r.message : '' };
|
|
633
|
+
const amt = r && r.status === 'checked-in' && fresh && r.amount ? ' +' + fmtNum(r.amount) : '';
|
|
634
|
+
const streak = r && !intl && r.streakDays ? ' · 连续 ' + r.streakDays + ' 天' : '';
|
|
635
|
+
return { cls: 'ok', text: '今日已领' + amt + streak, msg: r && r.message && !['checked-in', 'already'].includes(r.status) ? r.message : '' };
|
|
636
|
+
}
|
|
637
|
+
const failText = intl ? '活跃失败' : '领取失败';
|
|
638
|
+
if (r && fresh && r.status === 'failed') return { cls: 'fail', text: failText, msg: r.message, err: true };
|
|
639
|
+
if (r && fresh && r.status === 'no-activity') return { cls: 'pending', text: intl ? '待活跃' : '待领取', msg: r.message };
|
|
640
|
+
return { cls: 'pending', text: intl ? '未活跃' : '未领取', msg: '' };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function expiryCell(a) {
|
|
644
|
+
if (!a.expiresAt) return { label: a.isPat ? 'PAT 长期有效' : '—', warn: false };
|
|
645
|
+
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 < 7 * 86400e3 };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function creditHtml(a) {
|
|
651
|
+
const q = state.quotas[a.id];
|
|
652
|
+
if (!q) return '<div class="credit-line"><span class="dim">读取积分中…</span></div><div class="bar"></div>';
|
|
653
|
+
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
|
+
if (q.empty) return '<div class="credit-line"><span class="dim">无有效套餐(仅免费额度)</span></div><div class="bar"></div>';
|
|
655
|
+
const unit = q.unit ? ' ' + q.unit : '';
|
|
656
|
+
if (q.unit) {
|
|
657
|
+
const items = q.parts.map((x) => {
|
|
658
|
+
const base = esc(x.name) + ' ' + fmtNum(x.remaining) + '/' + fmtNum(x.total) + (x.unit ? ' ' + esc(x.unit) : '');
|
|
659
|
+
if (x.pending && x.startsAt) return base + '(' + fmtTime(x.startsAt).slice(5, 16) + ' 生效)';
|
|
660
|
+
return base + (x.expiresAt ? '(' + fmtTime(x.expiresAt).slice(5, 16) + (x.recurring ? ' 重置)' : ' 到期)') : '');
|
|
661
|
+
}).join(' · ');
|
|
662
|
+
const pct = q.total > 0 ? Math.min(1, q.remaining / q.total) : 0;
|
|
663
|
+
return '<div class="credit-line"><span class="credit"><span class="lbl">剩余</span><b>' + fmtNum(q.remaining) + '</b>' + esc(unit) + '</span>'
|
|
664
|
+
+ '<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><span class="seg used" style="flex:' + (1 - pct).toFixed(4) + ' 1 0"></span></div>';
|
|
666
|
+
}
|
|
667
|
+
const total = q.total || 0;
|
|
668
|
+
let bar = '';
|
|
669
|
+
if (total > 0) {
|
|
670
|
+
q.parts.filter((x) => x.remaining > 0).forEach((x, i) => {
|
|
671
|
+
bar += '<span class="seg" style="flex:' + (x.remaining / total).toFixed(4) + ' 1 0;--a:' + (x.recurring ? 1 : Math.max(.45, .85 - i * .05)) + '" title="'
|
|
672
|
+
+ esc(x.name) + ':剩余 ' + fmtNum(x.remaining) + ' / ' + fmtNum(x.total) + (x.expiresAt ? (x.recurring ? ',' + fmtTime(x.expiresAt).slice(0, 10) + ' 重置' : ',' + fmtTime(x.expiresAt).slice(0, 10) + ' 到期') : '') + '"></span>';
|
|
673
|
+
});
|
|
674
|
+
if (q.used > 0) bar += '<span class="seg used" style="flex:' + (q.used / total).toFixed(4) + ' 1 0" title="已使用 ' + fmtNum(q.used) + '"></span>';
|
|
675
|
+
}
|
|
676
|
+
const soon = q.parts.filter((x) => x.remaining > 0 && !x.recurring && x.expiresAt).sort((x, y) => x.expiresAt.localeCompare(y.expiresAt))[0];
|
|
677
|
+
return '<div class="credit-line">'
|
|
678
|
+
+ '<span class="credit"><span class="lbl">剩余</span>' + I.credit + '<b>' + fmtNum(q.remaining) + '</b></span>'
|
|
679
|
+
+ '<span class="credit dim">' + (soon ? fmtNum(soon.remaining) + ' 于 ' + fmtTime(soon.expiresAt).slice(5, 10) + ' 到期 · ' : '') + '已用 ' + fmtNum(q.used) + ' / 共 ' + fmtNum(total) + (q.exceeded ? ' · <span style="color:var(--err)">已超额</span>' : '') + '</span>'
|
|
680
|
+
+ '</div><div class="bar">' + bar + '</div>';
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function cardHtml(a) {
|
|
684
|
+
const st = statusOf(a);
|
|
685
|
+
const ex = expiryCell(a);
|
|
686
|
+
const ident = a.email || a.phone || (a.uid ? 'UID ' + a.uid.slice(0, 8) : a.tokenMasked);
|
|
687
|
+
const isCurrent = isClientCurrent(a);
|
|
688
|
+
const canCheckin = CHECKIN_PRODUCTS.has(a.product);
|
|
689
|
+
return '<div class="card" data-id="' + esc(a.id) + '">'
|
|
690
|
+
+ '<div class="row1"><div class="name-group">'
|
|
691
|
+
+ '<span class="name" data-act="rename" title="点击修改备注名">' + esc(a.name || '(未命名)') + '</span>'
|
|
692
|
+
+ provBadges(a.provider, true)
|
|
693
|
+
+ (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
|
|
694
|
+
+ (a.isPat ? '<span class="badge">PAT</span>' : '')
|
|
695
|
+
+ (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
|
|
696
|
+
+ (st.text ? '<span class="pill ' + st.cls + '" title="' + esc(a.lastResult ? fmtTime(a.lastResult.at) + ' ' + (a.lastResult.message || a.lastResult.status) : '') + '">' + esc(st.text) + '</span>' : '')
|
|
697
|
+
+ '</div><div class="ops">'
|
|
698
|
+
+ (a.product === 'zcode' ? '<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">' + I.gift + '</button>' : '')
|
|
699
|
+
+ (canCheckin ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="' + (a.provider === 'workbuddy-intl' ? '发送活跃请求' : '立即领取') + '">' + (state.busy[a.id] ? I.refresh : I.play) + '</button>' : '')
|
|
700
|
+
+ (CLIENT_LABEL[a.product] && a.canSwitch && !isCurrent && st.cls !== 'fail' ? '<button class="icon-btn" data-act="switch" title="切换 ' + CLIENT_LABEL[a.product] + ' 客户端到此账号">' + I.swap + '</button>' : '')
|
|
701
|
+
+ '<button class="icon-btn" data-act="quota" title="刷新积分">' + I.refresh + '</button>'
|
|
702
|
+
+ '<button class="icon-btn del" data-act="del" title="删除(点两次确认)">' + I.trash + '</button>'
|
|
703
|
+
+ '</div></div>'
|
|
704
|
+
+ '<div class="meta">'
|
|
705
|
+
+ '<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">有效期至</span><span class="val' + (ex.warn ? ' warn' : '') + '">' + esc(ex.label) + '</span></div>'
|
|
707
|
+
+ '</div>'
|
|
708
|
+
+ creditHtml(a)
|
|
709
|
+
+ (st.msg && !(state.quotas[a.id] && state.quotas[a.id].error && state.quotas[a.id].error.includes(st.msg.slice(-24)))
|
|
710
|
+
? '<div class="msg' + (st.err ? ' err' : '') + '" title="' + esc(st.msg) + '">' + esc(st.msg) + '</div>' : '')
|
|
711
|
+
+ '</div>';
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const isDone = (a) => Boolean(state.status && state.status.todayDone && state.status.todayDone[a.id] === state.status.today);
|
|
715
|
+
const ofProduct = (prod) => state.accounts.filter((a) => a.product === prod);
|
|
716
|
+
const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : null);
|
|
717
|
+
// 客户端当前登录判定:WorkBuddy 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
|
|
718
|
+
function isClientCurrent(a) {
|
|
719
|
+
const s = state.status;
|
|
720
|
+
if (!s) return false;
|
|
721
|
+
if (a.product === 'workbuddy') return Boolean(a.uid && s.workbuddyCurrentUid && s.workbuddyCurrentUid === a.uid);
|
|
722
|
+
if (a.product === 'zcode') {
|
|
723
|
+
const id = s.zcodeCurrentIdentity;
|
|
724
|
+
return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
|
|
725
|
+
}
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
// 积分合计:只统计「积分」口径(ZCode 是 token 额度,单位不同,不相加)
|
|
729
|
+
function creditSum(list) {
|
|
730
|
+
let sum = 0, known = 0, loading = 0;
|
|
731
|
+
for (const a of list) {
|
|
732
|
+
const q = state.quotas[a.id];
|
|
733
|
+
if (!q) { loading++; continue; }
|
|
734
|
+
if (q.error) continue;
|
|
735
|
+
known++;
|
|
736
|
+
if (!q.unit) sum += q.remaining || 0;
|
|
737
|
+
}
|
|
738
|
+
return { sum, known, loading };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function render() {
|
|
742
|
+
for (const p of PRODUCTS) $('cnt-' + p.id).textContent = ofProduct(p.id).length;
|
|
743
|
+
$('cnt-tenrouter').textContent = tr.quotas ? tr.quotas.connections.length : tr.config && tr.config.configured ? '…' : '–';
|
|
744
|
+
if (state.view === 'dash') renderDash();
|
|
745
|
+
else if (state.view === 'tenrouter') renderTenrouter();
|
|
746
|
+
else renderProd();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const EMPTY_TIP = {
|
|
750
|
+
qoder: '还没有 Qoder 账号。<br>点「添加账号」可一键读取本机 Qoder / Qoder CN 客户端已登录的账号,或用浏览器登录。',
|
|
751
|
+
workbuddy: '还没有 WorkBuddy 账号。<br>点「添加账号」可读取本机 WorkBuddy 客户端(当前 + 历史会话),或用浏览器登录 WorkBuddy / CodeBuddy 账号。',
|
|
752
|
+
zcode: '还没有 ZCode 账号。<br>点「添加账号」可读取本机 ZCode 客户端已登录的账号,或用浏览器登录(BigModel / Z.ai),多个账号可一键切换。',
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
function renderProd() {
|
|
756
|
+
const prod = state.view;
|
|
757
|
+
const all = ofProduct(prod);
|
|
758
|
+
const acc = all.filter((a) => !state.region || provOf(a.provider).cls === state.region);
|
|
759
|
+
const list = $('list');
|
|
760
|
+
if (!acc.length) list.innerHTML = '<div class="empty">' + (all.length ? '该地区下没有账号。' : EMPTY_TIP[prod]) + '</div>';
|
|
761
|
+
else list.innerHTML = acc.map(cardHtml).join('');
|
|
762
|
+
const canCheckin = CHECKIN_PRODUCTS.has(prod);
|
|
763
|
+
$('s-count').textContent = acc.length;
|
|
764
|
+
$('s-done-wrap').style.display = canCheckin ? 'contents' : 'none';
|
|
765
|
+
if (prod === 'workbuddy') {
|
|
766
|
+
// 国内版 = 每日签到送积分;国际版 = 保持活跃(活跃不等于必有奖励)
|
|
767
|
+
const cnAcc = acc.filter((a) => a.provider === 'workbuddy');
|
|
768
|
+
const intlAcc = acc.filter((a) => a.provider === 'workbuddy-intl');
|
|
769
|
+
$('s-done-wrap').innerHTML = (cnAcc.length ? '<div class="stat"><span>领取(国内)</span><strong>' + cnAcc.filter(isDone).length + ' / ' + cnAcc.length + '</strong></div><div class="divider"></div>' : '')
|
|
770
|
+
+ (intlAcc.length ? '<div class="stat"><span>活跃(国际)</span><strong>' + intlAcc.filter(isDone).length + ' / ' + intlAcc.length + '</strong></div><div class="divider"></div>' : '');
|
|
771
|
+
} else {
|
|
772
|
+
// 从 WorkBuddy 页切回来时 s-done 可能被 innerHTML 换没了,先重建结构再写值
|
|
773
|
+
if (!$('s-done')) $('s-done-wrap').innerHTML = '<div class="stat"><span>今日已领</span><strong id="s-done"></strong></div><div class="divider"></div>';
|
|
774
|
+
$('s-done').textContent = acc.filter(isDone).length + ' / ' + acc.length;
|
|
775
|
+
}
|
|
776
|
+
const c = creditSum(acc);
|
|
777
|
+
$('s-credits-label').textContent = prod === 'zcode' ? '有套餐' : '剩余积分';
|
|
778
|
+
$('s-credits').textContent = prod === 'zcode'
|
|
779
|
+
? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
|
|
780
|
+
: c.known ? fmtNum(c.sum) : '-';
|
|
781
|
+
const hasRegions = prod !== 'zcode';
|
|
782
|
+
$('region').style.display = hasRegions ? '' : 'none';
|
|
783
|
+
$('region-div').style.display = hasRegions ? '' : 'none';
|
|
784
|
+
document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
|
|
785
|
+
$('btn-checkin').style.display = canCheckin ? '' : 'none';
|
|
786
|
+
if (!$('btn-checkin').disabled) {
|
|
787
|
+
const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl');
|
|
788
|
+
$('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? '领取 / 活跃全部' : '领取全部 ' + prodLabel(prod));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function kpi(label, value, sub) {
|
|
793
|
+
return '<div class="kpi"><div class="kpi-label">' + label + '</div><div class="kpi-value">' + value + '</div><div class="kpi-sub" title="' + esc(sub.replace(/<[^>]+>/g, '')) + '">' + sub + '</div></div>';
|
|
794
|
+
}
|
|
795
|
+
const meter = (done, total) => '<div class="meter"><span style="width:' + (total ? Math.round(done / total * 100) : 0) + '%"></span></div>';
|
|
796
|
+
|
|
797
|
+
function renderDash() {
|
|
798
|
+
const s = state.status || {};
|
|
799
|
+
const sch = s.scheduler || {};
|
|
800
|
+
const signable = state.accounts.filter((a) => CHECKIN_PRODUCTS.has(a.product));
|
|
801
|
+
const done = signable.filter(isDone).length;
|
|
802
|
+
const failed = signable.filter((a) => statusOf(a).cls === 'fail').length;
|
|
803
|
+
const credit = creditSum(state.accounts.filter((a) => a.product !== 'zcode'));
|
|
804
|
+
const byProd = PRODUCTS.map((p) => p.label + ' ' + ofProduct(p.id).length).join(' · ');
|
|
805
|
+
const creditByProd = ['qoder', 'workbuddy'].map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
|
|
806
|
+
// 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
|
|
807
|
+
const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl');
|
|
808
|
+
const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
|
|
809
|
+
const doneDom = domAcc.filter(isDone).length;
|
|
810
|
+
const doneIntl = intlAcc.filter(isDone).length;
|
|
811
|
+
$('kpis').innerHTML =
|
|
812
|
+
kpi('账号', state.accounts.length, byProd)
|
|
813
|
+
+ kpi('今日领取' + (intlAcc.length ? ' / 活跃' : ''), intlAcc.length
|
|
814
|
+
? doneDom + ' <small>/ ' + domAcc.length + '</small><small class="kpi-side"> · 活跃 ' + doneIntl + ' / ' + intlAcc.length + '</small>'
|
|
815
|
+
: done + ' <small>/ ' + signable.length + '</small>',
|
|
816
|
+
!signable.length ? '暂无可领取的账号'
|
|
817
|
+
: (doneDom === domAcc.length && doneIntl === intlAcc.length ? (intlAcc.length ? '今天全部完成(国际为活跃请求,不保证奖励)' : '今天全部签完')
|
|
818
|
+
: '待办 ' + (signable.length - done) + (failed ? ' · 失败 ' + failed : '')))
|
|
819
|
+
+ kpi('剩余积分', credit.known ? fmtNum(credit.sum) : '-', credit.loading ? '读取中(' + credit.loading + ' 个账号)…' : creditByProd)
|
|
820
|
+
+ kpi('下次自动领取', sch.ticking ? '领取中…' : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
|
|
821
|
+
sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : '每 ~2 小时一轮');
|
|
822
|
+
|
|
823
|
+
$('prods').innerHTML = PRODUCTS.map((p) => {
|
|
824
|
+
const list = ofProduct(p.id);
|
|
825
|
+
let rows = '';
|
|
826
|
+
if (!list.length) {
|
|
827
|
+
rows = '<span class="lbl">账号</span><span class="val dim">还没有,点击进入添加</span>';
|
|
828
|
+
} else if (CHECKIN_PRODUCTS.has(p.id)) {
|
|
829
|
+
const d = list.filter(isDone).length;
|
|
830
|
+
const c = creditSum(list);
|
|
831
|
+
if (p.id === 'workbuddy') {
|
|
832
|
+
// 国内 = 每日领取;国际 = 保持活跃(活跃不等于必有奖励)——合并一行展示
|
|
833
|
+
const cnList = list.filter((a) => a.provider === 'workbuddy');
|
|
834
|
+
const intlList = list.filter((a) => a.provider === 'workbuddy-intl');
|
|
835
|
+
const parts = [];
|
|
836
|
+
if (cnList.length) parts.push(cnList.filter(isDone).length + ' / ' + cnList.length + '(国内)');
|
|
837
|
+
if (intlList.length) parts.push(intlList.filter(isDone).length + ' / ' + intlList.length + '(国际 · 不保证奖励)');
|
|
838
|
+
rows += '<span class="lbl">领取 / 活跃</span><span class="val" title="' + esc(parts.join(',') + (intlList.length ? '(国际为活跃请求,不保证奖励)' : '')) + '">'
|
|
839
|
+
+ (cnList.length ? cnList.filter(isDone).length + ' / ' + cnList.length + ' 国内' : '')
|
|
840
|
+
+ (cnList.length && intlList.length ? ' · ' : '')
|
|
841
|
+
+ (intlList.length ? intlList.filter(isDone).length + ' / ' + intlList.length + ' 国际' : '')
|
|
842
|
+
+ '</span>';
|
|
843
|
+
} else {
|
|
844
|
+
rows = '<span class="lbl">今日领取</span><span class="val">' + d + ' / ' + list.length + '</span>';
|
|
845
|
+
}
|
|
846
|
+
rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
|
|
847
|
+
+ '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
|
|
848
|
+
} else {
|
|
849
|
+
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
850
|
+
// ZCode 没有每日签到:不展示签到语义的行
|
|
851
|
+
rows = '<span class="lbl">活动领取</span><span class="val dim">手动领取,或在标签页开自动轮询</span>'
|
|
852
|
+
+ '<span class="lbl">有效套餐</span><span class="val">' + planned + ' / ' + list.length + '</span>';
|
|
853
|
+
}
|
|
854
|
+
if (p.id === 'qoder' && list.some((a) => a.provider === 'qoder')) {
|
|
855
|
+
rows += '<span class="lbl">国际版风控身份</span><span class="val' + (s.riskIdentity ? '' : ' warn') + '">' + (s.riskIdentity ? (s.riskSource === 'cli' ? '已就绪(设备身份组件)' : '已就绪') : umidOffer(s) ? '需安装设备身份组件' : '未检测到客户端') + '</span>';
|
|
856
|
+
}
|
|
857
|
+
if (CLIENT_LABEL[p.id]) {
|
|
858
|
+
const cur = list.find((a) => a.product === p.id && isClientCurrent(a));
|
|
859
|
+
const zid = p.id === 'zcode' ? s.zcodeCurrentIdentity : null;
|
|
860
|
+
const live = cur ? esc(cur.name || cur.email || cur.uid)
|
|
861
|
+
: zid && (zid.email || zid.uid || zid.username) ? '未导入的账号(' + esc(zid.email || zid.username || zid.uid) + ')'
|
|
862
|
+
: (p.id === 'workbuddy' && s.workbuddyCurrentUid ? '未导入的账号' : '—');
|
|
863
|
+
rows += '<span class="lbl">客户端当前</span><span class="val">' + live + '</span>';
|
|
864
|
+
}
|
|
865
|
+
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">查看 →</span></div><div class="prod-rows">' + rows + '</div></div>';
|
|
867
|
+
}).join('') + trDashCard();
|
|
868
|
+
|
|
869
|
+
// 需要处理:token 失效 / 即将过期、领取失败、国际版缺风控身份、10Router 额度告急 / 查询失败
|
|
870
|
+
const att = [];
|
|
871
|
+
if (!s.riskIdentity && state.accounts.some((a) => a.provider === 'qoder')) {
|
|
872
|
+
att.push({ view: 'qoder', name: 'Qoder 国际版', badges: '', pill: 'limited', text: umidOffer(s) ? '安装设备身份组件后才能领取' : '未检测到 Qoder 客户端,国际版无法领取' });
|
|
873
|
+
}
|
|
874
|
+
for (const a of state.accounts) {
|
|
875
|
+
const ex = expiryCell(a);
|
|
876
|
+
const st = statusOf(a);
|
|
877
|
+
const base = { view: a.product, name: a.name || a.email || a.id, badges: provBadges(a.provider) };
|
|
878
|
+
if (a.verified === false) att.push(Object.assign({ pill: 'fail', text: 'token 无效' }, base));
|
|
879
|
+
else if (ex.label === '已过期') att.push(Object.assign({ pill: 'fail', text: 'token 已过期' }, base));
|
|
880
|
+
else if (ex.warn) att.push(Object.assign({ pill: 'limited', text: ex.label.slice(5, 10) + ' 过期' }, base));
|
|
881
|
+
if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: a.provider === 'workbuddy-intl' ? '活跃失败' : '领取失败', title: st.msg }, base));
|
|
882
|
+
}
|
|
883
|
+
for (const c of (tr.quotas && tr.quotas.connections) || []) {
|
|
884
|
+
const base = { view: 'tenrouter', name: c.name || c.email || c.providerName, badges: '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>' };
|
|
885
|
+
if (c.error) att.push(Object.assign({ pill: 'fail', text: '额度查询失败', title: c.error }, base));
|
|
886
|
+
else if (c.isActive && quotaLow(c)) att.push(Object.assign({ pill: 'limited', text: '额度告急' }, base));
|
|
887
|
+
}
|
|
888
|
+
$('att-count').textContent = att.length ? att.length + ' 项' : '';
|
|
889
|
+
$('attention').innerHTML = att.length
|
|
890
|
+
? att.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
|
+
+ '<span class="item-side"><span class="pill ' + x.pill + '">' + esc(x.text) + '</span></span></div>').join('')
|
|
892
|
+
+ (att.length > 8 ? '<div class="muted-empty">还有 ' + (att.length - 8) + ' 项</div>' : '')
|
|
893
|
+
: '<div class="muted-empty">' + (state.accounts.length ? '✓ 一切正常' : '还没有账号') + '</div>';
|
|
894
|
+
|
|
895
|
+
// 最近领取记录
|
|
896
|
+
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).sort((x, y) => String(y.lastResult.at).localeCompare(String(x.lastResult.at))).slice(0, 8);
|
|
898
|
+
$('last-tick').textContent = sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) : '';
|
|
899
|
+
$('recent').innerHTML = recent.length
|
|
900
|
+
? recent.map((a) => {
|
|
901
|
+
const r = a.lastResult;
|
|
902
|
+
let [cls, txt] = RES[r.status] || ['pending', r.status];
|
|
903
|
+
// 国际版 WorkBuddy 是保持活跃(不保证奖励),不叫「领取」
|
|
904
|
+
if (a.provider === 'workbuddy-intl' && r.status === 'checked-in') txt = '活跃达成';
|
|
905
|
+
const label = r.status === 'checked-in' && r.amount ? txt + ' +' + fmtNum(r.amount) : txt;
|
|
906
|
+
return '<div class="item" data-view="' + a.product + '" title="' + esc(r.message || '') + '"><span class="item-main"><span class="item-name">' + esc(a.name || a.email || a.id) + '</span>' + provBadges(a.provider) + '</span>'
|
|
907
|
+
+ '<span class="item-side"><span class="pill ' + cls + '">' + esc(label) + '</span><span class="item-time">' + fmtTime(r.at).slice(5) + '</span></span></div>';
|
|
908
|
+
}).join('')
|
|
909
|
+
: '<div class="muted-empty">还没有领取记录</div>';
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function renderStatus() {
|
|
913
|
+
const s = state.status;
|
|
914
|
+
if (!s) return;
|
|
915
|
+
$('ver').textContent = 'v' + s.version + ' · 数据目录 ' + s.dataDir;
|
|
916
|
+
document.title = 'CreditDaddy v' + s.version;
|
|
917
|
+
if (s.homepage) $('btn-home').href = s.homepage;
|
|
918
|
+
const sch = s.scheduler || {};
|
|
919
|
+
$('sched').textContent = sch.ticking ? '正在领取…'
|
|
920
|
+
: sch.nextTickAt ? '下次自动领取 ' + fmtTime(sch.nextTickAt).slice(11) : '';
|
|
921
|
+
$('sched').title = sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at) + ':' + sch.lastTick.summary : '';
|
|
922
|
+
// 产品相关提示只在对应标签页出现
|
|
923
|
+
const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
|
|
924
|
+
const n = $('notice');
|
|
925
|
+
n.classList.toggle('info', state.view === 'zcode' || (hasIntl && Boolean(s.riskIdentity)));
|
|
926
|
+
if (state.view === 'zcode') {
|
|
927
|
+
n.style.display = '';
|
|
928
|
+
const pf = state.zProxyFirst === true;
|
|
929
|
+
const pu = state.zProxyUrl;
|
|
930
|
+
const ac = state.zAutoClaim === true;
|
|
931
|
+
n.innerHTML = 'ZCode 没有每日签到,积分靠「活动领取」。打开「自动领取」后,每轮签到结束会轮询可领活动并尝试领取(桌面版静默过验证码,需要人工时弹验证码窗口);平时保持关闭即可,活动期前再开。也可点卡片上的礼物图标手动领取。'
|
|
932
|
+
+ ' <button class="btn" id="btn-zac" style="margin-left:6px" title="ZCode 活动自动轮询领取(每轮签到后执行一次)">' + (ac ? '自动领取:开' : '自动领取:关') + '</button>'
|
|
933
|
+
+ ' <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>';
|
|
935
|
+
} else if (hasIntl && !s.riskIdentity && umidOffer(s)) {
|
|
936
|
+
n.style.display = '';
|
|
937
|
+
const busy = s.umid.installing || umidBusy;
|
|
938
|
+
n.innerHTML = 'Qoder 国际版只向带设备风控身份的请求下发「每天领 100 Credits」。这台 Linux / NAS 上没有 Qoder 客户端,'
|
|
939
|
+
+ '可以安装「设备身份组件」:从 npm 官方包 @qoder-ai/qodercli 提取其内置的 UMID 程序(约 30MB 下载,校验 npm 完整性),装好后国际版即可领取(本机每天限领一个账号)。'
|
|
940
|
+
+ ' <button class="btn primary" id="btn-umid" style="margin-left:6px"' + (busy ? ' disabled' : '') + '>' + (busy ? '安装中…' : '安装设备身份组件') + '</button>';
|
|
941
|
+
} else if (hasIntl && !s.riskIdentity) {
|
|
942
|
+
n.style.display = '';
|
|
943
|
+
n.textContent = 'Qoder 国际版领取需要本机安装 Qoder 客户端:Qoder 只向带设备风控身份(由客户端生成)的请求下发「每天领 100 Credits」。当前未检测到客户端,国际版账号将无法领取。';
|
|
944
|
+
} else if (hasIntl) {
|
|
945
|
+
n.style.display = '';
|
|
946
|
+
n.textContent = '提示:Qoder 国际版每台设备每天只能有一个账号领取每日 100 Credits(服务端按设备限领),其余国际版账号会显示「本机已领」。'
|
|
947
|
+
+ (s.riskSource === 'cli' && s.umid && s.umid.installed ? '(风控身份来自设备身份组件 qodercli ' + s.umid.installed.version + ')' : '');
|
|
948
|
+
} else {
|
|
949
|
+
n.style.display = 'none';
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Qoder 设备身份组件(Linux / fnOS)
|
|
954
|
+
const umidOffer = (s) => Boolean(s && s.umid && s.umid.supported && !s.umid.installed);
|
|
955
|
+
let umidBusy = false;
|
|
956
|
+
async function installUmid() {
|
|
957
|
+
umidBusy = true; renderStatus();
|
|
958
|
+
try {
|
|
959
|
+
const r = await api('/api/qoder/umid/install', { method: 'POST' });
|
|
960
|
+
toast('设备身份组件已安装(qodercli ' + r.installed.version + '),Qoder 国际版可以领取了');
|
|
961
|
+
} catch (e) { toast(e.message, false); }
|
|
962
|
+
umidBusy = false;
|
|
963
|
+
await loadStatus(); render(); loadLogs();
|
|
964
|
+
}
|
|
965
|
+
$('notice').addEventListener('click', (e) => { if (e.target.closest('#btn-umid')) installUmid(); });
|
|
966
|
+
$('notice').addEventListener('click', async (e) => {
|
|
967
|
+
if (e.target.closest('#btn-zac')) {
|
|
968
|
+
try {
|
|
969
|
+
const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: state.zAutoClaim !== true } });
|
|
970
|
+
state.zAutoClaim = r.autoClaim;
|
|
971
|
+
renderStatus();
|
|
972
|
+
toast('ZCode 自动轮询领取:' + (r.autoClaim ? '已开启(每轮签到后执行)' : '已关闭'));
|
|
973
|
+
} catch (err) { toast(err.message, false); }
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (e.target.closest('#btn-znet')) {
|
|
977
|
+
try {
|
|
978
|
+
const r = await api('/api/zcode/net', { method: 'PUT', json: { proxyFirst: state.zProxyFirst !== true } });
|
|
979
|
+
state.zProxyFirst = r.proxyFirst;
|
|
980
|
+
renderStatus();
|
|
981
|
+
toast('ZCode 出口:' + (r.proxyFirst ? '代理优先(直连兜底)' : '直连优先(代理兜底)'));
|
|
982
|
+
} catch (err) { toast(err.message, false); }
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (e.target.closest('#btn-zproxy')) {
|
|
986
|
+
const cur = state.zProxyUrl || '';
|
|
987
|
+
const v = await uiPrompt('ZCode 出口代理(http://host:port,支持口令 http://user:pass@host:port)\n留空 = 清除,退回 HTTPS_PROXY 环境变量', cur, { okText: '保存' });
|
|
988
|
+
if (v === null) return;
|
|
989
|
+
try {
|
|
990
|
+
await api('/api/zcode/net', { method: 'PUT', json: { proxyUrl: v.trim() } });
|
|
991
|
+
await loadZNet(); renderStatus();
|
|
992
|
+
toast(v.trim() ? '代理已保存' : '代理已清除');
|
|
993
|
+
} catch (err) { toast(err.message, false); }
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
async function loadZNet() {
|
|
997
|
+
try {
|
|
998
|
+
const d = await api('/api/zcode/net');
|
|
999
|
+
state.zProxyFirst = d.proxyFirst === true;
|
|
1000
|
+
state.zProxyUrl = d.proxyUrlMasked || null;
|
|
1001
|
+
state.zAutoClaim = d.autoClaim === true;
|
|
1002
|
+
state.zHasEnvProxy = d.hasEnvProxy === true;
|
|
1003
|
+
if (state.view === 'zcode') renderStatus();
|
|
1004
|
+
} catch (e) {}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ── 10Router ──
|
|
1008
|
+
function quotaPct(q) {
|
|
1009
|
+
if (Number.isFinite(q.remainingPercentage)) return Math.max(0, Math.min(100, q.remainingPercentage));
|
|
1010
|
+
if (q.total > 0) return Math.max(0, Math.min(100, ((q.remaining ?? (q.total - (q.used || 0))) / q.total) * 100));
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
const LOW_PCT = 10;
|
|
1014
|
+
const quotaRemaining = (q) => q.remaining ?? Math.max(0, (q.total || 0) - (q.used || 0));
|
|
1015
|
+
// 没有任何信息的行(总量 0、没用过、也没有百分比),如 Qoder 的「Plan Credits 已用 0」
|
|
1016
|
+
const quotaEmpty = (q) => !(q.total > 0) && !(q.used > 0) && !Number.isFinite(q.remainingPercentage);
|
|
1017
|
+
// 积分包:一次性赠送、会到期的(recurring === false),与主额度 / 周期额度分开展示
|
|
1018
|
+
const isPack = (q) => q.recurring === false;
|
|
1019
|
+
|
|
1020
|
+
/** 把一个连接的额度拆成:主额度(最上面的汇总数) / 周期额度 / 积分包 */
|
|
1021
|
+
function splitQuotas(c) {
|
|
1022
|
+
const list = (c.quotas || []).filter((q) => !quotaEmpty(q));
|
|
1023
|
+
const primary = list.find((q) => q.total > 0 || Number.isFinite(q.remainingPercentage)) || null;
|
|
1024
|
+
const rest = list.filter((q) => q !== primary);
|
|
1025
|
+
return { primary, windows: rest.filter((q) => !isPack(q)), packs: rest.filter(isPack) };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** 额度告急:只看主额度与按时间窗口计量的额度(如 Claude 5h / 7d),用完的赠送积分包不算 */
|
|
1029
|
+
function quotaLow(c) {
|
|
1030
|
+
const { primary, windows } = splitQuotas(c);
|
|
1031
|
+
const watch = [primary, ...windows.filter((q) => q.percentScale)].filter(Boolean);
|
|
1032
|
+
return watch.some((q) => { const v = quotaPct(q); return v !== null && v < LOW_PCT; });
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function quotaValue(q) {
|
|
1036
|
+
const pct = quotaPct(q);
|
|
1037
|
+
if (q.percentScale || (!(q.total > 0) && pct !== null)) return { big: fmtNum(pct) + '%', small: '剩余', pct };
|
|
1038
|
+
if (q.total > 0) return { big: fmtNum(quotaRemaining(q)), small: '/ ' + fmtNum(q.total) + (q.unit ? ' ' + q.unit : ''), pct };
|
|
1039
|
+
return { big: fmtNum(q.used || 0), small: '已用', pct };
|
|
1040
|
+
}
|
|
1041
|
+
const resetLabel = (q) => (q.resetAt ? fmtTime(q.resetAt).slice(5) + (q.recurring === false ? ' 到期' : ' 重置') : '');
|
|
1042
|
+
|
|
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
|
+
// 明细整行:名称 + 数值/相对重置 + 水位条(CLIProxyAPI quotaRow 版式)
|
|
1066
|
+
function trRow(q) {
|
|
1067
|
+
return trLine(q.name, lineVal(q)) + trMeter(quotaPct(q));
|
|
1068
|
+
}
|
|
1069
|
+
|
|
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
|
+
function lineVal(q) {
|
|
1075
|
+
const pct = quotaPct(q);
|
|
1076
|
+
// 百分比口径(限时限额额度):一律显示 %,节约空间;积分类保持实际数字
|
|
1077
|
+
const pctScale = q.percentScale || Number.isFinite(q.remainingPercentage);
|
|
1078
|
+
const txt = (pctScale || !(q.total > 0))
|
|
1079
|
+
? '剩 ' + (pct !== null ? fmtNum(pct) + '%' : fmtNum(quotaRemaining(q)))
|
|
1080
|
+
: '剩 ' + fmtNum(quotaRemaining(q)) + ' / ' + fmtNum(q.total || 0) + (q.unit ? ' ' + q.unit : '');
|
|
1081
|
+
return '<span>' + esc(txt) + '</span> ' + relResetHtml(q);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// 主额度行:名称靠左(可省略),数值与相对重置时间靠右,下方三档水位条。
|
|
1085
|
+
// 百分比口径的主额度(Antigravity 这类「几类限时额度是平等关系」的供应商)不放大,和窗口行同权重。
|
|
1086
|
+
function trPrimaryHtml(q) {
|
|
1087
|
+
if (q.percentScale) return trRow(q);
|
|
1088
|
+
const v = quotaValue(q);
|
|
1089
|
+
return '<div class="tr-primary"><div class="tr-line tr-primary-line"><span class="tr-q-name" title="' + esc(q.name) + '">' + esc(q.name) + '</span>'
|
|
1090
|
+
+ '<span class="tr-q-val"><b>' + esc(v.big) + '</b> <small class="dim">' + esc(v.small) + '</small> ' + relResetHtml(q) + '</span></div>'
|
|
1091
|
+
+ (v.pct !== null ? trMeter(v.pct) : '')
|
|
1092
|
+
+ '</div>';
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function trQuotaHtml(q, { dimWhenEmpty = false } = {}) {
|
|
1096
|
+
const v = quotaValue(q);
|
|
1097
|
+
const used = v.pct !== null && v.pct <= 0;
|
|
1098
|
+
const val = used && dimWhenEmpty ? '已用完' : (q.percentScale || !(q.total > 0) ? '剩余 ' + v.big : '剩余 ' + v.big + ' ' + v.small);
|
|
1099
|
+
return '<div class="tr-q' + (used && dimWhenEmpty ? ' spent' : '') + '"><div class="tr-q-line"><span class="tr-q-name" title="' + esc(q.name) + '">' + esc(q.name) + '</span>'
|
|
1100
|
+
+ '<span class="tr-q-val">' + esc(val) + relResetHtml(q) + '</span></div>'
|
|
1101
|
+
+ (!(used && dimWhenEmpty) ? trMeter(v.pct) : '')
|
|
1102
|
+
+ '</div>';
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// 积分包按名称系列聚合("Bonus Pack 18/19/20" → "Bonus Pack ×3"),去掉尾部序号作键
|
|
1106
|
+
const packKey = (name) => String(name || '').replace(/[\s#(]*\d+\s*[)]?\s*$/, '').trim() || '积分包';
|
|
1107
|
+
function trPacksHtml(c, packs) {
|
|
1108
|
+
if (!packs.length) return '';
|
|
1109
|
+
const alive = packs.filter((q) => quotaRemaining(q) > 0);
|
|
1110
|
+
const spent = packs.filter((q) => quotaRemaining(q) <= 0);
|
|
1111
|
+
const groups = new Map();
|
|
1112
|
+
for (const q of alive) {
|
|
1113
|
+
const k = packKey(q.name);
|
|
1114
|
+
if (!groups.has(k)) groups.set(k, { name: k, count: 0, rem: 0, tot: 0, resetAt: null, singles: [] });
|
|
1115
|
+
const g = groups.get(k);
|
|
1116
|
+
g.count++;
|
|
1117
|
+
g.rem += quotaRemaining(q);
|
|
1118
|
+
g.tot += q.total || 0;
|
|
1119
|
+
if (q.resetAt && (!g.resetAt || String(q.resetAt) < String(g.resetAt))) g.resetAt = q.resetAt;
|
|
1120
|
+
g.singles.push(q);
|
|
1121
|
+
}
|
|
1122
|
+
const gs = [...groups.values()].sort((a, b) => String(a.resetAt || '9999').localeCompare(String(b.resetAt || '9999')));
|
|
1123
|
+
const aliveSum = alive.reduce((n, q) => n + quotaRemaining(q), 0);
|
|
1124
|
+
const open = Boolean(state.trOpen && state.trOpen[c.id]);
|
|
1125
|
+
const head = '<div class="tr-packs-head">积分包 <b>' + alive.length + '</b> 个可用'
|
|
1126
|
+
+ (alive.length ? ',共剩 ' + fmtNum(aliveSum) : '')
|
|
1127
|
+
+ (spent.length ? ' · ' + spent.length + ' 个已用完' : '') + '</div>';
|
|
1128
|
+
const lines = gs.map((g) => trLine(
|
|
1129
|
+
g.name + (g.count > 1 ? ' ×' + g.count : ''),
|
|
1130
|
+
esc('剩 ' + fmtNum(g.rem) + (g.tot > 0 ? ' / ' + fmtNum(g.tot) : ''))
|
|
1131
|
+
+ (g.resetAt ? '<span class="tr-q-when"> · 最早 ' + esc(fmtTime(g.resetAt).slice(5, 10)) + ' 到期</span>' : ''),
|
|
1132
|
+
)).join('');
|
|
1133
|
+
const detail = open
|
|
1134
|
+
? [...alive.sort((a, b) => String(a.resetAt || '9999').localeCompare(String(b.resetAt || '9999'))), ...spent]
|
|
1135
|
+
.map((q) => trQuotaHtml(q, { dimWhenEmpty: true })).join('')
|
|
1136
|
+
: '';
|
|
1137
|
+
const collapsible = alive.length > gs.length || spent.length > 0;
|
|
1138
|
+
return '<div class="tr-packs">' + head + (open ? detail : lines)
|
|
1139
|
+
+ (collapsible ? '<button class="tr-more" data-tr="toggle" data-id="' + esc(c.id) + '">' + (open ? '收起' : '逐包明细') + '</button>' : '')
|
|
1140
|
+
+ '</div>';
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function trCardHtml(c) {
|
|
1144
|
+
const who = c.name || c.email || c.id.slice(0, 8);
|
|
1145
|
+
// 套餐名与供应商名重复时不再单独显示(如 StepFun CN · StepFun CN)
|
|
1146
|
+
const norm = (x) => String(x || '').toLowerCase().replace(/[^a-z0-9一-龥]/g, '');
|
|
1147
|
+
const showPlan = c.plan && !norm(c.plan).includes(norm(c.providerName)) && !norm(c.providerName).includes(norm(c.plan));
|
|
1148
|
+
let body;
|
|
1149
|
+
if (c.error) {
|
|
1150
|
+
body = '<div class="msg err" title="' + esc(c.error) + '">查询失败:' + esc(c.error.slice(0, 80)) + '</div>';
|
|
1151
|
+
} else {
|
|
1152
|
+
const { primary, windows, packs } = splitQuotas(c);
|
|
1153
|
+
if (!(primary || windows.length || packs.length)) {
|
|
1154
|
+
body = '<div class="msg">' + esc(/no separate quota|billed at standard/i.test(c.message || '') ? '按量计费,无额度可显示' : (c.message || '该供应商未返回额度数据')) + '</div>';
|
|
1155
|
+
} else {
|
|
1156
|
+
// 与汇总卡同款默认视图:主额度 + 每个周 / 月窗口额度一条(带进度条);积分包走聚合行,「逐包明细」展开单包
|
|
1157
|
+
body = (primary ? trPrimaryHtml(primary) : '')
|
|
1158
|
+
+ windows.map((q) => trRow(q)).join('')
|
|
1159
|
+
+ trPacksHtml(c, packs);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return '<div class="tr-card' + (c.isActive ? '' : ' off') + '"><div class="row1"><div class="name-group">'
|
|
1163
|
+
+ '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>'
|
|
1164
|
+
+ (TR_REGION[c.provider] ? '<span class="badge ' + TR_REGION[c.provider][1] + '">' + TR_REGION[c.provider][0] + '</span>' : '')
|
|
1165
|
+
+ '<span class="tr-name" title="' + esc([c.name, c.email].filter(Boolean).join(' · ')) + '">' + esc(who) + '</span>'
|
|
1166
|
+
+ (showPlan ? '<span class="badge">' + esc(c.plan) + '</span>' : '')
|
|
1167
|
+
+ (c.isActive ? '' : '<span class="pill pending">已停用</span>')
|
|
1168
|
+
+ (c.limitReached ? '<span class="pill fail">已达上限</span>' : '')
|
|
1169
|
+
+ (c.isActive && !c.error && quotaLow(c) ? '<span class="pill limited">告急</span>' : '')
|
|
1170
|
+
+ '</div></div>' + body + '</div>';
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// 启用的在前,再按供应商、账号名排序
|
|
1174
|
+
const trSorted = (list) => list.slice().sort((a, b) => (b.isActive - a.isActive)
|
|
1175
|
+
|| String(a.providerName).localeCompare(String(b.providerName)) || String(a.name || a.email || '').localeCompare(String(b.name || b.email || '')));
|
|
1176
|
+
|
|
1177
|
+
// 按供应商聚类:同一供应商的账号排在一行网格里,组内保持 trSorted 顺序
|
|
1178
|
+
function trGroups(list) {
|
|
1179
|
+
const map = new Map();
|
|
1180
|
+
for (const c of trSorted(list)) {
|
|
1181
|
+
const k = c.providerName || c.provider || '其他';
|
|
1182
|
+
if (!map.has(k)) map.set(k, []);
|
|
1183
|
+
map.get(k).push(c);
|
|
1184
|
+
}
|
|
1185
|
+
return [...map.entries()].map(([name, items]) => ({ name, items }));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// 10Router 连接的国内 / 国际版标签(沿用账号卡的徽标语义)
|
|
1189
|
+
const TR_REGION = {
|
|
1190
|
+
qoder: ['国际版', 'intl'], 'qoder-cn': ['国内版', 'cn'],
|
|
1191
|
+
'codebuddy-intl': ['国际版', 'intl'], 'codebuddy-cn': ['国内版', 'cn'],
|
|
1192
|
+
'glm-cn': ['国内版', 'cn'],
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
// 「全部」视图:一个供应商一张汇总卡。同名额度跨连接求和;百分比窗口(如限时窗口)取平均。
|
|
1196
|
+
// 命名含 Cycle/Capacity 的包本身是「全包汇总」,连接级聚合时跳过以避免重复计数(Total Points 这类)。
|
|
1197
|
+
const isAggregatePack = (name) => /cycle|capacity/i.test(String(name || ''));
|
|
1198
|
+
function trVendorCardHtml(g) {
|
|
1199
|
+
const items = g.items;
|
|
1200
|
+
const prov = items[0].provider;
|
|
1201
|
+
const lows = items.filter((x) => x.isActive && !x.error && quotaLow(x)).length;
|
|
1202
|
+
const errs = items.filter((x) => x.error).length;
|
|
1203
|
+
|
|
1204
|
+
const splitOf = items.map((x) => ({ x, s: splitQuotas(x) }));
|
|
1205
|
+
// Total Points / 含 Cycle|Capacity 的包是全包汇总:连接级聚合跳过它们,避免与子包重复计数
|
|
1206
|
+
const isAgg = (q) => isAggregatePack(q && q.name);
|
|
1207
|
+
const primaries = splitOf.filter(({ s }) => s.primary && !isAgg(s.primary)).map(({ s }) => s.primary);
|
|
1208
|
+
let primary = null;
|
|
1209
|
+
if (primaries.length) {
|
|
1210
|
+
const name = primaries[0].name;
|
|
1211
|
+
const same = primaries.filter((p) => p.name === name);
|
|
1212
|
+
const total = same.reduce((n, p) => n + (p.total > 0 ? p.total : 0), 0);
|
|
1213
|
+
// 源数据本身是百分比口径(percentScale / remainingPercentage)→ 聚合也用 %(反重力这类窗口间平等)
|
|
1214
|
+
const isPct = same[0].percentScale || Number.isFinite(same[0].remainingPercentage) || !(total > 0);
|
|
1215
|
+
if (isPct) {
|
|
1216
|
+
// 限时百分比窗口(如 Claude 5h):跨连接取平均百分比,而不是加总
|
|
1217
|
+
const pcts = same.map((p) => quotaPct(p)).filter((x) => x !== null);
|
|
1218
|
+
primary = {
|
|
1219
|
+
name,
|
|
1220
|
+
total: 0,
|
|
1221
|
+
remaining: 0,
|
|
1222
|
+
used: 0,
|
|
1223
|
+
unit: '',
|
|
1224
|
+
percentScale: true,
|
|
1225
|
+
remainingPercentage: pcts.length ? pcts.reduce((a, b) => a + b, 0) / pcts.length : null,
|
|
1226
|
+
recurring: same[0].recurring,
|
|
1227
|
+
resetAt: same.map((p) => p.resetAt).filter(Boolean).sort()[0] || null,
|
|
1228
|
+
};
|
|
1229
|
+
} else {
|
|
1230
|
+
primary = {
|
|
1231
|
+
name,
|
|
1232
|
+
total,
|
|
1233
|
+
remaining: same.reduce((n, p) => n + quotaRemaining(p), 0),
|
|
1234
|
+
used: same.reduce((n, p) => n + (p.used || 0), 0),
|
|
1235
|
+
unit: same[0].unit || '',
|
|
1236
|
+
percentScale: same[0].percentScale === true,
|
|
1237
|
+
recurring: same[0].recurring,
|
|
1238
|
+
resetAt: same.map((p) => p.resetAt).filter(Boolean).sort()[0] || null,
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// 周期窗口:同名加总;本身是百分比口径的行(percentScale / remainingPercentage)按 % 均值聚合
|
|
1244
|
+
const winMap = new Map();
|
|
1245
|
+
const packs = [];
|
|
1246
|
+
for (const { s } of splitOf) {
|
|
1247
|
+
for (const q of s.windows.filter((x) => !isAgg(x))) {
|
|
1248
|
+
if (!winMap.has(q.name)) winMap.set(q.name, { name: q.name, total: 0, rem: 0, anyTotal: false, pctScale: false, resetAt: null, pcts: [] });
|
|
1249
|
+
const w = winMap.get(q.name);
|
|
1250
|
+
const isPctRow = q.percentScale || Number.isFinite(q.remainingPercentage);
|
|
1251
|
+
if (isPctRow) w.pctScale = true;
|
|
1252
|
+
const pct = quotaPct(q);
|
|
1253
|
+
if (pct !== null) w.pcts.push(pct);
|
|
1254
|
+
if (!isPctRow && q.total > 0) { w.anyTotal = true; w.total += q.total; w.rem += quotaRemaining(q); }
|
|
1255
|
+
if (q.resetAt && (!w.resetAt || String(q.resetAt) < String(w.resetAt))) w.resetAt = q.resetAt;
|
|
1256
|
+
}
|
|
1257
|
+
packs.push(...s.packs.filter((x) => !isAgg(x)));
|
|
1258
|
+
}
|
|
1259
|
+
const wins = [...winMap.values()];
|
|
1260
|
+
const alivePacks = packs.filter((q) => quotaRemaining(q) > 0);
|
|
1261
|
+
const packRem = alivePacks.reduce((n, q) => n + quotaRemaining(q), 0);
|
|
1262
|
+
|
|
1263
|
+
// 周 / 月度等窗口额度也给进度条:百分比口径显示 % 均值;固定额度加总数字
|
|
1264
|
+
const avgPct = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : null);
|
|
1265
|
+
const winRows = wins.map((w) => trRow(w.pctScale
|
|
1266
|
+
? { name: w.name, total: 0, remainingPercentage: avgPct(w.pcts), percentScale: true, resetAt: w.resetAt, recurring: true }
|
|
1267
|
+
: (w.anyTotal
|
|
1268
|
+
? { name: w.name, total: w.total, remaining: w.rem, used: w.total - w.rem, unit: '', percentScale: false, resetAt: w.resetAt, recurring: true }
|
|
1269
|
+
: { name: w.name, total: 0, remainingPercentage: avgPct(w.pcts), percentScale: true, resetAt: w.resetAt, recurring: true }))).join('');
|
|
1270
|
+
|
|
1271
|
+
const briefs = [];
|
|
1272
|
+
if (alivePacks.length) briefs.push('积分包×' + alivePacks.length + ' 剩 ' + fmtNum(packRem));
|
|
1273
|
+
else if (packs.length) briefs.push('积分包已用完');
|
|
1274
|
+
const hasQuotas = Boolean(primary || wins.length || packs.length);
|
|
1275
|
+
let quotaHtml;
|
|
1276
|
+
if (errs === items.length) {
|
|
1277
|
+
quotaHtml = '<div class="msg err">全部连接查询失败</div>';
|
|
1278
|
+
} else if (!hasQuotas) {
|
|
1279
|
+
quotaHtml = '<div class="msg">该供应商没有可汇总的额度</div>';
|
|
1280
|
+
} else {
|
|
1281
|
+
// 「全部」视角不显示「最早到期」:每行自带相对重置时间,足够判断
|
|
1282
|
+
quotaHtml = (primary ? trPrimaryHtml(primary) : '')
|
|
1283
|
+
+ winRows
|
|
1284
|
+
+ (briefs.length
|
|
1285
|
+
? '<div class="tr-summary dim"><span class="tr-sum-txt">' + briefs.join(' · ') + '</span></div>'
|
|
1286
|
+
: '');
|
|
1287
|
+
}
|
|
1288
|
+
return '<div class="tr-card"><div class="row1"><div class="name-group">'
|
|
1289
|
+
+ '<span class="badge prod tenrouter">' + esc(g.name) + '</span>'
|
|
1290
|
+
+ (TR_REGION[prov] ? '<span class="badge ' + TR_REGION[prov][1] + '">' + TR_REGION[prov][0] + '</span>' : '')
|
|
1291
|
+
+ '<span class="badge">' + items.length + ' 个账号</span>'
|
|
1292
|
+
+ (lows ? '<span class="pill limited">告急 ×' + lows + '</span>' : '')
|
|
1293
|
+
+ (errs ? '<span class="pill fail" title="部分连接查询失败">失败 ×' + errs + '</span>' : '')
|
|
1294
|
+
+ '</div></div>' + quotaHtml + '</div>';
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function trSetupHtml() {
|
|
1298
|
+
return '<div class="setup"><h3>接入 10Router</h3>'
|
|
1299
|
+
+ '<div class="hint">填 10Router 的地址和一个<b>虚拟 key</b>(10Router 仪表盘 → API Keys 创建的 sk-…),就能在这里:</div>'
|
|
1300
|
+
+ '<div class="hint" style="margin:6px 0 12px">· 查看 10Router 里其他供应商(Claude / Codex / Kiro / GLM …)的额度卡片(需要 10Router 1.2.1+)<br>'
|
|
1301
|
+
+ '· 把本机 ZCode / OpenCode / mirasim / 小米 MiMo 的用量同步进 10Router 的用量统计(同 10router-sync 插件,自动每小时一次)</div>'
|
|
1302
|
+
+ '<button class="btn primary" data-tr="settings">' + I.plus + '配置 10Router</button></div>';
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function renderTenrouter() {
|
|
1306
|
+
const box = $('tr-panel');
|
|
1307
|
+
const c = tr.config;
|
|
1308
|
+
if (!c) { box.innerHTML = '<div class="empty">加载中…</div>'; return; }
|
|
1309
|
+
if (!c.configured) { box.innerHTML = trSetupHtml(); return; }
|
|
1310
|
+
const list = (tr.quotas && tr.quotas.connections) || [];
|
|
1311
|
+
const low = list.filter((x) => x.isActive && !x.error && quotaLow(x)).length;
|
|
1312
|
+
const failed = list.filter((x) => x.error).length;
|
|
1313
|
+
const ls = c.lastSync;
|
|
1314
|
+
const srcChips = (c.sources || []).map((x) => {
|
|
1315
|
+
const on = x.found && c.sync.sources.includes(x.id);
|
|
1316
|
+
return '<span class="chip' + (on ? ' on' : '') + '" title="' + (x.found ? (x.supported ? '已检测到' : '需要 Node 22.5+') : '本机未检测到') + '">' + esc(x.label) + (x.found ? '' : ' ·无') + '</span>';
|
|
1317
|
+
}).join('');
|
|
1318
|
+
let grid;
|
|
1319
|
+
let tabStrip = '';
|
|
1320
|
+
const groups = list.length ? trGroups(list) : [];
|
|
1321
|
+
const vendor = state.trVendor || '';
|
|
1322
|
+
// 「隐藏 0 / 无额度」与列表共用同一过滤:被隐藏的供应商不出现在标签条
|
|
1323
|
+
const shown = state.trHideEmpty
|
|
1324
|
+
? groups.filter((g) => {
|
|
1325
|
+
if (g.items.every((x) => x.error)) return true;
|
|
1326
|
+
for (const x of g.items) {
|
|
1327
|
+
if (x.error) continue;
|
|
1328
|
+
const s = splitQuotas(x);
|
|
1329
|
+
if (s.primary && quotaRemaining(s.primary) > 0) return true;
|
|
1330
|
+
if (s.windows.some((q) => quotaRemaining(q) > 0 || (quotaPct(q) || 0) > 0)) return true;
|
|
1331
|
+
if (s.packs.some((q) => quotaRemaining(q) > 0)) return true;
|
|
1332
|
+
}
|
|
1333
|
+
return false;
|
|
1334
|
+
})
|
|
1335
|
+
: groups;
|
|
1336
|
+
const activeVendor = vendor && shown.some((g) => g.name === vendor) ? vendor : '';
|
|
1337
|
+
if (groups.length) {
|
|
1338
|
+
tabStrip = '<div class="tr-tabs">'
|
|
1339
|
+
+ '<button class="btn' + (!activeVendor ? ' primary' : '') + '" data-tr="vendor" data-v="">全部</button>'
|
|
1340
|
+
+ shown.map((g) => '<button class="btn' + (activeVendor === g.name ? ' primary' : '') + '" data-tr="vendor" data-v="' + esc(g.name) + '">' + esc(g.name) + ' ' + g.items.length + '</button>').join('')
|
|
1341
|
+
+ '</div>';
|
|
1342
|
+
}
|
|
1343
|
+
if (tr.error) grid = '<div class="empty">' + esc(tr.error) + '</div>';
|
|
1344
|
+
else if (!tr.quotas) grid = '<div class="empty">正在读取 10Router 供应商额度…</div>';
|
|
1345
|
+
else if (!list.length) grid = '<div class="empty">10Router 里还没有可查额度的供应商连接(OAuth 或支持额度查询的 API Key)。</div>';
|
|
1346
|
+
else if (activeVendor) {
|
|
1347
|
+
const g = groups.find((x) => x.name === activeVendor) || { items: [] };
|
|
1348
|
+
grid = '<div class="tr-vcards">' + g.items.map(trCardHtml).join('') + '</div>';
|
|
1349
|
+
} else {
|
|
1350
|
+
grid = shown.length
|
|
1351
|
+
? '<div class="tr-vcards">' + shown.map(trVendorCardHtml).join('') + '</div>'
|
|
1352
|
+
: '<div class="empty">没有可显示的供应商(可在「设置」里关掉隐藏)</div>';
|
|
1353
|
+
}
|
|
1354
|
+
const h = tr.health;
|
|
1355
|
+
const healthStrong = !h ? '…' : h.ok ? '正常' : '异常';
|
|
1356
|
+
const healthDetail = !h ? '' : h.ok
|
|
1357
|
+
? (h.driver ? '存储驱动 ' + h.driver : '') + (h.lastDriverError ? '(驱动降级:' + h.lastDriverError + ')' : '')
|
|
1358
|
+
: (h.error || '');
|
|
1359
|
+
box.innerHTML = '<div class="toolbar"><div class="summary">'
|
|
1360
|
+
+ '<div class="stat" title="' + esc(healthDetail) + '"><span>10Router 健康</span><strong' + (h && !h.ok ? ' style="color:var(--err)"' : (h && h.lastDriverError ? ' style="color:var(--warn,#b45309)"' : '')) + '>' + healthStrong + '</strong></div><div class="divider"></div>'
|
|
1361
|
+
+ '<div class="stat"><span>供应商连接</span><strong>' + (tr.quotas ? list.length : '-') + '</strong></div><div class="divider"></div>'
|
|
1362
|
+
+ '<div class="stat"><span>额度告急</span><strong>' + (tr.quotas ? low : '-') + '</strong></div><div class="divider"></div>'
|
|
1363
|
+
+ '<div class="stat"><span>查询失败</span><strong>' + (tr.quotas ? failed : '-') + '</strong></div>'
|
|
1364
|
+
+ '</div><div class="actions">'
|
|
1365
|
+
+ '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' + I.refresh + (tr.loading ? '刷新中…' : '刷新额度') + '</button>'
|
|
1366
|
+
+ '<button class="btn" data-tr="sync"' + (tr.syncing || c.syncing ? ' disabled' : '') + '>' + I.upload + (tr.syncing || c.syncing ? '同步中…' : '同步用量') + '</button>'
|
|
1367
|
+
+ '<button class="btn" data-tr="settings">设置</button>'
|
|
1368
|
+
+ '</div></div>'
|
|
1369
|
+
+ '<div class="tr-bar"><span title="' + esc(c.endpoint) + '">' + esc(c.endpoint) + ' · ' + esc(c.keyMasked) + '</span>'
|
|
1370
|
+
+ '<span>用量同步:' + (c.sync.enabled ? '每小时自动' : '手动') + '</span>' + srcChips
|
|
1371
|
+
+ '<span style="margin-left:auto" title="' + esc(ls ? ls.summary : '') + '">' + (ls ? '上次同步 ' + esc(fmtTime(ls.at).slice(5)) + ' · ' + esc(ls.summary.slice(0, 40)) : '还没有同步过') + '</span>'
|
|
1372
|
+
+ (tr.quotas ? '<span>额度更新于 ' + esc(fmtTime(tr.quotas.generatedAt).slice(11)) + '</span>' : '')
|
|
1373
|
+
+ '</div>'
|
|
1374
|
+
+ tabStrip
|
|
1375
|
+
+ '<div class="tr-grid">' + grid + '</div>';
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function trDashCard() {
|
|
1379
|
+
const c = tr.config;
|
|
1380
|
+
if (!c || !c.configured) return '';
|
|
1381
|
+
const list = (tr.quotas && tr.quotas.connections) || [];
|
|
1382
|
+
const low = list.filter((x) => x.isActive && !x.error && quotaLow(x)).length;
|
|
1383
|
+
const ls = c.lastSync;
|
|
1384
|
+
const rows = '<span class="lbl">供应商连接</span><span class="val">' + (tr.quotas ? list.length : tr.error ? '读取失败' : '…') + '</span>'
|
|
1385
|
+
+ '<span class="lbl">额度告急</span><span class="val' + (low ? ' warn' : '') + '">' + (tr.quotas ? low : '-') + '</span>'
|
|
1386
|
+
+ '<span class="lbl">用量同步</span><span class="val">' + (ls ? esc(fmtTime(ls.at).slice(5)) : c.sync.enabled ? '等待首次同步' : '手动') + '</span>';
|
|
1387
|
+
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">查看 →</span></div><div class="prod-rows">' + rows + '</div></div>';
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
async function loadTrConfig() {
|
|
1392
|
+
try { tr.config = await api('/api/tenrouter'); } catch (e) { tr.config = null; }
|
|
1393
|
+
render();
|
|
1394
|
+
}
|
|
1395
|
+
async function loadTrHealth() {
|
|
1396
|
+
if (!tr.config || !tr.config.configured) { tr.health = null; render(); return; }
|
|
1397
|
+
try { tr.health = await api('/api/tenrouter/health'); } catch (e) { tr.health = { ok: false, error: e.message }; }
|
|
1398
|
+
render();
|
|
1399
|
+
}
|
|
1400
|
+
async function loadTrQuotas(force) {
|
|
1401
|
+
if (!tr.config || !tr.config.configured) return;
|
|
1402
|
+
loadTrHealth();
|
|
1403
|
+
tr.loading = true; render();
|
|
1404
|
+
try { tr.quotas = await api('/api/tenrouter/quotas' + (force ? '?force=1' : '')); tr.error = null; }
|
|
1405
|
+
catch (e) { tr.error = e.message; if (e.code === 'TOO_OLD') tr.quotas = null; }
|
|
1406
|
+
tr.loading = false; render();
|
|
1407
|
+
}
|
|
1408
|
+
async function trSync() {
|
|
1409
|
+
tr.syncing = true; render();
|
|
1410
|
+
try { const r = await api('/api/tenrouter/sync', { method: 'POST' }); toast(r.summary, !r.results.some((x) => x.status === 'failed')); }
|
|
1411
|
+
catch (e) { toast(e.message, false); }
|
|
1412
|
+
tr.syncing = false;
|
|
1413
|
+
await loadTrConfig(); loadLogs();
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function openTrSettings() {
|
|
1417
|
+
const c = tr.config || { sync: { enabled: true, sources: [] }, sources: [] };
|
|
1418
|
+
$('tr-endpoint').value = c.endpoint || '';
|
|
1419
|
+
$('tr-key').value = '';
|
|
1420
|
+
$('tr-key').placeholder = c.keyMasked ? '已保存 ' + c.keyMasked + '(留空保持不变)' : 'sk-…';
|
|
1421
|
+
$('tr-auto').checked = c.configured ? c.sync.enabled : true;
|
|
1422
|
+
$('tr-hide-empty').checked = state.trHideEmpty === true;
|
|
1423
|
+
$('tr-sources').innerHTML = (c.sources || []).map((x) => '<label class="check"><input type="checkbox" data-src="' + x.id + '"'
|
|
1424
|
+
+ ((c.configured ? c.sync.sources.includes(x.id) : true) ? ' checked' : '') + (x.supported ? '' : ' disabled') + '> ' + esc(x.label)
|
|
1425
|
+
+ '<span class="dim">' + (x.found ? '' : '(本机未检测到)') + (x.supported ? '' : '(需要 Node 22.5+)') + '</span></label>').join('');
|
|
1426
|
+
$('tr-disconnect').style.display = c.configured ? '' : 'none';
|
|
1427
|
+
$('tr-test-msg').textContent = '';
|
|
1428
|
+
openModal('m-10r');
|
|
1429
|
+
setTimeout(() => $(c.configured ? 'tr-key' : 'tr-endpoint').focus(), 30);
|
|
1430
|
+
}
|
|
1431
|
+
function trForm() {
|
|
1432
|
+
return {
|
|
1433
|
+
endpoint: $('tr-endpoint').value.trim(),
|
|
1434
|
+
key: $('tr-key').value.trim(),
|
|
1435
|
+
syncEnabled: $('tr-auto').checked,
|
|
1436
|
+
sources: [...document.querySelectorAll('#tr-sources input[data-src]:checked')].map((x) => x.dataset.src),
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
async function trTest() {
|
|
1440
|
+
const f = trForm();
|
|
1441
|
+
$('tr-test-msg').style.color = '';
|
|
1442
|
+
$('tr-test-msg').textContent = '测试中…';
|
|
1443
|
+
try {
|
|
1444
|
+
const r = await api('/api/tenrouter/test', { json: { endpoint: f.endpoint, key: f.key } });
|
|
1445
|
+
$('tr-test-msg').style.color = r.ok ? 'var(--ok)' : 'var(--err)';
|
|
1446
|
+
$('tr-test-msg').textContent = !r.ok ? '✗ ' + r.error
|
|
1447
|
+
: r.quotas ? '✓ 连接成功:' + r.connections + ' 个可查额度的供应商连接' : '✓ 已连接,但' + r.warning + '(用量同步可以正常使用)';
|
|
1448
|
+
} catch (e) { $('tr-test-msg').style.color = 'var(--err)'; $('tr-test-msg').textContent = '✗ ' + e.message; }
|
|
1449
|
+
}
|
|
1450
|
+
async function trSave() {
|
|
1451
|
+
const f = trForm();
|
|
1452
|
+
if (!f.endpoint) return toast('请填写 10Router 地址', false);
|
|
1453
|
+
if (!f.key && !(tr.config && tr.config.keyMasked)) return toast('请填写虚拟 key', false);
|
|
1454
|
+
// 显示偏好只存在面板本地(localStorage),不上服务端
|
|
1455
|
+
state.trHideEmpty = $('tr-hide-empty').checked;
|
|
1456
|
+
try { localStorage.setItem('qd_tr_hideempty', state.trHideEmpty ? '1' : '0'); } catch (e) {}
|
|
1457
|
+
try {
|
|
1458
|
+
tr.config = await api('/api/tenrouter', { method: 'PUT', json: f });
|
|
1459
|
+
closeModal('m-10r');
|
|
1460
|
+
toast('已保存 10Router 设置');
|
|
1461
|
+
tr.quotas = null; tr.error = null;
|
|
1462
|
+
loadTrQuotas(true);
|
|
1463
|
+
} catch (e) { toast(e.message, false); }
|
|
1464
|
+
}
|
|
1465
|
+
async function trDisconnect() {
|
|
1466
|
+
if (!(await uiConfirm('断开后会删除保存的 10Router 地址、key 与同步进度(10Router 里已同步的数据不受影响)。', '断开 10Router', '断开'))) return;
|
|
1467
|
+
try { tr.config = await api('/api/tenrouter', { method: 'DELETE' }); tr.quotas = null; tr.error = null; closeModal('m-10r'); render(); }
|
|
1468
|
+
catch (e) { toast(e.message, false); }
|
|
1469
|
+
}
|
|
1470
|
+
$('tr-panel').addEventListener('click', (e) => {
|
|
1471
|
+
const b = e.target.closest('[data-tr]');
|
|
1472
|
+
if (!b) return;
|
|
1473
|
+
if (b.dataset.tr === 'toggle') { state.trOpen = Object.assign({}, state.trOpen, { [b.dataset.id]: !(state.trOpen && state.trOpen[b.dataset.id]) }); render(); return; }
|
|
1474
|
+
if (b.dataset.tr === 'settings') openTrSettings();
|
|
1475
|
+
else if (b.dataset.tr === 'refresh') loadTrQuotas(true);
|
|
1476
|
+
else if (b.dataset.tr === 'sync') trSync();
|
|
1477
|
+
else if (b.dataset.tr === 'vendor') {
|
|
1478
|
+
const v = b.dataset.v || '';
|
|
1479
|
+
state.trVendor = v;
|
|
1480
|
+
history.replaceState(null, '', v ? '#tenrouter/' + encodeURIComponent(v) : '#tenrouter');
|
|
1481
|
+
render();
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
$('tr-test').onclick = trTest;
|
|
1485
|
+
$('tr-save').onclick = trSave;
|
|
1486
|
+
$('tr-disconnect').onclick = trDisconnect;
|
|
1487
|
+
|
|
1488
|
+
// ── 数据加载 ──
|
|
1489
|
+
async function loadStatus() {
|
|
1490
|
+
try { state.status = await api('/api/status'); renderStatus(); } catch (e) {}
|
|
1491
|
+
}
|
|
1492
|
+
async function loadAccounts() {
|
|
1493
|
+
const data = await api('/api/accounts');
|
|
1494
|
+
state.accounts = data.accounts;
|
|
1495
|
+
render();
|
|
1496
|
+
renderStatus();
|
|
1497
|
+
loadQuotas();
|
|
1498
|
+
}
|
|
1499
|
+
let quotaRun = 0;
|
|
1500
|
+
async function loadQuotas(ids) {
|
|
1501
|
+
const run = ++quotaRun;
|
|
1502
|
+
const queue = (ids || state.accounts.map((a) => a.id)).slice();
|
|
1503
|
+
if (ids) for (const id of ids) delete state.quotas[id];
|
|
1504
|
+
const worker = async () => {
|
|
1505
|
+
while (queue.length && run === quotaRun) {
|
|
1506
|
+
const id = queue.shift();
|
|
1507
|
+
try { state.quotas[id] = (await api('/api/accounts/' + encodeURIComponent(id) + '/quota')).quota; }
|
|
1508
|
+
catch (e) { state.quotas[id] = { error: e.message }; }
|
|
1509
|
+
render();
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
await Promise.all([worker(), worker(), worker()]);
|
|
1513
|
+
}
|
|
1514
|
+
async function loadLogs() {
|
|
1515
|
+
try {
|
|
1516
|
+
const data = await api('/api/logs');
|
|
1517
|
+
const el = $('logs');
|
|
1518
|
+
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
|
|
1519
|
+
el.innerHTML = data.logs.length ? data.logs.map((l) => '<div class="' + esc(l.level) + '">[' + esc(l.at) + '] ' + esc(l.tag) + ' · ' + esc(l.msg) + '</div>').join('') : '(暂无)';
|
|
1520
|
+
if (atBottom) el.scrollTop = el.scrollHeight;
|
|
1521
|
+
} catch (e) {}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// ── 账号操作 ──
|
|
1525
|
+
async function checkinOne(id) {
|
|
1526
|
+
state.busy[id] = true; render();
|
|
1527
|
+
try {
|
|
1528
|
+
const d = await api('/api/accounts/' + encodeURIComponent(id) + '/checkin', { method: 'POST' });
|
|
1529
|
+
const r = d.results && d.results[0];
|
|
1530
|
+
toast(r ? (r.account + ':' + (r.status === 'checked-in' ? '领取成功 +' + r.claimedAmount + ' Credits' : (r.error || r.message || r.status))) : d.summary, !(r && r.status === 'failed'));
|
|
1531
|
+
} catch (e) { toast(e.message, false); }
|
|
1532
|
+
delete state.busy[id];
|
|
1533
|
+
await loadStatus(); await loadAccounts(); loadLogs();
|
|
1534
|
+
}
|
|
1535
|
+
async function checkinAll(btn, product) {
|
|
1536
|
+
const label = btn.innerHTML;
|
|
1537
|
+
btn.disabled = true; btn.innerHTML = I.refresh + '领取中…';
|
|
1538
|
+
try {
|
|
1539
|
+
const d = await api('/api/checkin', { json: { skipIfCheckedToday: false, product } });
|
|
1540
|
+
toast(d.summary);
|
|
1541
|
+
} catch (e) { toast(e.message, false); }
|
|
1542
|
+
btn.disabled = false; btn.innerHTML = label;
|
|
1543
|
+
await loadStatus(); await loadAccounts(); loadLogs();
|
|
1544
|
+
}
|
|
1545
|
+
async function onCardClick(e) {
|
|
1546
|
+
const btn = e.target.closest('[data-act]');
|
|
1547
|
+
if (!btn) return;
|
|
1548
|
+
const id = btn.closest('.card').dataset.id;
|
|
1549
|
+
const act = btn.dataset.act;
|
|
1550
|
+
if (act === 'checkin') return checkinOne(id);
|
|
1551
|
+
if (act === 'quota') { loadQuotas([id]); return; }
|
|
1552
|
+
if (act === 'claim') return openClaim(id);
|
|
1553
|
+
if (act === 'switch') {
|
|
1554
|
+
const a = state.accounts.find((x) => x.id === id);
|
|
1555
|
+
const client = CLIENT_LABEL[a && a.product] || '客户端';
|
|
1556
|
+
const tip = a && a.product === 'zcode'
|
|
1557
|
+
? '\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
|
|
1558
|
+
: '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
|
|
1559
|
+
if (!(await uiConfirm('把 ' + client + ' 切换到「' + (a && a.name || id) + '」?' + tip, '切换账号', '切换'))) return;
|
|
1560
|
+
const doSwitch = (force) => api('/api/accounts/' + encodeURIComponent(id) + '/switch', { json: { force } });
|
|
1561
|
+
try {
|
|
1562
|
+
let r;
|
|
1563
|
+
try { r = await doSwitch(false); }
|
|
1564
|
+
catch (err) {
|
|
1565
|
+
if (err.code !== 'ZCODE_RUNNING') throw err;
|
|
1566
|
+
if (!(await uiConfirm(err.message + '\n\n仍要强制切换吗?强制切换会先关闭 ZCode 客户端再写入登录(未保存的对话内容可能丢失),重新打开 ZCode 即为新账号。', 'ZCode 正在运行', '关闭并强制切换'))) return;
|
|
1567
|
+
r = await doSwitch(true);
|
|
1568
|
+
}
|
|
1569
|
+
toast(r && r.alreadyActive ? client + ' 当前已是该账号' : (r && r.closedClient ? '已关闭 ZCode,' : '') + client + ' 已切换到 ' + (a && a.name || id));
|
|
1570
|
+
await loadStatus(); await loadAccounts();
|
|
1571
|
+
} catch (err) { toast(err.message, false); }
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (act === 'rename') {
|
|
1575
|
+
const a = state.accounts.find((x) => x.id === id);
|
|
1576
|
+
const name = await uiPrompt('修改备注名', a && a.name || '', { okText: '保存' });
|
|
1577
|
+
if (name === null) return;
|
|
1578
|
+
try { await api('/api/accounts/' + encodeURIComponent(id), { method: 'PATCH', json: { name } }); await loadAccounts(); }
|
|
1579
|
+
catch (err) { toast(err.message, false); }
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (act === 'del') {
|
|
1583
|
+
if (!armed[id]) {
|
|
1584
|
+
armed[id] = setTimeout(() => { delete armed[id]; btn.classList.remove('armed'); }, 3000);
|
|
1585
|
+
btn.classList.add('armed');
|
|
1586
|
+
toast('再点一次确认删除');
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
clearTimeout(armed[id]); delete armed[id];
|
|
1590
|
+
try { await api('/api/accounts/' + encodeURIComponent(id), { method: 'DELETE' }); toast('已删除'); delete state.quotas[id]; await loadAccounts(); }
|
|
1591
|
+
catch (err) { toast(err.message, false); }
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// ── ZCode 活动领取 ──
|
|
1596
|
+
const CAPTCHA_SDK_URL = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js';
|
|
1597
|
+
const claimState = { accountId: null, plans: [], captcha: null, busy: false };
|
|
1598
|
+
let captchaSdkPromise = null;
|
|
1599
|
+
|
|
1600
|
+
function loadCaptchaSdk() {
|
|
1601
|
+
if (typeof window.initAliyunCaptcha === 'function') return Promise.resolve();
|
|
1602
|
+
if (!captchaSdkPromise) {
|
|
1603
|
+
captchaSdkPromise = new Promise((resolve, reject) => {
|
|
1604
|
+
const s = document.createElement('script');
|
|
1605
|
+
s.src = CAPTCHA_SDK_URL;
|
|
1606
|
+
s.onload = () => resolve();
|
|
1607
|
+
s.onerror = () => reject(new Error('验证码组件加载失败(网络问题?)'));
|
|
1608
|
+
document.head.appendChild(s);
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
return captchaSdkPromise;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
async function openClaim(id) {
|
|
1615
|
+
const a = state.accounts.find((x) => x.id === id);
|
|
1616
|
+
claimState.accountId = id;
|
|
1617
|
+
claimState.plans = [];
|
|
1618
|
+
claimState.captcha = null;
|
|
1619
|
+
claimState.busy = false;
|
|
1620
|
+
$('claim-body').innerHTML = '<div class="dim">读取可领取的活动…</div>';
|
|
1621
|
+
openModal('m-claim');
|
|
1622
|
+
try {
|
|
1623
|
+
const [plansResp, cap] = await Promise.all([
|
|
1624
|
+
api('/api/accounts/' + encodeURIComponent(id) + '/zcode/plans'),
|
|
1625
|
+
api('/api/zcode/captcha-config').catch(() => ({ enabled: false })),
|
|
1626
|
+
]);
|
|
1627
|
+
if (claimState.accountId !== id) return; // 弹窗已被别的账号占用
|
|
1628
|
+
claimState.plans = plansResp.plans || [];
|
|
1629
|
+
claimState.captcha = cap;
|
|
1630
|
+
renderClaimBody();
|
|
1631
|
+
} catch (e) {
|
|
1632
|
+
$('claim-body').innerHTML = '<div class="msg err">' + esc(e.message) + '</div>';
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function renderClaimBody() {
|
|
1637
|
+
if (!claimState.plans.length) {
|
|
1638
|
+
$('claim-body').innerHTML = '<div class="muted-empty">当前没有可领取的活动</div>';
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
$('claim-body').innerHTML = claimState.plans.map((pl, i) =>
|
|
1642
|
+
'<div class="claim-plan"><div><b>' + esc(pl.name || pl.planId) + '</b>'
|
|
1643
|
+
+ (pl.description ? '<div class="dim grants">' + esc(pl.description) + '</div>' : '')
|
|
1644
|
+
+ (pl.grants && pl.grants.length ? '<div class="dim grants">' + pl.grants.map(esc).join(';') + '</div>' : '')
|
|
1645
|
+
+ '</div><button class="btn primary" data-claim="' + i + '"' + (claimState.busy ? ' disabled' : '') + '>领取</button></div>'
|
|
1646
|
+
).join('')
|
|
1647
|
+
+ '<div id="claim-status" class="dim" style="margin-top:8px"></div>'
|
|
1648
|
+
+ '<div id="cap-holder"></div><button class="btn" id="cap-btn" hidden>打开验证码</button>';
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// 阿里云验证码:先静默验证(8s 超时),不行就让用户点按钮弹验证码。移植自 zcode-switch captcha.js。
|
|
1652
|
+
function runCaptcha(cfg) {
|
|
1653
|
+
return new Promise((resolve, reject) => {
|
|
1654
|
+
window.AliyunCaptchaConfig = { region: cfg.region, prefix: cfg.prefix };
|
|
1655
|
+
const st = $('claim-status'), btn = $('cap-btn');
|
|
1656
|
+
let done = false;
|
|
1657
|
+
let timer = 0;
|
|
1658
|
+
const finish = (param) => {
|
|
1659
|
+
if (done || !param) return;
|
|
1660
|
+
done = true;
|
|
1661
|
+
clearTimeout(timer);
|
|
1662
|
+
st.textContent = '人机验证已通过';
|
|
1663
|
+
resolve(param);
|
|
1664
|
+
};
|
|
1665
|
+
const interactive = (why) => {
|
|
1666
|
+
if (done) return;
|
|
1667
|
+
clearTimeout(timer);
|
|
1668
|
+
st.textContent = '需要完成人机验证:点下方按钮,按提示完成验证码';
|
|
1669
|
+
if (btn) { btn.hidden = false; btn.focus(); }
|
|
1670
|
+
if (why && typeof why !== 'string') st.title = JSON.stringify(why).slice(0, 160);
|
|
1671
|
+
};
|
|
1672
|
+
try {
|
|
1673
|
+
window.initAliyunCaptcha({
|
|
1674
|
+
SceneId: cfg.sceneId,
|
|
1675
|
+
mode: 'popup',
|
|
1676
|
+
language: 'zh-CN',
|
|
1677
|
+
showErrorTip: false,
|
|
1678
|
+
element: '#cap-holder',
|
|
1679
|
+
button: '#cap-btn',
|
|
1680
|
+
getInstance: (instance) => {
|
|
1681
|
+
if (instance && typeof instance.startTracelessVerification === 'function') {
|
|
1682
|
+
st.textContent = '人机验证中…';
|
|
1683
|
+
instance.startTracelessVerification();
|
|
1684
|
+
timer = setTimeout(() => interactive(), 8000);
|
|
1685
|
+
} else interactive();
|
|
1686
|
+
},
|
|
1687
|
+
success: (param) => finish(typeof param === 'string' ? param : param && param.captchaVerifyParam),
|
|
1688
|
+
fail: (p) => interactive(p),
|
|
1689
|
+
onError: (p) => interactive(p),
|
|
1690
|
+
});
|
|
1691
|
+
} catch (e) { reject(e); }
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
async function doClaim(i) {
|
|
1696
|
+
const pl = claimState.plans[i];
|
|
1697
|
+
if (!pl || claimState.busy) return;
|
|
1698
|
+
const st = $('claim-status');
|
|
1699
|
+
claimState.busy = true;
|
|
1700
|
+
renderClaimBody();
|
|
1701
|
+
$('claim-status').textContent = '准备领取…';
|
|
1702
|
+
let captchaParam = '';
|
|
1703
|
+
let region = '';
|
|
1704
|
+
try {
|
|
1705
|
+
const cfg = claimState.captcha;
|
|
1706
|
+
if (cfg && cfg.enabled && cfg.sceneId) {
|
|
1707
|
+
await loadCaptchaSdk();
|
|
1708
|
+
region = cfg.region || '';
|
|
1709
|
+
captchaParam = await runCaptcha(cfg);
|
|
1710
|
+
} else {
|
|
1711
|
+
$('claim-status').textContent = '验证码配置不可用,按无验证码直接尝试…';
|
|
1712
|
+
}
|
|
1713
|
+
} catch (e) {
|
|
1714
|
+
$('claim-status').innerHTML = '<span class="msg err">' + esc('验证码失败:' + e.message) + '</span>';
|
|
1715
|
+
claimState.busy = false;
|
|
1716
|
+
renderClaimBody();
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
$('claim-status').textContent = '领取中…';
|
|
1720
|
+
try {
|
|
1721
|
+
const r = await api('/api/accounts/' + encodeURIComponent(claimState.accountId) + '/zcode/claim', {
|
|
1722
|
+
json: { planId: pl.planId, captchaParam, region },
|
|
1723
|
+
});
|
|
1724
|
+
toast('领取成功:' + (r.planName || pl.name || pl.planId));
|
|
1725
|
+
closeModal('m-claim');
|
|
1726
|
+
loadQuotas([claimState.accountId]);
|
|
1727
|
+
loadLogs();
|
|
1728
|
+
} catch (e) {
|
|
1729
|
+
const errBox = $('claim-status');
|
|
1730
|
+
if (errBox) {
|
|
1731
|
+
errBox.innerHTML = '<span class="msg err">' + esc(e.message) + (e.nextAt ? '(' + fmtTime(e.nextAt).slice(5, 16) + ' 后可再试)' : '') + '</span>';
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
claimState.busy = false;
|
|
1735
|
+
if ($('m-claim').classList.contains('show') && $('claim-status')) renderClaimBody();
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
$('claim-body').addEventListener('click', (e) => {
|
|
1739
|
+
const b = e.target.closest('[data-claim]');
|
|
1740
|
+
if (b) doClaim(Number(b.dataset.claim));
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
// ── 弹窗 ──
|
|
1744
|
+
function openModal(id) { $(id).classList.add('show'); }
|
|
1745
|
+
function closeModal(id) { $(id).classList.remove('show'); if (id === 'm-add') deviceStop(); if (id === 'm-dialog') settleDialog(null); }
|
|
1746
|
+
|
|
1747
|
+
// 页面内对话框(替代原生 confirm / prompt:面板嵌在飞牛桌面窗口里是跨域 iframe,原生弹框可能被浏览器拦截)
|
|
1748
|
+
// 返回 Promise:确认 → true / 输入的文本;取消 → false / null
|
|
1749
|
+
let dialogResolve = null;
|
|
1750
|
+
function settleDialog(v) { const r = dialogResolve; dialogResolve = null; if (r) r(v); }
|
|
1751
|
+
function uiDialog({ title, message, input, okText }) {
|
|
1752
|
+
settleDialog(null);
|
|
1753
|
+
$('dlg-title').textContent = title || '';
|
|
1754
|
+
$('dlg-msg').textContent = message || '';
|
|
1755
|
+
$('dlg-msg').style.display = message ? '' : 'none';
|
|
1756
|
+
$('dlg-field').style.display = input ? '' : 'none';
|
|
1757
|
+
if (input) { $('dlg-input').type = input.type || 'text'; $('dlg-input').value = input.value || ''; $('dlg-input').placeholder = input.placeholder || ''; }
|
|
1758
|
+
$('dlg-ok').textContent = okText || '确定';
|
|
1759
|
+
openModal('m-dialog');
|
|
1760
|
+
setTimeout(() => (input ? $('dlg-input') : $('dlg-ok')).focus(), 30);
|
|
1761
|
+
return new Promise((resolve) => { dialogResolve = (v) => resolve(input ? v : v !== null); });
|
|
1762
|
+
}
|
|
1763
|
+
const uiConfirm = (message, title, okText) => uiDialog({ title: title || '请确认', message, okText });
|
|
1764
|
+
const uiPrompt = (title, value, opts = {}) => uiDialog({ title, message: opts.message, input: { value, type: opts.type, placeholder: opts.placeholder }, okText: opts.okText });
|
|
1765
|
+
function okDialog() { const v = $('dlg-field').style.display === 'none' ? true : $('dlg-input').value; $('m-dialog').classList.remove('show'); settleDialog(v); }
|
|
1766
|
+
$('dlg-ok').addEventListener('click', okDialog);
|
|
1767
|
+
$('dlg-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') okDialog(); });
|
|
1768
|
+
document.querySelectorAll('.mask').forEach((m) => {
|
|
1769
|
+
m.addEventListener('mousedown', (e) => { if (e.target === m) closeModal(m.id); });
|
|
1770
|
+
m.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', () => closeModal(m.id)));
|
|
1771
|
+
});
|
|
1772
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') document.querySelectorAll('.mask.show').forEach((m) => closeModal(m.id)); });
|
|
1773
|
+
document.querySelectorAll('#m-add .tab').forEach((t) => t.addEventListener('click', () => switchTab(t.dataset.tab)));
|
|
1774
|
+
function switchTab(name) {
|
|
1775
|
+
document.querySelectorAll('#m-add .tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === name));
|
|
1776
|
+
document.querySelectorAll('#m-add .pane').forEach((p) => p.classList.toggle('active', p.dataset.pane === name));
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// 添加账号:在产品页打开时只显示该产品可用的方式
|
|
1780
|
+
const LOCAL_HINT = {
|
|
1781
|
+
'': '读取本机 Qoder 客户端(本地解密)、WorkBuddy 客户端(当前 + 历史会话)、ZCode 客户端(本地解密)已登录的账号,token 不出本机;同时扫描旧版 Qoder IDE / CLI 文件。',
|
|
1782
|
+
qoder: '读取本机 Qoder / Qoder CN 客户端已登录的账号(本地解密 auth.v1.dat),token 不出本机;同时扫描旧版 Qoder IDE / CLI 文件。',
|
|
1783
|
+
workbuddy: '读取本机 WorkBuddy 客户端的当前会话与历史会话(CodeBuddyExtension 凭据目录),token 不出本机。',
|
|
1784
|
+
zcode: '读取本机 ZCode 客户端当前登录的账号(本地解密 ~/.zcode/v2/credentials.json),token 不出本机。',
|
|
1785
|
+
};
|
|
1786
|
+
function openAdd(product) {
|
|
1787
|
+
state.addProduct = product || '';
|
|
1788
|
+
const tabShown = { local: true, device: true, token: product !== 'zcode' };
|
|
1789
|
+
document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
|
|
1790
|
+
$('local-hint').textContent = LOCAL_HINT[state.addProduct];
|
|
1791
|
+
let first = null;
|
|
1792
|
+
for (const o of $('f-provider').options) {
|
|
1793
|
+
const ok = !product || provOf(o.value).prod === product;
|
|
1794
|
+
o.hidden = !ok;
|
|
1795
|
+
if (ok && !first) first = o.value;
|
|
1796
|
+
}
|
|
1797
|
+
if (first) { $('f-provider').value = first; $('f-provider').dispatchEvent(new Event('change')); }
|
|
1798
|
+
// 浏览器登录的选项值以产品名开头(qoder / workbuddy / zcode-…)
|
|
1799
|
+
let firstDev = null;
|
|
1800
|
+
for (const o of $('dev-provider').options) {
|
|
1801
|
+
const ok = !product || o.value.split('-')[0] === product;
|
|
1802
|
+
o.hidden = !ok;
|
|
1803
|
+
if (ok && !firstDev) firstDev = o.value;
|
|
1804
|
+
}
|
|
1805
|
+
if (firstDev) $('dev-provider').value = firstDev;
|
|
1806
|
+
$('dev-hint').textContent = (hasIncognito() ? '在内置隐私窗口里登录(不带出系统浏览器已登录的账号),' : '在浏览器里登录并授权,')
|
|
1807
|
+
+ '完成后账号自动添加。' + (product === 'qoder' ? '' : 'WorkBuddy / ZCode 用浏览器登录的账号同样可以一键切换客户端。');
|
|
1808
|
+
openModal('m-add');
|
|
1809
|
+
switchTab('local');
|
|
1810
|
+
localScan();
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// 本机导入
|
|
1814
|
+
async function localScan() {
|
|
1815
|
+
const box = $('scan-result');
|
|
1816
|
+
$('scan-status').textContent = '扫描中(最多约 10 秒)…';
|
|
1817
|
+
box.innerHTML = '<div class="hint">正在读取本机 Qoder 客户端凭据…</div>';
|
|
1818
|
+
try {
|
|
1819
|
+
const r = await api('/api/local/scan', { method: 'POST' });
|
|
1820
|
+
$('scan-status').textContent = '';
|
|
1821
|
+
let html = '';
|
|
1822
|
+
const cands = r.candidates.filter((c) => !state.addProduct || (c.provider ? provOf(c.provider).prod : 'qoder') === state.addProduct);
|
|
1823
|
+
for (const c of cands) {
|
|
1824
|
+
const title = c.kind === 'app' ? esc(c.name || c.email || c.phone || '(未命名)') : '<span style="font-family:monospace">' + esc(c.tokenMasked) + '</span>';
|
|
1825
|
+
const prov = (c.provider ? provBadges(c.provider) : '<span class="badge">' + esc(c.kind) + '</span>') + (c.current ? '<span class="pill cur">当前登录</span>' : '');
|
|
1826
|
+
const sub = c.kind === 'app'
|
|
1827
|
+
? esc(c.source) + (c.email ? ' · ' + esc(mask(c.email)) : c.phone ? ' · ' + esc(c.phone) : '') + (c.expiresAt ? ' · 有效期至 ' + esc(fmtTime(c.expiresAt)) : '')
|
|
1828
|
+
: esc(c.source) + ' · ' + esc(String(c.file || '').split(/[\\/]/).pop()) + '(' + c.hits + ' 处)';
|
|
1829
|
+
const btns = c.imported ? '<span class="pill ok">已导入</span>' + (c.provider ? '<button class="btn" data-cid="' + esc(c.id) + '" title="用本机凭据更新 token / 有效期 / 邮箱">同步信息</button>' : '')
|
|
1830
|
+
: c.provider ? '<button class="btn primary" data-cid="' + esc(c.id) + '">导入</button>'
|
|
1831
|
+
: '<button class="btn" data-cid="' + esc(c.id) + '" data-prov="qoder">导入·国际</button><button class="btn" data-cid="' + esc(c.id) + '" data-prov="qoder-cn">导入·国内</button>';
|
|
1832
|
+
html += '<div class="cand"><div class="cand-main"><div class="cand-name">' + title + prov + '</div><div class="cand-sub">' + sub + '</div></div><div class="row">' + btns + '</div></div>';
|
|
1833
|
+
}
|
|
1834
|
+
for (const e of r.errors || []) html += '<div class="hint" style="color:var(--err)">读取失败:' + esc(e.file) + ':' + esc(e.error) + '</div>';
|
|
1835
|
+
if (!cands.length) {
|
|
1836
|
+
const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode';
|
|
1837
|
+
const alt = state.addProduct === 'zcode' ? ',或改用「浏览器登录」' : ',或改用「浏览器登录」「粘贴 Token」';
|
|
1838
|
+
html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录'
|
|
1839
|
+
+ (r.scannedDirs && r.scannedDirs.length ? '、' + r.scannedDirs.map(esc).join('、') : '') + '</div>';
|
|
1840
|
+
}
|
|
1841
|
+
box.innerHTML = html;
|
|
1842
|
+
box.querySelectorAll('button[data-cid]').forEach((b) => b.addEventListener('click', async () => {
|
|
1843
|
+
b.disabled = true;
|
|
1844
|
+
try {
|
|
1845
|
+
const d = await api('/api/local/import', { json: { candidateId: b.dataset.cid, provider: b.dataset.prov } });
|
|
1846
|
+
toast((d.updated ? '已更新:' : '已导入:') + (d.account.name || d.account.id));
|
|
1847
|
+
await loadAccounts();
|
|
1848
|
+
localScan();
|
|
1849
|
+
} catch (e) { toast(e.message, false); b.disabled = false; }
|
|
1850
|
+
}));
|
|
1851
|
+
} catch (e) {
|
|
1852
|
+
$('scan-status').textContent = '';
|
|
1853
|
+
box.innerHTML = '<div class="hint" style="color:var(--err)">扫描失败:' + esc(e.message) + '</div>';
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// 网络授权登录
|
|
1858
|
+
let devTimer = null;
|
|
1859
|
+
let devGen = 0; // 每次发起/取消授权递增,丢弃过期会话的在途轮询结果
|
|
1860
|
+
// 打开授权/登录页:桌面版用内置隐私(无痕)窗口,避免带出系统浏览器里已登录的账号;否则退回系统浏览器
|
|
1861
|
+
async function openAuth(url) {
|
|
1862
|
+
if (window.creditdaddy && window.creditdaddy.openAuthWindow) {
|
|
1863
|
+
const r = await window.creditdaddy.openAuthWindow(url).catch(() => ({ ok: false }));
|
|
1864
|
+
if (r && r.ok) return true;
|
|
1865
|
+
}
|
|
1866
|
+
window.open(url, '_blank');
|
|
1867
|
+
return false;
|
|
1868
|
+
}
|
|
1869
|
+
const hasIncognito = () => Boolean(window.creditdaddy && window.creditdaddy.openAuthWindow);
|
|
1870
|
+
|
|
1871
|
+
async function deviceLogin() {
|
|
1872
|
+
const provider = $('dev-provider').value;
|
|
1873
|
+
try {
|
|
1874
|
+
const flow = await api('/api/auth/device/start', { json: { provider } });
|
|
1875
|
+
$('device-box').style.display = '';
|
|
1876
|
+
$('dev-cancel').style.display = '';
|
|
1877
|
+
const where = hasIncognito() ? '已在隐私窗口打开登录页' : '请在浏览器打开下方链接';
|
|
1878
|
+
$('dev-status').textContent = where + ',登录并授权(' + Math.floor(flow.expiresIn / 60) + ' 分钟内有效)';
|
|
1879
|
+
$('dev-link').href = flow.url;
|
|
1880
|
+
$('dev-link').textContent = flow.url;
|
|
1881
|
+
openAuth(flow.url);
|
|
1882
|
+
pollDevice(flow.sessionId, 0, ++devGen);
|
|
1883
|
+
} catch (e) { toast('发起授权失败:' + e.message, false); }
|
|
1884
|
+
}
|
|
1885
|
+
async function pollDevice(sessionId, n, gen) {
|
|
1886
|
+
if (gen !== devGen) return;
|
|
1887
|
+
try {
|
|
1888
|
+
const r = await api('/api/auth/device/poll', { json: { sessionId } });
|
|
1889
|
+
if (gen !== devGen) return;
|
|
1890
|
+
if (r.status === 'pending') {
|
|
1891
|
+
if (n > 450) return deviceStop('授权超时,请重试');
|
|
1892
|
+
$('dev-status').textContent = '等待授权中…(' + n + ')';
|
|
1893
|
+
devTimer = setTimeout(() => pollDevice(sessionId, n + 1, gen), 2000);
|
|
1894
|
+
} else if (r.status === 'ok') {
|
|
1895
|
+
deviceStop('✓ 登录成功:' + (r.account.name || r.account.id) + (r.already ? '(账号已存在,已更新登录凭据)' : ''));
|
|
1896
|
+
toast(r.already ? '已更新「' + (r.account.name || r.account.id) + '」的登录凭据' : '已添加:' + (r.account.name || r.account.id));
|
|
1897
|
+
if (r.account.id) delete state.quotas[r.account.id]; // 旧 token 的积分查询结果(可能是失败)作废,马上重查
|
|
1898
|
+
await loadStatus(); await loadAccounts();
|
|
1899
|
+
} else if (r.status === 'expired') {
|
|
1900
|
+
deviceStop('会话已过期,请重新发起');
|
|
1901
|
+
} else {
|
|
1902
|
+
deviceStop('授权失败:' + (r.error || r.status));
|
|
1903
|
+
}
|
|
1904
|
+
} catch (e) { if (gen === devGen) deviceStop('轮询异常:' + e.message); }
|
|
1905
|
+
}
|
|
1906
|
+
function deviceStop(msg) {
|
|
1907
|
+
devGen++;
|
|
1908
|
+
if (devTimer) { clearTimeout(devTimer); devTimer = null; }
|
|
1909
|
+
$('dev-cancel').style.display = 'none';
|
|
1910
|
+
if (msg) $('dev-status').textContent = msg;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// 粘贴 token
|
|
1914
|
+
async function addToken() {
|
|
1915
|
+
const token = $('f-token').value.trim();
|
|
1916
|
+
if (!token) return toast('请粘贴 token', false);
|
|
1917
|
+
const b = $('btn-add-token'); b.disabled = true;
|
|
1918
|
+
try {
|
|
1919
|
+
const d = await api('/api/accounts', { json: { provider: $('f-provider').value, name: $('f-name').value.trim(), token } });
|
|
1920
|
+
toast('已添加:' + (d.account.name || d.account.id) + (d.account.verified === false ? '(token 校验失败,请检查)' : ''), d.account.verified !== false);
|
|
1921
|
+
$('f-token').value = ''; $('f-name').value = '';
|
|
1922
|
+
closeModal('m-add');
|
|
1923
|
+
await loadAccounts();
|
|
1924
|
+
} catch (e) { toast(e.message, false); }
|
|
1925
|
+
b.disabled = false;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// 导出
|
|
1929
|
+
async function doExport() {
|
|
1930
|
+
const password = $('ex-password').value;
|
|
1931
|
+
if (password && password.length < 4) return toast('口令至少 4 位', false);
|
|
1932
|
+
try {
|
|
1933
|
+
const sel = $('ex-provider').value;
|
|
1934
|
+
const scope = sel.startsWith('p:') ? { product: sel.slice(2) } : { provider: sel || undefined };
|
|
1935
|
+
const payload = await api('/api/export', { json: Object.assign({ password: password || undefined }, scope) });
|
|
1936
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
|
1937
|
+
const a = document.createElement('a');
|
|
1938
|
+
a.href = URL.createObjectURL(blob);
|
|
1939
|
+
a.download = 'creditdaddy-' + (sel.replace('p:', '') || 'all') + '-' + new Date().toISOString().slice(0, 10) + (password ? '.secure' : '') + '.json';
|
|
1940
|
+
a.click();
|
|
1941
|
+
URL.revokeObjectURL(a.href);
|
|
1942
|
+
$('ex-password').value = '';
|
|
1943
|
+
closeModal('m-export');
|
|
1944
|
+
toast(password ? '已导出加密文件(可导入 10router)' : '已导出明文文件,内含 token,请妥善保管');
|
|
1945
|
+
} catch (e) { toast(e.message, false); }
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// 导入
|
|
1949
|
+
let pendingImport = null;
|
|
1950
|
+
function pickImport() { $('file-input').value = ''; $('file-input').click(); }
|
|
1951
|
+
$('file-input').addEventListener('change', async () => {
|
|
1952
|
+
const f = $('file-input').files[0];
|
|
1953
|
+
if (!f) return;
|
|
1954
|
+
let data;
|
|
1955
|
+
try { data = JSON.parse(await f.text()); } catch (e) { return toast('文件不是合法 JSON', false); }
|
|
1956
|
+
pendingImport = data;
|
|
1957
|
+
if (data && data.format === '10router-oauth-secure-v1') {
|
|
1958
|
+
$('pass-hint').textContent = '「' + f.name + '」已加密(CreditDaddy / 10router 迁移格式),请输入导出时设置的口令。';
|
|
1959
|
+
$('pass-input').value = '';
|
|
1960
|
+
openModal('m-pass');
|
|
1961
|
+
setTimeout(() => $('pass-input').focus(), 50);
|
|
1962
|
+
} else {
|
|
1963
|
+
submitImport();
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
async function submitImport(password) {
|
|
1967
|
+
try {
|
|
1968
|
+
const d = await api('/api/import', { json: { data: pendingImport, password } });
|
|
1969
|
+
closeModal('m-pass');
|
|
1970
|
+
toast('导入完成(' + (d.source === '10router' ? '10router' : 'CreditDaddy') + '):新增 ' + d.added + ',续期 ' + d.updated + ',跳过 ' + d.skipped);
|
|
1971
|
+
pendingImport = null;
|
|
1972
|
+
await loadAccounts();
|
|
1973
|
+
} catch (e) {
|
|
1974
|
+
if (e.code === 'WRONG_PASSWORD' || e.code === 'NEED_PASSWORD') { $('pass-hint').textContent = e.message; $('pass-input').select(); openModal('m-pass'); }
|
|
1975
|
+
else toast(e.message, false);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// ── 绑定 ──
|
|
1980
|
+
$('btn-checkin-all').innerHTML = I.play + '全部领取';
|
|
1981
|
+
for (const id of ['btn-add', 'btn-add-dash']) $(id).innerHTML = I.plus + '添加账号';
|
|
1982
|
+
for (const id of ['btn-import', 'btn-import-dash']) { $(id).innerHTML = I.upload + '导入'; $(id).onclick = pickImport; }
|
|
1983
|
+
for (const id of ['btn-export', 'btn-export-dash']) $(id).innerHTML = I.download + '导出';
|
|
1984
|
+
$('btn-checkin-all').onclick = () => checkinAll($('btn-checkin-all'));
|
|
1985
|
+
$('btn-checkin').onclick = () => checkinAll($('btn-checkin'), state.view);
|
|
1986
|
+
$('btn-add-dash').onclick = () => openAdd('');
|
|
1987
|
+
$('btn-add').onclick = () => openAdd(state.view);
|
|
1988
|
+
$('btn-export-dash').onclick = () => { $('ex-provider').value = ''; openModal('m-export'); };
|
|
1989
|
+
$('btn-export').onclick = () => { $('ex-provider').value = state.view === 'zcode' ? 'zcode' : 'p:' + state.view; openModal('m-export'); };
|
|
1990
|
+
$('btn-do-export').onclick = doExport;
|
|
1991
|
+
$('btn-pass-ok').onclick = () => submitImport($('pass-input').value);
|
|
1992
|
+
$('pass-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') submitImport($('pass-input').value); });
|
|
1993
|
+
$('btn-scan').onclick = localScan;
|
|
1994
|
+
$('btn-device').onclick = deviceLogin;
|
|
1995
|
+
$('dev-cancel').onclick = () => deviceStop('已取消');
|
|
1996
|
+
$('dev-link').addEventListener('click', (e) => { e.preventDefault(); openAuth($('dev-link').href); });
|
|
1997
|
+
$('btn-add-token').onclick = addToken;
|
|
1998
|
+
$('f-token').addEventListener('keydown', (e) => { if (e.key === 'Enter') addToken(); });
|
|
1999
|
+
$('list').addEventListener('click', onCardClick);
|
|
2000
|
+
// 标签页:用 URL hash 记住(刷新 / 前进后退保持)
|
|
2001
|
+
function applyView() {
|
|
2002
|
+
const raw = location.hash.slice(1);
|
|
2003
|
+
const [v, sub] = raw.split('/');
|
|
2004
|
+
state.view = VIEWS.includes(v) ? v : 'dash';
|
|
2005
|
+
state.region = '';
|
|
2006
|
+
// 10Router 供应商标签可用 hash 直达:#tenrouter/<供应商名>
|
|
2007
|
+
state.trVendor = state.view === 'tenrouter' && sub ? decodeURIComponent(sub) : '';
|
|
2008
|
+
document.querySelectorAll('#nav button').forEach((x) => x.classList.toggle('active', x.dataset.view === state.view));
|
|
2009
|
+
$('view-dash').classList.toggle('active', state.view === 'dash');
|
|
2010
|
+
$('view-prod').classList.toggle('active', state.view !== 'dash' && state.view !== 'tenrouter');
|
|
2011
|
+
$('view-10r').classList.toggle('active', state.view === 'tenrouter');
|
|
2012
|
+
if (state.view === 'tenrouter' && tr.config && tr.config.configured && !tr.quotas && !tr.loading) loadTrQuotas();
|
|
2013
|
+
render();
|
|
2014
|
+
renderStatus();
|
|
2015
|
+
}
|
|
2016
|
+
function goView(v) {
|
|
2017
|
+
if (v === state.view) return;
|
|
2018
|
+
if (v === 'dash') history.pushState(null, '', location.pathname + location.search);
|
|
2019
|
+
else history.pushState(null, '', '#' + v);
|
|
2020
|
+
applyView();
|
|
2021
|
+
}
|
|
2022
|
+
window.addEventListener('popstate', applyView);
|
|
2023
|
+
$('nav').addEventListener('click', (e) => { const b = e.target.closest('button[data-view]'); if (b) goView(b.dataset.view); });
|
|
2024
|
+
$('view-dash').addEventListener('click', (e) => {
|
|
2025
|
+
const t = e.target.closest('[data-view]');
|
|
2026
|
+
if (t) { goView(t.dataset.view); window.scrollTo(0, 0); }
|
|
2027
|
+
});
|
|
2028
|
+
$('region').addEventListener('click', (e) => {
|
|
2029
|
+
const b = e.target.closest('button[data-r]');
|
|
2030
|
+
if (!b) return;
|
|
2031
|
+
state.region = b.dataset.r;
|
|
2032
|
+
render();
|
|
2033
|
+
renderStatus();
|
|
2034
|
+
});('change', () => {
|
|
2035
|
+
$('f-token').placeholder = $('f-provider').value.startsWith('workbuddy')
|
|
2036
|
+
? 'WorkBuddy / CodeBuddy 登录 token(eyJ…,可在 CodeBuddyExtension 的 auth/*.info 中找到)'
|
|
2037
|
+
: 'dt-… / jt-… 或 PAT(pt-…,自动兑换)';
|
|
2038
|
+
});
|
|
2039
|
+
$('btn-logs').onclick = () => {
|
|
2040
|
+
const c = $('log-card').classList.toggle('collapsed');
|
|
2041
|
+
$('btn-logs').textContent = c ? '展开' : '收起';
|
|
2042
|
+
if (!c) { loadLogs(); $('logs').scrollTop = $('logs').scrollHeight; }
|
|
2043
|
+
};
|
|
2044
|
+
|
|
2045
|
+
// ── 启动:先直接请求(本机模式无需密钥);只有服务端返回 401 时才询问密钥 ──
|
|
2046
|
+
initTheme();
|
|
2047
|
+
$('btn-home').innerHTML = I.github;
|
|
2048
|
+
applyView();
|
|
2049
|
+
(function boot() {
|
|
2050
|
+
function start() {
|
|
2051
|
+
loadStatus().then(loadAccounts).catch((e) => toast(e.message, false));
|
|
2052
|
+
loadZNet();
|
|
2053
|
+
loadTrConfig().then(() => loadTrQuotas());
|
|
2054
|
+
setInterval(() => loadTrQuotas(), 5 * 60 * 1000);
|
|
2055
|
+
loadLogs();
|
|
2056
|
+
setInterval(loadLogs, 10000);
|
|
2057
|
+
setInterval(async () => { await loadStatus(); render(); }, 30000);
|
|
2058
|
+
}
|
|
2059
|
+
// 先直接请求(本机模式无需密码);需要密码时 api() 会弹框并重试
|
|
2060
|
+
api('/api/status').then(start).catch((e) => toast(e.code === 'PANEL_KEY'
|
|
2061
|
+
? '未输入面板访问密码,刷新页面可重试' : '无法连接守护进程:' + e.message, false));
|
|
2062
|
+
})();
|
|
2063
|
+
</script>
|
|
2064
|
+
</body>
|
|
2065
|
+
</html>
|