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.
@@ -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 TARGET_LOCATIONS = [
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 = TARGET_LOCATIONS.map(
710
- (loc) =>
711
- `<button class="loc-option ${loc === currentLocation ? "active" : ""}" onclick="selectLocation('${safeId}','${loc}')">${loc}</button>`,
712
- ).join("");
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
- const TARGET_LOCATIONS = [
1602
- "AT",
1603
- "BE",
1604
- "CZ",
1605
- "DE",
1606
- "ES",
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, "&lt;");
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, "&amp;")
2073
+ .replace(/"/g, "&quot;")
2074
+ .replace(/</g, "&lt;");
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">⚙️ LLM</a>
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">