tt-help-cli-ycl 1.3.62 → 1.3.64
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/scripts/run-explore.bat +1 -1
- package/src/cli/explore.js +8 -4
- package/src/cli/refresh.js +437 -163
- package/src/lib/args.js +54 -8
- package/src/lib/browser/cdp.js +11 -5
- package/src/lib/browser/page.js +41 -0
- package/src/lib/constants.js +31 -9
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/data-store.js +145 -9
- package/src/watch/public/app.js +296 -42
- package/src/watch/public/index.html +63 -0
- package/src/watch/public/style.css +80 -3
- package/src/watch/server.js +91 -2
- package/src/scraper/refresh-core.js +0 -213
package/src/watch/public/app.js
CHANGED
|
@@ -368,6 +368,10 @@ function renderTable(users) {
|
|
|
368
368
|
}
|
|
369
369
|
const claimTime = u.claimedAt ? formatTime(u.claimedAt) : "-";
|
|
370
370
|
const procTime = u.processedAt ? formatTime(u.processedAt) : "-";
|
|
371
|
+
const refreshTime =
|
|
372
|
+
u.ttSeller && !u.verified && u.refreshTime
|
|
373
|
+
? formatTime(u.refreshTime)
|
|
374
|
+
: "-";
|
|
371
375
|
const statusCodeDisplay =
|
|
372
376
|
u.statusCode != null && u.statusCode !== 0
|
|
373
377
|
? `<span class="tag error" style="font-size:10px">${u.statusCode}</span>`
|
|
@@ -378,6 +382,7 @@ function renderTable(users) {
|
|
|
378
382
|
<td data-label="粉丝">${fans}</td>
|
|
379
383
|
<td data-label="视频">${videos}</td>
|
|
380
384
|
<td data-label="国家">${loc}</td>
|
|
385
|
+
<td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
|
|
381
386
|
<td data-label="最近发布" style="font-size:11px;color:#888">${latestVideo}</td>
|
|
382
387
|
<td data-label="猜测国家">${guessedLoc}</td>
|
|
383
388
|
<td data-label="来源">${sources || "-"}</td>
|
|
@@ -738,44 +743,9 @@ async function batchResetErrors() {
|
|
|
738
743
|
}
|
|
739
744
|
}
|
|
740
745
|
|
|
741
|
-
document
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
showLoading("正在导出目标用户...");
|
|
745
|
-
try {
|
|
746
|
-
const res = await fetch("/api/target-users", {
|
|
747
|
-
headers: { Accept: "text/csv" },
|
|
748
|
-
});
|
|
749
|
-
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
750
|
-
const blob = await res.blob();
|
|
751
|
-
const ext = blob.size < 200 ? "json" : "csv";
|
|
752
|
-
if (ext === "json") {
|
|
753
|
-
const text = await blob.text();
|
|
754
|
-
const data = JSON.parse(text);
|
|
755
|
-
if (!data.users.length) {
|
|
756
|
-
showToast("暂无目标用户", true);
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
760
|
-
const ids = data.users.map((u) => "@" + u.uniqueId).join(", ");
|
|
761
|
-
await navigator.clipboard.writeText(ids);
|
|
762
|
-
showToast(data.users.length + " 个目标用户 ID 已复制到剪贴板");
|
|
763
|
-
}
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
const url = URL.createObjectURL(blob);
|
|
767
|
-
const a = document.createElement("a");
|
|
768
|
-
a.href = url;
|
|
769
|
-
a.download = "target-users.csv";
|
|
770
|
-
a.click();
|
|
771
|
-
URL.revokeObjectURL(url);
|
|
772
|
-
showToast("CSV 文件已开始下载");
|
|
773
|
-
} catch (e) {
|
|
774
|
-
showToast("获取失败: " + e.message, true);
|
|
775
|
-
} finally {
|
|
776
|
-
hideLoading();
|
|
777
|
-
}
|
|
778
|
-
});
|
|
746
|
+
document.getElementById("statTargetCard").addEventListener("click", () => {
|
|
747
|
+
navigateToTarget();
|
|
748
|
+
});
|
|
779
749
|
|
|
780
750
|
// Hash 路由
|
|
781
751
|
window.addEventListener("hashchange", handleRoute);
|
|
@@ -789,6 +759,8 @@ function handleRoute() {
|
|
|
789
759
|
showUserUpdatePage();
|
|
790
760
|
} else if (hash === "#raw") {
|
|
791
761
|
showRawPage();
|
|
762
|
+
} else if (hash === "#target") {
|
|
763
|
+
showTargetPage();
|
|
792
764
|
} else {
|
|
793
765
|
showMainPage();
|
|
794
766
|
}
|
|
@@ -815,6 +787,7 @@ function showPendingPage() {
|
|
|
815
787
|
document.getElementById("pendingPage").classList.add("active");
|
|
816
788
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
817
789
|
document.getElementById("rawPage").classList.remove("active");
|
|
790
|
+
document.getElementById("targetPage").classList.remove("active");
|
|
818
791
|
fetchPendingByCountry();
|
|
819
792
|
fetchAttachStuckByCountry();
|
|
820
793
|
}
|
|
@@ -824,6 +797,7 @@ function showUserUpdatePage() {
|
|
|
824
797
|
document.getElementById("pendingPage").classList.remove("active");
|
|
825
798
|
document.getElementById("userUpdatePage").classList.add("active");
|
|
826
799
|
document.getElementById("rawPage").classList.remove("active");
|
|
800
|
+
document.getElementById("targetPage").classList.remove("active");
|
|
827
801
|
fetchUserUpdateByCountry();
|
|
828
802
|
fetchAttachStuckByCountry();
|
|
829
803
|
}
|
|
@@ -840,6 +814,7 @@ function showRawPage() {
|
|
|
840
814
|
document.getElementById("pendingPage").classList.remove("active");
|
|
841
815
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
842
816
|
document.getElementById("rawPage").classList.add("active");
|
|
817
|
+
document.getElementById("targetPage").classList.remove("active");
|
|
843
818
|
fetchRawByCountry();
|
|
844
819
|
fetchRawJobs();
|
|
845
820
|
}
|
|
@@ -849,8 +824,219 @@ function showMainPage() {
|
|
|
849
824
|
document.getElementById("pendingPage").classList.remove("active");
|
|
850
825
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
851
826
|
document.getElementById("rawPage").classList.remove("active");
|
|
827
|
+
document.getElementById("targetPage").classList.remove("active");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function navigateToTarget() {
|
|
831
|
+
window.location.hash = "#target";
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function showTargetPage() {
|
|
835
|
+
document.getElementById("mainPage").classList.add("hidden");
|
|
836
|
+
document.getElementById("pendingPage").classList.remove("active");
|
|
837
|
+
document.getElementById("userUpdatePage").classList.remove("active");
|
|
838
|
+
document.getElementById("rawPage").classList.remove("active");
|
|
839
|
+
document.getElementById("targetPage").classList.add("active");
|
|
840
|
+
fetchTargetByCountry();
|
|
841
|
+
// 同步统计
|
|
842
|
+
if (currentStats) {
|
|
843
|
+
document.getElementById("targetPageStatTotal").textContent = formatStatNum(
|
|
844
|
+
currentStats.targetUsers || 0,
|
|
845
|
+
{ full: true },
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
let currentTargetData = null;
|
|
851
|
+
|
|
852
|
+
async function fetchTargetByCountry() {
|
|
853
|
+
try {
|
|
854
|
+
const res = await fetch("/api/target-users-by-country");
|
|
855
|
+
const data = await res.json();
|
|
856
|
+
currentTargetData = data;
|
|
857
|
+
renderTargetCountryGrid(data.countries || []);
|
|
858
|
+
renderTargetLocationFilter(data.countries || []);
|
|
859
|
+
renderTargetTable(data.countries || []);
|
|
860
|
+
document.getElementById("targetPageStatTotal").textContent = formatStatNum(
|
|
861
|
+
data.total || 0,
|
|
862
|
+
{ full: true },
|
|
863
|
+
);
|
|
864
|
+
document.getElementById("targetPageStatCountries").textContent = (
|
|
865
|
+
data.countries || []
|
|
866
|
+
).length;
|
|
867
|
+
} catch (e) {
|
|
868
|
+
console.error("获取目标商家数据失败:", e);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function renderTargetCountryGrid(countries) {
|
|
873
|
+
const grid = document.getElementById("targetCountryGrid");
|
|
874
|
+
if (!countries.length) {
|
|
875
|
+
grid.innerHTML =
|
|
876
|
+
'<span style="color:#666;font-size:12px">暂无目标商家</span>';
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const total = countries.reduce((sum, c) => sum + c.count, 0);
|
|
880
|
+
grid.innerHTML = countries
|
|
881
|
+
.map((c) => {
|
|
882
|
+
const pct = ((c.count / total) * 100).toFixed(1);
|
|
883
|
+
const safeCountry = escapeJsString(c.country);
|
|
884
|
+
return `
|
|
885
|
+
<div class="pending-country-item has-target"
|
|
886
|
+
onclick="filterTargetByCountry('${safeCountry}')">
|
|
887
|
+
<div class="country-name">${c.country}</div>
|
|
888
|
+
<div class="country-count">${c.count}</div>
|
|
889
|
+
<div class="country-label">${pct}% 目标商家</div>
|
|
890
|
+
</div>
|
|
891
|
+
`;
|
|
892
|
+
})
|
|
893
|
+
.join("");
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function renderTargetLocationFilter(countries) {
|
|
897
|
+
const sel = document.getElementById("targetLocationFilter");
|
|
898
|
+
if (!sel) return;
|
|
899
|
+
const val = sel.value;
|
|
900
|
+
sel.innerHTML =
|
|
901
|
+
'<option value="">全部国家</option>' +
|
|
902
|
+
countries
|
|
903
|
+
.map(
|
|
904
|
+
(c) =>
|
|
905
|
+
`<option value="${c.country}"${val === c.country ? " selected" : ""}>${c.country} (${c.count})</option>`,
|
|
906
|
+
)
|
|
907
|
+
.join("");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function renderTargetTable(countries) {
|
|
911
|
+
const el = document.getElementById("targetTable");
|
|
912
|
+
const search = document.getElementById("targetSearchInput")
|
|
913
|
+
? document.getElementById("targetSearchInput").value.trim().toLowerCase()
|
|
914
|
+
: "";
|
|
915
|
+
const location = currentTargetLocation;
|
|
916
|
+
|
|
917
|
+
let allUsers = [];
|
|
918
|
+
for (const c of countries) {
|
|
919
|
+
if (location && c.country !== location) continue;
|
|
920
|
+
allUsers = allUsers.concat(c.users);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (search) {
|
|
924
|
+
allUsers = allUsers.filter(
|
|
925
|
+
(u) =>
|
|
926
|
+
(u.uniqueId || "").toLowerCase().includes(search) ||
|
|
927
|
+
(u.nickname || "").toLowerCase().includes(search),
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (!allUsers.length) {
|
|
932
|
+
el.innerHTML =
|
|
933
|
+
'<tr><td colspan="8" style="color:#666;text-align:center;padding:24px">暂无目标商家</td></tr>';
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
el.innerHTML = allUsers
|
|
938
|
+
.map((u) => {
|
|
939
|
+
const nick = (u.nickname || "")
|
|
940
|
+
.replace(/</g, "<")
|
|
941
|
+
.replace(/>/g, ">");
|
|
942
|
+
const fans = u.followerCount != null ? formatNum(u.followerCount) : "-";
|
|
943
|
+
const videos = u.videoCount != null ? u.videoCount : "-";
|
|
944
|
+
const latestVideo = u.latestVideoTime
|
|
945
|
+
? formatTime(u.latestVideoTime * 1000)
|
|
946
|
+
: "-";
|
|
947
|
+
const refreshTime = u.refreshTime ? formatTime(u.refreshTime) : "-";
|
|
948
|
+
const sources = (u.sources || []).join(", ");
|
|
949
|
+
|
|
950
|
+
let statusTag = "";
|
|
951
|
+
if (u.status === "done")
|
|
952
|
+
statusTag = '<span class="tag processed">已完成</span>';
|
|
953
|
+
else if (u.status === "processing")
|
|
954
|
+
statusTag = '<span class="tag processing">处理中</span>';
|
|
955
|
+
else if (u.status === "pending")
|
|
956
|
+
statusTag = '<span class="tag pending">待处理</span>';
|
|
957
|
+
else if (u.status === "error")
|
|
958
|
+
statusTag = '<span class="tag error">错误</span>';
|
|
959
|
+
else if (u.status === "restricted")
|
|
960
|
+
statusTag = '<span class="tag error">受限</span>';
|
|
961
|
+
else statusTag = u.status || "-";
|
|
962
|
+
|
|
963
|
+
return `<tr>
|
|
964
|
+
<td class="user-id" data-label="用户名">@${u.uniqueId}</td>
|
|
965
|
+
<td data-label="昵称">${nick}</td>
|
|
966
|
+
<td data-label="粉丝">${fans}</td>
|
|
967
|
+
<td data-label="视频">${videos}</td>
|
|
968
|
+
<td data-label="最近发布" style="font-size:11px;color:#888">${latestVideo}</td>
|
|
969
|
+
<td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
|
|
970
|
+
<td data-label="来源">${sources || "-"}</td>
|
|
971
|
+
<td data-label="状态">${statusTag}</td>
|
|
972
|
+
</tr>`;
|
|
973
|
+
})
|
|
974
|
+
.join("");
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function filterTargetByCountry(country) {
|
|
978
|
+
currentTargetLocation = country;
|
|
979
|
+
const sel = document.getElementById("targetLocationFilter");
|
|
980
|
+
if (sel) sel.value = country;
|
|
981
|
+
if (currentTargetData) {
|
|
982
|
+
renderTargetTable(currentTargetData.countries || []);
|
|
983
|
+
}
|
|
852
984
|
}
|
|
853
985
|
|
|
986
|
+
function onTargetLocationChange() {
|
|
987
|
+
const sel = document.getElementById("targetLocationFilter");
|
|
988
|
+
currentTargetLocation = sel.value;
|
|
989
|
+
if (currentTargetData) {
|
|
990
|
+
renderTargetTable(currentTargetData.countries || []);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function clearTargetFilters() {
|
|
995
|
+
currentTargetLocation = "";
|
|
996
|
+
const searchInput = document.getElementById("targetSearchInput");
|
|
997
|
+
const locationFilter = document.getElementById("targetLocationFilter");
|
|
998
|
+
if (searchInput) searchInput.value = "";
|
|
999
|
+
if (locationFilter) locationFilter.value = "";
|
|
1000
|
+
if (currentTargetData) {
|
|
1001
|
+
renderTargetTable(currentTargetData.countries || []);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
let targetSearchTimer = null;
|
|
1006
|
+
|
|
1007
|
+
document.getElementById("targetSearchInput").addEventListener("input", () => {
|
|
1008
|
+
if (targetSearchTimer) clearTimeout(targetSearchTimer);
|
|
1009
|
+
targetSearchTimer = setTimeout(() => {
|
|
1010
|
+
if (currentTargetData) {
|
|
1011
|
+
renderTargetTable(currentTargetData.countries || []);
|
|
1012
|
+
}
|
|
1013
|
+
}, 300);
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
document
|
|
1017
|
+
.getElementById("exportTargetCsvBtn")
|
|
1018
|
+
.addEventListener("click", async () => {
|
|
1019
|
+
showLoading("正在导出目标用户...");
|
|
1020
|
+
try {
|
|
1021
|
+
const res = await fetch("/api/target-users-by-country", {
|
|
1022
|
+
headers: { Accept: "text/csv" },
|
|
1023
|
+
});
|
|
1024
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
1025
|
+
const blob = await res.blob();
|
|
1026
|
+
const url = URL.createObjectURL(blob);
|
|
1027
|
+
const a = document.createElement("a");
|
|
1028
|
+
a.href = url;
|
|
1029
|
+
a.download = "target-users-by-country.csv";
|
|
1030
|
+
a.click();
|
|
1031
|
+
URL.revokeObjectURL(url);
|
|
1032
|
+
showToast("CSV 文件已开始下载");
|
|
1033
|
+
} catch (e) {
|
|
1034
|
+
showToast("导出失败: " + e.message, true);
|
|
1035
|
+
} finally {
|
|
1036
|
+
hideLoading();
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
854
1040
|
async function fetchPendingByCountry() {
|
|
855
1041
|
try {
|
|
856
1042
|
const res = await fetch("/api/pending-by-country");
|
|
@@ -877,7 +1063,10 @@ function renderPendingCountryGrid(countries) {
|
|
|
877
1063
|
return `
|
|
878
1064
|
<div class="pending-country-item${isUnknown ? "" : " has-target"}"
|
|
879
1065
|
onclick="filterByPendingCountry('${safeCountry}')">
|
|
880
|
-
<
|
|
1066
|
+
<div class="country-action-btns">
|
|
1067
|
+
<button class="country-action-btn restore" title="重置为需要预处理" onclick="event.stopPropagation(); resetPendingByCountry('${safeCountry}', ${c.count})">↺</button>
|
|
1068
|
+
<button class="country-action-btn" title="移到毛料库,暂不处理" onclick="event.stopPropagation(); moveCountryJobsToRaw('pending', '${safeCountry}', ${c.count})">✕</button>
|
|
1069
|
+
</div>
|
|
881
1070
|
<div class="country-name">${isUnknown ? "🌍 " : ""}${c.country}</div>
|
|
882
1071
|
<div class="country-count">${c.count}</div>
|
|
883
1072
|
<div class="country-label">${pct}% 待处理</div>
|
|
@@ -931,7 +1120,9 @@ function renderUserUpdateCountryGrid(countries) {
|
|
|
931
1120
|
return `
|
|
932
1121
|
<div class="pending-country-item${isUnknown ? "" : " has-target"}"
|
|
933
1122
|
onclick="filterByUserUpdateCountry('${safeCountry}')">
|
|
934
|
-
<
|
|
1123
|
+
<div class="country-action-btns">
|
|
1124
|
+
<button class="country-action-btn" title="移到毛料库,暂不处理" onclick="event.stopPropagation(); moveCountryJobsToRaw('userUpdate', '${safeCountry}', ${c.count})">✕</button>
|
|
1125
|
+
</div>
|
|
935
1126
|
<div class="country-name">${isUnknown ? "🌍 " : ""}${c.country}</div>
|
|
936
1127
|
<div class="country-count">${c.count}</div>
|
|
937
1128
|
<div class="country-label">${pct}% 待补资料</div>
|
|
@@ -968,7 +1159,9 @@ function renderAttachStuckGrid(gridId, countries) {
|
|
|
968
1159
|
const safeCountry = escapeJsString(c.country);
|
|
969
1160
|
return `
|
|
970
1161
|
<div class="pending-country-item${isUnknown ? "" : " has-target"}">
|
|
971
|
-
<
|
|
1162
|
+
<div class="country-action-btns">
|
|
1163
|
+
<button class="country-action-btn restore" title="恢复为待补资料" onclick="event.stopPropagation(); restoreAttachStuckByCountry('${safeCountry}', ${c.count})">↺</button>
|
|
1164
|
+
</div>
|
|
972
1165
|
<div class="country-name">${isUnknown ? "🌍 " : ""}${c.country}</div>
|
|
973
1166
|
<div class="country-count">${c.count}</div>
|
|
974
1167
|
<div class="country-label">${pct}% attach 未成功</div>
|
|
@@ -1053,6 +1246,40 @@ async function moveCountryJobsToRaw(scope, country, count) {
|
|
|
1053
1246
|
}
|
|
1054
1247
|
}
|
|
1055
1248
|
|
|
1249
|
+
async function resetPendingByCountry(country, count) {
|
|
1250
|
+
const countText = count != null ? `将重置 ${formatStatNum(count)} 条。` : "";
|
|
1251
|
+
if (
|
|
1252
|
+
!window.confirm(
|
|
1253
|
+
`确认将 ${country} 下已预处理的待处理任务重置为需要预处理吗?${countText}`,
|
|
1254
|
+
)
|
|
1255
|
+
) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
showLoading("正在重置...");
|
|
1259
|
+
try {
|
|
1260
|
+
const res = await fetch("/api/pending-by-country/reset", {
|
|
1261
|
+
method: "POST",
|
|
1262
|
+
headers: { "Content-Type": "application/json" },
|
|
1263
|
+
body: JSON.stringify({ country }),
|
|
1264
|
+
});
|
|
1265
|
+
const data = await res.json();
|
|
1266
|
+
if (!res.ok || data.error) {
|
|
1267
|
+
showToast(data.error || "重置失败", true);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
showToast(`${country} 的待处理任务已重置,共 ${data.reset} 条`);
|
|
1271
|
+
await fetchStats();
|
|
1272
|
+
await fetchPendingByCountry();
|
|
1273
|
+
if (!document.getElementById("mainPage").classList.contains("hidden")) {
|
|
1274
|
+
fetchUsers();
|
|
1275
|
+
}
|
|
1276
|
+
} catch (e) {
|
|
1277
|
+
showToast("重置失败: " + e.message, true);
|
|
1278
|
+
} finally {
|
|
1279
|
+
hideLoading();
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1056
1283
|
async function fetchRawByCountry() {
|
|
1057
1284
|
try {
|
|
1058
1285
|
const res = await fetch("/api/raw-by-country");
|
|
@@ -1080,7 +1307,9 @@ function renderRawCountryGrid(countries) {
|
|
|
1080
1307
|
return `
|
|
1081
1308
|
<div class="pending-country-item${isUnknown ? "" : " has-target"}"
|
|
1082
1309
|
onclick="filterRawByCountry('${safeCountry}')">
|
|
1083
|
-
<
|
|
1310
|
+
<div class="country-action-btns">
|
|
1311
|
+
<button class="country-action-btn restore" title="恢复到 jobs 队列" onclick="event.stopPropagation(); restoreRawJobsByCountry('${safeCountry}', ${c.count})">↺</button>
|
|
1312
|
+
</div>
|
|
1084
1313
|
<div class="country-name">${isUnknown ? "🌍 " : ""}${c.country}</div>
|
|
1085
1314
|
<div class="country-count">${c.count}</div>
|
|
1086
1315
|
<div class="country-label">${pct}% 毛料库</div>
|
|
@@ -1096,6 +1325,11 @@ async function fetchRawJobs() {
|
|
|
1096
1325
|
const search = document.getElementById("rawSearchInput").value.trim();
|
|
1097
1326
|
if (search) params.set("search", search);
|
|
1098
1327
|
if (currentRawLocation) params.set("location", currentRawLocation);
|
|
1328
|
+
const videoFilter = document.getElementById("rawVideoFilter");
|
|
1329
|
+
if (videoFilter && videoFilter.checked) params.set("hasVideo", "1");
|
|
1330
|
+
const followerFilter = document.getElementById("rawFollowerFilter");
|
|
1331
|
+
if (followerFilter && followerFilter.checked)
|
|
1332
|
+
params.set("hasFollower", "1");
|
|
1099
1333
|
params.set("limit", "200");
|
|
1100
1334
|
const res = await fetch("/api/raw-jobs?" + params.toString());
|
|
1101
1335
|
const data = await res.json();
|
|
@@ -1171,8 +1405,16 @@ function clearRawFilters() {
|
|
|
1171
1405
|
currentRawLocation = "";
|
|
1172
1406
|
const rawSearchInput = document.getElementById("rawSearchInput");
|
|
1173
1407
|
const rawLocationFilter = document.getElementById("rawLocationFilter");
|
|
1408
|
+
const rawVideoFilter = document.getElementById("rawVideoFilter");
|
|
1409
|
+
const rawFollowerFilter = document.getElementById("rawFollowerFilter");
|
|
1174
1410
|
if (rawSearchInput) rawSearchInput.value = "";
|
|
1175
1411
|
if (rawLocationFilter) rawLocationFilter.value = "";
|
|
1412
|
+
if (rawVideoFilter) rawVideoFilter.checked = false;
|
|
1413
|
+
if (rawFollowerFilter) rawFollowerFilter.checked = false;
|
|
1414
|
+
fetchRawJobs();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function onRawFilterChange() {
|
|
1176
1418
|
fetchRawJobs();
|
|
1177
1419
|
}
|
|
1178
1420
|
|
|
@@ -1206,10 +1448,20 @@ async function restoreRawJob(uniqueId) {
|
|
|
1206
1448
|
async function restoreFilteredRawJobs() {
|
|
1207
1449
|
const search = document.getElementById("rawSearchInput").value.trim();
|
|
1208
1450
|
const location = currentRawLocation;
|
|
1451
|
+
const videoFilter = document.getElementById("rawVideoFilter");
|
|
1452
|
+
const hasVideo = videoFilter && videoFilter.checked;
|
|
1453
|
+
const followerFilter = document.getElementById("rawFollowerFilter");
|
|
1454
|
+
const hasFollower = followerFilter && followerFilter.checked;
|
|
1209
1455
|
let desc = "当前筛选条件";
|
|
1210
1456
|
if (search && location) desc = `搜索="${search}" + 国家=${location}`;
|
|
1211
1457
|
else if (search) desc = `搜索="${search}"`;
|
|
1212
1458
|
else if (location) desc = `国家=${location}`;
|
|
1459
|
+
if (hasVideo || hasFollower) {
|
|
1460
|
+
const tags = [];
|
|
1461
|
+
if (hasVideo) tags.push("有视频");
|
|
1462
|
+
if (hasFollower) tags.push("有粉丝");
|
|
1463
|
+
desc += ` + ${tags.join("、")}`;
|
|
1464
|
+
}
|
|
1213
1465
|
if (
|
|
1214
1466
|
!window.confirm(`确认将毛料库中符合【${desc}】的任务恢复到 jobs 队列吗?`)
|
|
1215
1467
|
) {
|
|
@@ -1220,6 +1472,8 @@ async function restoreFilteredRawJobs() {
|
|
|
1220
1472
|
const body = {};
|
|
1221
1473
|
if (search) body.search = search;
|
|
1222
1474
|
if (location) body.location = location;
|
|
1475
|
+
if (hasVideo) body.hasVideo = true;
|
|
1476
|
+
if (hasFollower) body.hasFollower = true;
|
|
1223
1477
|
const res = await fetch("/api/raw-jobs/restore", {
|
|
1224
1478
|
method: "POST",
|
|
1225
1479
|
headers: { "Content-Type": "application/json" },
|
|
@@ -125,6 +125,7 @@
|
|
|
125
125
|
<th>粉丝</th>
|
|
126
126
|
<th>视频</th>
|
|
127
127
|
<th>国家</th>
|
|
128
|
+
<th class="sortable" data-sort="refreshTime">最近刷新 <span class="sort-icon">↕</span></th>
|
|
128
129
|
<th class="sortable" data-sort="latestVideoTime">最近发布 <span class="sort-icon">↕</span></th>
|
|
129
130
|
<th>猜测国家</th>
|
|
130
131
|
<th>来源</th>
|
|
@@ -244,6 +245,13 @@
|
|
|
244
245
|
style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
|
|
245
246
|
<option value="">全部国家</option>
|
|
246
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>
|
|
247
255
|
<button onclick="clearRawFilters()">清空筛选</button>
|
|
248
256
|
<button onclick="restoreFilteredRawJobs()"
|
|
249
257
|
style="background:#22c55e;color:#fff;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">恢复当前筛选到
|
|
@@ -271,6 +279,61 @@
|
|
|
271
279
|
</div>
|
|
272
280
|
</div>
|
|
273
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
|
+
</tr>
|
|
330
|
+
</thead>
|
|
331
|
+
<tbody id="targetTable"></tbody>
|
|
332
|
+
</table>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
274
337
|
<script src="app.js"></script>
|
|
275
338
|
</body>
|
|
276
339
|
|
|
@@ -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-
|
|
681
|
+
.country-action-btns {
|
|
674
682
|
position: absolute;
|
|
675
|
-
top:
|
|
676
|
-
right:
|
|
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
|
}
|