tt-help-cli-ycl 1.4.142 → 1.4.145
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/sync-raw-jobs.mjs +13 -7
- package/scripts/test-api-interceptor-429.mjs +122 -0
- package/scripts/test-daemon-failover.mjs +61 -4
- package/src/clash/checker.js +41 -0
- package/src/clash/daemon.js +26 -0
- package/src/cli/comments.js +5 -5
- package/src/cli/precheck.js +2 -2
- package/src/cli/refresh-mini.js +1 -1
- package/src/cli/tag.js +110 -353
- package/src/lib/api-interceptor.js +26 -8
- package/src/lib/app-config.js +119 -0
- package/src/lib/args.js +3 -1
- package/src/lib/constants.js +9 -6
- package/src/lib/target-locations.js +1 -0
- package/src/scraper/core.js +2 -2
- package/src/scraper/explore-core.js +2 -2
- package/src/watch/data-store.js +94 -8
- package/src/watch/public/app.js +412 -26
- package/src/watch/public/index.html +74 -1
- package/src/watch/public/style.css +218 -0
- package/src/watch/server.js +144 -40
- package/src/watchdog/process-manager.js +11 -6
package/src/watch/public/app.js
CHANGED
|
@@ -185,6 +185,7 @@ function renderStats() {
|
|
|
185
185
|
flashEl("statPending", d.pendingUsers);
|
|
186
186
|
flashEl("statError", d.errorUsers);
|
|
187
187
|
flashEl("statTarget", d.targetUsers, { full: true });
|
|
188
|
+
flashEl("statPrecise", d.preciseMerchants, { full: true });
|
|
188
189
|
// 检测目标商家新增,放烟花(仅首页,且开关开启时)
|
|
189
190
|
const hash = window.location.hash.replace(/^#/, "");
|
|
190
191
|
if (
|
|
@@ -683,7 +684,7 @@ function closeAddModal() {
|
|
|
683
684
|
|
|
684
685
|
// ========== 国家修改模态框 ==========
|
|
685
686
|
|
|
686
|
-
const
|
|
687
|
+
const DEFAULT_LOCATION_OPTIONS = [
|
|
687
688
|
"AT",
|
|
688
689
|
"BE",
|
|
689
690
|
"CZ",
|
|
@@ -697,19 +698,35 @@ const TARGET_LOCATIONS = [
|
|
|
697
698
|
"NL",
|
|
698
699
|
"PL",
|
|
699
700
|
"PT",
|
|
701
|
+
"TH",
|
|
702
|
+
"US",
|
|
700
703
|
];
|
|
704
|
+
let locationOptions = [...DEFAULT_LOCATION_OPTIONS];
|
|
701
705
|
|
|
702
|
-
function openLocationModal(uniqueId, currentLocation) {
|
|
706
|
+
async function openLocationModal(uniqueId, currentLocation) {
|
|
703
707
|
let overlay = document.getElementById("locationModalOverlay");
|
|
704
708
|
if (overlay) return;
|
|
709
|
+
// 优先使用服务端配置的目标国家作为候选,失败则回退默认
|
|
710
|
+
try {
|
|
711
|
+
const res = await fetch("/api/settings/target-locations");
|
|
712
|
+
const data = await res.json();
|
|
713
|
+
const locs = (data.locations || []).filter(Boolean);
|
|
714
|
+
if (locs.length) {
|
|
715
|
+
locationOptions = [...new Set([...locs, ...DEFAULT_LOCATION_OPTIONS])];
|
|
716
|
+
}
|
|
717
|
+
} catch {
|
|
718
|
+
locationOptions = [...DEFAULT_LOCATION_OPTIONS];
|
|
719
|
+
}
|
|
705
720
|
overlay = document.createElement("div");
|
|
706
721
|
overlay.id = "locationModalOverlay";
|
|
707
722
|
overlay.className = "modal-overlay";
|
|
708
723
|
const safeId = escapeJsString(uniqueId);
|
|
709
|
-
const options =
|
|
710
|
-
(
|
|
711
|
-
|
|
712
|
-
|
|
724
|
+
const options = locationOptions
|
|
725
|
+
.map(
|
|
726
|
+
(loc) =>
|
|
727
|
+
`<button class="loc-option ${loc === currentLocation ? "active" : ""}" onclick="selectLocation('${safeId}','${loc}')">${loc}</button>`,
|
|
728
|
+
)
|
|
729
|
+
.join("");
|
|
713
730
|
overlay.innerHTML = `
|
|
714
731
|
<div class="modal" style="max-width:420px">
|
|
715
732
|
<h3>修改用户国家</h3>
|
|
@@ -1121,6 +1138,10 @@ document.getElementById("statTargetCard").addEventListener("click", () => {
|
|
|
1121
1138
|
navigateToTarget();
|
|
1122
1139
|
});
|
|
1123
1140
|
|
|
1141
|
+
document.getElementById("statPreciseCard").addEventListener("click", () => {
|
|
1142
|
+
navigateToPrecise();
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1124
1145
|
// Hash 路由
|
|
1125
1146
|
window.addEventListener("hashchange", handleRoute);
|
|
1126
1147
|
window.addEventListener("DOMContentLoaded", handleRoute);
|
|
@@ -1274,6 +1295,8 @@ function handleRoute() {
|
|
|
1274
1295
|
showRawPage();
|
|
1275
1296
|
} else if (hash === "#target") {
|
|
1276
1297
|
showTargetPage();
|
|
1298
|
+
} else if (hash === "#precise") {
|
|
1299
|
+
showPrecisePage();
|
|
1277
1300
|
} else if (hash === "#tags") {
|
|
1278
1301
|
showTagsPage();
|
|
1279
1302
|
} else if (hash === "#settings") {
|
|
@@ -1307,6 +1330,7 @@ function showPendingPage() {
|
|
|
1307
1330
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1308
1331
|
document.getElementById("rawPage").classList.remove("active");
|
|
1309
1332
|
document.getElementById("targetPage").classList.remove("active");
|
|
1333
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1310
1334
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1311
1335
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1312
1336
|
fetchPendingByCountry();
|
|
@@ -1319,6 +1343,7 @@ function showUserUpdatePage() {
|
|
|
1319
1343
|
document.getElementById("userUpdatePage").classList.add("active");
|
|
1320
1344
|
document.getElementById("rawPage").classList.remove("active");
|
|
1321
1345
|
document.getElementById("targetPage").classList.remove("active");
|
|
1346
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1322
1347
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1323
1348
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1324
1349
|
fetchUserUpdateByCountry();
|
|
@@ -1338,6 +1363,7 @@ function showRawPage() {
|
|
|
1338
1363
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1339
1364
|
document.getElementById("rawPage").classList.add("active");
|
|
1340
1365
|
document.getElementById("targetPage").classList.remove("active");
|
|
1366
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1341
1367
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1342
1368
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1343
1369
|
fetchRawByCountry();
|
|
@@ -1350,6 +1376,7 @@ function showMainPage() {
|
|
|
1350
1376
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1351
1377
|
document.getElementById("rawPage").classList.remove("active");
|
|
1352
1378
|
document.getElementById("targetPage").classList.remove("active");
|
|
1379
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1353
1380
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1354
1381
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1355
1382
|
document.getElementById("watchdogPage").classList.remove("active");
|
|
@@ -1363,6 +1390,10 @@ function navigateToTarget() {
|
|
|
1363
1390
|
window.location.hash = "#target";
|
|
1364
1391
|
}
|
|
1365
1392
|
|
|
1393
|
+
function navigateToPrecise() {
|
|
1394
|
+
window.location.hash = "#precise";
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1366
1397
|
function navigateToTags() {
|
|
1367
1398
|
window.location.hash = "#tags";
|
|
1368
1399
|
}
|
|
@@ -1383,6 +1414,7 @@ function hideAllPages() {
|
|
|
1383
1414
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1384
1415
|
document.getElementById("rawPage").classList.remove("active");
|
|
1385
1416
|
document.getElementById("targetPage").classList.remove("active");
|
|
1417
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1386
1418
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1387
1419
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1388
1420
|
document.getElementById("watchdogPage").classList.remove("active");
|
|
@@ -1392,6 +1424,157 @@ function showSettingsPage() {
|
|
|
1392
1424
|
hideAllPages();
|
|
1393
1425
|
document.getElementById("settingsPage").classList.add("active");
|
|
1394
1426
|
fetchLlmConfig();
|
|
1427
|
+
loadTargetLocations();
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// ========== 目标国家配置(通用 app-config,落盘 ~/.tt-help-config.json) ==========
|
|
1431
|
+
|
|
1432
|
+
const AVAILABLE_COUNTRIES = [
|
|
1433
|
+
"AT",
|
|
1434
|
+
"BE",
|
|
1435
|
+
"CZ",
|
|
1436
|
+
"DE",
|
|
1437
|
+
"ES",
|
|
1438
|
+
"FR",
|
|
1439
|
+
"GR",
|
|
1440
|
+
"HU",
|
|
1441
|
+
"IE",
|
|
1442
|
+
"IT",
|
|
1443
|
+
"NL",
|
|
1444
|
+
"PL",
|
|
1445
|
+
"PT",
|
|
1446
|
+
"TH",
|
|
1447
|
+
"US",
|
|
1448
|
+
"GB",
|
|
1449
|
+
"SE",
|
|
1450
|
+
"NO",
|
|
1451
|
+
"FI",
|
|
1452
|
+
"DK",
|
|
1453
|
+
"RO",
|
|
1454
|
+
"BG",
|
|
1455
|
+
"CH",
|
|
1456
|
+
"SK",
|
|
1457
|
+
"SI",
|
|
1458
|
+
"HR",
|
|
1459
|
+
"RS",
|
|
1460
|
+
"UA",
|
|
1461
|
+
"TR",
|
|
1462
|
+
"MX",
|
|
1463
|
+
"BR",
|
|
1464
|
+
"AR",
|
|
1465
|
+
"CL",
|
|
1466
|
+
"CO",
|
|
1467
|
+
];
|
|
1468
|
+
|
|
1469
|
+
function normalizeCountryInput(str) {
|
|
1470
|
+
return [
|
|
1471
|
+
...new Set(
|
|
1472
|
+
String(str || "")
|
|
1473
|
+
.split(",")
|
|
1474
|
+
.map((s) => s.trim().toUpperCase())
|
|
1475
|
+
.filter(Boolean),
|
|
1476
|
+
),
|
|
1477
|
+
];
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
async function loadTargetLocations() {
|
|
1481
|
+
const input = document.getElementById("targetLocationsInput");
|
|
1482
|
+
const status = document.getElementById("targetLocationsStatus");
|
|
1483
|
+
if (!input) return;
|
|
1484
|
+
try {
|
|
1485
|
+
const res = await fetch("/api/settings/target-locations");
|
|
1486
|
+
const data = await res.json();
|
|
1487
|
+
const locations = data.locations || [];
|
|
1488
|
+
input.value = locations.join(",");
|
|
1489
|
+
renderTargetCountryGrid(locations);
|
|
1490
|
+
if (status) {
|
|
1491
|
+
status.textContent = `当前 ${locations.length} 个国家: ${locations.join(", ")}`;
|
|
1492
|
+
status.className = "settings-status ok";
|
|
1493
|
+
}
|
|
1494
|
+
} catch (e) {
|
|
1495
|
+
console.error("loadTargetLocations error:", e);
|
|
1496
|
+
if (status) {
|
|
1497
|
+
status.textContent = "读取目标国家失败";
|
|
1498
|
+
status.className = "settings-status warn";
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function renderTargetCountryGrid(selected) {
|
|
1504
|
+
const grid = document.getElementById("targetCountryGrid");
|
|
1505
|
+
if (!grid) return;
|
|
1506
|
+
const selectedSet = new Set(selected || []);
|
|
1507
|
+
grid.innerHTML = AVAILABLE_COUNTRIES.map(
|
|
1508
|
+
(loc) =>
|
|
1509
|
+
`<button type="button" class="loc-option ${selectedSet.has(loc) ? "active" : ""}" onclick="toggleTargetCountry('${loc}')">${loc}</button>`,
|
|
1510
|
+
).join("");
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function toggleTargetCountry(loc) {
|
|
1514
|
+
const input = document.getElementById("targetLocationsInput");
|
|
1515
|
+
if (!input) return;
|
|
1516
|
+
let current = normalizeCountryInput(input.value);
|
|
1517
|
+
if (current.includes(loc)) {
|
|
1518
|
+
current = current.filter((l) => l !== loc);
|
|
1519
|
+
} else {
|
|
1520
|
+
current.push(loc);
|
|
1521
|
+
}
|
|
1522
|
+
input.value = current.join(",");
|
|
1523
|
+
renderTargetCountryGrid(current);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
async function saveTargetLocations() {
|
|
1527
|
+
const input = document.getElementById("targetLocationsInput");
|
|
1528
|
+
const status = document.getElementById("targetLocationsStatus");
|
|
1529
|
+
if (!input) return;
|
|
1530
|
+
const locations = normalizeCountryInput(input.value);
|
|
1531
|
+
status.textContent = "保存中...";
|
|
1532
|
+
status.className = "settings-status loading";
|
|
1533
|
+
try {
|
|
1534
|
+
const res = await fetch("/api/settings/target-locations", {
|
|
1535
|
+
method: "POST",
|
|
1536
|
+
headers: { "Content-Type": "application/json" },
|
|
1537
|
+
body: JSON.stringify({ locations }),
|
|
1538
|
+
});
|
|
1539
|
+
const data = await res.json();
|
|
1540
|
+
input.value = (data.locations || []).join(",");
|
|
1541
|
+
renderTargetCountryGrid(data.locations || []);
|
|
1542
|
+
status.textContent = `已保存 ${(data.locations || []).length} 个国家`;
|
|
1543
|
+
status.className = "settings-status ok";
|
|
1544
|
+
} catch (e) {
|
|
1545
|
+
console.error("saveTargetLocations error:", e);
|
|
1546
|
+
status.textContent = "保存失败";
|
|
1547
|
+
status.className = "settings-status warn";
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
async function resetTargetLocations() {
|
|
1552
|
+
const status = document.getElementById("targetLocationsStatus");
|
|
1553
|
+
if (status) {
|
|
1554
|
+
status.textContent = "保存中...";
|
|
1555
|
+
status.className = "settings-status loading";
|
|
1556
|
+
}
|
|
1557
|
+
try {
|
|
1558
|
+
const res = await fetch("/api/settings/target-locations", {
|
|
1559
|
+
method: "POST",
|
|
1560
|
+
headers: { "Content-Type": "application/json" },
|
|
1561
|
+
body: JSON.stringify({ locations: [] }),
|
|
1562
|
+
});
|
|
1563
|
+
const data = await res.json();
|
|
1564
|
+
const input = document.getElementById("targetLocationsInput");
|
|
1565
|
+
if (input) input.value = (data.locations || []).join(",");
|
|
1566
|
+
renderTargetCountryGrid(data.locations || []);
|
|
1567
|
+
if (status) {
|
|
1568
|
+
status.textContent = `已恢复默认 ${(data.locations || []).length} 个国家`;
|
|
1569
|
+
status.className = "settings-status ok";
|
|
1570
|
+
}
|
|
1571
|
+
} catch (e) {
|
|
1572
|
+
console.error("resetTargetLocations error:", e);
|
|
1573
|
+
if (status) {
|
|
1574
|
+
status.textContent = "恢复默认失败";
|
|
1575
|
+
status.className = "settings-status warn";
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1395
1578
|
}
|
|
1396
1579
|
|
|
1397
1580
|
let _llmProvidersCache = null;
|
|
@@ -1596,27 +1779,14 @@ function openAddTagModal() {
|
|
|
1596
1779
|
if (e.target === overlay) closeAddTagModal();
|
|
1597
1780
|
});
|
|
1598
1781
|
|
|
1599
|
-
//
|
|
1782
|
+
// 渲染国家复选框(使用动态目标国家列表,含 TH/US,失败回退默认)
|
|
1600
1783
|
const countryGrid = document.getElementById("addTagCountryCheckboxes");
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
"
|
|
1607
|
-
"FR",
|
|
1608
|
-
"GR",
|
|
1609
|
-
"HU",
|
|
1610
|
-
"IE",
|
|
1611
|
-
"IT",
|
|
1612
|
-
"NL",
|
|
1613
|
-
"PL",
|
|
1614
|
-
"PT",
|
|
1615
|
-
];
|
|
1616
|
-
countryGrid.innerHTML = TARGET_LOCATIONS.map(
|
|
1617
|
-
(c) =>
|
|
1618
|
-
`<label class="checkbox-label"><input type="checkbox" value="${c}" checked> ${c}</label>`,
|
|
1619
|
-
).join("");
|
|
1784
|
+
countryGrid.innerHTML = locationOptions
|
|
1785
|
+
.map(
|
|
1786
|
+
(c) =>
|
|
1787
|
+
`<label class="checkbox-label"><input type="checkbox" value="${c}" checked> ${c}</label>`,
|
|
1788
|
+
)
|
|
1789
|
+
.join("");
|
|
1620
1790
|
|
|
1621
1791
|
const ta = document.getElementById("addTagModalInput");
|
|
1622
1792
|
ta.focus();
|
|
@@ -1704,6 +1874,220 @@ async function submitAddTags() {
|
|
|
1704
1874
|
}
|
|
1705
1875
|
}
|
|
1706
1876
|
|
|
1877
|
+
// ========== 精准商家页面 ==========
|
|
1878
|
+
|
|
1879
|
+
async function showPrecisePage() {
|
|
1880
|
+
hideAllPages();
|
|
1881
|
+
document.getElementById("precisePage").classList.add("active");
|
|
1882
|
+
// 每次进入都默认视频模式
|
|
1883
|
+
setPreciseView("video");
|
|
1884
|
+
showLoading("正在加载精准商家数据...");
|
|
1885
|
+
await loadPreciseUsersPage(false, true);
|
|
1886
|
+
hideLoading();
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
let currentPreciseUsers = [];
|
|
1890
|
+
let currentPreciseTotal = 0;
|
|
1891
|
+
let currentPreciseLoading = false;
|
|
1892
|
+
let currentPreciseAllLoaded = false;
|
|
1893
|
+
let currentPreciseSeq = 0;
|
|
1894
|
+
let currentPreciseView = "video";
|
|
1895
|
+
const PRECISE_PAGE_SIZE = 200;
|
|
1896
|
+
|
|
1897
|
+
async function loadPreciseUsersPage(append = false, skipLoading = false) {
|
|
1898
|
+
if (currentPreciseLoading) return;
|
|
1899
|
+
currentPreciseLoading = true;
|
|
1900
|
+
const seq = ++currentPreciseSeq;
|
|
1901
|
+
const search = document.getElementById("preciseSearchInput")
|
|
1902
|
+
? document.getElementById("preciseSearchInput").value.trim()
|
|
1903
|
+
: "";
|
|
1904
|
+
const offset = append ? currentPreciseUsers.length : 0;
|
|
1905
|
+
if (!append && !skipLoading) showLoading("正在加载数据...");
|
|
1906
|
+
|
|
1907
|
+
let data = null;
|
|
1908
|
+
let error = null;
|
|
1909
|
+
try {
|
|
1910
|
+
let url = `/api/precise-merchants?limit=${PRECISE_PAGE_SIZE}&offset=${offset}`;
|
|
1911
|
+
if (search) url += `&search=${encodeURIComponent(search)}`;
|
|
1912
|
+
const res = await fetch(url);
|
|
1913
|
+
data = await res.json();
|
|
1914
|
+
} catch (e) {
|
|
1915
|
+
error = e;
|
|
1916
|
+
console.error("加载精准商家失败:", e);
|
|
1917
|
+
} finally {
|
|
1918
|
+
if (!append && !skipLoading) hideLoading();
|
|
1919
|
+
currentPreciseLoading = false;
|
|
1920
|
+
if (error || !data) return;
|
|
1921
|
+
if (seq !== currentPreciseSeq && !append) return;
|
|
1922
|
+
if (!append) {
|
|
1923
|
+
currentPreciseUsers = data.users || [];
|
|
1924
|
+
currentPreciseTotal = data.total || 0;
|
|
1925
|
+
} else {
|
|
1926
|
+
currentPreciseUsers = currentPreciseUsers.concat(data.users || []);
|
|
1927
|
+
currentPreciseTotal = data.total || 0;
|
|
1928
|
+
}
|
|
1929
|
+
currentPreciseAllLoaded = currentPreciseUsers.length >= currentPreciseTotal;
|
|
1930
|
+
renderPreciseView();
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function setPreciseView(view) {
|
|
1935
|
+
currentPreciseView = view === "table" ? "table" : "video";
|
|
1936
|
+
const tableBtn = document.getElementById("preciseViewTableBtn");
|
|
1937
|
+
const videoBtn = document.getElementById("preciseViewVideoBtn");
|
|
1938
|
+
if (tableBtn)
|
|
1939
|
+
tableBtn.classList.toggle("active", currentPreciseView === "table");
|
|
1940
|
+
if (videoBtn)
|
|
1941
|
+
videoBtn.classList.toggle("active", currentPreciseView === "video");
|
|
1942
|
+
const videoWrap = document.getElementById("preciseVideoWrap");
|
|
1943
|
+
const tableWrap = document.getElementById("preciseTableWrap");
|
|
1944
|
+
if (videoWrap)
|
|
1945
|
+
videoWrap.style.display = currentPreciseView === "video" ? "" : "none";
|
|
1946
|
+
if (tableWrap)
|
|
1947
|
+
tableWrap.style.display = currentPreciseView === "table" ? "" : "none";
|
|
1948
|
+
renderPreciseView();
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
function renderPreciseView() {
|
|
1952
|
+
if (currentPreciseView === "table") {
|
|
1953
|
+
renderPreciseTable();
|
|
1954
|
+
} else {
|
|
1955
|
+
renderPreciseVideos();
|
|
1956
|
+
}
|
|
1957
|
+
updatePreciseMoreHint();
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
function updatePreciseMoreHint() {
|
|
1961
|
+
const moreHint = document.getElementById("preciseMoreHint");
|
|
1962
|
+
const totalEl = document.getElementById("precisePageStatTotal");
|
|
1963
|
+
if (totalEl)
|
|
1964
|
+
totalEl.textContent = formatStatNum(currentPreciseTotal, { full: true });
|
|
1965
|
+
if (!moreHint) return;
|
|
1966
|
+
if (currentPreciseTotal === 0) {
|
|
1967
|
+
moreHint.textContent = "";
|
|
1968
|
+
moreHint.style.display = "none";
|
|
1969
|
+
} else if (currentPreciseAllLoaded) {
|
|
1970
|
+
moreHint.textContent = `共 ${currentPreciseTotal} 个,已全部加载`;
|
|
1971
|
+
moreHint.onclick = null;
|
|
1972
|
+
moreHint.style.cursor = "default";
|
|
1973
|
+
moreHint.style.color = "#6b7280";
|
|
1974
|
+
} else {
|
|
1975
|
+
moreHint.textContent = "点击加载更多";
|
|
1976
|
+
moreHint.onclick = () => loadPreciseUsersPage(true);
|
|
1977
|
+
moreHint.style.cursor = "pointer";
|
|
1978
|
+
moreHint.style.color = "#60a5fa";
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function renderPreciseTable() {
|
|
1983
|
+
const el = document.getElementById("preciseTable");
|
|
1984
|
+
if (!el) return;
|
|
1985
|
+
if (!currentPreciseUsers.length) {
|
|
1986
|
+
el.innerHTML =
|
|
1987
|
+
'<tr><td colspan="12" style="text-align:center;color:#666;padding:20px">暂无精准商家</td></tr>';
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
el.innerHTML = currentPreciseUsers
|
|
1991
|
+
.map((u, i) => {
|
|
1992
|
+
const sold = u.productSoldCount ?? 0;
|
|
1993
|
+
const ec = u.ecOver1k ?? 0;
|
|
1994
|
+
const maxEc = u.maxEcPlay ?? 0;
|
|
1995
|
+
const title = (u.productName || "").replace(/</g, "<");
|
|
1996
|
+
return `<tr>
|
|
1997
|
+
<td style="text-align:right;color:#666">${i + 1}</td>
|
|
1998
|
+
<td><a href="https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}" target="_blank">@${escapeHtml(u.uniqueId)}</a></td>
|
|
1999
|
+
<td>${escapeHtml(u.nickname || "-")}</td>
|
|
2000
|
+
<td>${formatNum(u.followerCount || 0)}</td>
|
|
2001
|
+
<td>${u.videoCount || 0}</td>
|
|
2002
|
+
<td>${u.locationCreated || "-"}</td>
|
|
2003
|
+
<td style="color:#22c55e">${formatNum(sold)}</td>
|
|
2004
|
+
<td style="color:#f87171">${u.productReviewCount ?? 0}</td>
|
|
2005
|
+
<td>${u.productRating != null ? u.productRating : "-"}</td>
|
|
2006
|
+
<td>${ec}</td>
|
|
2007
|
+
<td>${formatNum(maxEc)}</td>
|
|
2008
|
+
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${title}">${title || "-"}</td>
|
|
2009
|
+
</tr>`;
|
|
2010
|
+
})
|
|
2011
|
+
.join("");
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function renderPreciseVideos() {
|
|
2015
|
+
const wrap = document.getElementById("preciseVideoWrap");
|
|
2016
|
+
if (!wrap) return;
|
|
2017
|
+
if (!currentPreciseUsers.length) {
|
|
2018
|
+
wrap.innerHTML =
|
|
2019
|
+
'<div style="color:#666;text-align:center;padding:24px">暂无精准商家</div>';
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
wrap.innerHTML = currentPreciseUsers
|
|
2023
|
+
.map((u) => {
|
|
2024
|
+
const vids = Array.isArray(u.recentEcVideos) ? u.recentEcVideos : [];
|
|
2025
|
+
const maxPlay = vids.reduce((m, v) => Math.max(m, v.playCount || 0), 0);
|
|
2026
|
+
const sold = u.productSoldCount ?? 0;
|
|
2027
|
+
const review = u.productReviewCount ?? 0;
|
|
2028
|
+
const rating = u.productRating != null ? u.productRating : "-";
|
|
2029
|
+
const userLink = `https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}`;
|
|
2030
|
+
const tiles = vids
|
|
2031
|
+
.reduce((acc, v) => {
|
|
2032
|
+
const play = v.playCount || 0;
|
|
2033
|
+
const time = v.createTime ? formatShortDate(v.createTime) : "";
|
|
2034
|
+
const isAd = v.isAd ? '<span class="pv-ad">广告</span>' : "";
|
|
2035
|
+
const prod = v.seoUrl
|
|
2036
|
+
? `<a class="pv-product" href="${escapeAttr(v.seoUrl)}" target="_blank" title="${escapeHtml(v.productTitle || "查看商品")}"><span class="pv-product-icon">🛒</span>${escapeHtml((v.productTitle || "查看商品").slice(0, 80))}</a>`
|
|
2037
|
+
: "";
|
|
2038
|
+
acc.push(`<div class="precise-video-item">
|
|
2039
|
+
<a class="precise-video-tile" href="${escapeAttr(v.href || "#")}" target="_blank" title="播放视频">
|
|
2040
|
+
<span class="pv-play">▶</span>
|
|
2041
|
+
<span class="pv-count${maxPlay > 0 && play === maxPlay ? " pv-count-max" : ""}">${formatNum(play)}</span>
|
|
2042
|
+
<span class="pv-time">${time}</span>
|
|
2043
|
+
${isAd}
|
|
2044
|
+
</a>
|
|
2045
|
+
${prod}
|
|
2046
|
+
</div>`);
|
|
2047
|
+
return acc;
|
|
2048
|
+
}, [])
|
|
2049
|
+
.join("");
|
|
2050
|
+
return `<div class="precise-merchant-card">
|
|
2051
|
+
<div class="precise-merchant-head">
|
|
2052
|
+
<a class="pm-user" href="${userLink}" target="_blank">@${escapeHtml(u.uniqueId)}</a>
|
|
2053
|
+
<span class="pm-nick">${escapeHtml(u.nickname || "-")}</span>
|
|
2054
|
+
<span class="pm-country">${u.locationCreated || "-"}</span>
|
|
2055
|
+
<span class="pm-stats">粉丝 ${formatNum(u.followerCount || 0)} · 视频 ${u.videoCount || 0} · 销量 ${formatNum(sold)} · 评论 ${review} · 评分 ${rating} · EC视频 ${vids.length}</span>
|
|
2056
|
+
</div>
|
|
2057
|
+
${tiles ? `<div class="precise-video-grid">${tiles}</div>` : '<div style="color:#666;font-size:12px;padding:6px 0">该商家暂无 EC 视频数据</div>'}
|
|
2058
|
+
</div>`;
|
|
2059
|
+
})
|
|
2060
|
+
.join("");
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function formatShortDate(sec) {
|
|
2064
|
+
if (!sec) return "";
|
|
2065
|
+
const d = new Date(sec * 1000);
|
|
2066
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2067
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
function escapeAttr(str) {
|
|
2071
|
+
return String(str)
|
|
2072
|
+
.replace(/&/g, "&")
|
|
2073
|
+
.replace(/"/g, """)
|
|
2074
|
+
.replace(/</g, "<");
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function clearPreciseFilters() {
|
|
2078
|
+
const input = document.getElementById("preciseSearchInput");
|
|
2079
|
+
if (input) input.value = "";
|
|
2080
|
+
loadPreciseUsersPage();
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
let preciseSearchTimer = null;
|
|
2084
|
+
document.getElementById("preciseSearchInput").addEventListener("input", () => {
|
|
2085
|
+
if (preciseSearchTimer) clearTimeout(preciseSearchTimer);
|
|
2086
|
+
preciseSearchTimer = setTimeout(() => {
|
|
2087
|
+
loadPreciseUsersPage();
|
|
2088
|
+
}, 300);
|
|
2089
|
+
});
|
|
2090
|
+
|
|
1707
2091
|
// ========== 关键词页面 ==========
|
|
1708
2092
|
|
|
1709
2093
|
let currentTags = [];
|
|
@@ -1725,6 +2109,7 @@ function showTagsPage() {
|
|
|
1725
2109
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1726
2110
|
document.getElementById("rawPage").classList.remove("active");
|
|
1727
2111
|
document.getElementById("targetPage").classList.remove("active");
|
|
2112
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1728
2113
|
document.getElementById("tagsPage").classList.add("active");
|
|
1729
2114
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1730
2115
|
showLoading("正在加载关键词...");
|
|
@@ -2199,6 +2584,7 @@ function showTargetPage() {
|
|
|
2199
2584
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
2200
2585
|
document.getElementById("rawPage").classList.remove("active");
|
|
2201
2586
|
document.getElementById("targetPage").classList.add("active");
|
|
2587
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
2202
2588
|
document.getElementById("tagsPage").classList.remove("active");
|
|
2203
2589
|
document.getElementById("settingsPage").classList.remove("active");
|
|
2204
2590
|
showLoading("正在加载目标商家数据...");
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<input type="checkbox" id="fireworkToggle" checked onchange="toggleFireworks()">
|
|
21
21
|
<span>🎆</span>
|
|
22
22
|
</label>
|
|
23
|
-
<a class="script-link" onclick="navigateToSettings()" style="cursor:pointer">⚙️
|
|
23
|
+
<a class="script-link" onclick="navigateToSettings()" style="cursor:pointer">⚙️ 配置</a>
|
|
24
24
|
<a class="script-link" onclick="navigateToWatchdog()" style="cursor:pointer">🐕 看门狗</a>
|
|
25
25
|
<span class="status" id="lastUpdate">--</span>
|
|
26
26
|
</div>
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
<div class="label">目标商家</div>
|
|
60
60
|
<div class="value target" id="statTarget">0</div>
|
|
61
61
|
</div>
|
|
62
|
+
<div class="stat-card clickable" id="statPreciseCard" onclick="navigateToPrecise()">
|
|
63
|
+
<div class="label">精准商家</div>
|
|
64
|
+
<div class="value target" id="statPrecise">0</div>
|
|
65
|
+
</div>
|
|
62
66
|
<div class="stat-card clickable pending-card" id="statCommentTaskCard">
|
|
63
67
|
<div class="label">待评论任务</div>
|
|
64
68
|
<div class="value rose" id="statCommentTask">0</div>
|
|
@@ -379,6 +383,55 @@
|
|
|
379
383
|
</div>
|
|
380
384
|
<div id="backToTop" title="回到顶部(可拖动)">↑</div>
|
|
381
385
|
</div>
|
|
386
|
+
<div id="precisePage">
|
|
387
|
+
<div class="stats" style="margin-bottom:16px">
|
|
388
|
+
<div class="stat-card">
|
|
389
|
+
<div class="label">精准商家</div>
|
|
390
|
+
<div class="value target" id="precisePageStatTotal">0</div>
|
|
391
|
+
</div>
|
|
392
|
+
<div class="stat-card clickable pending-card" onclick="navigateToMain()" style="background:rgba(167,139,250,0.1)">
|
|
393
|
+
<div class="label">← 返回主页面</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<div class="table-wrap">
|
|
397
|
+
<div class="precise-list-header">
|
|
398
|
+
<h3>精准商家列表</h3>
|
|
399
|
+
<div class="view-toggle">
|
|
400
|
+
<button id="preciseViewTableBtn" onclick="setPreciseView('table')">表格</button>
|
|
401
|
+
<button id="preciseViewVideoBtn" onclick="setPreciseView('video')" class="active">视频</button>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
<div class="controls">
|
|
405
|
+
<input type="text" id="preciseSearchInput" placeholder="搜索精准商家用户名 / 昵称...">
|
|
406
|
+
<button onclick="clearPreciseFilters()">清空筛选</button>
|
|
407
|
+
</div>
|
|
408
|
+
<div id="preciseVideoWrap"></div>
|
|
409
|
+
<div class="table-scroll" id="preciseTableWrap" style="display:none">
|
|
410
|
+
<table>
|
|
411
|
+
<thead>
|
|
412
|
+
<tr>
|
|
413
|
+
<th style="width:48px">#</th>
|
|
414
|
+
<th>用户名</th>
|
|
415
|
+
<th>昵称</th>
|
|
416
|
+
<th>粉丝</th>
|
|
417
|
+
<th>视频</th>
|
|
418
|
+
<th>国家</th>
|
|
419
|
+
<th>销量</th>
|
|
420
|
+
<th>评论</th>
|
|
421
|
+
<th>评分</th>
|
|
422
|
+
<th>EC超1k视频数</th>
|
|
423
|
+
<th>最高EC播放</th>
|
|
424
|
+
<th>商品</th>
|
|
425
|
+
</tr>
|
|
426
|
+
</thead>
|
|
427
|
+
<tbody id="preciseTable"></tbody>
|
|
428
|
+
</table>
|
|
429
|
+
</div>
|
|
430
|
+
<div id="preciseMoreHint" onclick="loadPreciseUsersPage(true)"
|
|
431
|
+
style="text-align:center;padding:10px 8px;font-size:13px;color:#6b7280;cursor:default;user-select:none;transition:color 0.2s">
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
382
435
|
<div id="tagsPage">
|
|
383
436
|
<div class="stats" style="margin-bottom:16px">
|
|
384
437
|
<div class="stat-card">
|
|
@@ -488,6 +541,26 @@
|
|
|
488
541
|
</div>
|
|
489
542
|
</div>
|
|
490
543
|
</div>
|
|
544
|
+
<div class="settings-card">
|
|
545
|
+
<h3>🎯 目标国家</h3>
|
|
546
|
+
<p class="settings-hint">设置采集的目标国家列表,保存后写入通用配置文件(~/.tt-help-config.json)。当前仅保存配置,后续接入采集流程后生效。</p>
|
|
547
|
+
<div class="settings-form">
|
|
548
|
+
<div class="settings-field">
|
|
549
|
+
<label>目标国家(英文国家代码,逗号分隔,如 CZ,GR,TH)</label>
|
|
550
|
+
<input type="text" id="targetLocationsInput" placeholder="CZ,GR,HU,PT,ES,PL,NL,BE,DE,FR,IT,IE,AT,TH,US">
|
|
551
|
+
<span class="settings-note">可手动输入,或点击下方国家快速选择。</span>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="settings-field">
|
|
554
|
+
<label>快速选择</label>
|
|
555
|
+
<div class="loc-grid" id="targetCountryGrid"></div>
|
|
556
|
+
</div>
|
|
557
|
+
<div class="settings-status" id="targetLocationsStatus"></div>
|
|
558
|
+
<div class="btn-row">
|
|
559
|
+
<button class="btn-submit" onclick="saveTargetLocations()">保存目标国家</button>
|
|
560
|
+
<button class="btn-cancel" onclick="resetTargetLocations()">恢复默认</button>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
491
564
|
</div>
|
|
492
565
|
<div id="watchdogPage">
|
|
493
566
|
<div class="stats" style="margin-bottom:16px">
|