tt-help-cli-ycl 1.3.63 → 1.3.65

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.
@@ -245,6 +245,13 @@
245
245
  style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
246
246
  <option value="">全部国家</option>
247
247
  </select>
248
+ <label style="display:inline-flex;align-items:center;gap:3px;font-size:12px;color:#999;cursor:pointer;">
249
+ <input type="checkbox" id="rawVideoFilter" onchange="onRawFilterChange()" style="accent-color:#22c55e;"> 有视频
250
+ </label>
251
+ <label style="display:inline-flex;align-items:center;gap:3px;font-size:12px;color:#999;cursor:pointer;">
252
+ <input type="checkbox" id="rawFollowerFilter" onchange="onRawFilterChange()" style="accent-color:#22c55e;">
253
+ 有粉丝
254
+ </label>
248
255
  <button onclick="clearRawFilters()">清空筛选</button>
249
256
  <button onclick="restoreFilteredRawJobs()"
250
257
  style="background:#22c55e;color:#fff;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">恢复当前筛选到
@@ -272,6 +279,62 @@
272
279
  </div>
273
280
  </div>
274
281
  </div>
282
+ <div id="targetPage">
283
+ <div class="stats" style="margin-bottom:16px">
284
+ <div class="stat-card">
285
+ <div class="label">目标商家</div>
286
+ <div class="value target" id="targetPageStatTotal">0</div>
287
+ </div>
288
+ <div class="stat-card clickable pending-card" onclick="navigateToMain()" style="background:rgba(167,139,250,0.1)">
289
+ <div class="label">← 返回主页面</div>
290
+ </div>
291
+ <div class="stat-card">
292
+ <div class="label">目标国家</div>
293
+ <div class="value" id="targetPageStatCountries" style="font-size:17px;color:#a78bfa">0</div>
294
+ </div>
295
+ <div class="stat-card clickable" id="exportTargetCsvBtn" style="background:rgba(34,197,94,0.12);cursor:pointer">
296
+ <div class="label">导出 CSV</div>
297
+ </div>
298
+ </div>
299
+ <div class="target-page-layout">
300
+ <div class="target-side-card">
301
+ <h3>🎯 目标商家国家分布</h3>
302
+ <div class="pending-country-grid" id="targetCountryGrid">
303
+ <span style="color:#666;font-size:12px">加载中...</span>
304
+ </div>
305
+ <div class="muted-tip">点击国家卡片可筛选列表。</div>
306
+ </div>
307
+ <div class="target-table-card">
308
+ <h3>目标商家列表</h3>
309
+ <div class="controls">
310
+ <input type="text" id="targetSearchInput" placeholder="搜索目标商家用户名 / 昵称...">
311
+ <select id="targetLocationFilter" onchange="onTargetLocationChange()"
312
+ style="padding:6px 10px;border:1px solid #7c3aed;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
313
+ <option value="">全部国家</option>
314
+ </select>
315
+ <button onclick="clearTargetFilters()">清空筛选</button>
316
+ </div>
317
+ <div class="table-scroll">
318
+ <table>
319
+ <thead>
320
+ <tr>
321
+ <th>用户名</th>
322
+ <th>昵称</th>
323
+ <th>粉丝</th>
324
+ <th>视频</th>
325
+ <th>国家</th>
326
+ <th>最近发布</th>
327
+ <th>最近刷新</th>
328
+ <th>来源</th>
329
+ <th>状态</th>
330
+ </tr>
331
+ </thead>
332
+ <tbody id="targetTable"></tbody>
333
+ </table>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </div>
275
338
  <script src="app.js"></script>
276
339
  </body>
277
340
 
@@ -634,6 +634,14 @@ td.user-id:hover {
634
634
  display: block;
635
635
  }
636
636
 
637
+ #targetPage {
638
+ display: none;
639
+ }
640
+
641
+ #targetPage.active {
642
+ display: block;
643
+ }
644
+
637
645
  #mainPage.hidden {
638
646
  display: none;
639
647
  }
@@ -670,10 +678,15 @@ td.user-id:hover {
670
678
  position: relative;
671
679
  }
672
680
 
673
- .country-action-btn {
681
+ .country-action-btns {
674
682
  position: absolute;
675
- top: 10px;
676
- right: 10px;
683
+ top: 8px;
684
+ right: 8px;
685
+ display: flex;
686
+ gap: 4px;
687
+ }
688
+
689
+ .country-action-btn {
677
690
  width: 28px;
678
691
  height: 28px;
679
692
  border: 1px solid rgba(248, 113, 113, 0.35);
@@ -937,6 +950,66 @@ td.user-id:hover {
937
950
  color: #a78bfa;
938
951
  }
939
952
 
953
+ .target-country-group {
954
+ background: #1a1a24;
955
+ border-radius: 8px;
956
+ padding: 20px;
957
+ margin-bottom: 20px;
958
+ }
959
+
960
+ .target-country-header {
961
+ display: flex;
962
+ align-items: center;
963
+ gap: 12px;
964
+ margin-bottom: 16px;
965
+ padding-bottom: 12px;
966
+ border-bottom: 1px solid #2a2a3a;
967
+ }
968
+
969
+ .target-country-header .country-flag {
970
+ font-size: 20px;
971
+ font-weight: 700;
972
+ color: #a78bfa;
973
+ }
974
+
975
+ .target-country-header .country-user-count {
976
+ font-size: 13px;
977
+ color: #888;
978
+ background: #2a2a3a;
979
+ padding: 2px 10px;
980
+ border-radius: 10px;
981
+ }
982
+
983
+ .target-page-layout {
984
+ display: grid;
985
+ grid-template-columns: 320px 1fr;
986
+ gap: 16px;
987
+ }
988
+
989
+ .target-side-card {
990
+ background: #1a1a24;
991
+ border-radius: 8px;
992
+ padding: 16px;
993
+ }
994
+
995
+ .target-side-card h3 {
996
+ font-size: 14px;
997
+ color: #a78bfa;
998
+ margin-bottom: 12px;
999
+ }
1000
+
1001
+ .target-table-card {
1002
+ background: #1a1a24;
1003
+ border-radius: 8px;
1004
+ padding: 16px;
1005
+ }
1006
+
1007
+ .target-table-card h3 {
1008
+ font-size: 14px;
1009
+ color: #888;
1010
+ margin-bottom: 12px;
1011
+ }
1012
+
940
1013
  @media (max-width: 768px) {
941
1014
  body {
942
1015
  padding: 8px;
@@ -977,6 +1050,10 @@ td.user-id:hover {
977
1050
  grid-template-columns: 1fr;
978
1051
  }
979
1052
 
1053
+ .target-page-layout {
1054
+ grid-template-columns: 1fr;
1055
+ }
1056
+
980
1057
  .table-wrap {
981
1058
  padding: 10px;
982
1059
  }
@@ -526,6 +526,66 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
526
526
  return;
527
527
  }
528
528
 
529
+ if (
530
+ req.method === "GET" &&
531
+ routePath === "/api/target-users-by-country"
532
+ ) {
533
+ const result = store.getTargetUsersByCountry(DEFAULT_TARGET_LOCATIONS);
534
+ if (req.headers["accept"]?.includes("text/csv")) {
535
+ const columns = [
536
+ "uniqueId",
537
+ "nickname",
538
+ "followerCount",
539
+ "videoCount",
540
+ "locationCreated",
541
+ "latestVideoTime",
542
+ "refreshTime",
543
+ "status",
544
+ "sources",
545
+ ];
546
+ const allUsers = [];
547
+ for (const country of result.countries) {
548
+ for (const u of country.users) {
549
+ allUsers.push({
550
+ uniqueId: u.uniqueId,
551
+ nickname: u.nickname || "",
552
+ followerCount: u.followerCount ?? 0,
553
+ videoCount: u.videoCount ?? 0,
554
+ locationCreated: u.locationCreated || "",
555
+ latestVideoTime: u.latestVideoTime
556
+ ? new Date(u.latestVideoTime * 1000)
557
+ .toISOString()
558
+ .slice(0, 19)
559
+ .replace("T", " ")
560
+ : "",
561
+ refreshTime: u.refreshTime
562
+ ? new Date(u.refreshTime)
563
+ .toISOString()
564
+ .slice(0, 19)
565
+ .replace("T", " ")
566
+ : "",
567
+ status: u.status || "",
568
+ sources: (u.sources || []).join(";"),
569
+ });
570
+ }
571
+ }
572
+ res.writeHead(200, {
573
+ "Content-Type": "text/csv; charset=utf-8",
574
+ "Content-Disposition":
575
+ 'attachment; filename="target-users-by-country.csv"',
576
+ });
577
+ const BOM = "\uFEFF";
578
+ const header = columns.join(",");
579
+ const lines = allUsers.map((r) =>
580
+ columns.map((c) => csvEscape(r[c])).join(","),
581
+ );
582
+ res.end(BOM + [header, ...lines].join("\r\n"));
583
+ } else {
584
+ sendJSON(res, 200, result);
585
+ }
586
+ return;
587
+ }
588
+
529
589
  if (req.method === "GET" && routePath === "/api/client-errors") {
530
590
  sendJSON(res, 200, { clients: store.getClientErrors() });
531
591
  return;
@@ -564,6 +624,8 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
564
624
  location: params.location,
565
625
  limit: params.limit,
566
626
  offset: params.offset,
627
+ hasVideo: params.hasVideo,
628
+ hasFollower: params.hasFollower,
567
629
  });
568
630
  sendJSON(res, 200, result);
569
631
  return;
@@ -590,10 +652,17 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
590
652
  let result;
591
653
  if (body.uniqueId) {
592
654
  result = store.restoreRawJobById(body.uniqueId);
593
- } else if (body.search || body.location) {
655
+ } else if (
656
+ body.search ||
657
+ body.location ||
658
+ body.hasVideo ||
659
+ body.hasFollower
660
+ ) {
594
661
  result = store.restoreRawJobsByFilter({
595
662
  search: body.search || "",
596
663
  location: body.location || "",
664
+ hasVideo: body.hasVideo,
665
+ hasFollower: body.hasFollower,
597
666
  });
598
667
  } else if (body.country) {
599
668
  result = store.restoreRawJobsByCountry(body.country);
@@ -629,6 +698,24 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
629
698
  return;
630
699
  }
631
700
 
701
+ if (
702
+ req.method === "POST" &&
703
+ routePath === "/api/pending-by-country/reset"
704
+ ) {
705
+ try {
706
+ const body = await readBody(req);
707
+ const result = store.resetPendingByCountry(body.country);
708
+ if (result.error) {
709
+ sendJSON(res, 400, result);
710
+ return;
711
+ }
712
+ sendJSON(res, 200, result);
713
+ } catch (e) {
714
+ sendJSON(res, 400, { error: e.message });
715
+ }
716
+ return;
717
+ }
718
+
632
719
  if (
633
720
  req.method === "DELETE" &&
634
721
  routePath.startsWith("/api/client-error/")