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.
- package/package.json +1 -1
- package/src/cli/attach.js +8 -4
- package/src/cli/refresh.js +16 -4
- package/src/lib/parse-ssr.mjs +1 -0
- package/src/scraper/explore-core.js +36 -19
- package/src/watch/data-store.js +390 -155
- package/src/watch/public/app.js +325 -62
- package/src/watch/public/index.html +10 -6
- package/src/watch/public/style.css +88 -1
- package/src/watch/server.js +73 -3
package/src/watch/public/app.js
CHANGED
|
@@ -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", "
|
|
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
|
|
452
|
-
currentLocation =
|
|
453
|
-
fetchUsers();
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
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
|
-
|
|
967
|
+
currentTargetSummary = data;
|
|
857
968
|
renderTargetCountryGrid(data.countries || []);
|
|
858
|
-
|
|
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
|
|
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">${
|
|
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
|
|
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
|
|
911
|
-
|
|
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()
|
|
1031
|
+
? document.getElementById("targetSearchInput").value.trim()
|
|
914
1032
|
: "";
|
|
915
|
-
const
|
|
1033
|
+
const offset = append ? currentTargetUsers.length : 0;
|
|
916
1034
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
allUsers = allUsers.concat(c.users);
|
|
1035
|
+
// 非追加模式(重置加载)时显示 loading,除非外层已经显示了
|
|
1036
|
+
if (!append && !skipLoading) {
|
|
1037
|
+
showLoading("正在加载数据...");
|
|
921
1038
|
}
|
|
922
1039
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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 (
|
|
1087
|
+
if (currentTargetUsers.length === 0) {
|
|
932
1088
|
el.innerHTML =
|
|
933
|
-
'<tr><td colspan="
|
|
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 =
|
|
938
|
-
.map((u) => {
|
|
1096
|
+
el.innerHTML = currentTargetUsers
|
|
1097
|
+
.map((u, i) => {
|
|
939
1098
|
const nick = (u.nickname || "")
|
|
940
1099
|
.replace(/</g, "<")
|
|
941
1100
|
.replace(/>/g, ">");
|
|
942
1101
|
const fans = u.followerCount != null ? formatNum(u.followerCount) : "-";
|
|
943
1102
|
const videos = u.videoCount != null ? u.videoCount : "-";
|
|
944
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
984
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
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 (
|
|
1013
|
-
|
|
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>
|