tt-help-cli-ycl 1.3.33 → 1.3.35
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/package.json +1 -1
- package/scripts/run-explore.bat +6 -1
- package/scripts/run-explore.ps1 +5 -1
- package/scripts/run-explore.sh +7 -1
- package/src/cli/explore.js +10 -3
- package/src/lib/api-interceptor.js +43 -202
- package/src/lib/args.js +1 -1
- package/src/lib/constants.js +8 -5
- package/src/scraper/explore-core.js +19 -9
- package/src/watch/data-store.js +13 -3
- package/src/watch/public/index.html +1381 -676
- package/src/watch/server.js +8 -2
|
@@ -1,144 +1,807 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="zh-CN">
|
|
3
|
+
|
|
3
4
|
<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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>TikTok 采集监控</title>
|
|
8
|
+
<style>
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
17
|
+
background: #0f0f13;
|
|
18
|
+
color: #e0e0e0;
|
|
19
|
+
padding: 16px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.header {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
padding: 12px 16px;
|
|
27
|
+
background: #1a1a24;
|
|
28
|
+
border-radius: 8px;
|
|
29
|
+
margin-bottom: 16px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.header h1 {
|
|
33
|
+
font-size: 18px;
|
|
34
|
+
color: #fe2c55;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.header .meta {
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
color: #888;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.header .status {
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
color: #4ade80;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.script-link {
|
|
48
|
+
font-size: 12px;
|
|
49
|
+
color: #60a5fa;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
padding: 2px 8px;
|
|
52
|
+
border: 1px solid #60a5fa;
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.script-link:hover {
|
|
57
|
+
background: #60a5fa;
|
|
58
|
+
color: #fff;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.stats {
|
|
62
|
+
display: grid;
|
|
63
|
+
grid-template-columns: repeat(7, 1fr);
|
|
64
|
+
gap: 12px;
|
|
65
|
+
margin-bottom: 16px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.stat-card {
|
|
69
|
+
background: #1a1a24;
|
|
70
|
+
border-radius: 8px;
|
|
71
|
+
padding: 16px;
|
|
72
|
+
text-align: center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stat-card .label {
|
|
76
|
+
font-size: 12px;
|
|
77
|
+
color: #888;
|
|
78
|
+
margin-bottom: 8px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.stat-card .value {
|
|
82
|
+
font-size: 28px;
|
|
83
|
+
font-weight: 700;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.stat-card .value.total {
|
|
87
|
+
color: #60a5fa;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.stat-card .value.done {
|
|
91
|
+
color: #4ade80;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.stat-card .value.pending {
|
|
95
|
+
color: #facc15;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.stat-card .value.error {
|
|
99
|
+
color: #f87171;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.stat-card .value.target {
|
|
103
|
+
color: #a78bfa;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.stat-card.clickable {
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.stat-card.clickable:hover {
|
|
111
|
+
background: #25253a;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.charts {
|
|
115
|
+
display: grid;
|
|
116
|
+
grid-template-columns: 1fr 1fr;
|
|
117
|
+
gap: 12px;
|
|
118
|
+
margin-bottom: 16px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.chart-box {
|
|
122
|
+
background: #1a1a24;
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
padding: 16px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.chart-box h3 {
|
|
128
|
+
font-size: 14px;
|
|
129
|
+
color: #888;
|
|
130
|
+
margin-bottom: 12px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.bar-row {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
margin-bottom: 8px;
|
|
137
|
+
font-size: 13px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.bar-row .name {
|
|
141
|
+
width: 50px;
|
|
142
|
+
color: #ccc;
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.bar-row .bar-bg {
|
|
147
|
+
flex: 1;
|
|
148
|
+
background: #2a2a3a;
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
height: 20px;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
margin: 0 8px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.bar-row .bar-fill {
|
|
156
|
+
height: 100%;
|
|
157
|
+
border-radius: 4px;
|
|
158
|
+
transition: width 0.3s;
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
padding-left: 6px;
|
|
162
|
+
font-size: 11px;
|
|
163
|
+
color: #fff;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.bar-row .count {
|
|
167
|
+
width: 80px;
|
|
168
|
+
text-align: right;
|
|
169
|
+
color: #888;
|
|
170
|
+
flex-shrink: 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.source-row {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
margin-bottom: 6px;
|
|
177
|
+
font-size: 13px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.source-row .s-name {
|
|
181
|
+
width: 80px;
|
|
182
|
+
color: #ccc;
|
|
183
|
+
flex-shrink: 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.source-row .s-val {
|
|
187
|
+
color: #888;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.table-wrap {
|
|
191
|
+
background: #1a1a24;
|
|
192
|
+
border-radius: 8px;
|
|
193
|
+
padding: 16px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.table-wrap h3 {
|
|
197
|
+
font-size: 14px;
|
|
198
|
+
color: #888;
|
|
199
|
+
margin-bottom: 12px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.controls {
|
|
203
|
+
display: flex;
|
|
204
|
+
gap: 8px;
|
|
205
|
+
margin-bottom: 12px;
|
|
206
|
+
flex-wrap: wrap;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.controls input {
|
|
210
|
+
flex: 1;
|
|
211
|
+
min-width: 150px;
|
|
212
|
+
padding: 6px 12px;
|
|
213
|
+
border: 1px solid #333;
|
|
214
|
+
border-radius: 6px;
|
|
215
|
+
background: #0f0f13;
|
|
216
|
+
color: #e0e0e0;
|
|
217
|
+
font-size: 13px;
|
|
218
|
+
outline: none;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.controls input:focus {
|
|
222
|
+
border-color: #fe2c55;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.controls button {
|
|
226
|
+
padding: 6px 14px;
|
|
227
|
+
border: 1px solid #333;
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
background: #2a2a3a;
|
|
230
|
+
color: #ccc;
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
transition: all 0.2s;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.controls button:hover {
|
|
237
|
+
border-color: #fe2c55;
|
|
238
|
+
color: #fff;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.controls button.active {
|
|
242
|
+
background: #fe2c55;
|
|
243
|
+
color: #fff;
|
|
244
|
+
border-color: #fe2c55;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.add-users {
|
|
248
|
+
display: flex;
|
|
249
|
+
gap: 8px;
|
|
250
|
+
margin-bottom: 12px;
|
|
251
|
+
align-items: center;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.add-users button {
|
|
255
|
+
padding: 6px 16px;
|
|
256
|
+
border: none;
|
|
257
|
+
border-radius: 6px;
|
|
258
|
+
background: #fe2c55;
|
|
259
|
+
color: #fff;
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
font-weight: 600;
|
|
263
|
+
transition: all 0.2s;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.add-users button:hover {
|
|
267
|
+
background: #e61944;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.modal-overlay {
|
|
271
|
+
position: fixed;
|
|
272
|
+
inset: 0;
|
|
273
|
+
background: rgba(0, 0, 0, 0.65);
|
|
274
|
+
z-index: 1000;
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
justify-content: center;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.modal {
|
|
281
|
+
background: #1a1a24;
|
|
282
|
+
border-radius: 12px;
|
|
283
|
+
padding: 24px;
|
|
284
|
+
width: 520px;
|
|
285
|
+
max-width: 90vw;
|
|
286
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.modal h3 {
|
|
290
|
+
font-size: 16px;
|
|
291
|
+
color: #e0e0e0;
|
|
292
|
+
margin-bottom: 6px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.modal .hint {
|
|
296
|
+
font-size: 12px;
|
|
297
|
+
color: #888;
|
|
298
|
+
margin-bottom: 16px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.modal textarea {
|
|
302
|
+
width: 100%;
|
|
303
|
+
height: 180px;
|
|
304
|
+
padding: 10px 14px;
|
|
305
|
+
border: 1px solid #333;
|
|
306
|
+
border-radius: 8px;
|
|
307
|
+
background: #0f0f13;
|
|
308
|
+
color: #e0e0e0;
|
|
309
|
+
font-size: 13px;
|
|
310
|
+
font-family: inherit;
|
|
311
|
+
outline: none;
|
|
312
|
+
resize: vertical;
|
|
313
|
+
line-height: 1.6;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.modal textarea:focus {
|
|
317
|
+
border-color: #fe2c55;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.modal textarea::placeholder {
|
|
321
|
+
color: #555;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.modal .preview {
|
|
325
|
+
margin-top: 8px;
|
|
326
|
+
font-size: 12px;
|
|
327
|
+
color: #60a5fa;
|
|
328
|
+
min-height: 20px;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.modal .btn-row {
|
|
332
|
+
display: flex;
|
|
333
|
+
gap: 8px;
|
|
334
|
+
margin-top: 16px;
|
|
335
|
+
justify-content: flex-end;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.modal .btn-row button {
|
|
339
|
+
padding: 8px 20px;
|
|
340
|
+
border: none;
|
|
341
|
+
border-radius: 6px;
|
|
342
|
+
font-size: 13px;
|
|
343
|
+
cursor: pointer;
|
|
344
|
+
font-weight: 600;
|
|
345
|
+
transition: all 0.2s;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.modal .btn-cancel {
|
|
349
|
+
background: #2a2a3a;
|
|
350
|
+
color: #ccc;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.modal .btn-cancel:hover {
|
|
354
|
+
background: #333;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.modal .btn-submit {
|
|
358
|
+
background: #fe2c55;
|
|
359
|
+
color: #fff;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.modal .btn-submit:hover {
|
|
363
|
+
background: #e61944;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.toast {
|
|
367
|
+
position: fixed;
|
|
368
|
+
top: 16px;
|
|
369
|
+
right: 16px;
|
|
370
|
+
padding: 10px 20px;
|
|
371
|
+
border-radius: 6px;
|
|
372
|
+
font-size: 13px;
|
|
373
|
+
z-index: 999;
|
|
374
|
+
transition: opacity 0.3s;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.toast.success {
|
|
378
|
+
background: #166534;
|
|
379
|
+
color: #fff;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.toast.error {
|
|
383
|
+
background: #991b1b;
|
|
384
|
+
color: #fff;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@keyframes flashChange {
|
|
388
|
+
0% {
|
|
389
|
+
filter: brightness(1.8);
|
|
390
|
+
box-shadow: 0 0 12px rgba(254, 44, 85, 0.6);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
100% {
|
|
394
|
+
filter: brightness(1);
|
|
395
|
+
box-shadow: none;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.flash-change {
|
|
400
|
+
animation: flashChange 0.6s ease-out;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@keyframes rowFlash {
|
|
404
|
+
0% {
|
|
405
|
+
background: rgba(254, 44, 85, 0.25);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
100% {
|
|
409
|
+
background: transparent;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
tr.row-flash {
|
|
414
|
+
animation: rowFlash 0.8s ease-out;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@keyframes barFlash {
|
|
418
|
+
0% {
|
|
419
|
+
filter: brightness(1.6);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
100% {
|
|
423
|
+
filter: brightness(1);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.bar-fill.bar-flash {
|
|
428
|
+
animation: barFlash 0.5s ease-out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.table-scroll {
|
|
432
|
+
max-height: 500px;
|
|
433
|
+
overflow-y: auto;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
table {
|
|
437
|
+
width: 100%;
|
|
438
|
+
border-collapse: collapse;
|
|
439
|
+
font-size: 12px;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
th {
|
|
443
|
+
position: sticky;
|
|
444
|
+
top: 0;
|
|
445
|
+
background: #22222e;
|
|
446
|
+
padding: 8px 10px;
|
|
447
|
+
text-align: left;
|
|
448
|
+
color: #888;
|
|
449
|
+
font-weight: 600;
|
|
450
|
+
border-bottom: 1px solid #333;
|
|
451
|
+
white-space: nowrap;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
td {
|
|
455
|
+
padding: 6px 10px;
|
|
456
|
+
border-bottom: 1px solid #1f1f2a;
|
|
457
|
+
white-space: nowrap;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
tr:hover {
|
|
461
|
+
background: #1f1f2a;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
td.user-id {
|
|
465
|
+
cursor: pointer;
|
|
466
|
+
color: #60a5fa;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
td.user-id:hover {
|
|
470
|
+
color: #fe2c55;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.tag {
|
|
474
|
+
display: inline-block;
|
|
475
|
+
padding: 1px 6px;
|
|
476
|
+
border-radius: 3px;
|
|
477
|
+
font-size: 10px;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.tag.seller {
|
|
481
|
+
background: #dc2626;
|
|
482
|
+
color: #fff;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.tag.verified {
|
|
486
|
+
background: #2563eb;
|
|
487
|
+
color: #fff;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.tag.pending {
|
|
491
|
+
background: #ca8a04;
|
|
492
|
+
color: #000;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.tag.processing {
|
|
496
|
+
background: #0ea5e9;
|
|
497
|
+
color: #fff;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.tag.error {
|
|
501
|
+
background: #991b1b;
|
|
502
|
+
color: #fff;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.tag.processed {
|
|
506
|
+
background: #166534;
|
|
507
|
+
color: #fff;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.tag.no-video {
|
|
511
|
+
background: #7c3aed;
|
|
512
|
+
color: #fff;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.tag.no-follow {
|
|
516
|
+
background: #b45309;
|
|
517
|
+
color: #fff;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.tag.keep-follow {
|
|
521
|
+
background: #059669;
|
|
522
|
+
color: #fff;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.tag.pinned {
|
|
526
|
+
background: #f59e0b;
|
|
527
|
+
color: #000;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.context-menu {
|
|
531
|
+
position: fixed;
|
|
532
|
+
background: #1e1e2e;
|
|
533
|
+
border: 1px solid #333;
|
|
534
|
+
border-radius: 6px;
|
|
535
|
+
padding: 4px 0;
|
|
536
|
+
min-width: 140px;
|
|
537
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
538
|
+
z-index: 1000;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.context-menu-item {
|
|
542
|
+
padding: 8px 16px;
|
|
543
|
+
font-size: 13px;
|
|
544
|
+
color: #ccc;
|
|
545
|
+
cursor: pointer;
|
|
546
|
+
display: flex;
|
|
547
|
+
align-items: center;
|
|
548
|
+
gap: 8px;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.context-menu-item:hover {
|
|
552
|
+
background: #fe2c55;
|
|
553
|
+
color: #fff;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.context-menu-item.danger {
|
|
557
|
+
color: #f87171;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.context-menu-item.danger:hover {
|
|
561
|
+
background: #991b1b;
|
|
562
|
+
color: #fff;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
::-webkit-scrollbar {
|
|
566
|
+
width: 6px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
::-webkit-scrollbar-track {
|
|
570
|
+
background: #1a1a24;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
::-webkit-scrollbar-thumb {
|
|
574
|
+
background: #333;
|
|
575
|
+
border-radius: 3px;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.client-errors-section {
|
|
579
|
+
margin-bottom: 16px;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.section-header {
|
|
583
|
+
display: flex;
|
|
584
|
+
align-items: center;
|
|
585
|
+
gap: 8px;
|
|
586
|
+
margin-bottom: 8px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.section-header h3 {
|
|
590
|
+
font-size: 14px;
|
|
591
|
+
color: #e0e0e0;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.error-badge {
|
|
595
|
+
background: #991b1b;
|
|
596
|
+
color: #fff;
|
|
597
|
+
font-size: 11px;
|
|
598
|
+
padding: 2px 8px;
|
|
599
|
+
border-radius: 10px;
|
|
600
|
+
font-weight: 600;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.client-errors-table {
|
|
604
|
+
width: 100%;
|
|
605
|
+
border-collapse: collapse;
|
|
606
|
+
background: #1a1a24;
|
|
607
|
+
border-radius: 8px;
|
|
608
|
+
overflow: hidden;
|
|
609
|
+
font-size: 13px;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.client-errors-table thead tr {
|
|
613
|
+
background: #22222e;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.client-errors-table th {
|
|
617
|
+
padding: 10px 12px;
|
|
618
|
+
text-align: left;
|
|
619
|
+
color: #888;
|
|
620
|
+
font-weight: 600;
|
|
621
|
+
font-size: 12px;
|
|
622
|
+
border-bottom: 1px solid #2a2a3a;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.client-errors-table td {
|
|
626
|
+
padding: 8px 12px;
|
|
627
|
+
border-bottom: 1px solid #1f1f2e;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.client-errors-table tbody tr:last-child td {
|
|
631
|
+
border-bottom: none;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.client-errors-table tbody tr:hover {
|
|
635
|
+
background: #22222e;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.error-type-captcha {
|
|
639
|
+
color: #f59e0b;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.error-type-network {
|
|
643
|
+
color: #f87171;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.error-type-other {
|
|
647
|
+
color: #a78bfa;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
@media (max-width: 768px) {
|
|
651
|
+
body {
|
|
652
|
+
padding: 8px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.header {
|
|
656
|
+
flex-direction: column;
|
|
657
|
+
gap: 6px;
|
|
658
|
+
align-items: flex-start;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.header h1 {
|
|
662
|
+
font-size: 16px;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.stats {
|
|
666
|
+
grid-template-columns: repeat(2, 1fr);
|
|
667
|
+
gap: 8px;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.stat-card {
|
|
671
|
+
padding: 10px;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.stat-card .label {
|
|
675
|
+
font-size: 11px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.stat-card .value {
|
|
679
|
+
font-size: 18px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.charts {
|
|
683
|
+
grid-template-columns: 1fr;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.table-wrap {
|
|
687
|
+
padding: 10px;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.controls {
|
|
691
|
+
flex-wrap: wrap;
|
|
692
|
+
gap: 6px;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.controls input {
|
|
696
|
+
flex: 0 0 100%;
|
|
697
|
+
width: 100%;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.controls button {
|
|
701
|
+
flex: 0 0 calc(33.33% - 4px);
|
|
702
|
+
min-width: 0;
|
|
703
|
+
text-align: center;
|
|
704
|
+
white-space: nowrap;
|
|
705
|
+
font-size: 11px;
|
|
706
|
+
padding: 8px 4px;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
#batchResetBtn {
|
|
710
|
+
flex: 0 0 100% !important;
|
|
711
|
+
font-size: 12px !important;
|
|
712
|
+
padding: 8px 12px !important;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.controls select {
|
|
716
|
+
flex: 0 0 100%;
|
|
717
|
+
width: 100%;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.table-scroll {
|
|
721
|
+
max-height: none;
|
|
722
|
+
overflow: visible;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
table,
|
|
726
|
+
thead,
|
|
727
|
+
tbody,
|
|
728
|
+
th,
|
|
729
|
+
td,
|
|
730
|
+
tr {
|
|
731
|
+
display: block;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
thead {
|
|
735
|
+
display: none;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
tr {
|
|
739
|
+
background: #22222e;
|
|
740
|
+
border-radius: 8px;
|
|
741
|
+
padding: 10px 12px;
|
|
742
|
+
margin-bottom: 8px;
|
|
743
|
+
border: 1px solid #2a2a3a;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
tr:hover {
|
|
747
|
+
background: #2a2a3a;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
td {
|
|
751
|
+
padding: 4px 0;
|
|
752
|
+
border: none;
|
|
753
|
+
text-align: left;
|
|
754
|
+
position: relative;
|
|
755
|
+
padding-left: 40%;
|
|
756
|
+
font-size: 13px;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
td::before {
|
|
760
|
+
content: attr(data-label);
|
|
761
|
+
position: absolute;
|
|
762
|
+
left: 0;
|
|
763
|
+
width: 36%;
|
|
764
|
+
text-align: right;
|
|
765
|
+
color: #888;
|
|
766
|
+
font-size: 12px;
|
|
767
|
+
font-weight: 600;
|
|
768
|
+
white-space: nowrap;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
td.user-id {
|
|
772
|
+
font-size: 15px;
|
|
773
|
+
font-weight: 700;
|
|
774
|
+
color: #60a5fa;
|
|
775
|
+
padding-left: 0;
|
|
776
|
+
border-bottom: 1px solid #2a2a3a;
|
|
777
|
+
margin-bottom: 4px;
|
|
778
|
+
padding-bottom: 6px;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
td.user-id::before {
|
|
782
|
+
display: none;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
td.user-id:hover {
|
|
786
|
+
color: #fe2c55;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.add-users {
|
|
790
|
+
justify-content: center;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.modal {
|
|
794
|
+
width: 95vw;
|
|
795
|
+
padding: 16px;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.modal textarea {
|
|
799
|
+
height: 140px;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
</style>
|
|
141
803
|
</head>
|
|
804
|
+
|
|
142
805
|
<body>
|
|
143
806
|
<div class="header">
|
|
144
807
|
<h1>TikTok 采集监控</h1>
|
|
@@ -149,151 +812,191 @@
|
|
|
149
812
|
<span class="status" id="lastUpdate">--</span>
|
|
150
813
|
</div>
|
|
151
814
|
</div>
|
|
152
|
-
<div class="stats">
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<div class="
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
</
|
|
180
|
-
</
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
|
-
<div class="chart-box">
|
|
186
|
-
<h3>国家统计</h3>
|
|
187
|
-
<div id="countryChart"></div>
|
|
188
|
-
</div>
|
|
189
|
-
<div class="chart-box">
|
|
190
|
-
<h3>来源分布</h3>
|
|
191
|
-
<div id="sourceChart"></div>
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
<div class="table-wrap">
|
|
195
|
-
<h3>用户列表</h3>
|
|
196
|
-
<div class="add-users">
|
|
197
|
-
<button onclick="openAddModal()">+ 插入队列</button>
|
|
198
|
-
</div>
|
|
199
|
-
<div id="toast" class="toast" style="display:none"></div>
|
|
200
|
-
<div class="controls">
|
|
201
|
-
<input type="text" id="searchInput" placeholder="搜索用户名 / 昵称...">
|
|
202
|
-
<button data-filter="all" class="active" onclick="setFilter('all')">全部</button>
|
|
203
|
-
<button data-filter="processing" onclick="setFilter('processing')">处理中</button>
|
|
204
|
-
<button data-filter="pending" onclick="setFilter('pending')">待处理</button>
|
|
205
|
-
<button data-filter="done" onclick="setFilter('done')">已处理</button>
|
|
206
|
-
<button data-filter="error" onclick="setFilter('error')">错误</button>
|
|
207
|
-
<button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
|
|
208
|
-
<button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标用户(EU)</button>
|
|
209
|
-
<select id="targetLocationFilter" onchange="onTargetLocationChange()" style="padding:6px 10px;border:1px solid #7c3aed;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;display:none">
|
|
210
|
-
<option value="">全部目标国家</option>
|
|
211
|
-
</select>
|
|
212
|
-
<button id="batchResetBtn" onclick="batchResetErrors()" style="display:none;padding:6px 10px;border:1px solid #f87171;border-radius:6px;background:transparent;color:#f87171;font-size:12px;cursor:pointer;font-weight:600;transition:all 0.2s;white-space:nowrap;">↻ 批量重新处理 (<span id="batchResetCount">0</span>)</button>
|
|
213
|
-
<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;">
|
|
214
|
-
<option value="">全部国家</option>
|
|
215
|
-
</select>
|
|
815
|
+
<div class="stats">
|
|
816
|
+
<div class="stat-card">
|
|
817
|
+
<div class="label">总任务</div>
|
|
818
|
+
<div class="value total" id="statTotal">0</div>
|
|
819
|
+
</div>
|
|
820
|
+
<div class="stat-card">
|
|
821
|
+
<div class="label">处理中</div>
|
|
822
|
+
<div class="value total" id="statProcessing">0</div>
|
|
823
|
+
</div>
|
|
824
|
+
<div class="stat-card">
|
|
825
|
+
<div class="label">已完成</div>
|
|
826
|
+
<div class="value done" id="statDone">0</div>
|
|
827
|
+
</div>
|
|
828
|
+
<div class="stat-card">
|
|
829
|
+
<div class="label">待处理</div>
|
|
830
|
+
<div class="value pending" id="statPending">0</div>
|
|
831
|
+
</div>
|
|
832
|
+
<div class="stat-card">
|
|
833
|
+
<div class="label">错误</div>
|
|
834
|
+
<div class="value error" id="statError">0</div>
|
|
835
|
+
</div>
|
|
836
|
+
<div class="stat-card">
|
|
837
|
+
<div class="label">受限</div>
|
|
838
|
+
<div class="value error" id="statRestricted">0</div>
|
|
839
|
+
</div>
|
|
840
|
+
<div class="stat-card">
|
|
841
|
+
<div class="label">预处理任务</div>
|
|
842
|
+
<div class="value target" id="statUserUpdateTasks">0</div>
|
|
843
|
+
</div>
|
|
844
|
+
<div class="stat-card clickable" id="statTargetCard">
|
|
845
|
+
<div class="label">目标用户(EU商家)</div>
|
|
846
|
+
<div class="value target" id="statTarget">0</div>
|
|
847
|
+
</div>
|
|
216
848
|
</div>
|
|
217
|
-
<div class="
|
|
218
|
-
<
|
|
849
|
+
<div class="client-errors-section" id="clientErrorsSection" style="display:none">
|
|
850
|
+
<div class="section-header">
|
|
851
|
+
<h3>客户端异常</h3>
|
|
852
|
+
<span class="error-badge" id="clientErrorsBadge">0</span>
|
|
853
|
+
</div>
|
|
854
|
+
<table class="client-errors-table">
|
|
219
855
|
<thead>
|
|
220
|
-
<tr
|
|
856
|
+
<tr>
|
|
857
|
+
<th>客户端</th>
|
|
858
|
+
<th>错误类型</th>
|
|
859
|
+
<th>错误次数</th>
|
|
860
|
+
<th>验证码次数</th>
|
|
861
|
+
<th>阶段</th>
|
|
862
|
+
<th>错误详情</th>
|
|
863
|
+
<th>当时处理的 TikTok 用户</th>
|
|
864
|
+
<th>时间</th>
|
|
865
|
+
<th>操作</th>
|
|
866
|
+
</tr>
|
|
221
867
|
</thead>
|
|
222
|
-
<tbody id="
|
|
868
|
+
<tbody id="clientErrorsBody"></tbody>
|
|
223
869
|
</table>
|
|
224
870
|
</div>
|
|
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
|
-
|
|
871
|
+
<div class="charts">
|
|
872
|
+
<div class="chart-box">
|
|
873
|
+
<h3>国家统计</h3>
|
|
874
|
+
<div id="countryChart"></div>
|
|
875
|
+
</div>
|
|
876
|
+
<div class="chart-box">
|
|
877
|
+
<h3>来源分布</h3>
|
|
878
|
+
<div id="sourceChart"></div>
|
|
879
|
+
</div>
|
|
880
|
+
</div>
|
|
881
|
+
<div class="table-wrap">
|
|
882
|
+
<h3>用户列表</h3>
|
|
883
|
+
<div class="add-users">
|
|
884
|
+
<button onclick="openAddModal()">+ 插入队列</button>
|
|
885
|
+
</div>
|
|
886
|
+
<div id="toast" class="toast" style="display:none"></div>
|
|
887
|
+
<div class="controls">
|
|
888
|
+
<input type="text" id="searchInput" placeholder="搜索用户名 / 昵称...">
|
|
889
|
+
<button data-filter="all" class="active" onclick="setFilter('all')">全部</button>
|
|
890
|
+
<button data-filter="processing" onclick="setFilter('processing')">处理中</button>
|
|
891
|
+
<button data-filter="pending" onclick="setFilter('pending')">待处理</button>
|
|
892
|
+
<button data-filter="done" onclick="setFilter('done')">已处理</button>
|
|
893
|
+
<button data-filter="error" onclick="setFilter('error')">错误</button>
|
|
894
|
+
<button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
|
|
895
|
+
<button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标用户(EU)</button>
|
|
896
|
+
<select id="targetLocationFilter" onchange="onTargetLocationChange()"
|
|
897
|
+
style="padding:6px 10px;border:1px solid #7c3aed;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;display:none">
|
|
898
|
+
<option value="">全部目标国家</option>
|
|
899
|
+
</select>
|
|
900
|
+
<button id="batchResetBtn" onclick="batchResetErrors()"
|
|
901
|
+
style="display:none;padding:6px 10px;border:1px solid #f87171;border-radius:6px;background:transparent;color:#f87171;font-size:12px;cursor:pointer;font-weight:600;transition:all 0.2s;white-space:nowrap;">↻
|
|
902
|
+
批量重新处理 (<span id="batchResetCount">0</span>)</button>
|
|
903
|
+
<select id="locationFilter" onchange="onLocationChange()"
|
|
904
|
+
style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
|
|
905
|
+
<option value="">全部国家</option>
|
|
906
|
+
</select>
|
|
907
|
+
</div>
|
|
908
|
+
<div class="table-scroll">
|
|
909
|
+
<table>
|
|
910
|
+
<thead>
|
|
911
|
+
<tr>
|
|
912
|
+
<th>用户名</th>
|
|
913
|
+
<th>昵称</th>
|
|
914
|
+
<th>粉丝</th>
|
|
915
|
+
<th>视频</th>
|
|
916
|
+
<th>国家</th>
|
|
917
|
+
<th>猜测国家</th>
|
|
918
|
+
<th>来源</th>
|
|
919
|
+
<th>状态</th>
|
|
920
|
+
<th>接收人</th>
|
|
921
|
+
<th>领取时间</th>
|
|
922
|
+
<th>提交时间</th>
|
|
923
|
+
</tr>
|
|
924
|
+
</thead>
|
|
925
|
+
<tbody id="userTable"></tbody>
|
|
926
|
+
</table>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
<script>
|
|
930
|
+
const COLORS = ['#fe2c55', '#60a5fa', '#4ade80', '#facc15', '#f97316', '#a855f7', '#ec4899', '#14b8a6', '#e11d48', '#0ea5e9', '#8b5cf6', '#84cc16'];
|
|
931
|
+
let currentFilter = 'all';
|
|
932
|
+
let currentStats = null;
|
|
933
|
+
let currentUsers = [];
|
|
934
|
+
let currentLocation = '';
|
|
935
|
+
let currentTargetLocation = '';
|
|
936
|
+
let prevStatValues = {};
|
|
937
|
+
let prevUserMap = {};
|
|
938
|
+
|
|
939
|
+
async function fetchStats() {
|
|
940
|
+
try {
|
|
941
|
+
const res = await fetch('/api/stats');
|
|
942
|
+
currentStats = await res.json();
|
|
943
|
+
renderStats();
|
|
944
|
+
renderLocationFilter();
|
|
945
|
+
} catch (e) {
|
|
946
|
+
document.getElementById('lastUpdate').textContent = '\u8fde\u63a5\u5931\u8d25';
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function fetchUsers() {
|
|
951
|
+
try {
|
|
952
|
+
const params = new URLSearchParams();
|
|
953
|
+
if (currentFilter === 'target') {
|
|
954
|
+
params.set('target', '1');
|
|
955
|
+
if (currentTargetLocation) params.set('targetLocation', currentTargetLocation);
|
|
956
|
+
} else if (currentFilter !== 'all') {
|
|
957
|
+
params.set('status', currentFilter);
|
|
958
|
+
}
|
|
959
|
+
const search = document.getElementById('searchInput').value.trim();
|
|
960
|
+
if (search) params.set('search', search);
|
|
961
|
+
if (currentLocation) params.set('location', currentLocation);
|
|
962
|
+
params.set('limit', '200');
|
|
963
|
+
const res = await fetch('/api/users?' + params.toString());
|
|
964
|
+
const data = await res.json();
|
|
965
|
+
currentUsers = data.users || [];
|
|
966
|
+
renderTable(currentUsers);
|
|
967
|
+
} catch (e) { }
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function escapeHtml(str) {
|
|
971
|
+
const div = document.createElement('div');
|
|
972
|
+
div.textContent = str;
|
|
973
|
+
return div.innerHTML;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function fetchClientErrors() {
|
|
977
|
+
try {
|
|
978
|
+
const res = await fetch('/api/client-errors');
|
|
979
|
+
const data = await res.json();
|
|
980
|
+
const clients = data.clients || [];
|
|
981
|
+
const section = document.getElementById('clientErrorsSection');
|
|
982
|
+
const badge = document.getElementById('clientErrorsBadge');
|
|
983
|
+
const tbody = document.getElementById('clientErrorsBody');
|
|
984
|
+
if (clients.length === 0) {
|
|
985
|
+
section.style.display = 'none';
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
section.style.display = '';
|
|
989
|
+
badge.textContent = clients.length;
|
|
990
|
+
const typeMap = { captcha: ['验证码', 'error-type-captcha'], network: ['网络', 'error-type-network'], other: ['其他', 'error-type-other'], '被封': ['被封', 'error-type-captcha'] };
|
|
991
|
+
const stageMap = { 'video-page': '视频页', 'comment': '评论', 'follow': '关注/粉丝', 'scrape': 'scrape', 'process': '处理' };
|
|
992
|
+
tbody.innerHTML = clients.map(c => {
|
|
993
|
+
const [typeText, typeClass] = typeMap[c.errorType] || ['未知', ''];
|
|
994
|
+
const stageText = c.stage ? (stageMap[c.stage] || c.stage) : '';
|
|
995
|
+
const captchaText = c.captchaCount ? `${c.captchaCount}次${c.captchaStage ? ' (' + (stageMap[c.captchaStage] || c.captchaStage) + ')' : ''}` : '-';
|
|
996
|
+
const msgDetail = c.errorMessage ? `<br><span style="color:#666;font-size:11px">${escapeHtml(c.errorMessage)}</span>` : '';
|
|
997
|
+
const stackDetail = c.errorStack ? `<br><span style="color:#555;font-size:10px;max-width:250px;display:block;word-break:break-all">${escapeHtml(c.errorStack)}</span>` : '';
|
|
998
|
+
const captchaDetail = c.captchaMessage ? `<br><span style="color:#666;font-size:11px">${escapeHtml(c.captchaMessage)}</span>` : '';
|
|
999
|
+
return `<tr>
|
|
297
1000
|
<td style="font-family:monospace;font-weight:600;color:#60a5fa">${escapeHtml(c.userId)}</td>
|
|
298
1001
|
<td class="${typeClass}">${typeText}</td>
|
|
299
1002
|
<td style="color:#f87171;font-weight:600">${c.reportCount || 1}</td>
|
|
@@ -304,118 +1007,119 @@ async function fetchClientErrors() {
|
|
|
304
1007
|
<td style="color:#888;font-size:12px">${new Date(c.timestamp).toLocaleTimeString()}</td>
|
|
305
1008
|
<td><button class="btn-delete" onclick="deleteClientError('${escapeHtml(c.userId)}')" style="background:#991b1b;color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:12px">删除</button></td>
|
|
306
1009
|
</tr>`;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
1010
|
+
}).join('');
|
|
1011
|
+
} catch (e) { }
|
|
1012
|
+
}
|
|
310
1013
|
|
|
311
|
-
async function deleteClientError(userId) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function flashEl(id, value) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function renderStats() {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function renderTargetLocationFilter(targetCountryStats) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function renderCountryChart(countries) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1014
|
+
async function deleteClientError(userId) {
|
|
1015
|
+
try {
|
|
1016
|
+
await fetch(`/api/client-error/${encodeURIComponent(userId)}`, { method: 'DELETE' });
|
|
1017
|
+
fetchClientErrors();
|
|
1018
|
+
} catch (e) { }
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function flashEl(id, value) {
|
|
1022
|
+
const el = document.getElementById(id);
|
|
1023
|
+
if (!el) return;
|
|
1024
|
+
const prev = prevStatValues[id];
|
|
1025
|
+
el.textContent = value;
|
|
1026
|
+
if (prev !== undefined && prev !== value) {
|
|
1027
|
+
el.classList.remove('flash-change');
|
|
1028
|
+
void el.offsetWidth;
|
|
1029
|
+
el.classList.add('flash-change');
|
|
1030
|
+
}
|
|
1031
|
+
prevStatValues[id] = value;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function renderStats() {
|
|
1035
|
+
if (!currentStats) return;
|
|
1036
|
+
const d = currentStats;
|
|
1037
|
+
flashEl('statTotal', d.totalUsers);
|
|
1038
|
+
flashEl('statProcessing', d.processingUsers || 0);
|
|
1039
|
+
flashEl('statDone', d.processedUsers);
|
|
1040
|
+
flashEl('statPending', d.pendingUsers);
|
|
1041
|
+
flashEl('statError', d.errorUsers);
|
|
1042
|
+
flashEl('statRestricted', d.restrictedUsers);
|
|
1043
|
+
flashEl('statTarget', d.targetUsers);
|
|
1044
|
+
flashEl('statUserUpdateTasks', d.userUpdateTasks || 0);
|
|
1045
|
+
document.getElementById('lastUpdate').textContent = '\u66f4\u65b0\u4e8e ' + new Date().toLocaleTimeString();
|
|
1046
|
+
document.getElementById('fileMeta').textContent = (d.processingUsers || 0) + ' \u5904\u7406\u4e2d, ' + d.totalUsers + ' \u4e2a\u7528\u6237';
|
|
1047
|
+
|
|
1048
|
+
renderCountryChart(d.countryStats);
|
|
1049
|
+
renderSourceChart(d.sourceStats);
|
|
1050
|
+
renderTargetLocationFilter(d.targetCountryStats);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function renderTargetLocationFilter(targetCountryStats) {
|
|
1054
|
+
const sel = document.getElementById('targetLocationFilter');
|
|
1055
|
+
if (!sel) return;
|
|
1056
|
+
const val = sel.value;
|
|
1057
|
+
sel.innerHTML = '<option value="">全部目标国家</option>' +
|
|
1058
|
+
(targetCountryStats || []).map(c => `<option value="${c.country}"${val === c.country ? ' selected' : ''}>${c.country} (${c.count})</option>`).join('');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function renderCountryChart(countries) {
|
|
1062
|
+
const el = document.getElementById('countryChart');
|
|
1063
|
+
const filtered = countries.filter(c => c.country !== '\u672a\u77e5');
|
|
1064
|
+
if (!filtered.length) { el.innerHTML = '<span style="color:#666;font-size:12px">\u6682\u65e0\u6570\u636e</span>'; return; }
|
|
1065
|
+
const max = filtered[0].count;
|
|
1066
|
+
const top = filtered.slice(0, 15);
|
|
1067
|
+
el.innerHTML = top.map((c, i) => `
|
|
364
1068
|
<div class="bar-row">
|
|
365
1069
|
<span class="name">${c.country}</span>
|
|
366
1070
|
<div class="bar-bg"><div class="bar-fill" style="width:${(c.count / max * 100)}%;background:${COLORS[i % COLORS.length]}">${c.count}</div></div>
|
|
367
1071
|
<span class="count">${(currentStats ? (c.count / currentStats.totalUsers * 100).toFixed(1) : 0)}%</span>
|
|
368
1072
|
</div>
|
|
369
1073
|
`).join('');
|
|
370
|
-
}
|
|
1074
|
+
}
|
|
371
1075
|
|
|
372
|
-
function renderSourceChart(sources) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1076
|
+
function renderSourceChart(sources) {
|
|
1077
|
+
const el = document.getElementById('sourceChart');
|
|
1078
|
+
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' };
|
|
1079
|
+
const entries = Object.entries(sources);
|
|
1080
|
+
el.innerHTML = entries.map(([key, val]) => `
|
|
377
1081
|
<div class="source-row"><span class="s-name">${labels[key] || key}:</span><span class="s-val">${val}</span></div>
|
|
378
1082
|
`).join('');
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function renderTable(users) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function renderTable(users) {
|
|
1086
|
+
const el = document.getElementById('userTable');
|
|
1087
|
+
|
|
1088
|
+
const newUserMap = {};
|
|
1089
|
+
for (const u of users) newUserMap[u.uniqueId] = u;
|
|
1090
|
+
|
|
1091
|
+
el.innerHTML = users.map(u => {
|
|
1092
|
+
const wasStatus = prevUserMap[u.uniqueId]?.status;
|
|
1093
|
+
const nowStatus = u.status;
|
|
1094
|
+
const changed = wasStatus !== nowStatus &&
|
|
1095
|
+
(nowStatus === 'done' || nowStatus === 'restricted' || nowStatus === 'error');
|
|
1096
|
+
const rowClass = changed ? ' class="row-flash"' : '';
|
|
1097
|
+
|
|
1098
|
+
const statusTags = {
|
|
1099
|
+
restricted: '<span class="tag error">\u53d7\u9650(\u8df3\u8fc7)</span>',
|
|
1100
|
+
error: '<span class="tag error">\u9519\u8bef(\u5f85\u91cd\u8bd5)</span>',
|
|
1101
|
+
done: '<span class="tag processed">\u5df2\u5904\u7406</span>',
|
|
1102
|
+
processing: '<span class="tag processing">\u5904\u7406\u4e2d</span>',
|
|
1103
|
+
pending: '<span class="tag pending">\u5f85\u5904\u7406</span>',
|
|
1104
|
+
};
|
|
1105
|
+
const statusTag = statusTags[u.status] || '<span class="tag pending">' + (u.status || '\u672a\u77e5') + '</span>';
|
|
1106
|
+
const sources = (u.sources || []).join(', ');
|
|
1107
|
+
const extraTags = [];
|
|
1108
|
+
if (u.pinned) extraTags.push('<span class="tag pinned">📌 置顶</span>');
|
|
1109
|
+
if (u.ttSeller) extraTags.push('<span class="tag seller">\u5546\u5bb6</span>');
|
|
1110
|
+
if (u.verified) extraTags.push('<span class="tag verified">\u8ba4\u8bc1</span>');
|
|
1111
|
+
if (u.noVideo) extraTags.push('<span class="tag no-video">\u65e0\u89c6\u9891</span>');
|
|
1112
|
+
if (u.keepFollow) extraTags.push('<span class="tag keep-follow">\u5173\u6ce8\u5df2\u4fdd\u7559</span>');
|
|
1113
|
+
if (u.hasFollowData === false) extraTags.push('<span class="tag no-follow">\u5173\u6ce8\u672a\u83b7\u53d6</span>');
|
|
1114
|
+
const nick = (u.nickname || '').replace(/</g, '<').replace(/>/g, '>');
|
|
1115
|
+
const fans = u.followerCount != null ? formatNum(u.followerCount) : '-';
|
|
1116
|
+
const videos = u.videoCount != null ? u.videoCount : '-';
|
|
1117
|
+
const loc = u.locationCreated || '-';
|
|
1118
|
+
const guessedLoc = u.guessedLocation || '-';
|
|
1119
|
+
const claimer = u.claimedBy || '-';
|
|
1120
|
+
const claimTime = u.claimedAt ? formatTime(u.claimedAt) : '-';
|
|
1121
|
+
const procTime = u.processedAt ? formatTime(u.processedAt) : '-';
|
|
1122
|
+
return `<tr${rowClass}>
|
|
419
1123
|
<td class="user-id" data-label="用户名">@${u.uniqueId}</td>
|
|
420
1124
|
<td data-label="昵称">${nick}</td>
|
|
421
1125
|
<td data-label="粉丝">${fans}</td>
|
|
@@ -428,83 +1132,83 @@ function renderTable(users) {
|
|
|
428
1132
|
<td data-label="领取时间" style="font-size:11px;color:#888">${claimTime}</td>
|
|
429
1133
|
<td data-label="提交时间" style="font-size:11px;color:#888">${procTime}</td>
|
|
430
1134
|
</tr>`;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function formatNum(n) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function formatTime(ts) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function setFilter(f) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function renderLocationFilter() {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function onLocationChange() {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function onTargetLocationChange() {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
let searchTimer = null;
|
|
492
|
-
document.getElementById('searchInput').addEventListener('input', () => {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
function parseUsernames(raw) {
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function openAddModal() {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1135
|
+
}).join('');
|
|
1136
|
+
|
|
1137
|
+
const errorCount = users.filter(u => u.status === 'error').length;
|
|
1138
|
+
const countEl = document.getElementById('batchResetCount');
|
|
1139
|
+
if (countEl) countEl.textContent = errorCount;
|
|
1140
|
+
|
|
1141
|
+
prevUserMap = newUserMap;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function formatNum(n) {
|
|
1145
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
1146
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
1147
|
+
return n;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function formatTime(ts) {
|
|
1151
|
+
const d = new Date(ts);
|
|
1152
|
+
const pad = n => String(n).padStart(2, '0');
|
|
1153
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function setFilter(f) {
|
|
1157
|
+
currentFilter = f;
|
|
1158
|
+
document.querySelectorAll('.controls button').forEach(b => {
|
|
1159
|
+
b.classList.toggle('active', b.dataset.filter === f);
|
|
1160
|
+
});
|
|
1161
|
+
const btn = document.getElementById('batchResetBtn');
|
|
1162
|
+
btn.style.display = f === 'error' ? '' : 'none';
|
|
1163
|
+
const targetLocSel = document.getElementById('targetLocationFilter');
|
|
1164
|
+
if (f === 'target') {
|
|
1165
|
+
targetLocSel.style.display = '';
|
|
1166
|
+
} else {
|
|
1167
|
+
targetLocSel.style.display = 'none';
|
|
1168
|
+
currentTargetLocation = '';
|
|
1169
|
+
}
|
|
1170
|
+
fetchUsers();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function renderLocationFilter() {
|
|
1174
|
+
if (!currentStats || !currentStats.countryStats) return;
|
|
1175
|
+
const sel = document.getElementById('locationFilter');
|
|
1176
|
+
if (!sel) return;
|
|
1177
|
+
const val = sel.value;
|
|
1178
|
+
const entries = currentStats.countryStats.sort((a, b) => b.count - a.count);
|
|
1179
|
+
sel.innerHTML = '<option value="">全部国家</option>' +
|
|
1180
|
+
entries.map(c => `<option value="${c.country}"${val === c.country ? ' selected' : ''}>${c.country} (${c.count})</option>`).join('');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function onLocationChange() {
|
|
1184
|
+
const sel = document.getElementById('locationFilter');
|
|
1185
|
+
currentLocation = sel.value;
|
|
1186
|
+
fetchUsers();
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function onTargetLocationChange() {
|
|
1190
|
+
const sel = document.getElementById('targetLocationFilter');
|
|
1191
|
+
currentTargetLocation = sel.value;
|
|
1192
|
+
fetchUsers();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
let searchTimer = null;
|
|
1196
|
+
document.getElementById('searchInput').addEventListener('input', () => {
|
|
1197
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
1198
|
+
searchTimer = setTimeout(fetchUsers, 300);
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
function parseUsernames(raw) {
|
|
1202
|
+
return raw.split(/[,,\n\r]+/).map(s => s.replace(/^@/, '').trim()).filter(Boolean);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function openAddModal() {
|
|
1206
|
+
let overlay = document.getElementById('addModalOverlay');
|
|
1207
|
+
if (overlay) return;
|
|
1208
|
+
overlay = document.createElement('div');
|
|
1209
|
+
overlay.id = 'addModalOverlay';
|
|
1210
|
+
overlay.className = 'modal-overlay';
|
|
1211
|
+
overlay.innerHTML = `
|
|
508
1212
|
<div class="modal">
|
|
509
1213
|
<h3>插入用户到队列</h3>
|
|
510
1214
|
<div class="hint">每行一个用户名,或用逗号分隔。支持 @username 或 username 格式。插入到队列最前面优先处理。</div>
|
|
@@ -516,239 +1220,240 @@ function openAddModal() {
|
|
|
516
1220
|
</div>
|
|
517
1221
|
</div>
|
|
518
1222
|
`;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1223
|
+
document.body.appendChild(overlay);
|
|
1224
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) closeAddModal(); });
|
|
1225
|
+
const ta = document.getElementById('modalUserInput');
|
|
1226
|
+
ta.focus();
|
|
1227
|
+
ta.addEventListener('input', () => {
|
|
1228
|
+
const names = parseUsernames(ta.value);
|
|
1229
|
+
const preview = document.getElementById('modalPreview');
|
|
1230
|
+
preview.textContent = names.length ? `共 ${names.length} 个用户名` : '';
|
|
1231
|
+
});
|
|
1232
|
+
ta.addEventListener('keydown', e => {
|
|
1233
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
1234
|
+
e.preventDefault();
|
|
1235
|
+
submitAddUsers();
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
532
1238
|
}
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
1239
|
|
|
536
|
-
function closeAddModal() {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
1240
|
+
function closeAddModal() {
|
|
1241
|
+
const overlay = document.getElementById('addModalOverlay');
|
|
1242
|
+
if (overlay) overlay.remove();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
async function submitAddUsers() {
|
|
1246
|
+
const ta = document.getElementById('modalUserInput');
|
|
1247
|
+
const raw = ta.value.trim();
|
|
1248
|
+
if (!raw) return;
|
|
1249
|
+
|
|
1250
|
+
const names = parseUsernames(raw);
|
|
1251
|
+
if (names.length === 0) return;
|
|
1252
|
+
|
|
1253
|
+
try {
|
|
1254
|
+
const res = await fetch('/api/users', {
|
|
1255
|
+
method: 'POST',
|
|
1256
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1257
|
+
body: JSON.stringify({ usernames: names })
|
|
1258
|
+
});
|
|
1259
|
+
const data = await res.json();
|
|
1260
|
+
if (data.error) { showToast(data.error, true); return; }
|
|
1261
|
+
closeAddModal();
|
|
1262
|
+
showToast(data.message || `\u5df2\u63d2\u5165 ${data.added} \u4e2a\u7528\u6237`);
|
|
1263
|
+
fetchStats();
|
|
1264
|
+
fetchUsers();
|
|
1265
|
+
} catch (e) {
|
|
1266
|
+
showToast('\u63d2\u5165\u5931\u8d25: ' + e.message, true);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
540
1269
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1270
|
+
function showToast(msg, isError) {
|
|
1271
|
+
const toast = document.getElementById('toast');
|
|
1272
|
+
toast.textContent = msg;
|
|
1273
|
+
toast.className = 'toast' + (isError ? ' error' : '');
|
|
1274
|
+
toast.style.display = 'block';
|
|
1275
|
+
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
|
1276
|
+
}
|
|
545
1277
|
|
|
546
|
-
|
|
547
|
-
|
|
1278
|
+
document.addEventListener('keydown', e => {
|
|
1279
|
+
if (e.key === 'Escape') closeAddModal();
|
|
1280
|
+
});
|
|
548
1281
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1282
|
+
document.getElementById('userTable').addEventListener('click', e => {
|
|
1283
|
+
const td = e.target.closest('td.user-id');
|
|
1284
|
+
if (!td) return;
|
|
1285
|
+
hideContextMenu();
|
|
1286
|
+
const username = td.textContent.trim().replace(/^@/, '');
|
|
1287
|
+
if (!username) return;
|
|
1288
|
+
window.open('https://www.tiktok.com/@' + username, '_blank');
|
|
554
1289
|
});
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const toast = document.getElementById('toast');
|
|
568
|
-
toast.textContent = msg;
|
|
569
|
-
toast.className = 'toast' + (isError ? ' error' : '');
|
|
570
|
-
toast.style.display = 'block';
|
|
571
|
-
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
document.addEventListener('keydown', e => {
|
|
575
|
-
if (e.key === 'Escape') closeAddModal();
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
document.getElementById('userTable').addEventListener('click', e => {
|
|
579
|
-
const td = e.target.closest('td.user-id');
|
|
580
|
-
if (!td) return;
|
|
581
|
-
hideContextMenu();
|
|
582
|
-
const username = td.textContent.trim().replace(/^@/, '');
|
|
583
|
-
if (!username) return;
|
|
584
|
-
window.open('https://www.tiktok.com/@' + username, '_blank');
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
let contextMenuEl = null;
|
|
588
|
-
let contextMenuUserId = null;
|
|
589
|
-
let contextMenuPinned = false;
|
|
590
|
-
|
|
591
|
-
function showContextMenu(x, y, uniqueId, pinned) {
|
|
592
|
-
hideContextMenu();
|
|
593
|
-
contextMenuUserId = uniqueId;
|
|
594
|
-
contextMenuPinned = !!pinned;
|
|
595
|
-
contextMenuEl = document.createElement('div');
|
|
596
|
-
contextMenuEl.className = 'context-menu';
|
|
597
|
-
contextMenuEl.innerHTML = `
|
|
1290
|
+
|
|
1291
|
+
let contextMenuEl = null;
|
|
1292
|
+
let contextMenuUserId = null;
|
|
1293
|
+
let contextMenuPinned = false;
|
|
1294
|
+
|
|
1295
|
+
function showContextMenu(x, y, uniqueId, pinned) {
|
|
1296
|
+
hideContextMenu();
|
|
1297
|
+
contextMenuUserId = uniqueId;
|
|
1298
|
+
contextMenuPinned = !!pinned;
|
|
1299
|
+
contextMenuEl = document.createElement('div');
|
|
1300
|
+
contextMenuEl.className = 'context-menu';
|
|
1301
|
+
contextMenuEl.innerHTML = `
|
|
598
1302
|
<div class="context-menu-item" data-action="pin">${contextMenuPinned ? '📌 取消置顶' : '📍 置顶优先'}</div>
|
|
599
1303
|
<div class="context-menu-item" data-action="reset">↻ 重新处理</div>
|
|
600
1304
|
<div class="context-menu-item" data-action="open">🔗 打开主页</div>
|
|
601
1305
|
`;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function hideContextMenu() {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
document.getElementById('userTable').addEventListener('contextmenu', e => {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
document.addEventListener('click', e => {
|
|
638
|
-
if (contextMenuEl && !contextMenuEl.contains(e.target)) {
|
|
639
|
-
hideContextMenu();
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
async function togglePin(uniqueId) {
|
|
644
|
-
try {
|
|
645
|
-
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/pin', {
|
|
646
|
-
method: 'POST',
|
|
647
|
-
});
|
|
648
|
-
const data = await res.json();
|
|
649
|
-
if (data.saved) {
|
|
650
|
-
showToast(data.pinned ? '已置顶' : '已取消置顶');
|
|
651
|
-
fetchUsers();
|
|
652
|
-
} else {
|
|
653
|
-
showToast(data.error || '操作失败', true);
|
|
654
|
-
}
|
|
655
|
-
} catch (e) {
|
|
656
|
-
showToast('操作失败: ' + e.message, true);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
async function resetJob(uniqueId) {
|
|
661
|
-
try {
|
|
662
|
-
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/reset', {
|
|
663
|
-
method: 'POST',
|
|
1306
|
+
document.body.appendChild(contextMenuEl);
|
|
1307
|
+
contextMenuEl.style.left = Math.min(x, window.innerWidth - 160) + 'px';
|
|
1308
|
+
contextMenuEl.style.top = Math.min(y, window.innerHeight - 100) + 'px';
|
|
1309
|
+
|
|
1310
|
+
contextMenuEl.addEventListener('click', e => {
|
|
1311
|
+
const item = e.target.closest('.context-menu-item');
|
|
1312
|
+
if (!item) return;
|
|
1313
|
+
const action = item.dataset.action;
|
|
1314
|
+
if (action === 'pin') togglePin(contextMenuUserId);
|
|
1315
|
+
if (action === 'reset') resetJob(contextMenuUserId);
|
|
1316
|
+
if (action === 'open') window.open('https://www.tiktok.com/@' + contextMenuUserId, '_blank');
|
|
1317
|
+
hideContextMenu();
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function hideContextMenu() {
|
|
1322
|
+
if (contextMenuEl) {
|
|
1323
|
+
contextMenuEl.remove();
|
|
1324
|
+
contextMenuEl = null;
|
|
1325
|
+
contextMenuUserId = null;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
document.getElementById('userTable').addEventListener('contextmenu', e => {
|
|
1330
|
+
const td = e.target.closest('td');
|
|
1331
|
+
if (!td || td.parentElement.tagName !== 'TR') return;
|
|
1332
|
+
e.preventDefault();
|
|
1333
|
+
const tr = td.parentElement;
|
|
1334
|
+
const userIdTd = tr.querySelector('td.user-id');
|
|
1335
|
+
if (!userIdTd) return;
|
|
1336
|
+
const uniqueId = userIdTd.textContent.trim().replace(/^@/, '');
|
|
1337
|
+
const pinned = !!tr.querySelector('.tag.pinned');
|
|
1338
|
+
showContextMenu(e.clientX, e.clientY, uniqueId, pinned);
|
|
664
1339
|
});
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
} else {
|
|
671
|
-
showToast(data.error || '\u91cd\u7f6e\u5931\u8d25', true);
|
|
672
|
-
}
|
|
673
|
-
} catch (e) {
|
|
674
|
-
showToast('\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
async function batchResetErrors() {
|
|
679
|
-
const btn = document.getElementById('batchResetBtn');
|
|
680
|
-
const errorUsers = currentUsers.filter(u => u.status === 'error');
|
|
681
|
-
if (errorUsers.length === 0) {
|
|
682
|
-
showToast('\u6ca1\u6709\u9700\u8981\u91cd\u7f6e\u7684\u9519\u8bef\u7528\u6237', true);
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
const userIds = errorUsers.map(u => u.uniqueId);
|
|
686
|
-
const origText = btn.innerHTML;
|
|
687
|
-
btn.disabled = true;
|
|
688
|
-
btn.style.opacity = '0.6';
|
|
689
|
-
btn.style.cursor = 'not-allowed';
|
|
690
|
-
btn.innerHTML = '↻ 处理中...';
|
|
691
|
-
try {
|
|
692
|
-
const res = await fetch('/api/jobs/batch-reset', {
|
|
693
|
-
method: 'POST',
|
|
694
|
-
headers: { 'Content-Type': 'application/json' },
|
|
695
|
-
body: JSON.stringify({ userIds })
|
|
1340
|
+
|
|
1341
|
+
document.addEventListener('click', e => {
|
|
1342
|
+
if (contextMenuEl && !contextMenuEl.contains(e.target)) {
|
|
1343
|
+
hideContextMenu();
|
|
1344
|
+
}
|
|
696
1345
|
});
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1346
|
+
|
|
1347
|
+
async function togglePin(uniqueId) {
|
|
1348
|
+
try {
|
|
1349
|
+
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/pin', {
|
|
1350
|
+
method: 'POST',
|
|
1351
|
+
});
|
|
1352
|
+
const data = await res.json();
|
|
1353
|
+
if (data.saved) {
|
|
1354
|
+
showToast(data.pinned ? '已置顶' : '已取消置顶');
|
|
1355
|
+
fetchUsers();
|
|
1356
|
+
} else {
|
|
1357
|
+
showToast(data.error || '操作失败', true);
|
|
1358
|
+
}
|
|
1359
|
+
} catch (e) {
|
|
1360
|
+
showToast('操作失败: ' + e.message, true);
|
|
1361
|
+
}
|
|
701
1362
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1363
|
+
|
|
1364
|
+
async function resetJob(uniqueId) {
|
|
1365
|
+
try {
|
|
1366
|
+
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/reset', {
|
|
1367
|
+
method: 'POST',
|
|
1368
|
+
});
|
|
1369
|
+
const data = await res.json();
|
|
1370
|
+
if (data.saved) {
|
|
1371
|
+
showToast('\u5df2\u91cd\u7f6e\u4efb\u52a1');
|
|
1372
|
+
fetchUsers();
|
|
1373
|
+
fetchStats();
|
|
1374
|
+
} else {
|
|
1375
|
+
showToast(data.error || '\u91cd\u7f6e\u5931\u8d25', true);
|
|
1376
|
+
}
|
|
1377
|
+
} catch (e) {
|
|
1378
|
+
showToast('\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async function batchResetErrors() {
|
|
1383
|
+
const btn = document.getElementById('batchResetBtn');
|
|
1384
|
+
const errorUsers = currentUsers.filter(u => u.status === 'error');
|
|
1385
|
+
if (errorUsers.length === 0) {
|
|
1386
|
+
showToast('\u6ca1\u6709\u9700\u8981\u91cd\u7f6e\u7684\u9519\u8bef\u7528\u6237', true);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const userIds = errorUsers.map(u => u.uniqueId);
|
|
1390
|
+
const origText = btn.innerHTML;
|
|
1391
|
+
btn.disabled = true;
|
|
1392
|
+
btn.style.opacity = '0.6';
|
|
1393
|
+
btn.style.cursor = 'not-allowed';
|
|
1394
|
+
btn.innerHTML = '↻ 处理中...';
|
|
1395
|
+
try {
|
|
1396
|
+
const res = await fetch('/api/jobs/batch-reset', {
|
|
1397
|
+
method: 'POST',
|
|
1398
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1399
|
+
body: JSON.stringify({ userIds })
|
|
1400
|
+
});
|
|
1401
|
+
const data = await res.json();
|
|
1402
|
+
if (data.error) {
|
|
1403
|
+
showToast(data.error, true);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
showToast(`\u5df2\u91cd\u7f6e ${data.reset} / ${data.total} \u4e2a\u7528\u6237`);
|
|
1407
|
+
fetchUsers();
|
|
1408
|
+
fetchStats();
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
showToast('\u6279\u91cf\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
1411
|
+
} finally {
|
|
1412
|
+
btn.disabled = false;
|
|
1413
|
+
btn.style.opacity = '1';
|
|
1414
|
+
btn.style.cursor = 'pointer';
|
|
1415
|
+
btn.innerHTML = origText;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
document.getElementById('statTargetCard').addEventListener('click', async () => {
|
|
1420
|
+
try {
|
|
1421
|
+
const res = await fetch('/api/target-users', {
|
|
1422
|
+
headers: { 'Accept': 'text/csv' },
|
|
1423
|
+
});
|
|
1424
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1425
|
+
const blob = await res.blob();
|
|
1426
|
+
const ext = blob.size < 200 ? 'json' : 'csv';
|
|
1427
|
+
if (ext === 'json') {
|
|
1428
|
+
const text = await blob.text();
|
|
1429
|
+
const data = JSON.parse(text);
|
|
1430
|
+
if (!data.users.length) { showToast('暂无目标用户', true); return; }
|
|
1431
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1432
|
+
const ids = data.users.map(u => '@' + u.uniqueId).join(', ');
|
|
1433
|
+
await navigator.clipboard.writeText(ids);
|
|
1434
|
+
showToast(data.users.length + ' 个目标用户 ID 已复制到剪贴板');
|
|
1435
|
+
}
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const url = URL.createObjectURL(blob);
|
|
1439
|
+
const a = document.createElement('a');
|
|
1440
|
+
a.href = url;
|
|
1441
|
+
a.download = 'target-users.csv';
|
|
1442
|
+
a.click();
|
|
1443
|
+
URL.revokeObjectURL(url);
|
|
1444
|
+
showToast('CSV 文件已开始下载');
|
|
1445
|
+
} catch (e) {
|
|
1446
|
+
showToast('获取失败: ' + e.message, true);
|
|
1447
|
+
}
|
|
719
1448
|
});
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const ids = data.users.map(u => '@' + u.uniqueId).join(', ');
|
|
729
|
-
await navigator.clipboard.writeText(ids);
|
|
730
|
-
showToast(data.users.length + ' 个目标用户 ID 已复制到剪贴板');
|
|
731
|
-
}
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
const url = URL.createObjectURL(blob);
|
|
735
|
-
const a = document.createElement('a');
|
|
736
|
-
a.href = url;
|
|
737
|
-
a.download = 'target-users.csv';
|
|
738
|
-
a.click();
|
|
739
|
-
URL.revokeObjectURL(url);
|
|
740
|
-
showToast('CSV 文件已开始下载');
|
|
741
|
-
} catch (e) {
|
|
742
|
-
showToast('获取失败: ' + e.message, true);
|
|
743
|
-
}
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
fetchStats();
|
|
747
|
-
fetchUsers();
|
|
748
|
-
fetchClientErrors();
|
|
749
|
-
setInterval(fetchStats, 10000);
|
|
750
|
-
setInterval(fetchUsers, 10000);
|
|
751
|
-
setInterval(fetchClientErrors, 10000);
|
|
752
|
-
</script>
|
|
1449
|
+
|
|
1450
|
+
fetchStats();
|
|
1451
|
+
fetchUsers();
|
|
1452
|
+
fetchClientErrors();
|
|
1453
|
+
setInterval(fetchStats, 10000);
|
|
1454
|
+
setInterval(fetchUsers, 10000);
|
|
1455
|
+
setInterval(fetchClientErrors, 10000);
|
|
1456
|
+
</script>
|
|
753
1457
|
</body>
|
|
1458
|
+
|
|
754
1459
|
</html>
|