tt-help-cli-ycl 1.3.65 → 1.3.73

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.
@@ -273,6 +273,18 @@ body {
273
273
  border-color: #fe2c55;
274
274
  }
275
275
 
276
+ #targetReprocessBtn {
277
+ background: #dc2626;
278
+ border-color: #dc2626;
279
+ color: #fff;
280
+ }
281
+
282
+ #targetReprocessBtn:hover {
283
+ background: #ef4444;
284
+ border-color: #ef4444;
285
+ color: #fff;
286
+ }
287
+
276
288
  .add-users {
277
289
  display: flex;
278
290
  gap: 8px;
@@ -392,6 +404,58 @@ body {
392
404
  background: #e61944;
393
405
  }
394
406
 
407
+ .loc-grid {
408
+ display: grid;
409
+ grid-template-columns: repeat(4, 1fr);
410
+ gap: 8px;
411
+ margin-top: 12px;
412
+ }
413
+
414
+ .loc-option {
415
+ padding: 10px 8px;
416
+ border: 1px solid #333;
417
+ border-radius: 6px;
418
+ background: #2a2a3a;
419
+ color: #ccc;
420
+ font-size: 13px;
421
+ font-weight: 600;
422
+ cursor: pointer;
423
+ transition: all 0.15s;
424
+ }
425
+
426
+ .loc-option:hover {
427
+ border-color: #fe2c55;
428
+ background: rgba(254, 44, 85, 0.1);
429
+ color: #fe2c55;
430
+ }
431
+
432
+ .loc-option.active {
433
+ border-color: #fe2c55;
434
+ background: rgba(254, 44, 85, 0.15);
435
+ color: #fe2c55;
436
+ }
437
+
438
+ .location-cell {
439
+ cursor: pointer;
440
+ transition: all 0.15s;
441
+ position: relative;
442
+ }
443
+
444
+ .location-cell:hover {
445
+ background: rgba(254, 44, 85, 0.08);
446
+ color: #fe2c55;
447
+ }
448
+
449
+ .location-cell.modified {
450
+ color: #f59e0b !important;
451
+ font-weight: 600 !important;
452
+ }
453
+
454
+ .location-cell.modified:hover {
455
+ color: #fbbf24 !important;
456
+ background: rgba(245, 158, 11, 0.1) !important;
457
+ }
458
+
395
459
  .toast {
396
460
  position: fixed;
397
461
  top: 16px;
@@ -507,8 +571,9 @@ tr.row-flash {
507
571
  }
508
572
 
509
573
  .table-scroll {
510
- max-height: 500px;
574
+ max-height: none;
511
575
  overflow-y: auto;
576
+ height: 100%;
512
577
  }
513
578
 
514
579
  table {
@@ -788,6 +853,25 @@ td.user-id:hover {
788
853
  color: #a78bfa;
789
854
  }
790
855
 
856
+ .pending-country-item.selected {
857
+ background: #7c3aed;
858
+ border-color: #a78bfa;
859
+ box-shadow: 0 0 16px rgba(124, 58, 237, 0.6);
860
+ }
861
+
862
+ .pending-country-item.selected .country-name {
863
+ color: #fff;
864
+ font-weight: 700;
865
+ }
866
+
867
+ .pending-country-item.selected .country-count {
868
+ color: #fff;
869
+ }
870
+
871
+ .pending-country-item.selected .country-label {
872
+ color: #ddd6fe;
873
+ }
874
+
791
875
  .back-btn {
792
876
  padding: 6px 14px;
793
877
  border: 1px solid #333;
@@ -983,7 +1067,10 @@ td.user-id:hover {
983
1067
  .target-page-layout {
984
1068
  display: grid;
985
1069
  grid-template-columns: 320px 1fr;
1070
+ grid-template-rows: 1fr;
986
1071
  gap: 16px;
1072
+ min-height: calc(100vh - 280px);
1073
+ align-items: stretch;
987
1074
  }
988
1075
 
989
1076
  .target-side-card {
@@ -24,8 +24,24 @@ function getLocalIP() {
24
24
  const __dirname = dirname(__filename);
25
25
  const publicDir = join(__dirname, "public");
26
26
 
27
+ // Stats 缓存:避免每次请求都扫描 100 万+ 条 jobs 数据
28
+ let statsCache = null;
29
+ let statsCacheTime = 0;
30
+ const STATS_CACHE_TTL = 10000; // 10 秒缓存
31
+
32
+ function invalidateStatsCache() {
33
+ statsCache = null;
34
+ statsCacheTime = 0;
35
+ }
36
+
27
37
  function computeStatsIncremental(st) {
28
- return st.getDashboardStats(DEFAULT_TARGET_LOCATIONS);
38
+ const now = Date.now();
39
+ if (statsCache && now - statsCacheTime < STATS_CACHE_TTL) {
40
+ return statsCache;
41
+ }
42
+ statsCache = st.getDashboardStats(DEFAULT_TARGET_LOCATIONS);
43
+ statsCacheTime = now;
44
+ return statsCache;
29
45
  }
30
46
 
31
47
  function readBody(req) {
@@ -439,6 +455,29 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
439
455
  return;
440
456
  }
441
457
 
458
+ const userLocationMatch = routePath.match(
459
+ /^\/api\/user-location\/([^/]+)$/,
460
+ );
461
+ if (req.method === "PUT" && userLocationMatch) {
462
+ const uniqueId = userLocationMatch[1];
463
+ try {
464
+ const body = await readBody(req);
465
+ const ret = store.updateUserLocation(uniqueId, body.location);
466
+ if (ret.error) {
467
+ sendJSON(res, 404, { error: ret.error });
468
+ return;
469
+ }
470
+ const ts = new Date().toISOString().slice(11, 19);
471
+ console.error(
472
+ `[JOB ${ts}] USER-LOCATION: ${uniqueId} → ${body.location} (modifiedAt=${ret.modifiedAt})`,
473
+ );
474
+ sendJSON(res, 200, ret);
475
+ } catch (e) {
476
+ sendJSON(res, 400, { error: e.message });
477
+ }
478
+ return;
479
+ }
480
+
442
481
  if (req.method === "GET" && routePath === "/api/comment-tasks") {
443
482
  const limit = parseInt(params.limit) || 1;
444
483
  const tasks = store.getPendingCommentTasks(limit);
@@ -530,8 +569,21 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
530
569
  req.method === "GET" &&
531
570
  routePath === "/api/target-users-by-country"
532
571
  ) {
533
- const result = store.getTargetUsersByCountry(DEFAULT_TARGET_LOCATIONS);
572
+ // 摘要模式:只返回各国统计数
573
+ if (params.summary === "1") {
574
+ const result = store.getTargetUsersByCountry(
575
+ DEFAULT_TARGET_LOCATIONS,
576
+ { summaryOnly: true },
577
+ );
578
+ sendJSON(res, 200, result);
579
+ return;
580
+ }
581
+
582
+ // CSV 导出:全量数据
534
583
  if (req.headers["accept"]?.includes("text/csv")) {
584
+ const result = store.getTargetUsersByCountry(
585
+ DEFAULT_TARGET_LOCATIONS,
586
+ );
535
587
  const columns = [
536
588
  "uniqueId",
537
589
  "nickname",
@@ -580,9 +632,27 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
580
632
  columns.map((c) => csvEscape(r[c])).join(","),
581
633
  );
582
634
  res.end(BOM + [header, ...lines].join("\r\n"));
583
- } else {
635
+ return;
636
+ }
637
+
638
+ // 分页模式:按国家+搜索+分页查询用户
639
+ if (params.limit) {
640
+ const result = store.getTargetUsersByCountry(
641
+ DEFAULT_TARGET_LOCATIONS,
642
+ {
643
+ country: params.country || undefined,
644
+ search: params.search || undefined,
645
+ limit: parseInt(params.limit, 10),
646
+ offset: parseInt(params.offset || "0", 10),
647
+ },
648
+ );
584
649
  sendJSON(res, 200, result);
650
+ return;
585
651
  }
652
+
653
+ // 默认:全量(兼容旧调用)
654
+ const result = store.getTargetUsersByCountry(DEFAULT_TARGET_LOCATIONS);
655
+ sendJSON(res, 200, result);
586
656
  return;
587
657
  }
588
658