tt-help-cli-ycl 1.3.72 → 1.3.74
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 +5 -3
- package/src/cli/open.js +51 -3
- package/src/lib/args.js +4 -0
- package/src/lib/parse-ssr.mjs +1 -0
- package/src/watch/data-store.js +346 -155
- package/src/watch/public/app.js +194 -126
- package/src/watch/public/index.html +7 -0
- package/src/watch/server.js +50 -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
|
|
|
@@ -434,7 +436,7 @@ function setFilter(f) {
|
|
|
434
436
|
targetLocSel.style.display = "none";
|
|
435
437
|
currentTargetLocation = "";
|
|
436
438
|
}
|
|
437
|
-
fetchUsers();
|
|
439
|
+
fetchUsers(true);
|
|
438
440
|
}
|
|
439
441
|
|
|
440
442
|
function renderLocationFilter() {
|
|
@@ -454,15 +456,20 @@ function renderLocationFilter() {
|
|
|
454
456
|
}
|
|
455
457
|
|
|
456
458
|
function onLocationChange() {
|
|
457
|
-
const
|
|
458
|
-
currentLocation =
|
|
459
|
-
fetchUsers();
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
+
}
|
|
466
473
|
}
|
|
467
474
|
|
|
468
475
|
let searchTimer = null;
|
|
@@ -591,40 +598,6 @@ async function selectLocation(uniqueId, location) {
|
|
|
591
598
|
}
|
|
592
599
|
showToast(`@${uniqueId} 国家已更新为 ${location}`);
|
|
593
600
|
|
|
594
|
-
// 同步更新内存数据:将用户从旧国家组移到新国家组
|
|
595
|
-
if (currentTargetData && currentTargetData.countries) {
|
|
596
|
-
let oldCountry = null;
|
|
597
|
-
let userObj = null;
|
|
598
|
-
for (const country of currentTargetData.countries) {
|
|
599
|
-
const idx = country.users.findIndex((u) => u.uniqueId === uniqueId);
|
|
600
|
-
if (idx !== -1) {
|
|
601
|
-
oldCountry = country;
|
|
602
|
-
userObj = country.users.splice(idx, 1)[0];
|
|
603
|
-
break;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (userObj) {
|
|
607
|
-
userObj.locationCreated = location;
|
|
608
|
-
userObj.modifiedAt = Date.now();
|
|
609
|
-
// 找到或创建新国家组
|
|
610
|
-
let newCountry = currentTargetData.countries.find(
|
|
611
|
-
(c) => c.country === location,
|
|
612
|
-
);
|
|
613
|
-
if (!newCountry) {
|
|
614
|
-
newCountry = { country: location, count: 0, users: [] };
|
|
615
|
-
currentTargetData.countries.push(newCountry);
|
|
616
|
-
}
|
|
617
|
-
newCountry.users.push(userObj);
|
|
618
|
-
newCountry.count = newCountry.users.length;
|
|
619
|
-
// 更新旧国家组的 count
|
|
620
|
-
if (oldCountry) oldCountry.count = oldCountry.users.length;
|
|
621
|
-
// 重新渲染左侧国家列表和表格
|
|
622
|
-
renderTargetCountryGrid(currentTargetData.countries);
|
|
623
|
-
renderTargetLocationFilter(currentTargetData.countries);
|
|
624
|
-
renderTargetTable(currentTargetData.countries);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
601
|
// 只更新当前行 DOM
|
|
629
602
|
const tr = document.querySelector(`tr[data-user="${uniqueId}"]`);
|
|
630
603
|
if (tr) {
|
|
@@ -634,8 +607,16 @@ async function selectLocation(uniqueId, location) {
|
|
|
634
607
|
locCell.className = "location-cell modified";
|
|
635
608
|
locCell.innerHTML = `${location}${editIcon}`;
|
|
636
609
|
locCell.title = `点击修改国家(已修改: ${formatTime(Date.now())})`;
|
|
610
|
+
locCell.onclick = () => openLocationModal(uniqueId, location);
|
|
637
611
|
}
|
|
638
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
|
+
}
|
|
639
620
|
} catch (e) {
|
|
640
621
|
showToast("更新失败: " + e.message, true);
|
|
641
622
|
} finally {
|
|
@@ -960,6 +941,7 @@ function showTargetPage() {
|
|
|
960
941
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
961
942
|
document.getElementById("rawPage").classList.remove("active");
|
|
962
943
|
document.getElementById("targetPage").classList.add("active");
|
|
944
|
+
showLoading("正在加载目标商家数据...");
|
|
963
945
|
fetchTargetByCountry();
|
|
964
946
|
// 同步统计
|
|
965
947
|
if (currentStats) {
|
|
@@ -970,16 +952,21 @@ function showTargetPage() {
|
|
|
970
952
|
}
|
|
971
953
|
}
|
|
972
954
|
|
|
973
|
-
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;
|
|
974
962
|
|
|
975
963
|
async function fetchTargetByCountry() {
|
|
976
964
|
try {
|
|
977
|
-
const res = await fetch("/api/target-users-by-country");
|
|
965
|
+
const res = await fetch("/api/target-users-by-country?summary=1");
|
|
978
966
|
const data = await res.json();
|
|
979
|
-
|
|
967
|
+
currentTargetSummary = data;
|
|
980
968
|
renderTargetCountryGrid(data.countries || []);
|
|
981
|
-
|
|
982
|
-
renderTargetTable(data.countries || []);
|
|
969
|
+
renderTargetPageLocationFilter(data.countries || []);
|
|
983
970
|
document.getElementById("targetPageStatTotal").textContent = formatStatNum(
|
|
984
971
|
data.total || 0,
|
|
985
972
|
{ full: true },
|
|
@@ -987,8 +974,11 @@ async function fetchTargetByCountry() {
|
|
|
987
974
|
document.getElementById("targetPageStatCountries").textContent = (
|
|
988
975
|
data.countries || []
|
|
989
976
|
).length;
|
|
977
|
+
await loadTargetUsersPage(false, true);
|
|
990
978
|
} catch (e) {
|
|
991
979
|
console.error("获取目标商家数据失败:", e);
|
|
980
|
+
} finally {
|
|
981
|
+
hideLoading();
|
|
992
982
|
}
|
|
993
983
|
}
|
|
994
984
|
|
|
@@ -999,17 +989,18 @@ function renderTargetCountryGrid(countries) {
|
|
|
999
989
|
'<span style="color:#666;font-size:12px">暂无目标商家</span>';
|
|
1000
990
|
return;
|
|
1001
991
|
}
|
|
1002
|
-
const total = countries.reduce((sum, c) => sum + c.count, 0);
|
|
992
|
+
const total = countries.reduce((sum, c) => sum + (c.count || 0), 0);
|
|
1003
993
|
grid.innerHTML = countries
|
|
1004
994
|
.map((c) => {
|
|
1005
|
-
const
|
|
995
|
+
const cnt = c.count || 0;
|
|
996
|
+
const pct = total > 0 ? ((cnt / total) * 100).toFixed(1) : "0.0";
|
|
1006
997
|
const safeCountry = escapeJsString(c.country);
|
|
1007
998
|
const sel = currentTargetLocation === c.country ? " selected" : "";
|
|
1008
999
|
return `
|
|
1009
1000
|
<div class="pending-country-item has-target${sel}"
|
|
1010
1001
|
onclick="filterTargetByCountry('${safeCountry}')">
|
|
1011
1002
|
<div class="country-name">${c.country}</div>
|
|
1012
|
-
<div class="country-count">${
|
|
1003
|
+
<div class="country-count">${cnt}</div>
|
|
1013
1004
|
<div class="country-label">${pct}% 目标商家</div>
|
|
1014
1005
|
</div>
|
|
1015
1006
|
`;
|
|
@@ -1017,7 +1008,7 @@ function renderTargetCountryGrid(countries) {
|
|
|
1017
1008
|
.join("");
|
|
1018
1009
|
}
|
|
1019
1010
|
|
|
1020
|
-
function
|
|
1011
|
+
function renderTargetPageLocationFilter(countries) {
|
|
1021
1012
|
const sel = document.getElementById("targetLocationFilter");
|
|
1022
1013
|
if (!sel) return;
|
|
1023
1014
|
const val = sel.value;
|
|
@@ -1026,46 +1017,90 @@ function renderTargetLocationFilter(countries) {
|
|
|
1026
1017
|
countries
|
|
1027
1018
|
.map(
|
|
1028
1019
|
(c) =>
|
|
1029
|
-
`<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>`,
|
|
1030
1021
|
)
|
|
1031
1022
|
.join("");
|
|
1032
1023
|
}
|
|
1033
1024
|
|
|
1034
|
-
function
|
|
1035
|
-
|
|
1025
|
+
async function loadTargetUsersPage(append = false, skipLoading = false) {
|
|
1026
|
+
if (currentTargetLoading) return;
|
|
1027
|
+
currentTargetLoading = true;
|
|
1028
|
+
|
|
1029
|
+
const seq = ++currentTargetSeq;
|
|
1036
1030
|
const search = document.getElementById("targetSearchInput")
|
|
1037
|
-
? document.getElementById("targetSearchInput").value.trim()
|
|
1031
|
+
? document.getElementById("targetSearchInput").value.trim()
|
|
1038
1032
|
: "";
|
|
1039
|
-
const
|
|
1033
|
+
const offset = append ? currentTargetUsers.length : 0;
|
|
1040
1034
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
allUsers = allUsers.concat(c.users);
|
|
1035
|
+
// 非追加模式(重置加载)时显示 loading,除非外层已经显示了
|
|
1036
|
+
if (!append && !skipLoading) {
|
|
1037
|
+
showLoading("正在加载数据...");
|
|
1045
1038
|
}
|
|
1046
1039
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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();
|
|
1053
1076
|
}
|
|
1077
|
+
}
|
|
1054
1078
|
|
|
1055
|
-
|
|
1079
|
+
function updateLoadMoreButton() {
|
|
1080
|
+
// 按钮已移除,保留函数名以兼容旧调用
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function renderTargetTable() {
|
|
1084
|
+
const el = document.getElementById("targetTable");
|
|
1085
|
+
const moreHint = document.getElementById("targetMoreHint");
|
|
1086
|
+
|
|
1087
|
+
if (currentTargetUsers.length === 0) {
|
|
1056
1088
|
el.innerHTML =
|
|
1057
|
-
'<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
|
+
}
|
|
1058
1093
|
return;
|
|
1059
1094
|
}
|
|
1060
1095
|
|
|
1061
|
-
el.innerHTML =
|
|
1062
|
-
.map((u) => {
|
|
1096
|
+
el.innerHTML = currentTargetUsers
|
|
1097
|
+
.map((u, i) => {
|
|
1063
1098
|
const nick = (u.nickname || "")
|
|
1064
1099
|
.replace(/</g, "<")
|
|
1065
1100
|
.replace(/>/g, ">");
|
|
1066
1101
|
const fans = u.followerCount != null ? formatNum(u.followerCount) : "-";
|
|
1067
1102
|
const videos = u.videoCount != null ? u.videoCount : "-";
|
|
1068
|
-
const
|
|
1103
|
+
const userLocation = u.locationCreated || "-";
|
|
1069
1104
|
const confirmedLocation = u.confirmedLocation
|
|
1070
1105
|
? u.confirmedLocation === u.locationCreated
|
|
1071
1106
|
? `<span style="color:#22c55e">${u.confirmedLocation} ✓</span>`
|
|
@@ -1075,13 +1110,11 @@ function renderTargetTable(countries) {
|
|
|
1075
1110
|
? formatTime(u.latestVideoTime * 1000)
|
|
1076
1111
|
: "-";
|
|
1077
1112
|
const refreshTime = u.refreshTime ? formatTime(u.refreshTime) : "-";
|
|
1078
|
-
const sources = (u.sources || []).join(", ");
|
|
1079
1113
|
|
|
1080
|
-
// 修改过的国家用特殊颜色,带编辑图标
|
|
1081
1114
|
const editIcon = ' <span style="font-size:10px;opacity:0.5">✏️</span>';
|
|
1082
1115
|
const locationCell = u.modifiedAt
|
|
1083
|
-
? `<td data-label="国家" class="location-cell modified" onclick="openLocationModal('${u.uniqueId}','${
|
|
1084
|
-
: `<td data-label="国家" class="location-cell" onclick="openLocationModal('${u.uniqueId}','${
|
|
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>`;
|
|
1085
1118
|
|
|
1086
1119
|
let statusTag = "";
|
|
1087
1120
|
if (u.status === "done")
|
|
@@ -1097,6 +1130,7 @@ function renderTargetTable(countries) {
|
|
|
1097
1130
|
else statusTag = u.status || "-";
|
|
1098
1131
|
|
|
1099
1132
|
return `<tr data-user="${u.uniqueId}">
|
|
1133
|
+
<td style="color:#9ca3af;font-size:12px;text-align:center" data-label="#">${i + 1}</td>
|
|
1100
1134
|
<td class="user-id" data-label="用户名">@${u.uniqueId}</td>
|
|
1101
1135
|
<td data-label="昵称">${nick}</td>
|
|
1102
1136
|
<td data-label="粉丝">${fans}</td>
|
|
@@ -1108,6 +1142,24 @@ function renderTargetTable(countries) {
|
|
|
1108
1142
|
</tr>`;
|
|
1109
1143
|
})
|
|
1110
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();
|
|
1111
1163
|
}
|
|
1112
1164
|
|
|
1113
1165
|
function filterTargetByCountry(country) {
|
|
@@ -1116,18 +1168,9 @@ function filterTargetByCountry(country) {
|
|
|
1116
1168
|
if (sel) sel.value = country;
|
|
1117
1169
|
const btn = document.getElementById("targetReprocessBtn");
|
|
1118
1170
|
if (btn) btn.style.display = "";
|
|
1119
|
-
if (
|
|
1120
|
-
renderTargetCountryGrid(
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function onTargetLocationChange() {
|
|
1126
|
-
const sel = document.getElementById("targetLocationFilter");
|
|
1127
|
-
currentTargetLocation = sel.value;
|
|
1128
|
-
if (currentTargetData) {
|
|
1129
|
-
renderTargetCountryGrid(currentTargetData.countries || []);
|
|
1130
|
-
renderTargetTable(currentTargetData.countries || []);
|
|
1171
|
+
if (currentTargetSummary) {
|
|
1172
|
+
renderTargetCountryGrid(currentTargetSummary.countries || []);
|
|
1173
|
+
loadTargetUsersPage();
|
|
1131
1174
|
}
|
|
1132
1175
|
}
|
|
1133
1176
|
|
|
@@ -1139,53 +1182,54 @@ function clearTargetFilters() {
|
|
|
1139
1182
|
if (locationFilter) locationFilter.value = "";
|
|
1140
1183
|
const btn = document.getElementById("targetReprocessBtn");
|
|
1141
1184
|
if (btn) btn.style.display = "none";
|
|
1142
|
-
if (
|
|
1143
|
-
renderTargetCountryGrid(
|
|
1144
|
-
|
|
1185
|
+
if (currentTargetSummary) {
|
|
1186
|
+
renderTargetCountryGrid(currentTargetSummary.countries || []);
|
|
1187
|
+
loadTargetUsersPage();
|
|
1145
1188
|
}
|
|
1146
1189
|
}
|
|
1147
1190
|
|
|
1148
1191
|
async function reprocessTargetUsers() {
|
|
1149
|
-
if (!currentTargetLocation
|
|
1192
|
+
if (!currentTargetLocation) return;
|
|
1150
1193
|
const country = currentTargetLocation;
|
|
1151
|
-
const countryData = currentTargetData.countries.find((c) => c.country === country);
|
|
1152
|
-
const users = countryData ? countryData.users : [];
|
|
1153
1194
|
const search = document.getElementById("targetSearchInput")
|
|
1154
|
-
? document.getElementById("targetSearchInput").value.trim()
|
|
1195
|
+
? document.getElementById("targetSearchInput").value.trim()
|
|
1155
1196
|
: "";
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
(u.uniqueId || "").toLowerCase().includes(search) ||
|
|
1160
|
-
(u.nickname || "").toLowerCase().includes(search),
|
|
1161
|
-
)
|
|
1162
|
-
: users;
|
|
1163
|
-
if (filtered.length === 0) {
|
|
1164
|
-
showToast("当前筛选结果为空", true);
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
if (
|
|
1168
|
-
!confirm(
|
|
1169
|
-
`确定要重新处理 ${country} 的 ${filtered.length} 个目标商家吗?\n这将重置它们的任务状态,使客户端重新采集。`,
|
|
1170
|
-
)
|
|
1171
|
-
) {
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
const userIds = filtered.map((u) => u.uniqueId);
|
|
1175
|
-
showLoading("正在批量重置...");
|
|
1197
|
+
|
|
1198
|
+
// 加载该国家全量用户用于重置
|
|
1199
|
+
showLoading("正在加载用户列表...");
|
|
1176
1200
|
try {
|
|
1177
|
-
|
|
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", {
|
|
1178
1222
|
method: "POST",
|
|
1179
1223
|
headers: { "Content-Type": "application/json" },
|
|
1180
1224
|
body: JSON.stringify({ userIds }),
|
|
1181
1225
|
});
|
|
1182
|
-
const
|
|
1183
|
-
if (
|
|
1184
|
-
showToast(
|
|
1226
|
+
const resetData = await resetRes.json();
|
|
1227
|
+
if (resetData.error) {
|
|
1228
|
+
showToast(resetData.error, true);
|
|
1185
1229
|
return;
|
|
1186
1230
|
}
|
|
1187
|
-
showToast(`已重置 ${
|
|
1188
|
-
|
|
1231
|
+
showToast(`已重置 ${resetData.reset} / ${resetData.total} 个用户`);
|
|
1232
|
+
fetchTargetByCountry();
|
|
1189
1233
|
} catch (e) {
|
|
1190
1234
|
showToast("批量重置失败: " + e.message, true);
|
|
1191
1235
|
} finally {
|
|
@@ -1198,8 +1242,8 @@ let targetSearchTimer = null;
|
|
|
1198
1242
|
document.getElementById("targetSearchInput").addEventListener("input", () => {
|
|
1199
1243
|
if (targetSearchTimer) clearTimeout(targetSearchTimer);
|
|
1200
1244
|
targetSearchTimer = setTimeout(() => {
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1245
|
+
if (currentTargetSummary) {
|
|
1246
|
+
loadTargetUsersPage();
|
|
1203
1247
|
}
|
|
1204
1248
|
}, 300);
|
|
1205
1249
|
});
|
|
@@ -1228,6 +1272,29 @@ document
|
|
|
1228
1272
|
}
|
|
1229
1273
|
});
|
|
1230
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
|
+
|
|
1231
1298
|
async function fetchPendingByCountry() {
|
|
1232
1299
|
try {
|
|
1233
1300
|
const res = await fetch("/api/pending-by-country");
|
|
@@ -1771,6 +1838,7 @@ fetchStats();
|
|
|
1771
1838
|
fetchUsers();
|
|
1772
1839
|
fetchClientErrors();
|
|
1773
1840
|
initTableSorting();
|
|
1841
|
+
|
|
1774
1842
|
setInterval(fetchStats, 10000);
|
|
1775
1843
|
setInterval(fetchUsers, 10000);
|
|
1776
1844
|
setInterval(fetchClientErrors, 10000);
|
|
@@ -296,6 +296,9 @@
|
|
|
296
296
|
<div class="stat-card clickable" id="exportTargetCsvBtn" style="background:rgba(34,197,94,0.12);cursor:pointer">
|
|
297
297
|
<div class="label">导出 CSV</div>
|
|
298
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>
|
|
299
302
|
</div>
|
|
300
303
|
<div class="target-page-layout">
|
|
301
304
|
<div class="target-side-card">
|
|
@@ -316,6 +319,7 @@
|
|
|
316
319
|
<table>
|
|
317
320
|
<thead>
|
|
318
321
|
<tr>
|
|
322
|
+
<th style="width:48px">#</th>
|
|
319
323
|
<th>用户名</th>
|
|
320
324
|
<th>昵称</th>
|
|
321
325
|
<th>粉丝</th>
|
|
@@ -328,6 +332,9 @@
|
|
|
328
332
|
</thead>
|
|
329
333
|
<tbody id="targetTable"></tbody>
|
|
330
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>
|
|
331
338
|
</div>
|
|
332
339
|
</div>
|
|
333
340
|
</div>
|
package/src/watch/server.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
|
@@ -553,8 +569,21 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
553
569
|
req.method === "GET" &&
|
|
554
570
|
routePath === "/api/target-users-by-country"
|
|
555
571
|
) {
|
|
556
|
-
|
|
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 导出:全量数据
|
|
557
583
|
if (req.headers["accept"]?.includes("text/csv")) {
|
|
584
|
+
const result = store.getTargetUsersByCountry(
|
|
585
|
+
DEFAULT_TARGET_LOCATIONS,
|
|
586
|
+
);
|
|
558
587
|
const columns = [
|
|
559
588
|
"uniqueId",
|
|
560
589
|
"nickname",
|
|
@@ -603,9 +632,27 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
603
632
|
columns.map((c) => csvEscape(r[c])).join(","),
|
|
604
633
|
);
|
|
605
634
|
res.end(BOM + [header, ...lines].join("\r\n"));
|
|
606
|
-
|
|
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
|
+
);
|
|
607
649
|
sendJSON(res, 200, result);
|
|
650
|
+
return;
|
|
608
651
|
}
|
|
652
|
+
|
|
653
|
+
// 默认:全量(兼容旧调用)
|
|
654
|
+
const result = store.getTargetUsersByCountry(DEFAULT_TARGET_LOCATIONS);
|
|
655
|
+
sendJSON(res, 200, result);
|
|
609
656
|
return;
|
|
610
657
|
}
|
|
611
658
|
|