tt-help-cli-ycl 1.3.95 → 1.3.97
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/tag.js +66 -11
- package/src/lib/api-interceptor.js +2 -0
- package/src/lib/tag-fetcher.js +48 -37
- package/src/lib/tiktok-scraper.mjs +16 -0
- package/src/scraper/explore-core.js +6 -3
- package/src/watch/data-store.js +32 -1
- package/src/watch/db-stats.js +3 -0
- package/src/watch/db-tags.js +82 -11
- package/src/watch/public/app.js +493 -10
- package/src/watch/public/index.html +76 -0
- package/src/watch/public/style.css +150 -0
- package/src/watch/server.js +80 -11
- package/src/watch/tag-service.js +43 -22
package/src/watch/public/app.js
CHANGED
|
@@ -167,6 +167,7 @@ function renderStats() {
|
|
|
167
167
|
flashEl("statTarget", d.targetUsers, { full: true });
|
|
168
168
|
flashEl("statUserUpdateTasks", d.userUpdateTasks || 0, { full: true });
|
|
169
169
|
flashEl("statRawJobs", d.rawJobs || 0);
|
|
170
|
+
flashEl("statTags", d.tagCount || 0, { full: true });
|
|
170
171
|
// 同步子页面 stats
|
|
171
172
|
const pendingTotal = document.getElementById("pendingStatTotal");
|
|
172
173
|
if (pendingTotal) pendingTotal.textContent = formatStatNum(d.totalUsers);
|
|
@@ -659,16 +660,21 @@ function openLocationModal(uniqueId, currentLocation) {
|
|
|
659
660
|
overlay = document.createElement("div");
|
|
660
661
|
overlay.id = "locationModalOverlay";
|
|
661
662
|
overlay.className = "modal-overlay";
|
|
663
|
+
const safeId = escapeJsString(uniqueId);
|
|
662
664
|
const options = TARGET_LOCATIONS.map(
|
|
663
665
|
(loc) =>
|
|
664
|
-
`<button class="loc-option ${loc === currentLocation ? "active" : ""}" onclick="selectLocation('${
|
|
666
|
+
`<button class="loc-option ${loc === currentLocation ? "active" : ""}" onclick="selectLocation('${safeId}','${loc}')">${loc}</button>`,
|
|
665
667
|
).join("");
|
|
666
668
|
overlay.innerHTML = `
|
|
667
669
|
<div class="modal" style="max-width:420px">
|
|
668
670
|
<h3>修改用户国家</h3>
|
|
669
|
-
<div class="hint">用户: @${
|
|
671
|
+
<div class="hint">用户: @${safeId},当前国家: ${currentLocation}</div>
|
|
670
672
|
<div class="loc-grid">${options}</div>
|
|
671
|
-
<div class="
|
|
673
|
+
<div class="custom-loc-row">
|
|
674
|
+
<input type="text" id="customLocationInput" class="custom-loc-input" placeholder="或手动输入国家代码,如 UK" maxlength="10" onkeydown="if(event.key==='Enter')confirmCustomLocation('${safeId}')">
|
|
675
|
+
</div>
|
|
676
|
+
<div class="btn-row" style="margin-top:12px">
|
|
677
|
+
<button class="btn-submit" onclick="confirmCustomLocation('${safeId}')">确认</button>
|
|
672
678
|
<button class="btn-cancel" onclick="closeLocationModal()">取消</button>
|
|
673
679
|
</div>
|
|
674
680
|
</div>
|
|
@@ -677,6 +683,21 @@ function openLocationModal(uniqueId, currentLocation) {
|
|
|
677
683
|
overlay.addEventListener("click", (e) => {
|
|
678
684
|
if (e.target === overlay) closeLocationModal();
|
|
679
685
|
});
|
|
686
|
+
// 自动聚焦输入框
|
|
687
|
+
setTimeout(() => {
|
|
688
|
+
const input = document.getElementById("customLocationInput");
|
|
689
|
+
if (input) input.focus();
|
|
690
|
+
}, 100);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function confirmCustomLocation(uniqueId) {
|
|
694
|
+
const input = document.getElementById("customLocationInput");
|
|
695
|
+
const val = input ? input.value.trim().toUpperCase() : "";
|
|
696
|
+
if (!val) {
|
|
697
|
+
showToast("请输入国家代码", true);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
selectLocation(uniqueId, val);
|
|
680
701
|
}
|
|
681
702
|
|
|
682
703
|
function closeLocationModal() {
|
|
@@ -726,6 +747,56 @@ async function selectLocation(uniqueId, location) {
|
|
|
726
747
|
}
|
|
727
748
|
}
|
|
728
749
|
|
|
750
|
+
async function confirmNonSeller(uniqueId) {
|
|
751
|
+
if (
|
|
752
|
+
!confirm(
|
|
753
|
+
`确定要将 @${uniqueId} 标记为非商家吗?\n这将把 ta 的商家标识(ttSeller)设为 false。`,
|
|
754
|
+
)
|
|
755
|
+
) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
showLoading("正在更新...");
|
|
759
|
+
try {
|
|
760
|
+
const res = await fetch(
|
|
761
|
+
`/api/user-non-seller/${encodeURIComponent(uniqueId)}`,
|
|
762
|
+
{
|
|
763
|
+
method: "PUT",
|
|
764
|
+
headers: { "Content-Type": "application/json" },
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
const data = await res.json();
|
|
768
|
+
if (data.error) {
|
|
769
|
+
showToast(data.error, true);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
showToast(`@${uniqueId} 已标记为非商家`);
|
|
773
|
+
|
|
774
|
+
// 从当前列表中移除该用户
|
|
775
|
+
currentTargetUsers = currentTargetUsers.filter(
|
|
776
|
+
(u) => u.uniqueId !== uniqueId,
|
|
777
|
+
);
|
|
778
|
+
currentTargetTotal = Math.max(0, currentTargetTotal - 1);
|
|
779
|
+
renderTargetTable();
|
|
780
|
+
|
|
781
|
+
// 同时更新统计数据
|
|
782
|
+
const statEl = document.getElementById("targetPageStatTotal");
|
|
783
|
+
if (statEl)
|
|
784
|
+
statEl.textContent = formatStatNum(currentTargetTotal, { full: true });
|
|
785
|
+
// 更新主页面统计
|
|
786
|
+
if (currentStats) {
|
|
787
|
+
currentStats.targetUsers = Math.max(
|
|
788
|
+
0,
|
|
789
|
+
(currentStats.targetUsers || 0) - 1,
|
|
790
|
+
);
|
|
791
|
+
flashEl("statTarget", currentStats.targetUsers, { full: true });
|
|
792
|
+
}
|
|
793
|
+
} catch (e) {
|
|
794
|
+
showToast("更新失败: " + e.message, true);
|
|
795
|
+
} finally {
|
|
796
|
+
hideLoading();
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
729
800
|
async function submitAddUsers() {
|
|
730
801
|
const ta = document.getElementById("modalUserInput");
|
|
731
802
|
const raw = ta.value.trim();
|
|
@@ -967,6 +1038,8 @@ function handleRoute() {
|
|
|
967
1038
|
showRawPage();
|
|
968
1039
|
} else if (hash === "#target") {
|
|
969
1040
|
showTargetPage();
|
|
1041
|
+
} else if (hash === "#tags") {
|
|
1042
|
+
showTagsPage();
|
|
970
1043
|
} else {
|
|
971
1044
|
showMainPage();
|
|
972
1045
|
}
|
|
@@ -994,6 +1067,7 @@ function showPendingPage() {
|
|
|
994
1067
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
995
1068
|
document.getElementById("rawPage").classList.remove("active");
|
|
996
1069
|
document.getElementById("targetPage").classList.remove("active");
|
|
1070
|
+
document.getElementById("tagsPage").classList.remove("active");
|
|
997
1071
|
fetchPendingByCountry();
|
|
998
1072
|
fetchAttachStuckByCountry();
|
|
999
1073
|
}
|
|
@@ -1004,6 +1078,7 @@ function showUserUpdatePage() {
|
|
|
1004
1078
|
document.getElementById("userUpdatePage").classList.add("active");
|
|
1005
1079
|
document.getElementById("rawPage").classList.remove("active");
|
|
1006
1080
|
document.getElementById("targetPage").classList.remove("active");
|
|
1081
|
+
document.getElementById("tagsPage").classList.remove("active");
|
|
1007
1082
|
fetchUserUpdateByCountry();
|
|
1008
1083
|
fetchAttachStuckByCountry();
|
|
1009
1084
|
}
|
|
@@ -1021,6 +1096,7 @@ function showRawPage() {
|
|
|
1021
1096
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1022
1097
|
document.getElementById("rawPage").classList.add("active");
|
|
1023
1098
|
document.getElementById("targetPage").classList.remove("active");
|
|
1099
|
+
document.getElementById("tagsPage").classList.remove("active");
|
|
1024
1100
|
fetchRawByCountry();
|
|
1025
1101
|
fetchRawJobs();
|
|
1026
1102
|
}
|
|
@@ -1031,18 +1107,419 @@ function showMainPage() {
|
|
|
1031
1107
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1032
1108
|
document.getElementById("rawPage").classList.remove("active");
|
|
1033
1109
|
document.getElementById("targetPage").classList.remove("active");
|
|
1110
|
+
document.getElementById("tagsPage").classList.remove("active");
|
|
1034
1111
|
}
|
|
1035
1112
|
|
|
1036
1113
|
function navigateToTarget() {
|
|
1037
1114
|
window.location.hash = "#target";
|
|
1038
1115
|
}
|
|
1039
1116
|
|
|
1117
|
+
function navigateToTags() {
|
|
1118
|
+
window.location.hash = "#tags";
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// ========== 添加关键词弹窗 ==========
|
|
1122
|
+
|
|
1123
|
+
function openAddTagModal() {
|
|
1124
|
+
let overlay = document.getElementById("addTagModalOverlay");
|
|
1125
|
+
if (overlay) return;
|
|
1126
|
+
overlay = document.createElement("div");
|
|
1127
|
+
overlay.id = "addTagModalOverlay";
|
|
1128
|
+
overlay.className = "modal-overlay";
|
|
1129
|
+
overlay.innerHTML = `
|
|
1130
|
+
<div class="modal">
|
|
1131
|
+
<h3>添加关键词</h3>
|
|
1132
|
+
<div class="hint">每行一个关键词,不带 # 号。新关键词默认待打分状态。</div>
|
|
1133
|
+
<textarea id="addTagModalInput" placeholder="例如: fashion beauty skincare travel" style="height:160px"></textarea>
|
|
1134
|
+
<div class="preview" id="addTagModalPreview"></div>
|
|
1135
|
+
<div class="form-row">
|
|
1136
|
+
<label style="color:#aaa;font-size:12px">选择国家(可多选):</label>
|
|
1137
|
+
<div id="addTagCountryCheckboxes" style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">
|
|
1138
|
+
</div>
|
|
1139
|
+
</div>
|
|
1140
|
+
<div class="btn-row">
|
|
1141
|
+
<button class="btn-cancel" onclick="closeAddTagModal()">取消</button>
|
|
1142
|
+
<button class="btn-submit" onclick="submitAddTags()">确认添加</button>
|
|
1143
|
+
</div>
|
|
1144
|
+
</div>
|
|
1145
|
+
`;
|
|
1146
|
+
document.body.appendChild(overlay);
|
|
1147
|
+
overlay.addEventListener("click", (e) => {
|
|
1148
|
+
if (e.target === overlay) closeAddTagModal();
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
// 渲染国家复选框
|
|
1152
|
+
const countryGrid = document.getElementById("addTagCountryCheckboxes");
|
|
1153
|
+
const TARGET_LOCATIONS = [
|
|
1154
|
+
"AT",
|
|
1155
|
+
"BE",
|
|
1156
|
+
"CZ",
|
|
1157
|
+
"DE",
|
|
1158
|
+
"ES",
|
|
1159
|
+
"FR",
|
|
1160
|
+
"GR",
|
|
1161
|
+
"HU",
|
|
1162
|
+
"IE",
|
|
1163
|
+
"IT",
|
|
1164
|
+
"NL",
|
|
1165
|
+
"PL",
|
|
1166
|
+
"PT",
|
|
1167
|
+
];
|
|
1168
|
+
countryGrid.innerHTML = TARGET_LOCATIONS.map(
|
|
1169
|
+
(c) =>
|
|
1170
|
+
`<label class="checkbox-label"><input type="checkbox" value="${c}" checked> ${c}</label>`,
|
|
1171
|
+
).join("");
|
|
1172
|
+
|
|
1173
|
+
const ta = document.getElementById("addTagModalInput");
|
|
1174
|
+
ta.focus();
|
|
1175
|
+
ta.addEventListener("input", () => updateAddTagPreview());
|
|
1176
|
+
ta.addEventListener("keydown", (e) => {
|
|
1177
|
+
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
1178
|
+
e.preventDefault();
|
|
1179
|
+
submitAddTags();
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
document.addEventListener(
|
|
1183
|
+
"keydown",
|
|
1184
|
+
(window.addTagEscHandler = (e) => {
|
|
1185
|
+
if (e.key === "Escape") closeAddTagModal();
|
|
1186
|
+
}),
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function closeAddTagModal() {
|
|
1191
|
+
const overlay = document.getElementById("addTagModalOverlay");
|
|
1192
|
+
if (overlay) overlay.remove();
|
|
1193
|
+
if (window.addTagEscHandler) {
|
|
1194
|
+
document.removeEventListener("keydown", window.addTagEscHandler);
|
|
1195
|
+
delete window.addTagEscHandler;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function updateAddTagPreview() {
|
|
1200
|
+
const ta = document.getElementById("addTagModalInput");
|
|
1201
|
+
const preview = document.getElementById("addTagModalPreview");
|
|
1202
|
+
if (!ta || !preview) return;
|
|
1203
|
+
const tags = ta.value
|
|
1204
|
+
.split("\n")
|
|
1205
|
+
.map((s) => s.trim().replace(/^#+/, ""))
|
|
1206
|
+
.filter(Boolean);
|
|
1207
|
+
preview.textContent = tags.length ? `共 ${tags.length} 个关键词` : "";
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async function submitAddTags() {
|
|
1211
|
+
const ta = document.getElementById("addTagModalInput");
|
|
1212
|
+
const raw = ta ? ta.value.trim() : "";
|
|
1213
|
+
if (!raw) {
|
|
1214
|
+
showToast("请输入关键词", true);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const tags = raw
|
|
1219
|
+
.split("\n")
|
|
1220
|
+
.map((s) => s.trim().replace(/^#+/, ""))
|
|
1221
|
+
.filter(Boolean);
|
|
1222
|
+
if (tags.length === 0) {
|
|
1223
|
+
showToast("请输入有效关键词", true);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// 获取选中的国家
|
|
1228
|
+
const checkedBoxes = document.querySelectorAll(
|
|
1229
|
+
"#addTagCountryCheckboxes input[type=checkbox]:checked",
|
|
1230
|
+
);
|
|
1231
|
+
const countries = Array.from(checkedBoxes).map((cb) => cb.value);
|
|
1232
|
+
if (countries.length === 0) {
|
|
1233
|
+
showToast("请至少选择一个国家", true);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
showLoading("正在添加关键词...");
|
|
1238
|
+
try {
|
|
1239
|
+
const res = await fetch("/api/tags/batch-add", {
|
|
1240
|
+
method: "POST",
|
|
1241
|
+
headers: { "Content-Type": "application/json" },
|
|
1242
|
+
body: JSON.stringify({ tags, countries }),
|
|
1243
|
+
});
|
|
1244
|
+
const data = await res.json();
|
|
1245
|
+
if (data.error) {
|
|
1246
|
+
showToast(data.error, true);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
closeAddTagModal();
|
|
1250
|
+
showToast(`已添加 ${data.added} 个关键词`);
|
|
1251
|
+
loadTagsPage();
|
|
1252
|
+
} catch (e) {
|
|
1253
|
+
showToast("添加失败: " + e.message, true);
|
|
1254
|
+
} finally {
|
|
1255
|
+
hideLoading();
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// ========== 关键词页面 ==========
|
|
1260
|
+
|
|
1261
|
+
let currentTags = [];
|
|
1262
|
+
let currentTagsFilter = "all";
|
|
1263
|
+
let currentTagsCountry = "";
|
|
1264
|
+
let currentTagsSort = { key: null, asc: true };
|
|
1265
|
+
let currentTagsTotal = 0;
|
|
1266
|
+
let currentTagsLoading = false;
|
|
1267
|
+
let currentTagsAllLoaded = false;
|
|
1268
|
+
let currentTagsSeq = 0;
|
|
1269
|
+
const TAGS_PAGE_SIZE = 200;
|
|
1270
|
+
let cachedTagCountries = [];
|
|
1271
|
+
|
|
1272
|
+
function showTagsPage() {
|
|
1273
|
+
document.getElementById("mainPage").classList.add("hidden");
|
|
1274
|
+
document.getElementById("pendingPage").classList.remove("active");
|
|
1275
|
+
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1276
|
+
document.getElementById("rawPage").classList.remove("active");
|
|
1277
|
+
document.getElementById("targetPage").classList.remove("active");
|
|
1278
|
+
document.getElementById("tagsPage").classList.add("active");
|
|
1279
|
+
showLoading("正在加载关键词...");
|
|
1280
|
+
Promise.all([loadTagStats(), loadTagsPage()]).finally(hideLoading);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function setTagsFilter(filter) {
|
|
1284
|
+
currentTagsFilter = filter;
|
|
1285
|
+
currentTags = [];
|
|
1286
|
+
currentTagsAllLoaded = false;
|
|
1287
|
+
document
|
|
1288
|
+
.querySelectorAll("#tagsPage .controls button[data-tags-filter]")
|
|
1289
|
+
.forEach((b) => {
|
|
1290
|
+
b.classList.toggle("active", b.dataset.tagsFilter === filter);
|
|
1291
|
+
});
|
|
1292
|
+
loadTagsPage();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
async function loadTagStats() {
|
|
1296
|
+
try {
|
|
1297
|
+
const res = await fetch("/api/tags/stats");
|
|
1298
|
+
const stats = await res.json();
|
|
1299
|
+
if (!stats) return;
|
|
1300
|
+
document.getElementById("tagsPageStatTotal").textContent = stats.total || 0;
|
|
1301
|
+
flashEl("tagsPageStatProductive", stats.productive || 0);
|
|
1302
|
+
flashEl("tagsPageStatDead", stats.dead || 0);
|
|
1303
|
+
flashEl("tagsPageStatNew", (stats.new || 0) + (stats.scoring || 0));
|
|
1304
|
+
cachedTagCountries = stats.countries || [];
|
|
1305
|
+
// 初始化国家下拉
|
|
1306
|
+
renderTagsCountryFilter();
|
|
1307
|
+
} catch (e) {
|
|
1308
|
+
console.error("获取关键词统计失败:", e);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function renderTagsCountryFilter() {
|
|
1313
|
+
const sel = document.getElementById("tagsCountryFilter");
|
|
1314
|
+
if (!sel) return;
|
|
1315
|
+
const val = sel.value;
|
|
1316
|
+
sel.innerHTML =
|
|
1317
|
+
'<option value="">全部国家</option>' +
|
|
1318
|
+
cachedTagCountries
|
|
1319
|
+
.map(
|
|
1320
|
+
(c) =>
|
|
1321
|
+
`<option value="${c}"${val === c ? " selected" : ""}>${c}</option>`,
|
|
1322
|
+
)
|
|
1323
|
+
.join("");
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
async function loadTagsPage(append = false) {
|
|
1327
|
+
if (append && (currentTagsAllLoaded || currentTagsLoading)) return;
|
|
1328
|
+
if (append) currentTagsLoading = true;
|
|
1329
|
+
|
|
1330
|
+
const seq = ++currentTagsSeq;
|
|
1331
|
+
const country = document.getElementById("tagsCountryFilter").value;
|
|
1332
|
+
const offset = append ? currentTags.length : 0;
|
|
1333
|
+
|
|
1334
|
+
try {
|
|
1335
|
+
let url = `/api/tags?limit=${TAGS_PAGE_SIZE}&offset=${offset}`;
|
|
1336
|
+
if (currentTagsFilter !== "all") url += `&status=${currentTagsFilter}`;
|
|
1337
|
+
if (country) url += `&country=${encodeURIComponent(country)}`;
|
|
1338
|
+
|
|
1339
|
+
const res = await fetch(url);
|
|
1340
|
+
const data = await res.json();
|
|
1341
|
+
|
|
1342
|
+
if (seq !== currentTagsSeq) return; // 旧请求丢弃
|
|
1343
|
+
|
|
1344
|
+
if (append) {
|
|
1345
|
+
currentTags = currentTags.concat(data.tags || []);
|
|
1346
|
+
} else {
|
|
1347
|
+
currentTags = data.tags || [];
|
|
1348
|
+
currentTagsTotal = data.total || 0;
|
|
1349
|
+
}
|
|
1350
|
+
currentTagsAllLoaded = currentTags.length >= currentTagsTotal;
|
|
1351
|
+
|
|
1352
|
+
renderTagsTable();
|
|
1353
|
+
} catch (e) {
|
|
1354
|
+
console.error("加载关键词失败:", e);
|
|
1355
|
+
} finally {
|
|
1356
|
+
if (append) currentTagsLoading = false;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function renderTagsTable() {
|
|
1361
|
+
const el = document.getElementById("tagsTable");
|
|
1362
|
+
const moreHint = document.getElementById("tagsMoreHint");
|
|
1363
|
+
let display = [...currentTags];
|
|
1364
|
+
|
|
1365
|
+
// 搜索过滤(本地二次过滤)
|
|
1366
|
+
const searchVal =
|
|
1367
|
+
document.getElementById("tagsSearchInput")?.value.trim().toLowerCase() ||
|
|
1368
|
+
"";
|
|
1369
|
+
if (searchVal) {
|
|
1370
|
+
display = display.filter((t) => t.tag.toLowerCase().includes(searchVal));
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// 本地排序
|
|
1374
|
+
if (currentTagsSort.key) {
|
|
1375
|
+
display.sort((a, b) => {
|
|
1376
|
+
let va = a[currentTagsSort.key];
|
|
1377
|
+
let vb = b[currentTagsSort.key];
|
|
1378
|
+
if (va == null && vb == null) return 0;
|
|
1379
|
+
if (va == null) return 1;
|
|
1380
|
+
if (vb == null) return -1;
|
|
1381
|
+
if (typeof va === "number" && typeof vb === "number") {
|
|
1382
|
+
return currentTagsSort.asc ? va - vb : vb - va;
|
|
1383
|
+
}
|
|
1384
|
+
va = String(va).toLowerCase();
|
|
1385
|
+
vb = String(vb).toLowerCase();
|
|
1386
|
+
return currentTagsSort.asc ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (display.length === 0) {
|
|
1391
|
+
el.innerHTML =
|
|
1392
|
+
'<tr><td colspan="11" style="text-align:center;color:#888;padding:24px">暂无数据</td></tr>';
|
|
1393
|
+
if (moreHint) moreHint.style.display = "none";
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const statusLabels = {
|
|
1398
|
+
new: '<span class="tag pending">待打分</span>',
|
|
1399
|
+
scoring: '<span class="tag processing">打分中</span>',
|
|
1400
|
+
productive: '<span class="tag seller">有效</span>',
|
|
1401
|
+
dead: '<span class="tag no-video">无效</span>',
|
|
1402
|
+
};
|
|
1403
|
+
const sourceLabels = {
|
|
1404
|
+
llm: "LLM",
|
|
1405
|
+
manual: "手动",
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
el.innerHTML = display
|
|
1409
|
+
.map((t, i) => {
|
|
1410
|
+
const countries = (t.countries || []).join(", ") || "-";
|
|
1411
|
+
const statusTag =
|
|
1412
|
+
statusLabels[t.status] ||
|
|
1413
|
+
`<span class="tag pending">${t.status}</span>`;
|
|
1414
|
+
const source = sourceLabels[t.source] || t.source || "-";
|
|
1415
|
+
const created = t.created_at || "-";
|
|
1416
|
+
const score = t.score != null ? t.score.toFixed(1) : "-";
|
|
1417
|
+
const authorCount = t.author_count ?? "-";
|
|
1418
|
+
const totalPosts = t.total_posts ?? "-";
|
|
1419
|
+
const matchedAuthors = t.matched_authors ?? "-";
|
|
1420
|
+
const pushedUsers = t.pushed_users ?? "-";
|
|
1421
|
+
|
|
1422
|
+
return `<tr>
|
|
1423
|
+
<td style="color:#9ca3af;font-size:12px;text-align:center">${i + 1}</td>
|
|
1424
|
+
<td style="font-weight:600;color:#e0e0e0">#${escapeHtml(t.tag)}</td>
|
|
1425
|
+
<td style="color:${t.score >= 5 ? "#22c55e" : t.score >= 3 ? "#facc15" : "#888"}">${score}</td>
|
|
1426
|
+
<td>${authorCount}</td>
|
|
1427
|
+
<td>${totalPosts}</td>
|
|
1428
|
+
<td>${matchedAuthors}</td>
|
|
1429
|
+
<td>${pushedUsers}</td>
|
|
1430
|
+
<td style="font-size:11px">${countries}</td>
|
|
1431
|
+
<td>${statusTag}</td>
|
|
1432
|
+
<td style="font-size:11px;color:#888">${source}</td>
|
|
1433
|
+
<td style="font-size:11px;color:#888">${created}</td>
|
|
1434
|
+
</tr>`;
|
|
1435
|
+
})
|
|
1436
|
+
.join("");
|
|
1437
|
+
|
|
1438
|
+
if (moreHint) {
|
|
1439
|
+
moreHint.style.display = "";
|
|
1440
|
+
if (currentTagsAllLoaded) {
|
|
1441
|
+
moreHint.innerHTML = `已全部加载`;
|
|
1442
|
+
moreHint.style.color = "#9ca3af";
|
|
1443
|
+
moreHint.style.cursor = "default";
|
|
1444
|
+
} else if (currentTagsLoading) {
|
|
1445
|
+
moreHint.innerHTML = `加载中...`;
|
|
1446
|
+
moreHint.style.color = "#9ca3af";
|
|
1447
|
+
moreHint.style.cursor = "wait";
|
|
1448
|
+
} else {
|
|
1449
|
+
moreHint.innerHTML = `<u style="color:#3b82f6">点击加载更多</u> ↓`;
|
|
1450
|
+
moreHint.style.color = "#6b7280";
|
|
1451
|
+
moreHint.style.cursor = "pointer";
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// ====== 关键词页面事件绑定 ======
|
|
1457
|
+
|
|
1458
|
+
// 刷新
|
|
1459
|
+
document
|
|
1460
|
+
.getElementById("refreshTagsBtn")
|
|
1461
|
+
.addEventListener("click", async () => {
|
|
1462
|
+
showLoading("正在刷新...");
|
|
1463
|
+
try {
|
|
1464
|
+
await Promise.all([loadTagStats(), loadTagsPage()]);
|
|
1465
|
+
showToast("刷新完成");
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
showToast("刷新失败: " + e.message, true);
|
|
1468
|
+
} finally {
|
|
1469
|
+
hideLoading();
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
// 国家筛选(事件委托,避免替换 innerHTML 后绑定丢失)
|
|
1474
|
+
document.getElementById("tagsPage").addEventListener("change", (e) => {
|
|
1475
|
+
if (e.target && e.target.id === "tagsCountryFilter") {
|
|
1476
|
+
currentTagsCountry = e.target.value;
|
|
1477
|
+
showLoading("正在加载...");
|
|
1478
|
+
loadTagsPage().finally(hideLoading);
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
// 搜索防抖
|
|
1483
|
+
let tagsSearchTimer = null;
|
|
1484
|
+
document.getElementById("tagsSearchInput").addEventListener("input", () => {
|
|
1485
|
+
if (tagsSearchTimer) clearTimeout(tagsSearchTimer);
|
|
1486
|
+
tagsSearchTimer = setTimeout(() => {
|
|
1487
|
+
showLoading("正在搜索...");
|
|
1488
|
+
loadTagsPage().finally(hideLoading);
|
|
1489
|
+
}, 300);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
// 加载更多
|
|
1493
|
+
function loadMoreTags() {
|
|
1494
|
+
loadTagsPage(true);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// 排序
|
|
1498
|
+
document.querySelectorAll(".sortable-tag").forEach((th) => {
|
|
1499
|
+
th.addEventListener("click", () => {
|
|
1500
|
+
const key = th.dataset.sort;
|
|
1501
|
+
if (currentTagsSort.key === key) {
|
|
1502
|
+
currentTagsSort.asc = !currentTagsSort.asc;
|
|
1503
|
+
} else {
|
|
1504
|
+
currentTagsSort.key = key;
|
|
1505
|
+
currentTagsSort.asc = true;
|
|
1506
|
+
}
|
|
1507
|
+
th.querySelectorAll(".sort-icon").forEach(
|
|
1508
|
+
(icon) => (icon.textContent = "↕"),
|
|
1509
|
+
);
|
|
1510
|
+
const icon = th.querySelector(".sort-icon");
|
|
1511
|
+
if (icon) icon.textContent = currentTagsSort.asc ? "↑" : "↓";
|
|
1512
|
+
renderTagsTable();
|
|
1513
|
+
});
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1040
1516
|
function showTargetPage() {
|
|
1041
1517
|
document.getElementById("mainPage").classList.add("hidden");
|
|
1042
1518
|
document.getElementById("pendingPage").classList.remove("active");
|
|
1043
1519
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1044
1520
|
document.getElementById("rawPage").classList.remove("active");
|
|
1045
1521
|
document.getElementById("targetPage").classList.add("active");
|
|
1522
|
+
document.getElementById("tagsPage").classList.remove("active");
|
|
1046
1523
|
showLoading("正在加载目标商家数据...");
|
|
1047
1524
|
fetchTargetByCountry();
|
|
1048
1525
|
// 同步统计
|
|
@@ -1209,7 +1686,7 @@ function renderTargetTable() {
|
|
|
1209
1686
|
|
|
1210
1687
|
if (displayUsers.length === 0) {
|
|
1211
1688
|
el.innerHTML =
|
|
1212
|
-
'<tr><td colspan="
|
|
1689
|
+
'<tr><td colspan="11" style="text-align:center;color:#888;padding:24px">暂无数据</td></tr>';
|
|
1213
1690
|
if (moreHint) {
|
|
1214
1691
|
moreHint.style.display = "none";
|
|
1215
1692
|
}
|
|
@@ -1224,17 +1701,20 @@ function renderTargetTable() {
|
|
|
1224
1701
|
const fans = u.followerCount != null ? formatNum(u.followerCount) : "-";
|
|
1225
1702
|
const videos = u.videoCount != null ? u.videoCount : "-";
|
|
1226
1703
|
const userLocation = u.locationCreated || "-";
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1704
|
+
// 如果用户手工修改过国家,确认国家列显示"已修正"
|
|
1705
|
+
const confirmedLocation = u.modifiedAt
|
|
1706
|
+
? `<span style="color:#f59e0b;font-weight:600">已修正</span>`
|
|
1707
|
+
: u.confirmedLocation
|
|
1708
|
+
? u.confirmedLocation === u.locationCreated
|
|
1709
|
+
? `<span style="color:#22c55e">${u.confirmedLocation} ✓</span>`
|
|
1710
|
+
: `<span style="color:#ef4444">${u.confirmedLocation} ✗</span>`
|
|
1711
|
+
: "-";
|
|
1232
1712
|
const latestVideo = u.latestVideoTime
|
|
1233
1713
|
? formatTime(u.latestVideoTime * 1000)
|
|
1234
1714
|
: "-";
|
|
1235
1715
|
const refreshTime = u.refreshTime ? formatTime(u.refreshTime) : "-";
|
|
1236
1716
|
const topPlayCount =
|
|
1237
|
-
u.topVideoPlayCount != null && u.topVideoPlayCount
|
|
1717
|
+
u.topVideoPlayCount != null && u.topVideoPlayCount >= 1000
|
|
1238
1718
|
? formatNum(u.topVideoPlayCount)
|
|
1239
1719
|
: "-";
|
|
1240
1720
|
const topPlayCountCell = u.topVideoHref
|
|
@@ -1270,6 +1750,9 @@ function renderTargetTable() {
|
|
|
1270
1750
|
${topPlayCountCell}
|
|
1271
1751
|
<td data-label="最近发布" style="font-size:11px;color:#888">${latestVideo}</td>
|
|
1272
1752
|
<td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
|
|
1753
|
+
<td data-label="操作">
|
|
1754
|
+
<button class="btn-non-seller" onclick="confirmNonSeller('${escapeJsString(u.uniqueId)}')" title="将此用户标记为非商家">设为非商家</button>
|
|
1755
|
+
</td>
|
|
1273
1756
|
</tr>`;
|
|
1274
1757
|
})
|
|
1275
1758
|
.join("");
|