tt-help-cli-ycl 1.3.6 → 1.3.8
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 +17 -17
- package/cli.js +9 -9
- package/package.json +45 -45
- package/src/cli/auto.js +131 -121
- package/src/cli/explore.js +147 -138
- package/src/cli/progress.js +111 -111
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/watch.js +31 -31
- package/src/lib/args.js +391 -391
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +142 -142
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +87 -87
- package/src/lib/constants.js +109 -95
- package/src/lib/delay.js +54 -54
- package/src/lib/explore-fetch.js +118 -118
- package/src/lib/fetcher.js +45 -45
- package/src/lib/filter.js +66 -66
- package/src/lib/io.js +54 -54
- package/src/lib/mac-or-uuid.js +82 -0
- package/src/lib/output.js +80 -80
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +44 -44
- package/src/lib/scrape.js +40 -40
- package/src/lib/url.js +52 -52
- package/src/main.mjs +221 -221
- package/src/scraper/auto-core.mjs +185 -185
- package/src/scraper/core.mjs +190 -190
- package/src/scraper/explore-core.mjs +162 -162
- package/src/scraper/modules/captcha-handler.mjs +114 -114
- package/src/scraper/modules/comment-extractor.mjs +69 -69
- package/src/scraper/modules/follow-extractor.mjs +121 -121
- package/src/scraper/modules/guess-extractor.mjs +51 -51
- package/src/scraper/modules/page-error-detector.mjs +70 -70
- package/src/scraper/modules/page-helpers.mjs +48 -48
- package/src/scraper/modules/scroll-collector.mjs +189 -189
- package/src/test-auto-follow.cjs +109 -0
- package/src/test-extractors.cjs +75 -0
- package/src/test-follow.cjs +41 -0
- package/src/videos/core.mjs +126 -126
- package/src/watch/data-store.mjs +258 -261
- package/src/watch/public/index.html +580 -464
- package/src/watch/server.mjs +308 -281
- package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
|
@@ -1,466 +1,582 @@
|
|
|
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>TikTok 采集监控</title>
|
|
7
|
-
<style>
|
|
8
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f0f13; color: #e0e0e0; padding: 16px; }
|
|
10
|
-
.header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: #1a1a24; border-radius: 8px; margin-bottom: 16px; }
|
|
11
|
-
.header h1 { font-size: 18px; color: #fe2c55; }
|
|
12
|
-
.header .meta { font-size: 12px; color: #888; }
|
|
13
|
-
.header .status { font-size: 12px; color: #4ade80; }
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.stat-card
|
|
18
|
-
.stat-card .
|
|
19
|
-
.stat-card .value
|
|
20
|
-
.stat-card .value.
|
|
21
|
-
.stat-card .value.
|
|
22
|
-
.stat-card .value.
|
|
23
|
-
.stat-card.
|
|
24
|
-
.stat-card.
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.bar-row
|
|
31
|
-
.bar-row .
|
|
32
|
-
.bar-row .
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
.source-row
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
.controls
|
|
41
|
-
.controls
|
|
42
|
-
.controls
|
|
43
|
-
.controls button
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
.add-users
|
|
47
|
-
.add-users button { padding: 6px 16px; border: none; border-radius: 6px; background: #fe2c55; color: #fff; font-size: 13px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
48
|
-
.add-users button:hover { background: #e61944; }
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
.
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
.
|
|
79
|
-
.
|
|
80
|
-
.
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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>TikTok 采集监控</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f0f13; color: #e0e0e0; padding: 16px; }
|
|
10
|
+
.header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: #1a1a24; border-radius: 8px; margin-bottom: 16px; }
|
|
11
|
+
.header h1 { font-size: 18px; color: #fe2c55; }
|
|
12
|
+
.header .meta { font-size: 12px; color: #888; }
|
|
13
|
+
.header .status { font-size: 12px; color: #4ade80; }
|
|
14
|
+
.script-link { font-size: 12px; color: #60a5fa; text-decoration: none; padding: 2px 8px; border: 1px solid #60a5fa; border-radius: 4px; }
|
|
15
|
+
.script-link:hover { background: #60a5fa; color: #fff; }
|
|
16
|
+
.stats { display: grid; grid-template-columns: repeat(7, 1fr); gap: 12px; margin-bottom: 16px; }
|
|
17
|
+
.stat-card { background: #1a1a24; border-radius: 8px; padding: 16px; text-align: center; }
|
|
18
|
+
.stat-card .label { font-size: 12px; color: #888; margin-bottom: 8px; }
|
|
19
|
+
.stat-card .value { font-size: 28px; font-weight: 700; }
|
|
20
|
+
.stat-card .value.total { color: #60a5fa; }
|
|
21
|
+
.stat-card .value.done { color: #4ade80; }
|
|
22
|
+
.stat-card .value.pending { color: #facc15; }
|
|
23
|
+
.stat-card .value.error { color: #f87171; }
|
|
24
|
+
.stat-card .value.target { color: #a78bfa; }
|
|
25
|
+
.stat-card.clickable { cursor: pointer; }
|
|
26
|
+
.stat-card.clickable:hover { background: #25253a; }
|
|
27
|
+
.charts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
|
|
28
|
+
.chart-box { background: #1a1a24; border-radius: 8px; padding: 16px; }
|
|
29
|
+
.chart-box h3 { font-size: 14px; color: #888; margin-bottom: 12px; }
|
|
30
|
+
.bar-row { display: flex; align-items: center; margin-bottom: 8px; font-size: 13px; }
|
|
31
|
+
.bar-row .name { width: 50px; color: #ccc; flex-shrink: 0; }
|
|
32
|
+
.bar-row .bar-bg { flex: 1; background: #2a2a3a; border-radius: 4px; height: 20px; overflow: hidden; margin: 0 8px; }
|
|
33
|
+
.bar-row .bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s; display: flex; align-items: center; padding-left: 6px; font-size: 11px; color: #fff; }
|
|
34
|
+
.bar-row .count { width: 80px; text-align: right; color: #888; flex-shrink: 0; }
|
|
35
|
+
.source-row { display: flex; align-items: center; margin-bottom: 6px; font-size: 13px; }
|
|
36
|
+
.source-row .s-name { width: 80px; color: #ccc; flex-shrink: 0; }
|
|
37
|
+
.source-row .s-val { color: #888; }
|
|
38
|
+
.table-wrap { background: #1a1a24; border-radius: 8px; padding: 16px; }
|
|
39
|
+
.table-wrap h3 { font-size: 14px; color: #888; margin-bottom: 12px; }
|
|
40
|
+
.controls { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
41
|
+
.controls input { flex: 1; min-width: 150px; padding: 6px 12px; border: 1px solid #333; border-radius: 6px; background: #0f0f13; color: #e0e0e0; font-size: 13px; outline: none; }
|
|
42
|
+
.controls input:focus { border-color: #fe2c55; }
|
|
43
|
+
.controls button { padding: 6px 14px; border: 1px solid #333; border-radius: 6px; background: #2a2a3a; color: #ccc; font-size: 12px; cursor: pointer; transition: all 0.2s; }
|
|
44
|
+
.controls button:hover { border-color: #fe2c55; color: #fff; }
|
|
45
|
+
.controls button.active { background: #fe2c55; color: #fff; border-color: #fe2c55; }
|
|
46
|
+
.add-users { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
|
47
|
+
.add-users button { padding: 6px 16px; border: none; border-radius: 6px; background: #fe2c55; color: #fff; font-size: 13px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
48
|
+
.add-users button:hover { background: #e61944; }
|
|
49
|
+
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.65); z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
|
50
|
+
.modal { background: #1a1a24; border-radius: 12px; padding: 24px; width: 520px; max-width: 90vw; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
|
51
|
+
.modal h3 { font-size: 16px; color: #e0e0e0; margin-bottom: 6px; }
|
|
52
|
+
.modal .hint { font-size: 12px; color: #888; margin-bottom: 16px; }
|
|
53
|
+
.modal textarea { width: 100%; height: 180px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #0f0f13; color: #e0e0e0; font-size: 13px; font-family: inherit; outline: none; resize: vertical; line-height: 1.6; }
|
|
54
|
+
.modal textarea:focus { border-color: #fe2c55; }
|
|
55
|
+
.modal textarea::placeholder { color: #555; }
|
|
56
|
+
.modal .preview { margin-top: 8px; font-size: 12px; color: #60a5fa; min-height: 20px; }
|
|
57
|
+
.modal .btn-row { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
|
|
58
|
+
.modal .btn-row button { padding: 8px 20px; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
59
|
+
.modal .btn-cancel { background: #2a2a3a; color: #ccc; }
|
|
60
|
+
.modal .btn-cancel:hover { background: #333; }
|
|
61
|
+
.modal .btn-submit { background: #fe2c55; color: #fff; }
|
|
62
|
+
.modal .btn-submit:hover { background: #e61944; }
|
|
63
|
+
.toast { position: fixed; top: 16px; right: 16px; padding: 10px 20px; border-radius: 6px; font-size: 13px; z-index: 999; transition: opacity 0.3s; }
|
|
64
|
+
.toast.success { background: #166534; color: #fff; }
|
|
65
|
+
.toast.error { background: #991b1b; color: #fff; }
|
|
66
|
+
@keyframes flashChange { 0% { filter: brightness(1.8); box-shadow: 0 0 12px rgba(254,44,85,0.6); } 100% { filter: brightness(1); box-shadow: none; } }
|
|
67
|
+
.flash-change { animation: flashChange 0.6s ease-out; }
|
|
68
|
+
@keyframes rowFlash { 0% { background: rgba(254,44,85,0.25); } 100% { background: transparent; } }
|
|
69
|
+
tr.row-flash { animation: rowFlash 0.8s ease-out; }
|
|
70
|
+
@keyframes barFlash { 0% { filter: brightness(1.6); } 100% { filter: brightness(1); } }
|
|
71
|
+
.bar-fill.bar-flash { animation: barFlash 0.5s ease-out; }
|
|
72
|
+
.table-scroll { max-height: 500px; overflow-y: auto; }
|
|
73
|
+
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
74
|
+
th { position: sticky; top: 0; background: #22222e; padding: 8px 10px; text-align: left; color: #888; font-weight: 600; border-bottom: 1px solid #333; white-space: nowrap; }
|
|
75
|
+
td { padding: 6px 10px; border-bottom: 1px solid #1f1f2a; white-space: nowrap; }
|
|
76
|
+
tr:hover { background: #1f1f2a; }
|
|
77
|
+
td.user-id { cursor: pointer; color: #60a5fa; }
|
|
78
|
+
td.user-id:hover { color: #fe2c55; }
|
|
79
|
+
.tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; }
|
|
80
|
+
.tag.seller { background: #dc2626; color: #fff; }
|
|
81
|
+
.tag.verified { background: #2563eb; color: #fff; }
|
|
82
|
+
.tag.pending { background: #ca8a04; color: #000; }
|
|
83
|
+
.tag.processing { background: #0ea5e9; color: #fff; }
|
|
84
|
+
.tag.error { background: #991b1b; color: #fff; }
|
|
85
|
+
.tag.processed { background: #166534; color: #fff; }
|
|
86
|
+
.tag.no-video { background: #7c3aed; color: #fff; }
|
|
87
|
+
.tag.no-follow { background: #b45309; color: #fff; }
|
|
88
|
+
.tag.keep-follow { background: #059669; color: #fff; }
|
|
89
|
+
.tag.pinned { background: #f59e0b; color: #000; }
|
|
90
|
+
.context-menu { position: fixed; background: #1e1e2e; border: 1px solid #333; border-radius: 6px; padding: 4px 0; min-width: 140px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); z-index: 1000; }
|
|
91
|
+
.context-menu-item { padding: 8px 16px; font-size: 13px; color: #ccc; cursor: pointer; display: flex; align-items: center; gap: 8px; }
|
|
92
|
+
.context-menu-item:hover { background: #fe2c55; color: #fff; }
|
|
93
|
+
.context-menu-item.danger { color: #f87171; }
|
|
94
|
+
.context-menu-item.danger:hover { background: #991b1b; color: #fff; }
|
|
95
|
+
::-webkit-scrollbar { width: 6px; }
|
|
96
|
+
::-webkit-scrollbar-track { background: #1a1a24; }
|
|
97
|
+
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
|
98
|
+
@media (max-width: 768px) {
|
|
99
|
+
body { padding: 8px; }
|
|
100
|
+
.header { flex-direction: column; gap: 6px; align-items: flex-start; }
|
|
101
|
+
.header h1 { font-size: 16px; }
|
|
102
|
+
.stats { grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
|
103
|
+
.stat-card { padding: 10px; }
|
|
104
|
+
.stat-card .label { font-size: 11px; }
|
|
105
|
+
.stat-card .value { font-size: 18px; }
|
|
106
|
+
.charts { grid-template-columns: 1fr; }
|
|
107
|
+
.table-wrap { padding: 10px; }
|
|
108
|
+
.controls { flex-wrap: wrap; gap: 6px; }
|
|
109
|
+
.controls input { flex: 0 0 100%; width: 100%; }
|
|
110
|
+
.controls button { flex: 0 0 calc(33.33% - 4px); min-width: 0; text-align: center; white-space: nowrap; font-size: 11px; padding: 8px 4px; }
|
|
111
|
+
.controls select { flex: 0 0 100%; width: 100%; }
|
|
112
|
+
.table-scroll { max-height: none; overflow: visible; }
|
|
113
|
+
table, thead, tbody, th, td, tr { display: block; }
|
|
114
|
+
thead { display: none; }
|
|
115
|
+
tr { background: #22222e; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; border: 1px solid #2a2a3a; }
|
|
116
|
+
tr:hover { background: #2a2a3a; }
|
|
117
|
+
td { padding: 4px 0; border: none; text-align: left; position: relative; padding-left: 40%; font-size: 13px; }
|
|
118
|
+
td::before { content: attr(data-label); position: absolute; left: 0; width: 36%; text-align: right; color: #888; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
119
|
+
td.user-id { font-size: 15px; font-weight: 700; color: #60a5fa; padding-left: 0; border-bottom: 1px solid #2a2a3a; margin-bottom: 4px; padding-bottom: 6px; }
|
|
120
|
+
td.user-id::before { display: none; }
|
|
121
|
+
td.user-id:hover { color: #fe2c55; }
|
|
122
|
+
.add-users { justify-content: center; }
|
|
123
|
+
.modal { width: 95vw; padding: 16px; }
|
|
124
|
+
.modal textarea { height: 140px; }
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<div class="header">
|
|
130
|
+
<h1>TikTok 采集监控</h1>
|
|
131
|
+
<div class="meta" id="fileMeta">加载中...</div>
|
|
132
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
133
|
+
<a href="/scripts/run-explore.sh" class="script-link" download>mac</a>
|
|
134
|
+
<a href="/scripts/run-explore.bat" class="script-link" download>windows</a>
|
|
135
|
+
<span class="status" id="lastUpdate">--</span>
|
|
136
|
+
</div>
|
|
107
137
|
</div>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
<div class="
|
|
114
|
-
<
|
|
115
|
-
<div class="
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<
|
|
262
|
-
<
|
|
263
|
-
<
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
document.getElementById('
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
let
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
138
|
+
<div class="stats">
|
|
139
|
+
<div class="stat-card"><div class="label">总任务</div><div class="value total" id="statTotal">0</div></div>
|
|
140
|
+
<div class="stat-card"><div class="label">处理中</div><div class="value total" id="statProcessing">0</div></div>
|
|
141
|
+
<div class="stat-card"><div class="label">已完成</div><div class="value done" id="statDone">0</div></div>
|
|
142
|
+
<div class="stat-card"><div class="label">待处理</div><div class="value pending" id="statPending">0</div></div>
|
|
143
|
+
<div class="stat-card"><div class="label">错误</div><div class="value error" id="statError">0</div></div>
|
|
144
|
+
<div class="stat-card"><div class="label">受限</div><div class="value error" id="statRestricted">0</div></div>
|
|
145
|
+
<div class="stat-card clickable" id="statTargetCard"><div class="label">目标用户(ES商家)</div><div class="value target" id="statTarget">0</div></div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="charts">
|
|
148
|
+
<div class="chart-box">
|
|
149
|
+
<h3>国家统计</h3>
|
|
150
|
+
<div id="countryChart"></div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="chart-box">
|
|
153
|
+
<h3>来源分布</h3>
|
|
154
|
+
<div id="sourceChart"></div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="table-wrap">
|
|
158
|
+
<h3>用户列表</h3>
|
|
159
|
+
<div class="add-users">
|
|
160
|
+
<button onclick="openAddModal()">+ 插入队列</button>
|
|
161
|
+
</div>
|
|
162
|
+
<div id="toast" class="toast" style="display:none"></div>
|
|
163
|
+
<div class="controls">
|
|
164
|
+
<input type="text" id="searchInput" placeholder="搜索用户名 / 昵称...">
|
|
165
|
+
<button data-filter="all" class="active" onclick="setFilter('all')">全部</button>
|
|
166
|
+
<button data-filter="processing" onclick="setFilter('processing')">处理中</button>
|
|
167
|
+
<button data-filter="pending" onclick="setFilter('pending')">待处理</button>
|
|
168
|
+
<button data-filter="done" onclick="setFilter('done')">已处理</button>
|
|
169
|
+
<button data-filter="error" onclick="setFilter('error')">错误</button>
|
|
170
|
+
<button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
|
|
171
|
+
<button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标用户</button>
|
|
172
|
+
<select id="locationFilter" onchange="onLocationChange()" style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
|
|
173
|
+
<option value="">全部国家</option>
|
|
174
|
+
</select>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="table-scroll">
|
|
177
|
+
<table>
|
|
178
|
+
<thead>
|
|
179
|
+
<tr><th>用户名</th><th>昵称</th><th>粉丝</th><th>视频</th><th>国家</th><th>来源</th><th>状态</th><th>接收人</th><th>领取时间</th><th>提交时间</th></tr>
|
|
180
|
+
</thead>
|
|
181
|
+
<tbody id="userTable"></tbody>
|
|
182
|
+
</table>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<script>
|
|
186
|
+
const COLORS = ['#fe2c55','#60a5fa','#4ade80','#facc15','#f97316','#a855f7','#ec4899','#14b8a6','#e11d48','#0ea5e9','#8b5cf6','#84cc16'];
|
|
187
|
+
let currentFilter = 'all';
|
|
188
|
+
let currentStats = null;
|
|
189
|
+
let currentUsers = [];
|
|
190
|
+
let currentLocation = '';
|
|
191
|
+
let prevStatValues = {};
|
|
192
|
+
let prevUserMap = {};
|
|
193
|
+
|
|
194
|
+
async function fetchStats() {
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch('/api/stats');
|
|
197
|
+
currentStats = await res.json();
|
|
198
|
+
renderStats();
|
|
199
|
+
renderLocationFilter();
|
|
200
|
+
} catch (e) {
|
|
201
|
+
document.getElementById('lastUpdate').textContent = '\u8fde\u63a5\u5931\u8d25';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function fetchUsers() {
|
|
206
|
+
try {
|
|
207
|
+
const params = new URLSearchParams();
|
|
208
|
+
if (currentFilter === 'target') {
|
|
209
|
+
params.set('target', '1');
|
|
210
|
+
} else if (currentFilter !== 'all') {
|
|
211
|
+
params.set('status', currentFilter);
|
|
212
|
+
}
|
|
213
|
+
const search = document.getElementById('searchInput').value.trim();
|
|
214
|
+
if (search) params.set('search', search);
|
|
215
|
+
if (currentLocation) params.set('location', currentLocation);
|
|
216
|
+
params.set('limit', '200');
|
|
217
|
+
const res = await fetch('/api/users?' + params.toString());
|
|
218
|
+
const data = await res.json();
|
|
219
|
+
currentUsers = data.users || [];
|
|
220
|
+
renderTable(currentUsers);
|
|
221
|
+
} catch (e) {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function flashEl(id, value) {
|
|
225
|
+
const el = document.getElementById(id);
|
|
226
|
+
if (!el) return;
|
|
227
|
+
const prev = prevStatValues[id];
|
|
228
|
+
el.textContent = value;
|
|
229
|
+
if (prev !== undefined && prev !== value) {
|
|
230
|
+
el.classList.remove('flash-change');
|
|
231
|
+
void el.offsetWidth;
|
|
232
|
+
el.classList.add('flash-change');
|
|
233
|
+
}
|
|
234
|
+
prevStatValues[id] = value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function renderStats() {
|
|
238
|
+
if (!currentStats) return;
|
|
239
|
+
const d = currentStats;
|
|
240
|
+
flashEl('statTotal', d.totalUsers);
|
|
241
|
+
flashEl('statProcessing', d.processingUsers || 0);
|
|
242
|
+
flashEl('statDone', d.processedUsers);
|
|
243
|
+
flashEl('statPending', d.pendingUsers);
|
|
244
|
+
flashEl('statError', d.errorUsers);
|
|
245
|
+
flashEl('statRestricted', d.restrictedUsers);
|
|
246
|
+
flashEl('statTarget', d.targetUsers);
|
|
247
|
+
document.getElementById('lastUpdate').textContent = '\u66f4\u65b0\u4e8e ' + new Date().toLocaleTimeString();
|
|
248
|
+
document.getElementById('fileMeta').textContent = (d.processingUsers || 0) + ' \u5904\u7406\u4e2d, ' + d.totalUsers + ' \u4e2a\u7528\u6237';
|
|
249
|
+
|
|
250
|
+
renderCountryChart(d.countryStats);
|
|
251
|
+
renderSourceChart(d.sourceStats);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function renderCountryChart(countries) {
|
|
255
|
+
const el = document.getElementById('countryChart');
|
|
256
|
+
if (!countries.length) { el.innerHTML = '<span style="color:#666;font-size:12px">\u6682\u65e0\u6570\u636e</span>'; return; }
|
|
257
|
+
const max = countries[0].count;
|
|
258
|
+
const top = countries.slice(0, 15);
|
|
259
|
+
el.innerHTML = top.map((c, i) => `
|
|
260
|
+
<div class="bar-row">
|
|
261
|
+
<span class="name">${c.country}</span>
|
|
262
|
+
<div class="bar-bg"><div class="bar-fill" style="width:${(c.count / max * 100)}%;background:${COLORS[i % COLORS.length]}">${c.count}</div></div>
|
|
263
|
+
<span class="count">${(currentStats ? (c.count / currentStats.totalUsers * 100).toFixed(1) : 0)}%</span>
|
|
264
|
+
</div>
|
|
265
|
+
`).join('');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderSourceChart(sources) {
|
|
269
|
+
const el = document.getElementById('sourceChart');
|
|
270
|
+
const labels = { seed: '\u79cd\u5b50', video: '\u89c6\u9891\u53d1\u73b0', comment: '\u8bc4\u8bba\u53d1\u73b0', guess: '\u731c\u4f60\u559c\u6b22', following: '\u5173\u6ce8', follower: '\u7c89\u4e1d', processed: '\u5df2\u5904\u7406', restricted: '\u53d7\u9650(\u8df3\u8fc7)', error: '\u9519\u8bef(\u5f85\u91cd\u8bd5)', noVideo: '\u65e0\u89c6\u9891' };
|
|
271
|
+
const entries = Object.entries(sources);
|
|
272
|
+
el.innerHTML = entries.map(([key, val]) => `
|
|
273
|
+
<div class="source-row"><span class="s-name">${labels[key] || key}:</span><span class="s-val">${val}</span></div>
|
|
274
|
+
`).join('');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderTable(users) {
|
|
278
|
+
const el = document.getElementById('userTable');
|
|
279
|
+
|
|
280
|
+
const newUserMap = {};
|
|
281
|
+
for (const u of users) newUserMap[u.uniqueId] = u;
|
|
282
|
+
|
|
283
|
+
el.innerHTML = users.map(u => {
|
|
284
|
+
const wasStatus = prevUserMap[u.uniqueId]?.status;
|
|
285
|
+
const nowStatus = u.status;
|
|
286
|
+
const changed = wasStatus !== nowStatus &&
|
|
287
|
+
(nowStatus === 'done' || nowStatus === 'restricted' || nowStatus === 'error');
|
|
288
|
+
const rowClass = changed ? ' class="row-flash"' : '';
|
|
289
|
+
|
|
290
|
+
const statusTags = {
|
|
291
|
+
restricted: '<span class="tag error">\u53d7\u9650(\u8df3\u8fc7)</span>',
|
|
292
|
+
error: '<span class="tag error">\u9519\u8bef(\u5f85\u91cd\u8bd5)</span>',
|
|
293
|
+
done: '<span class="tag processed">\u5df2\u5904\u7406</span>',
|
|
294
|
+
processing: '<span class="tag processing">\u5904\u7406\u4e2d</span>',
|
|
295
|
+
pending: '<span class="tag pending">\u5f85\u5904\u7406</span>',
|
|
296
|
+
};
|
|
297
|
+
const statusTag = statusTags[u.status] || '<span class="tag pending">' + (u.status || '\u672a\u77e5') + '</span>';
|
|
298
|
+
const sources = (u.sources || []).join(', ');
|
|
299
|
+
const extraTags = [];
|
|
300
|
+
if (u.pinned) extraTags.push('<span class="tag pinned">📌 置顶</span>');
|
|
301
|
+
if (u.ttSeller) extraTags.push('<span class="tag seller">\u5546\u5bb6</span>');
|
|
302
|
+
if (u.verified) extraTags.push('<span class="tag verified">\u8ba4\u8bc1</span>');
|
|
303
|
+
if (u.noVideo) extraTags.push('<span class="tag no-video">\u65e0\u89c6\u9891</span>');
|
|
304
|
+
if (u.keepFollow) extraTags.push('<span class="tag keep-follow">\u5173\u6ce8\u5df2\u4fdd\u7559</span>');
|
|
305
|
+
if (u.hasFollowData === false) extraTags.push('<span class="tag no-follow">\u5173\u6ce8\u672a\u83b7\u53d6</span>');
|
|
306
|
+
const nick = (u.nickname || '').replace(/</g, '<').replace(/>/g, '>');
|
|
307
|
+
const fans = u.followerCount != null ? formatNum(u.followerCount) : '-';
|
|
308
|
+
const videos = u.videoCount != null ? u.videoCount : '-';
|
|
309
|
+
const loc = u.locationCreated || '-';
|
|
310
|
+
const claimer = u.claimedBy || '-';
|
|
311
|
+
const claimTime = u.claimedAt ? formatTime(u.claimedAt) : '-';
|
|
312
|
+
const procTime = u.processedAt ? formatTime(u.processedAt) : '-';
|
|
313
|
+
return `<tr${rowClass}>
|
|
314
|
+
<td class="user-id" data-label="用户名">@${u.uniqueId}</td>
|
|
315
|
+
<td data-label="昵称">${nick}</td>
|
|
316
|
+
<td data-label="粉丝">${fans}</td>
|
|
317
|
+
<td data-label="视频">${videos}</td>
|
|
318
|
+
<td data-label="国家">${loc}</td>
|
|
319
|
+
<td data-label="来源">${sources || '-'}</td>
|
|
320
|
+
<td data-label="状态">${statusTag} ${extraTags.join(' ')}</td>
|
|
321
|
+
<td data-label="接收人" style="font-size:11px;color:#888">${claimer}</td>
|
|
322
|
+
<td data-label="领取时间" style="font-size:11px;color:#888">${claimTime}</td>
|
|
323
|
+
<td data-label="提交时间" style="font-size:11px;color:#888">${procTime}</td>
|
|
324
|
+
</tr>`;
|
|
325
|
+
}).join('');
|
|
326
|
+
|
|
327
|
+
prevUserMap = newUserMap;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatNum(n) {
|
|
331
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
332
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
333
|
+
return n;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function formatTime(ts) {
|
|
337
|
+
const d = new Date(ts);
|
|
338
|
+
const pad = n => String(n).padStart(2, '0');
|
|
339
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function setFilter(f) {
|
|
343
|
+
currentFilter = f;
|
|
344
|
+
document.querySelectorAll('.controls button').forEach(b => {
|
|
345
|
+
b.classList.toggle('active', b.dataset.filter === f);
|
|
346
|
+
});
|
|
347
|
+
fetchUsers();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderLocationFilter() {
|
|
351
|
+
if (!currentStats || !currentStats.countryStats) return;
|
|
352
|
+
const sel = document.getElementById('locationFilter');
|
|
353
|
+
if (!sel) return;
|
|
354
|
+
const val = sel.value;
|
|
355
|
+
const entries = currentStats.countryStats.sort((a, b) => b.count - a.count);
|
|
356
|
+
sel.innerHTML = '<option value="">全部国家</option>' +
|
|
357
|
+
entries.map(c => `<option value="${c.country}"${val === c.country ? ' selected' : ''}>${c.country} (${c.count})</option>`).join('');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function onLocationChange() {
|
|
361
|
+
const sel = document.getElementById('locationFilter');
|
|
362
|
+
currentLocation = sel.value;
|
|
363
|
+
fetchUsers();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
document.getElementById('searchInput').addEventListener('input', () => {
|
|
367
|
+
fetchUsers();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
function parseUsernames(raw) {
|
|
371
|
+
return raw.split(/[,,\n\r]+/).map(s => s.replace(/^@/, '').trim()).filter(Boolean);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function openAddModal() {
|
|
375
|
+
let overlay = document.getElementById('addModalOverlay');
|
|
376
|
+
if (overlay) return;
|
|
377
|
+
overlay = document.createElement('div');
|
|
378
|
+
overlay.id = 'addModalOverlay';
|
|
379
|
+
overlay.className = 'modal-overlay';
|
|
380
|
+
overlay.innerHTML = `
|
|
381
|
+
<div class="modal">
|
|
382
|
+
<h3>插入用户到队列</h3>
|
|
383
|
+
<div class="hint">每行一个用户名,或用逗号分隔。支持 @username 或 username 格式。插入到队列最前面优先处理。</div>
|
|
384
|
+
<textarea id="modalUserInput" placeholder="例如: user1 user2 user3 或:user1, user2, user3"></textarea>
|
|
385
|
+
<div class="preview" id="modalPreview"></div>
|
|
386
|
+
<div class="btn-row">
|
|
387
|
+
<button class="btn-cancel" onclick="closeAddModal()">取消</button>
|
|
388
|
+
<button class="btn-submit" onclick="submitAddUsers()">确认插入</button>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
`;
|
|
392
|
+
document.body.appendChild(overlay);
|
|
393
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) closeAddModal(); });
|
|
394
|
+
const ta = document.getElementById('modalUserInput');
|
|
395
|
+
ta.focus();
|
|
396
|
+
ta.addEventListener('input', () => {
|
|
397
|
+
const names = parseUsernames(ta.value);
|
|
398
|
+
const preview = document.getElementById('modalPreview');
|
|
399
|
+
preview.textContent = names.length ? `共 ${names.length} 个用户名` : '';
|
|
400
|
+
});
|
|
401
|
+
ta.addEventListener('keydown', e => {
|
|
402
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
submitAddUsers();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function closeAddModal() {
|
|
410
|
+
const overlay = document.getElementById('addModalOverlay');
|
|
411
|
+
if (overlay) overlay.remove();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function submitAddUsers() {
|
|
415
|
+
const ta = document.getElementById('modalUserInput');
|
|
416
|
+
const raw = ta.value.trim();
|
|
417
|
+
if (!raw) return;
|
|
418
|
+
|
|
419
|
+
const names = parseUsernames(raw);
|
|
420
|
+
if (names.length === 0) return;
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const res = await fetch('/api/users', {
|
|
424
|
+
method: 'POST',
|
|
425
|
+
headers: { 'Content-Type': 'application/json' },
|
|
426
|
+
body: JSON.stringify({ usernames: names })
|
|
427
|
+
});
|
|
428
|
+
const data = await res.json();
|
|
429
|
+
if (data.error) { showToast(data.error, true); return; }
|
|
430
|
+
closeAddModal();
|
|
431
|
+
showToast(data.message || `\u5df2\u63d2\u5165 ${data.added} \u4e2a\u7528\u6237`);
|
|
432
|
+
fetchStats();
|
|
433
|
+
fetchUsers();
|
|
434
|
+
} catch (e) {
|
|
435
|
+
showToast('\u63d2\u5165\u5931\u8d25: ' + e.message, true);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function showToast(msg, isError) {
|
|
440
|
+
const toast = document.getElementById('toast');
|
|
441
|
+
toast.textContent = msg;
|
|
442
|
+
toast.className = 'toast' + (isError ? ' error' : '');
|
|
443
|
+
toast.style.display = 'block';
|
|
444
|
+
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
document.addEventListener('keydown', e => {
|
|
448
|
+
if (e.key === 'Escape') closeAddModal();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
document.getElementById('userTable').addEventListener('click', e => {
|
|
452
|
+
const td = e.target.closest('td.user-id');
|
|
453
|
+
if (!td) return;
|
|
454
|
+
hideContextMenu();
|
|
455
|
+
const username = td.textContent.trim().replace(/^@/, '');
|
|
456
|
+
if (!username) return;
|
|
457
|
+
window.open('https://www.tiktok.com/@' + username, '_blank');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
let contextMenuEl = null;
|
|
461
|
+
let contextMenuUserId = null;
|
|
462
|
+
let contextMenuPinned = false;
|
|
463
|
+
|
|
464
|
+
function showContextMenu(x, y, uniqueId, pinned) {
|
|
465
|
+
hideContextMenu();
|
|
466
|
+
contextMenuUserId = uniqueId;
|
|
467
|
+
contextMenuPinned = !!pinned;
|
|
468
|
+
contextMenuEl = document.createElement('div');
|
|
469
|
+
contextMenuEl.className = 'context-menu';
|
|
470
|
+
contextMenuEl.innerHTML = `
|
|
471
|
+
<div class="context-menu-item" data-action="pin">${contextMenuPinned ? '📌 取消置顶' : '📍 置顶优先'}</div>
|
|
472
|
+
<div class="context-menu-item" data-action="reset">↻ 重新处理</div>
|
|
473
|
+
<div class="context-menu-item" data-action="open">🔗 打开主页</div>
|
|
474
|
+
`;
|
|
475
|
+
document.body.appendChild(contextMenuEl);
|
|
476
|
+
contextMenuEl.style.left = Math.min(x, window.innerWidth - 160) + 'px';
|
|
477
|
+
contextMenuEl.style.top = Math.min(y, window.innerHeight - 100) + 'px';
|
|
478
|
+
|
|
479
|
+
contextMenuEl.addEventListener('click', e => {
|
|
480
|
+
const item = e.target.closest('.context-menu-item');
|
|
481
|
+
if (!item) return;
|
|
482
|
+
const action = item.dataset.action;
|
|
483
|
+
if (action === 'pin') togglePin(contextMenuUserId);
|
|
484
|
+
if (action === 'reset') resetJob(contextMenuUserId);
|
|
485
|
+
if (action === 'open') window.open('https://www.tiktok.com/@' + contextMenuUserId, '_blank');
|
|
486
|
+
hideContextMenu();
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function hideContextMenu() {
|
|
491
|
+
if (contextMenuEl) {
|
|
492
|
+
contextMenuEl.remove();
|
|
493
|
+
contextMenuEl = null;
|
|
494
|
+
contextMenuUserId = null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
document.getElementById('userTable').addEventListener('contextmenu', e => {
|
|
499
|
+
const td = e.target.closest('td');
|
|
500
|
+
if (!td || td.parentElement.tagName !== 'TR') return;
|
|
501
|
+
e.preventDefault();
|
|
502
|
+
const tr = td.parentElement;
|
|
503
|
+
const userIdTd = tr.querySelector('td.user-id');
|
|
504
|
+
if (!userIdTd) return;
|
|
505
|
+
const uniqueId = userIdTd.textContent.trim().replace(/^@/, '');
|
|
506
|
+
const pinned = !!tr.querySelector('.tag.pinned');
|
|
507
|
+
showContextMenu(e.clientX, e.clientY, uniqueId, pinned);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
document.addEventListener('click', e => {
|
|
511
|
+
if (contextMenuEl && !contextMenuEl.contains(e.target)) {
|
|
512
|
+
hideContextMenu();
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
async function togglePin(uniqueId) {
|
|
517
|
+
try {
|
|
518
|
+
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/pin', {
|
|
519
|
+
method: 'POST',
|
|
520
|
+
});
|
|
521
|
+
const data = await res.json();
|
|
522
|
+
if (data.saved) {
|
|
523
|
+
showToast(data.pinned ? '已置顶' : '已取消置顶');
|
|
524
|
+
fetchUsers();
|
|
525
|
+
} else {
|
|
526
|
+
showToast(data.error || '操作失败', true);
|
|
527
|
+
}
|
|
528
|
+
} catch (e) {
|
|
529
|
+
showToast('操作失败: ' + e.message, true);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function resetJob(uniqueId) {
|
|
534
|
+
try {
|
|
535
|
+
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/reset', {
|
|
536
|
+
method: 'POST',
|
|
537
|
+
});
|
|
538
|
+
const data = await res.json();
|
|
539
|
+
if (data.saved) {
|
|
540
|
+
showToast('\u5df2\u91cd\u7f6e\u4efb\u52a1');
|
|
541
|
+
fetchUsers();
|
|
542
|
+
fetchStats();
|
|
543
|
+
} else {
|
|
544
|
+
showToast(data.error || '\u91cd\u7f6e\u5931\u8d25', true);
|
|
545
|
+
}
|
|
546
|
+
} catch (e) {
|
|
547
|
+
showToast('\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
document.getElementById('statTargetCard').addEventListener('click', async () => {
|
|
552
|
+
try {
|
|
553
|
+
const res = await fetch('/api/target-users');
|
|
554
|
+
const data = await res.json();
|
|
555
|
+
if (!data.users.length) { showToast('暂无目标用户', true); return; }
|
|
556
|
+
const text = data.users.map(u => '@' + u.uniqueId).join(', ');
|
|
557
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
558
|
+
await navigator.clipboard.writeText(text);
|
|
559
|
+
showToast(data.users.length + ' 个目标用户 ID 已复制到剪贴板');
|
|
560
|
+
} else {
|
|
561
|
+
const ta = document.createElement('textarea');
|
|
562
|
+
ta.value = text;
|
|
563
|
+
ta.style.position = 'fixed';
|
|
564
|
+
ta.style.left = '-9999px';
|
|
565
|
+
document.body.appendChild(ta);
|
|
566
|
+
ta.select();
|
|
567
|
+
document.execCommand('copy');
|
|
568
|
+
document.body.removeChild(ta);
|
|
569
|
+
showToast(data.users.length + ' 个目标用户 ID 已复制到剪贴板');
|
|
570
|
+
}
|
|
571
|
+
} catch (e) {
|
|
572
|
+
showToast('获取失败: ' + e.message, true);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
fetchStats();
|
|
577
|
+
fetchUsers();
|
|
578
|
+
setInterval(fetchStats, 1000);
|
|
579
|
+
setInterval(fetchUsers, 2000);
|
|
580
|
+
</script>
|
|
581
|
+
</body>
|
|
466
582
|
</html>
|