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.
@@ -33,7 +33,7 @@ async function fetchStats() {
33
33
  }
34
34
  }
35
35
 
36
- async function fetchUsers() {
36
+ async function fetchUsers(showLoad) {
37
37
  try {
38
38
  const params = new URLSearchParams();
39
39
  if (currentFilter === "target") {
@@ -46,11 +46,13 @@ async function fetchUsers() {
46
46
  const search = document.getElementById("searchInput").value.trim();
47
47
  if (search) params.set("search", search);
48
48
  if (currentLocation) params.set("location", currentLocation);
49
- params.set("limit", "200");
49
+ params.set("limit", "100");
50
+ if (showLoad) showLoading("加载中...");
50
51
  const res = await fetch("/api/users?" + params.toString());
51
52
  const data = await res.json();
52
53
  currentUsers = data.users || [];
53
54
  renderTable(currentUsers);
55
+ if (showLoad) hideLoading();
54
56
  } catch (e) {}
55
57
  }
56
58
 
@@ -309,7 +311,7 @@ function renderTable(users) {
309
311
  for (const u of users) newUserMap[u.uniqueId] = u;
310
312
 
311
313
  el.innerHTML = users
312
- .map((u) => {
314
+ .map((u, idx) => {
313
315
  const wasStatus = prevUserMap[u.uniqueId]?.status;
314
316
  const nowStatus = u.status;
315
317
  const changed =
@@ -345,6 +347,11 @@ function renderTable(users) {
345
347
  const fans = u.followerCount != null ? formatNum(u.followerCount) : "-";
346
348
  const videos = u.videoCount != null ? u.videoCount : "-";
347
349
  const loc = u.locationCreated || "-";
350
+ const confirmedLoc = u.confirmedLocation
351
+ ? u.confirmedLocation === u.locationCreated
352
+ ? `<span style="color:#22c55e">${u.confirmedLocation} ✓</span>`
353
+ : `<span style="color:#ef4444">${u.confirmedLocation} ✗</span>`
354
+ : "-";
348
355
  const latestVideo = u.latestVideoTime
349
356
  ? formatTime(u.latestVideoTime * 1000)
350
357
  : "-";
@@ -377,6 +384,7 @@ function renderTable(users) {
377
384
  ? `<span class="tag error" style="font-size:10px">${u.statusCode}</span>`
378
385
  : "";
379
386
  return `<tr${rowClass}>
387
+ <td style="text-align:right;color:#555;padding-right:8px;font-size:12px">${idx + 1}</td>
380
388
  <td class="user-id" data-label="用户名">@${u.uniqueId}</td>
381
389
  <td data-label="昵称">${nick}</td>
382
390
  <td data-label="粉丝">${fans}</td>
@@ -428,7 +436,7 @@ function setFilter(f) {
428
436
  targetLocSel.style.display = "none";
429
437
  currentTargetLocation = "";
430
438
  }
431
- fetchUsers();
439
+ fetchUsers(true);
432
440
  }
433
441
 
434
442
  function renderLocationFilter() {
@@ -448,15 +456,20 @@ function renderLocationFilter() {
448
456
  }
449
457
 
450
458
  function onLocationChange() {
451
- const sel = document.getElementById("locationFilter");
452
- currentLocation = sel.value;
453
- fetchUsers();
454
- }
455
-
456
- function onTargetLocationChange() {
457
- const sel = document.getElementById("targetLocationFilter");
458
- currentTargetLocation = sel.value;
459
- fetchUsers();
459
+ const locationSel = document.getElementById("locationFilter");
460
+ currentLocation = locationSel.value;
461
+ fetchUsers(true);
462
+ const targetSel = document.getElementById("targetLocationFilter");
463
+ currentTargetLocation = targetSel.value;
464
+ // 判断当前在哪个页面
465
+ if (document.getElementById("targetPage").classList.contains("active")) {
466
+ if (currentTargetSummary) {
467
+ renderTargetCountryGrid(currentTargetSummary.countries || []);
468
+ loadTargetUsersPage();
469
+ }
470
+ } else {
471
+ fetchUsers();
472
+ }
460
473
  }
461
474
 
462
475
  let searchTimer = null;
@@ -520,6 +533,97 @@ function closeAddModal() {
520
533
  if (overlay) overlay.remove();
521
534
  }
522
535
 
536
+ // ========== 国家修改模态框 ==========
537
+
538
+ const TARGET_LOCATIONS = [
539
+ "AT",
540
+ "BE",
541
+ "CZ",
542
+ "DE",
543
+ "ES",
544
+ "FR",
545
+ "GR",
546
+ "HU",
547
+ "IE",
548
+ "IT",
549
+ "NL",
550
+ "PL",
551
+ "PT",
552
+ ];
553
+
554
+ function openLocationModal(uniqueId, currentLocation) {
555
+ let overlay = document.getElementById("locationModalOverlay");
556
+ if (overlay) return;
557
+ overlay = document.createElement("div");
558
+ overlay.id = "locationModalOverlay";
559
+ overlay.className = "modal-overlay";
560
+ const options = TARGET_LOCATIONS.map(
561
+ (loc) =>
562
+ `<button class="loc-option ${loc === currentLocation ? "active" : ""}" onclick="selectLocation('${uniqueId}','${loc}')">${loc}</button>`,
563
+ ).join("");
564
+ overlay.innerHTML = `
565
+ <div class="modal" style="max-width:420px">
566
+ <h3>修改用户国家</h3>
567
+ <div class="hint">用户: @${uniqueId},当前国家: ${currentLocation}</div>
568
+ <div class="loc-grid">${options}</div>
569
+ <div class="btn-row" style="margin-top:16px">
570
+ <button class="btn-cancel" onclick="closeLocationModal()">取消</button>
571
+ </div>
572
+ </div>
573
+ `;
574
+ document.body.appendChild(overlay);
575
+ overlay.addEventListener("click", (e) => {
576
+ if (e.target === overlay) closeLocationModal();
577
+ });
578
+ }
579
+
580
+ function closeLocationModal() {
581
+ const overlay = document.getElementById("locationModalOverlay");
582
+ if (overlay) overlay.remove();
583
+ }
584
+
585
+ async function selectLocation(uniqueId, location) {
586
+ closeLocationModal();
587
+ showLoading("正在更新...");
588
+ try {
589
+ const res = await fetch(`/api/user-location/${uniqueId}`, {
590
+ method: "PUT",
591
+ headers: { "Content-Type": "application/json" },
592
+ body: JSON.stringify({ location }),
593
+ });
594
+ const data = await res.json();
595
+ if (data.error) {
596
+ showToast(data.error, true);
597
+ return;
598
+ }
599
+ showToast(`@${uniqueId} 国家已更新为 ${location}`);
600
+
601
+ // 只更新当前行 DOM
602
+ const tr = document.querySelector(`tr[data-user="${uniqueId}"]`);
603
+ if (tr) {
604
+ const locCell = tr.querySelector(".location-cell");
605
+ if (locCell) {
606
+ const editIcon = ' <span style="font-size:10px;opacity:0.5">✏️</span>';
607
+ locCell.className = "location-cell modified";
608
+ locCell.innerHTML = `${location}${editIcon}`;
609
+ locCell.title = `点击修改国家(已修改: ${formatTime(Date.now())})`;
610
+ locCell.onclick = () => openLocationModal(uniqueId, location);
611
+ }
612
+ }
613
+
614
+ // 在内存用户列表中也更新
615
+ const userInMem = currentTargetUsers.find((u) => u.uniqueId === uniqueId);
616
+ if (userInMem) {
617
+ userInMem.locationCreated = location;
618
+ userInMem.modifiedAt = Date.now();
619
+ }
620
+ } catch (e) {
621
+ showToast("更新失败: " + e.message, true);
622
+ } finally {
623
+ hideLoading();
624
+ }
625
+ }
626
+
523
627
  async function submitAddUsers() {
524
628
  const ta = document.getElementById("modalUserInput");
525
629
  const raw = ta.value.trim();
@@ -837,6 +941,7 @@ function showTargetPage() {
837
941
  document.getElementById("userUpdatePage").classList.remove("active");
838
942
  document.getElementById("rawPage").classList.remove("active");
839
943
  document.getElementById("targetPage").classList.add("active");
944
+ showLoading("正在加载目标商家数据...");
840
945
  fetchTargetByCountry();
841
946
  // 同步统计
842
947
  if (currentStats) {
@@ -847,16 +952,21 @@ function showTargetPage() {
847
952
  }
848
953
  }
849
954
 
850
- let currentTargetData = null;
955
+ let currentTargetSummary = null;
956
+ let currentTargetUsers = [];
957
+ let currentTargetTotal = 0;
958
+ let currentTargetLoading = false;
959
+ let currentTargetAllLoaded = false;
960
+ let currentTargetSeq = 0;
961
+ const TARGET_PAGE_SIZE = 200;
851
962
 
852
963
  async function fetchTargetByCountry() {
853
964
  try {
854
- const res = await fetch("/api/target-users-by-country");
965
+ const res = await fetch("/api/target-users-by-country?summary=1");
855
966
  const data = await res.json();
856
- currentTargetData = data;
967
+ currentTargetSummary = data;
857
968
  renderTargetCountryGrid(data.countries || []);
858
- renderTargetLocationFilter(data.countries || []);
859
- renderTargetTable(data.countries || []);
969
+ renderTargetPageLocationFilter(data.countries || []);
860
970
  document.getElementById("targetPageStatTotal").textContent = formatStatNum(
861
971
  data.total || 0,
862
972
  { full: true },
@@ -864,8 +974,11 @@ async function fetchTargetByCountry() {
864
974
  document.getElementById("targetPageStatCountries").textContent = (
865
975
  data.countries || []
866
976
  ).length;
977
+ await loadTargetUsersPage(false, true);
867
978
  } catch (e) {
868
979
  console.error("获取目标商家数据失败:", e);
980
+ } finally {
981
+ hideLoading();
869
982
  }
870
983
  }
871
984
 
@@ -876,16 +989,18 @@ function renderTargetCountryGrid(countries) {
876
989
  '<span style="color:#666;font-size:12px">暂无目标商家</span>';
877
990
  return;
878
991
  }
879
- const total = countries.reduce((sum, c) => sum + c.count, 0);
992
+ const total = countries.reduce((sum, c) => sum + (c.count || 0), 0);
880
993
  grid.innerHTML = countries
881
994
  .map((c) => {
882
- const pct = ((c.count / total) * 100).toFixed(1);
995
+ const cnt = c.count || 0;
996
+ const pct = total > 0 ? ((cnt / total) * 100).toFixed(1) : "0.0";
883
997
  const safeCountry = escapeJsString(c.country);
998
+ const sel = currentTargetLocation === c.country ? " selected" : "";
884
999
  return `
885
- <div class="pending-country-item has-target"
1000
+ <div class="pending-country-item has-target${sel}"
886
1001
  onclick="filterTargetByCountry('${safeCountry}')">
887
1002
  <div class="country-name">${c.country}</div>
888
- <div class="country-count">${c.count}</div>
1003
+ <div class="country-count">${cnt}</div>
889
1004
  <div class="country-label">${pct}% 目标商家</div>
890
1005
  </div>
891
1006
  `;
@@ -893,7 +1008,7 @@ function renderTargetCountryGrid(countries) {
893
1008
  .join("");
894
1009
  }
895
1010
 
896
- function renderTargetLocationFilter(countries) {
1011
+ function renderTargetPageLocationFilter(countries) {
897
1012
  const sel = document.getElementById("targetLocationFilter");
898
1013
  if (!sel) return;
899
1014
  const val = sel.value;
@@ -902,51 +1017,104 @@ function renderTargetLocationFilter(countries) {
902
1017
  countries
903
1018
  .map(
904
1019
  (c) =>
905
- `<option value="${c.country}"${val === c.country ? " selected" : ""}>${c.country} (${c.count})</option>`,
1020
+ `<option value="${c.country}"${val === c.country ? " selected" : ""}>${c.country} (${c.count || 0})</option>`,
906
1021
  )
907
1022
  .join("");
908
1023
  }
909
1024
 
910
- function renderTargetTable(countries) {
911
- const el = document.getElementById("targetTable");
1025
+ async function loadTargetUsersPage(append = false, skipLoading = false) {
1026
+ if (currentTargetLoading) return;
1027
+ currentTargetLoading = true;
1028
+
1029
+ const seq = ++currentTargetSeq;
912
1030
  const search = document.getElementById("targetSearchInput")
913
- ? document.getElementById("targetSearchInput").value.trim().toLowerCase()
1031
+ ? document.getElementById("targetSearchInput").value.trim()
914
1032
  : "";
915
- const location = currentTargetLocation;
1033
+ const offset = append ? currentTargetUsers.length : 0;
916
1034
 
917
- let allUsers = [];
918
- for (const c of countries) {
919
- if (location && c.country !== location) continue;
920
- allUsers = allUsers.concat(c.users);
1035
+ // 非追加模式(重置加载)时显示 loading,除非外层已经显示了
1036
+ if (!append && !skipLoading) {
1037
+ showLoading("正在加载数据...");
921
1038
  }
922
1039
 
923
- if (search) {
924
- allUsers = allUsers.filter(
925
- (u) =>
926
- (u.uniqueId || "").toLowerCase().includes(search) ||
927
- (u.nickname || "").toLowerCase().includes(search),
928
- );
1040
+ let data = null;
1041
+ let error = null;
1042
+ try {
1043
+ let url = `/api/target-users-by-country?limit=${TARGET_PAGE_SIZE}&offset=${offset}`;
1044
+ if (currentTargetLocation)
1045
+ url += `&country=${encodeURIComponent(currentTargetLocation)}`;
1046
+ if (search) url += `&search=${encodeURIComponent(search)}`;
1047
+
1048
+ const res = await fetch(url);
1049
+ data = await res.json();
1050
+ } catch (e) {
1051
+ error = e;
1052
+ console.error("加载目标用户失败:", e);
1053
+ } finally {
1054
+ // 非追加模式时隐藏 loading,除非外层负责隐藏
1055
+ if (!append && !skipLoading) {
1056
+ hideLoading();
1057
+ }
1058
+ currentTargetLoading = false;
1059
+
1060
+ if (error || !data) return;
1061
+
1062
+ // 防止旧请求覆盖新请求的结果
1063
+ if (seq !== currentTargetSeq && !append) return;
1064
+
1065
+ if (!append) {
1066
+ currentTargetUsers = data.users || [];
1067
+ currentTargetTotal = data.total || 0;
1068
+ currentTargetAllLoaded = currentTargetUsers.length >= currentTargetTotal;
1069
+ } else {
1070
+ currentTargetUsers = currentTargetUsers.concat(data.users || []);
1071
+ currentTargetAllLoaded = currentTargetUsers.length >= currentTargetTotal;
1072
+ }
1073
+
1074
+ // 所有状态更新完成后,再渲染
1075
+ renderTargetTable();
929
1076
  }
1077
+ }
1078
+
1079
+ function updateLoadMoreButton() {
1080
+ // 按钮已移除,保留函数名以兼容旧调用
1081
+ }
1082
+
1083
+ function renderTargetTable() {
1084
+ const el = document.getElementById("targetTable");
1085
+ const moreHint = document.getElementById("targetMoreHint");
930
1086
 
931
- if (!allUsers.length) {
1087
+ if (currentTargetUsers.length === 0) {
932
1088
  el.innerHTML =
933
- '<tr><td colspan="8" style="color:#666;text-align:center;padding:24px">暂无目标商家</td></tr>';
1089
+ '<tr><td colspan="9" style="text-align:center;color:#888;padding:24px">暂无数据</td></tr>';
1090
+ if (moreHint) {
1091
+ moreHint.style.display = "none";
1092
+ }
934
1093
  return;
935
1094
  }
936
1095
 
937
- el.innerHTML = allUsers
938
- .map((u) => {
1096
+ el.innerHTML = currentTargetUsers
1097
+ .map((u, i) => {
939
1098
  const nick = (u.nickname || "")
940
1099
  .replace(/</g, "&lt;")
941
1100
  .replace(/>/g, "&gt;");
942
1101
  const fans = u.followerCount != null ? formatNum(u.followerCount) : "-";
943
1102
  const videos = u.videoCount != null ? u.videoCount : "-";
944
- const location = u.locationCreated || "-";
1103
+ const userLocation = u.locationCreated || "-";
1104
+ const confirmedLocation = u.confirmedLocation
1105
+ ? u.confirmedLocation === u.locationCreated
1106
+ ? `<span style="color:#22c55e">${u.confirmedLocation} ✓</span>`
1107
+ : `<span style="color:#ef4444">${u.confirmedLocation} ✗</span>`
1108
+ : "-";
945
1109
  const latestVideo = u.latestVideoTime
946
1110
  ? formatTime(u.latestVideoTime * 1000)
947
1111
  : "-";
948
1112
  const refreshTime = u.refreshTime ? formatTime(u.refreshTime) : "-";
949
- const sources = (u.sources || []).join(", ");
1113
+
1114
+ const editIcon = ' <span style="font-size:10px;opacity:0.5">✏️</span>';
1115
+ const locationCell = u.modifiedAt
1116
+ ? `<td data-label="国家" class="location-cell modified" onclick="openLocationModal('${u.uniqueId}','${userLocation}')" title="点击修改国家(已修改: ${formatTime(u.modifiedAt)})">${userLocation}${editIcon}</td>`
1117
+ : `<td data-label="国家" class="location-cell" onclick="openLocationModal('${u.uniqueId}','${userLocation}')" title="点击修改国家">${userLocation}${editIcon}</td>`;
950
1118
 
951
1119
  let statusTag = "";
952
1120
  if (u.status === "done")
@@ -961,35 +1129,48 @@ function renderTargetTable(countries) {
961
1129
  statusTag = '<span class="tag error">受限</span>';
962
1130
  else statusTag = u.status || "-";
963
1131
 
964
- return `<tr>
1132
+ return `<tr data-user="${u.uniqueId}">
1133
+ <td style="color:#9ca3af;font-size:12px;text-align:center" data-label="#">${i + 1}</td>
965
1134
  <td class="user-id" data-label="用户名">@${u.uniqueId}</td>
966
1135
  <td data-label="昵称">${nick}</td>
967
1136
  <td data-label="粉丝">${fans}</td>
968
1137
  <td data-label="视频">${videos}</td>
969
- <td data-label="国家">${location}</td>
1138
+ ${locationCell}
1139
+ <td data-label="确认国家" style="font-size:11px">${confirmedLocation}</td>
970
1140
  <td data-label="最近发布" style="font-size:11px;color:#888">${latestVideo}</td>
971
1141
  <td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
972
- <td data-label="来源">${sources || "-"}</td>
973
- <td data-label="状态">${statusTag}</td>
974
1142
  </tr>`;
975
1143
  })
976
1144
  .join("");
1145
+
1146
+ if (moreHint) {
1147
+ moreHint.style.display = "";
1148
+ if (currentTargetAllLoaded) {
1149
+ moreHint.innerHTML = `共 <b>${currentTargetTotal}</b> 条,已全部加载`;
1150
+ moreHint.style.color = "#9ca3af";
1151
+ moreHint.style.cursor = "default";
1152
+ } else if (currentTargetLoading) {
1153
+ moreHint.innerHTML = `已加载 <b>${currentTargetUsers.length}</b> / ${currentTargetTotal} 条,加载中...`;
1154
+ moreHint.style.color = "#9ca3af";
1155
+ moreHint.style.cursor = "wait";
1156
+ } else {
1157
+ moreHint.innerHTML = `已加载 <b>${currentTargetUsers.length}</b> / ${currentTargetTotal} 条,<u style="color:#3b82f6">点击此处加载更多</u> ↓`;
1158
+ moreHint.style.color = "#6b7280";
1159
+ moreHint.style.cursor = "pointer";
1160
+ }
1161
+ }
1162
+ updateLoadMoreButton();
977
1163
  }
978
1164
 
979
1165
  function filterTargetByCountry(country) {
980
1166
  currentTargetLocation = country;
981
1167
  const sel = document.getElementById("targetLocationFilter");
982
1168
  if (sel) sel.value = country;
983
- if (currentTargetData) {
984
- renderTargetTable(currentTargetData.countries || []);
985
- }
986
- }
987
-
988
- function onTargetLocationChange() {
989
- const sel = document.getElementById("targetLocationFilter");
990
- currentTargetLocation = sel.value;
991
- if (currentTargetData) {
992
- renderTargetTable(currentTargetData.countries || []);
1169
+ const btn = document.getElementById("targetReprocessBtn");
1170
+ if (btn) btn.style.display = "";
1171
+ if (currentTargetSummary) {
1172
+ renderTargetCountryGrid(currentTargetSummary.countries || []);
1173
+ loadTargetUsersPage();
993
1174
  }
994
1175
  }
995
1176
 
@@ -999,8 +1180,60 @@ function clearTargetFilters() {
999
1180
  const locationFilter = document.getElementById("targetLocationFilter");
1000
1181
  if (searchInput) searchInput.value = "";
1001
1182
  if (locationFilter) locationFilter.value = "";
1002
- if (currentTargetData) {
1003
- renderTargetTable(currentTargetData.countries || []);
1183
+ const btn = document.getElementById("targetReprocessBtn");
1184
+ if (btn) btn.style.display = "none";
1185
+ if (currentTargetSummary) {
1186
+ renderTargetCountryGrid(currentTargetSummary.countries || []);
1187
+ loadTargetUsersPage();
1188
+ }
1189
+ }
1190
+
1191
+ async function reprocessTargetUsers() {
1192
+ if (!currentTargetLocation) return;
1193
+ const country = currentTargetLocation;
1194
+ const search = document.getElementById("targetSearchInput")
1195
+ ? document.getElementById("targetSearchInput").value.trim()
1196
+ : "";
1197
+
1198
+ // 加载该国家全量用户用于重置
1199
+ showLoading("正在加载用户列表...");
1200
+ try {
1201
+ let url = `/api/target-users-by-country?limit=99999&offset=0&country=${encodeURIComponent(country)}`;
1202
+ if (search) url += `&search=${encodeURIComponent(search)}`;
1203
+ const res = await fetch(url);
1204
+ const data = await res.json();
1205
+ const users = data.users || [];
1206
+
1207
+ if (users.length === 0) {
1208
+ showToast("当前筛选结果为空", true);
1209
+ return;
1210
+ }
1211
+ if (
1212
+ !confirm(
1213
+ `确定要重新处理 ${country} 的 ${users.length} 个目标商家吗?\n这将重置它们的任务状态,使客户端重新采集。`,
1214
+ )
1215
+ ) {
1216
+ hideLoading();
1217
+ return;
1218
+ }
1219
+ showLoading("正在批量重置...");
1220
+ const userIds = users.map((u) => u.uniqueId);
1221
+ const resetRes = await fetch("/api/jobs/batch-reset", {
1222
+ method: "POST",
1223
+ headers: { "Content-Type": "application/json" },
1224
+ body: JSON.stringify({ userIds }),
1225
+ });
1226
+ const resetData = await resetRes.json();
1227
+ if (resetData.error) {
1228
+ showToast(resetData.error, true);
1229
+ return;
1230
+ }
1231
+ showToast(`已重置 ${resetData.reset} / ${resetData.total} 个用户`);
1232
+ fetchTargetByCountry();
1233
+ } catch (e) {
1234
+ showToast("批量重置失败: " + e.message, true);
1235
+ } finally {
1236
+ hideLoading();
1004
1237
  }
1005
1238
  }
1006
1239
 
@@ -1009,8 +1242,8 @@ let targetSearchTimer = null;
1009
1242
  document.getElementById("targetSearchInput").addEventListener("input", () => {
1010
1243
  if (targetSearchTimer) clearTimeout(targetSearchTimer);
1011
1244
  targetSearchTimer = setTimeout(() => {
1012
- if (currentTargetData) {
1013
- renderTargetTable(currentTargetData.countries || []);
1245
+ if (currentTargetSummary) {
1246
+ loadTargetUsersPage();
1014
1247
  }
1015
1248
  }, 300);
1016
1249
  });
@@ -1039,6 +1272,29 @@ document
1039
1272
  }
1040
1273
  });
1041
1274
 
1275
+ document
1276
+ .getElementById("refreshTargetBtn")
1277
+ .addEventListener("click", async () => {
1278
+ showLoading("正在刷新...");
1279
+ try {
1280
+ currentTargetLocation = "";
1281
+ const searchInput = document.getElementById("targetSearchInput");
1282
+ const locationFilter = document.getElementById("targetLocationFilter");
1283
+ if (searchInput) searchInput.value = "";
1284
+ if (locationFilter) locationFilter.value = "";
1285
+ const btn = document.getElementById("targetReprocessBtn");
1286
+ if (btn) btn.style.display = "none";
1287
+ await fetchTargetByCountry();
1288
+ showToast("刷新完成");
1289
+ } catch (e) {
1290
+ showToast("刷新失败: " + e.message, true);
1291
+ } finally {
1292
+ hideLoading();
1293
+ }
1294
+ });
1295
+
1296
+ // 加载更多按钮由 HTML 中 onclick="loadTargetUsersPage(true)" 直接触发
1297
+
1042
1298
  async function fetchPendingByCountry() {
1043
1299
  try {
1044
1300
  const res = await fetch("/api/pending-by-country");
@@ -1356,6 +1612,11 @@ function renderRawJobsTable(users) {
1356
1612
  const fans = u.followerCount != null ? formatNum(u.followerCount) : "-";
1357
1613
  const videos = u.videoCount != null ? u.videoCount : "-";
1358
1614
  const loc = u.locationCreated || "-";
1615
+ const confirmedLocRaw = u.confirmedLocation
1616
+ ? u.confirmedLocation === u.locationCreated
1617
+ ? `<span style="color:#22c55e">${u.confirmedLocation} ✓</span>`
1618
+ : `<span style="color:#ef4444">${u.confirmedLocation} ✗</span>`
1619
+ : "-";
1359
1620
  const guessedLoc = u.guessedLocation || "-";
1360
1621
  const sources = (u.sources || []).join(", ");
1361
1622
  const created = u.createdAt ? formatTime(u.createdAt) : "-";
@@ -1366,6 +1627,7 @@ function renderRawJobsTable(users) {
1366
1627
  <td data-label="粉丝">${fans}</td>
1367
1628
  <td data-label="视频">${videos}</td>
1368
1629
  <td data-label="国家">${loc}</td>
1630
+ <td data-label="确认国家" style="font-size:11px">${confirmedLocRaw}</td>
1369
1631
  <td data-label="猜测国家">${guessedLoc}</td>
1370
1632
  <td data-label="来源">${sources || "-"}</td>
1371
1633
  <td data-label="状态">${statusTag}</td>
@@ -1576,6 +1838,7 @@ fetchStats();
1576
1838
  fetchUsers();
1577
1839
  fetchClientErrors();
1578
1840
  initTableSorting();
1841
+
1579
1842
  setInterval(fetchStats, 10000);
1580
1843
  setInterval(fetchUsers, 10000);
1581
1844
  setInterval(fetchClientErrors, 10000);
@@ -120,6 +120,7 @@
120
120
  <table>
121
121
  <thead>
122
122
  <tr>
123
+ <th style="width:40px;text-align:right;color:#666">#</th>
123
124
  <th>用户名</th>
124
125
  <th>昵称</th>
125
126
  <th>粉丝</th>
@@ -295,6 +296,9 @@
295
296
  <div class="stat-card clickable" id="exportTargetCsvBtn" style="background:rgba(34,197,94,0.12);cursor:pointer">
296
297
  <div class="label">导出 CSV</div>
297
298
  </div>
299
+ <div class="stat-card clickable" id="refreshTargetBtn" style="background:rgba(59,130,246,0.12);cursor:pointer">
300
+ <div class="label">🔄 刷新</div>
301
+ </div>
298
302
  </div>
299
303
  <div class="target-page-layout">
300
304
  <div class="target-side-card">
@@ -308,29 +312,29 @@
308
312
  <h3>目标商家列表</h3>
309
313
  <div class="controls">
310
314
  <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
315
  <button onclick="clearTargetFilters()">清空筛选</button>
316
+ <button id="targetReprocessBtn" onclick="reprocessTargetUsers()" style="display:none">重新处理</button>
316
317
  </div>
317
318
  <div class="table-scroll">
318
319
  <table>
319
320
  <thead>
320
321
  <tr>
322
+ <th style="width:48px">#</th>
321
323
  <th>用户名</th>
322
324
  <th>昵称</th>
323
325
  <th>粉丝</th>
324
326
  <th>视频</th>
325
327
  <th>国家</th>
328
+ <th>确认国家</th>
326
329
  <th>最近发布</th>
327
330
  <th>最近刷新</th>
328
- <th>来源</th>
329
- <th>状态</th>
330
331
  </tr>
331
332
  </thead>
332
333
  <tbody id="targetTable"></tbody>
333
334
  </table>
335
+ <div id="targetMoreHint" onclick="loadTargetUsersPage(true)"
336
+ style="text-align:center;padding:10px 8px;font-size:13px;color:#6b7280;cursor:default;user-select:none;transition:color 0.2s">
337
+ </div>
334
338
  </div>
335
339
  </div>
336
340
  </div>