tt-help-cli-ycl 1.4.144 → 1.4.146

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 (
@@ -1137,6 +1138,10 @@ document.getElementById("statTargetCard").addEventListener("click", () => {
1137
1138
  navigateToTarget();
1138
1139
  });
1139
1140
 
1141
+ document.getElementById("statPreciseCard").addEventListener("click", () => {
1142
+ navigateToPrecise();
1143
+ });
1144
+
1140
1145
  // Hash 路由
1141
1146
  window.addEventListener("hashchange", handleRoute);
1142
1147
  window.addEventListener("DOMContentLoaded", handleRoute);
@@ -1290,6 +1295,8 @@ function handleRoute() {
1290
1295
  showRawPage();
1291
1296
  } else if (hash === "#target") {
1292
1297
  showTargetPage();
1298
+ } else if (hash === "#precise") {
1299
+ showPrecisePage();
1293
1300
  } else if (hash === "#tags") {
1294
1301
  showTagsPage();
1295
1302
  } else if (hash === "#settings") {
@@ -1323,6 +1330,7 @@ function showPendingPage() {
1323
1330
  document.getElementById("userUpdatePage").classList.remove("active");
1324
1331
  document.getElementById("rawPage").classList.remove("active");
1325
1332
  document.getElementById("targetPage").classList.remove("active");
1333
+ document.getElementById("precisePage").classList.remove("active");
1326
1334
  document.getElementById("tagsPage").classList.remove("active");
1327
1335
  document.getElementById("settingsPage").classList.remove("active");
1328
1336
  fetchPendingByCountry();
@@ -1335,6 +1343,7 @@ function showUserUpdatePage() {
1335
1343
  document.getElementById("userUpdatePage").classList.add("active");
1336
1344
  document.getElementById("rawPage").classList.remove("active");
1337
1345
  document.getElementById("targetPage").classList.remove("active");
1346
+ document.getElementById("precisePage").classList.remove("active");
1338
1347
  document.getElementById("tagsPage").classList.remove("active");
1339
1348
  document.getElementById("settingsPage").classList.remove("active");
1340
1349
  fetchUserUpdateByCountry();
@@ -1354,6 +1363,7 @@ function showRawPage() {
1354
1363
  document.getElementById("userUpdatePage").classList.remove("active");
1355
1364
  document.getElementById("rawPage").classList.add("active");
1356
1365
  document.getElementById("targetPage").classList.remove("active");
1366
+ document.getElementById("precisePage").classList.remove("active");
1357
1367
  document.getElementById("tagsPage").classList.remove("active");
1358
1368
  document.getElementById("settingsPage").classList.remove("active");
1359
1369
  fetchRawByCountry();
@@ -1366,6 +1376,7 @@ function showMainPage() {
1366
1376
  document.getElementById("userUpdatePage").classList.remove("active");
1367
1377
  document.getElementById("rawPage").classList.remove("active");
1368
1378
  document.getElementById("targetPage").classList.remove("active");
1379
+ document.getElementById("precisePage").classList.remove("active");
1369
1380
  document.getElementById("tagsPage").classList.remove("active");
1370
1381
  document.getElementById("settingsPage").classList.remove("active");
1371
1382
  document.getElementById("watchdogPage").classList.remove("active");
@@ -1379,6 +1390,20 @@ function navigateToTarget() {
1379
1390
  window.location.hash = "#target";
1380
1391
  }
1381
1392
 
1393
+ function navigateToPrecise() {
1394
+ window.location.hash = "#precise";
1395
+ }
1396
+
1397
+ // product-fill 任务查询条件表单:折叠 / 展开
1398
+ function togglePreciseCriteria() {
1399
+ const body = document.getElementById("preciseCriteriaBody");
1400
+ const arrow = document.getElementById("preciseCriteriaArrow");
1401
+ if (!body) return;
1402
+ const collapsed = body.style.display === "none";
1403
+ body.style.display = collapsed ? "" : "none";
1404
+ if (arrow) arrow.textContent = collapsed ? "▼" : "▶";
1405
+ }
1406
+
1382
1407
  function navigateToTags() {
1383
1408
  window.location.hash = "#tags";
1384
1409
  }
@@ -1399,6 +1424,7 @@ function hideAllPages() {
1399
1424
  document.getElementById("userUpdatePage").classList.remove("active");
1400
1425
  document.getElementById("rawPage").classList.remove("active");
1401
1426
  document.getElementById("targetPage").classList.remove("active");
1427
+ document.getElementById("precisePage").classList.remove("active");
1402
1428
  document.getElementById("tagsPage").classList.remove("active");
1403
1429
  document.getElementById("settingsPage").classList.remove("active");
1404
1430
  document.getElementById("watchdogPage").classList.remove("active");
@@ -1858,6 +1884,220 @@ async function submitAddTags() {
1858
1884
  }
1859
1885
  }
1860
1886
 
1887
+ // ========== 精准商家页面 ==========
1888
+
1889
+ async function showPrecisePage() {
1890
+ hideAllPages();
1891
+ document.getElementById("precisePage").classList.add("active");
1892
+ // 每次进入都默认视频模式
1893
+ setPreciseView("video");
1894
+ showLoading("正在加载精准商家数据...");
1895
+ await loadPreciseUsersPage(false, true);
1896
+ hideLoading();
1897
+ }
1898
+
1899
+ let currentPreciseUsers = [];
1900
+ let currentPreciseTotal = 0;
1901
+ let currentPreciseLoading = false;
1902
+ let currentPreciseAllLoaded = false;
1903
+ let currentPreciseSeq = 0;
1904
+ let currentPreciseView = "video";
1905
+ const PRECISE_PAGE_SIZE = 200;
1906
+
1907
+ async function loadPreciseUsersPage(append = false, skipLoading = false) {
1908
+ if (currentPreciseLoading) return;
1909
+ currentPreciseLoading = true;
1910
+ const seq = ++currentPreciseSeq;
1911
+ const search = document.getElementById("preciseSearchInput")
1912
+ ? document.getElementById("preciseSearchInput").value.trim()
1913
+ : "";
1914
+ const offset = append ? currentPreciseUsers.length : 0;
1915
+ if (!append && !skipLoading) showLoading("正在加载数据...");
1916
+
1917
+ let data = null;
1918
+ let error = null;
1919
+ try {
1920
+ let url = `/api/precise-merchants?limit=${PRECISE_PAGE_SIZE}&offset=${offset}`;
1921
+ if (search) url += `&search=${encodeURIComponent(search)}`;
1922
+ const res = await fetch(url);
1923
+ data = await res.json();
1924
+ } catch (e) {
1925
+ error = e;
1926
+ console.error("加载精准商家失败:", e);
1927
+ } finally {
1928
+ if (!append && !skipLoading) hideLoading();
1929
+ currentPreciseLoading = false;
1930
+ if (error || !data) return;
1931
+ if (seq !== currentPreciseSeq && !append) return;
1932
+ if (!append) {
1933
+ currentPreciseUsers = data.users || [];
1934
+ currentPreciseTotal = data.total || 0;
1935
+ } else {
1936
+ currentPreciseUsers = currentPreciseUsers.concat(data.users || []);
1937
+ currentPreciseTotal = data.total || 0;
1938
+ }
1939
+ currentPreciseAllLoaded = currentPreciseUsers.length >= currentPreciseTotal;
1940
+ renderPreciseView();
1941
+ }
1942
+ }
1943
+
1944
+ function setPreciseView(view) {
1945
+ currentPreciseView = view === "table" ? "table" : "video";
1946
+ const tableBtn = document.getElementById("preciseViewTableBtn");
1947
+ const videoBtn = document.getElementById("preciseViewVideoBtn");
1948
+ if (tableBtn)
1949
+ tableBtn.classList.toggle("active", currentPreciseView === "table");
1950
+ if (videoBtn)
1951
+ videoBtn.classList.toggle("active", currentPreciseView === "video");
1952
+ const videoWrap = document.getElementById("preciseVideoWrap");
1953
+ const tableWrap = document.getElementById("preciseTableWrap");
1954
+ if (videoWrap)
1955
+ videoWrap.style.display = currentPreciseView === "video" ? "" : "none";
1956
+ if (tableWrap)
1957
+ tableWrap.style.display = currentPreciseView === "table" ? "" : "none";
1958
+ renderPreciseView();
1959
+ }
1960
+
1961
+ function renderPreciseView() {
1962
+ if (currentPreciseView === "table") {
1963
+ renderPreciseTable();
1964
+ } else {
1965
+ renderPreciseVideos();
1966
+ }
1967
+ updatePreciseMoreHint();
1968
+ }
1969
+
1970
+ function updatePreciseMoreHint() {
1971
+ const moreHint = document.getElementById("preciseMoreHint");
1972
+ const totalEl = document.getElementById("precisePageStatTotal");
1973
+ if (totalEl)
1974
+ totalEl.textContent = formatStatNum(currentPreciseTotal, { full: true });
1975
+ if (!moreHint) return;
1976
+ if (currentPreciseTotal === 0) {
1977
+ moreHint.textContent = "";
1978
+ moreHint.style.display = "none";
1979
+ } else if (currentPreciseAllLoaded) {
1980
+ moreHint.textContent = `共 ${currentPreciseTotal} 个,已全部加载`;
1981
+ moreHint.onclick = null;
1982
+ moreHint.style.cursor = "default";
1983
+ moreHint.style.color = "#6b7280";
1984
+ } else {
1985
+ moreHint.textContent = "点击加载更多";
1986
+ moreHint.onclick = () => loadPreciseUsersPage(true);
1987
+ moreHint.style.cursor = "pointer";
1988
+ moreHint.style.color = "#60a5fa";
1989
+ }
1990
+ }
1991
+
1992
+ function renderPreciseTable() {
1993
+ const el = document.getElementById("preciseTable");
1994
+ if (!el) return;
1995
+ if (!currentPreciseUsers.length) {
1996
+ el.innerHTML =
1997
+ '<tr><td colspan="12" style="text-align:center;color:#666;padding:20px">暂无精准商家</td></tr>';
1998
+ return;
1999
+ }
2000
+ el.innerHTML = currentPreciseUsers
2001
+ .map((u, i) => {
2002
+ const sold = u.productSoldCount ?? 0;
2003
+ const ec = u.ecOver1k ?? 0;
2004
+ const maxEc = u.maxEcPlay ?? 0;
2005
+ const title = (u.productName || "").replace(/</g, "&lt;");
2006
+ return `<tr>
2007
+ <td style="text-align:right;color:#666">${i + 1}</td>
2008
+ <td><a href="https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}" target="_blank">@${escapeHtml(u.uniqueId)}</a></td>
2009
+ <td>${escapeHtml(u.nickname || "-")}</td>
2010
+ <td>${formatNum(u.followerCount || 0)}</td>
2011
+ <td>${u.videoCount || 0}</td>
2012
+ <td>${u.locationCreated || "-"}</td>
2013
+ <td style="color:#22c55e">${formatNum(sold)}</td>
2014
+ <td style="color:#f87171">${u.productReviewCount ?? 0}</td>
2015
+ <td>${u.productRating != null ? u.productRating : "-"}</td>
2016
+ <td>${ec}</td>
2017
+ <td>${formatNum(maxEc)}</td>
2018
+ <td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${title}">${title || "-"}</td>
2019
+ </tr>`;
2020
+ })
2021
+ .join("");
2022
+ }
2023
+
2024
+ function renderPreciseVideos() {
2025
+ const wrap = document.getElementById("preciseVideoWrap");
2026
+ if (!wrap) return;
2027
+ if (!currentPreciseUsers.length) {
2028
+ wrap.innerHTML =
2029
+ '<div style="color:#666;text-align:center;padding:24px">暂无精准商家</div>';
2030
+ return;
2031
+ }
2032
+ wrap.innerHTML = currentPreciseUsers
2033
+ .map((u) => {
2034
+ const vids = Array.isArray(u.recentEcVideos) ? u.recentEcVideos : [];
2035
+ const maxPlay = vids.reduce((m, v) => Math.max(m, v.playCount || 0), 0);
2036
+ const sold = u.productSoldCount ?? 0;
2037
+ const review = u.productReviewCount ?? 0;
2038
+ const rating = u.productRating != null ? u.productRating : "-";
2039
+ const userLink = `https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}`;
2040
+ const tiles = vids
2041
+ .reduce((acc, v) => {
2042
+ const play = v.playCount || 0;
2043
+ const time = v.createTime ? formatShortDate(v.createTime) : "";
2044
+ const isAd = v.isAd ? '<span class="pv-ad">广告</span>' : "";
2045
+ const prod = v.seoUrl
2046
+ ? `<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>`
2047
+ : "";
2048
+ acc.push(`<div class="precise-video-item">
2049
+ <a class="precise-video-tile" href="${escapeAttr(v.href || "#")}" target="_blank" title="播放视频">
2050
+ <span class="pv-play">▶</span>
2051
+ <span class="pv-count${maxPlay > 0 && play === maxPlay ? " pv-count-max" : ""}">${formatNum(play)}</span>
2052
+ <span class="pv-time">${time}</span>
2053
+ ${isAd}
2054
+ </a>
2055
+ ${prod}
2056
+ </div>`);
2057
+ return acc;
2058
+ }, [])
2059
+ .join("");
2060
+ return `<div class="precise-merchant-card">
2061
+ <div class="precise-merchant-head">
2062
+ <a class="pm-user" href="${userLink}" target="_blank">@${escapeHtml(u.uniqueId)}</a>
2063
+ <span class="pm-nick">${escapeHtml(u.nickname || "-")}</span>
2064
+ <span class="pm-country">${u.locationCreated || "-"}</span>
2065
+ <span class="pm-stats">粉丝 ${formatNum(u.followerCount || 0)} · 视频 ${u.videoCount || 0} · 销量 ${formatNum(sold)} · 评论 ${review} · 评分 ${rating} · EC视频 ${vids.length}</span>
2066
+ </div>
2067
+ ${tiles ? `<div class="precise-video-grid">${tiles}</div>` : '<div style="color:#666;font-size:12px;padding:6px 0">该商家暂无 EC 视频数据</div>'}
2068
+ </div>`;
2069
+ })
2070
+ .join("");
2071
+ }
2072
+
2073
+ function formatShortDate(sec) {
2074
+ if (!sec) return "";
2075
+ const d = new Date(sec * 1000);
2076
+ const pad = (n) => String(n).padStart(2, "0");
2077
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
2078
+ }
2079
+
2080
+ function escapeAttr(str) {
2081
+ return String(str)
2082
+ .replace(/&/g, "&amp;")
2083
+ .replace(/"/g, "&quot;")
2084
+ .replace(/</g, "&lt;");
2085
+ }
2086
+
2087
+ function clearPreciseFilters() {
2088
+ const input = document.getElementById("preciseSearchInput");
2089
+ if (input) input.value = "";
2090
+ loadPreciseUsersPage();
2091
+ }
2092
+
2093
+ let preciseSearchTimer = null;
2094
+ document.getElementById("preciseSearchInput").addEventListener("input", () => {
2095
+ if (preciseSearchTimer) clearTimeout(preciseSearchTimer);
2096
+ preciseSearchTimer = setTimeout(() => {
2097
+ loadPreciseUsersPage();
2098
+ }, 300);
2099
+ });
2100
+
1861
2101
  // ========== 关键词页面 ==========
1862
2102
 
1863
2103
  let currentTags = [];
@@ -1879,6 +2119,7 @@ function showTagsPage() {
1879
2119
  document.getElementById("userUpdatePage").classList.remove("active");
1880
2120
  document.getElementById("rawPage").classList.remove("active");
1881
2121
  document.getElementById("targetPage").classList.remove("active");
2122
+ document.getElementById("precisePage").classList.remove("active");
1882
2123
  document.getElementById("tagsPage").classList.add("active");
1883
2124
  document.getElementById("settingsPage").classList.remove("active");
1884
2125
  showLoading("正在加载关键词...");
@@ -2353,6 +2594,7 @@ function showTargetPage() {
2353
2594
  document.getElementById("userUpdatePage").classList.remove("active");
2354
2595
  document.getElementById("rawPage").classList.remove("active");
2355
2596
  document.getElementById("targetPage").classList.add("active");
2597
+ document.getElementById("precisePage").classList.remove("active");
2356
2598
  document.getElementById("tagsPage").classList.remove("active");
2357
2599
  document.getElementById("settingsPage").classList.remove("active");
2358
2600
  showLoading("正在加载目标商家数据...");
@@ -3569,12 +3811,14 @@ fetchUsers();
3569
3811
  fetchClientErrors();
3570
3812
  initTableSorting();
3571
3813
  initTargetTableSorting();
3814
+ fetchWatchdogBadge();
3572
3815
 
3573
3816
  // 错峰轮询(服务端已缓存,瞬时响应无压力)
3574
3817
  setInterval(fetchStats, 10000);
3575
3818
  setTimeout(() => setInterval(fetchUsers, 10000), 2500);
3576
3819
  setTimeout(() => setInterval(fetchClientErrors, 10000), 5000);
3577
3820
  setTimeout(() => setInterval(refreshUserUpdateIfActive, 10000), 7500);
3821
+ setTimeout(() => setInterval(fetchWatchdogBadge, 30000), 9000);
3578
3822
 
3579
3823
  // ========== 看门狗页面 ==========
3580
3824
 
@@ -3598,6 +3842,7 @@ async function fetchWatchdogData() {
3598
3842
  try {
3599
3843
  const res = await fetch("/api/watchdog/clients");
3600
3844
  _wdData = await res.json();
3845
+ updateWatchdogBadge(_wdData.clients || [], _wdData.clientCommands || {});
3601
3846
  renderWatchdogDashboard(
3602
3847
  _wdData.clients || [],
3603
3848
  _wdData.clientCommands || {},
@@ -3613,6 +3858,51 @@ async function fetchWatchdogData() {
3613
3858
  }
3614
3859
  }
3615
3860
 
3861
+ // ── 导航栏"看门狗"离线 badge ──
3862
+ // 离线数口径与看门狗页面卡片一致:
3863
+ // = 已注册但离线的客户端数 + 配置了命令但从未注册(renderOfflineCard)的 userId 数
3864
+ function updateWatchdogBadge(clients, clientCommands) {
3865
+ const badge = document.getElementById("wdOfflineBadge");
3866
+ if (!badge) return;
3867
+ const offlineClients = clients.filter(function (c) {
3868
+ return !c.online;
3869
+ }).length;
3870
+ const registeredUids = {};
3871
+ clients.forEach(function (c) {
3872
+ if (c.config && c.config.userId) {
3873
+ registeredUids[c.config.userId] = true;
3874
+ }
3875
+ });
3876
+ let neverRegistered = 0;
3877
+ for (var uid in clientCommands) {
3878
+ if (
3879
+ clientCommands[uid] &&
3880
+ clientCommands[uid].length > 0 &&
3881
+ !registeredUids[uid]
3882
+ ) {
3883
+ neverRegistered++;
3884
+ }
3885
+ }
3886
+ const total = offlineClients + neverRegistered;
3887
+ if (total > 0) {
3888
+ badge.textContent = total;
3889
+ badge.style.display = "";
3890
+ } else {
3891
+ badge.style.display = "none";
3892
+ }
3893
+ }
3894
+
3895
+ // 独立轻量轮询:导航栏 badge 在任何页面都实时显示离线数
3896
+ async function fetchWatchdogBadge() {
3897
+ try {
3898
+ const res = await fetch("/api/watchdog/clients");
3899
+ const data = await res.json();
3900
+ updateWatchdogBadge(data.clients || [], data.clientCommands || {});
3901
+ } catch (e) {
3902
+ // 静默失败,保留旧值
3903
+ }
3904
+ }
3905
+
3616
3906
  // ── 渲染辅助 ──
3617
3907
 
3618
3908
  function tag(tagName, attrs, content) {
@@ -21,7 +21,9 @@
21
21
  <span>🎆</span>
22
22
  </label>
23
23
  <a class="script-link" onclick="navigateToSettings()" style="cursor:pointer">⚙️ 配置</a>
24
- <a class="script-link" onclick="navigateToWatchdog()" style="cursor:pointer">🐕 看门狗</a>
24
+ <a class="script-link" onclick="navigateToWatchdog()" style="cursor:pointer;position:relative"
25
+ title="看门狗离线客户端数">🐕 看门狗<span id="wdOfflineBadge"
26
+ style="display:none;position:absolute;top:-9px;right:-8px;background:#ef4444;color:#fff;border-radius:10px;font-size:10px;font-weight:700;line-height:1;padding:3px 6px;box-shadow:0 0 6px rgba(239,68,68,0.6);white-space:nowrap">0</span></a>
25
27
  <span class="status" id="lastUpdate">--</span>
26
28
  </div>
27
29
  </div>
@@ -59,6 +61,10 @@
59
61
  <div class="label">目标商家</div>
60
62
  <div class="value target" id="statTarget">0</div>
61
63
  </div>
64
+ <div class="stat-card clickable" id="statPreciseCard" onclick="navigateToPrecise()">
65
+ <div class="label">精准商家</div>
66
+ <div class="value target" id="statPrecise">0</div>
67
+ </div>
62
68
  <div class="stat-card clickable pending-card" id="statCommentTaskCard">
63
69
  <div class="label">待评论任务</div>
64
70
  <div class="value rose" id="statCommentTask">0</div>
@@ -379,6 +385,98 @@
379
385
  </div>
380
386
  <div id="backToTop" title="回到顶部(可拖动)">↑</div>
381
387
  </div>
388
+ <div id="precisePage">
389
+ <div class="stats" style="margin-bottom:16px">
390
+ <div class="stat-card">
391
+ <div class="label">精准商家</div>
392
+ <div class="value target" id="precisePageStatTotal">0</div>
393
+ </div>
394
+ <div class="stat-card clickable pending-card" onclick="navigateToMain()" style="background:rgba(167,139,250,0.1)">
395
+ <div class="label">← 返回主页面</div>
396
+ </div>
397
+ </div>
398
+ <!-- 精准商家检索条件(静态表单展示,与 src/watch/data-store.js getPreciseMerchantsFromDb 保持一致) -->
399
+ <div style="margin-bottom:16px;background:#1a1a24;border:1px solid #333;border-radius:8px;overflow:hidden">
400
+ <div
401
+ style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;cursor:pointer;user-select:none"
402
+ onclick="togglePreciseCriteria()" title="点击展开/收起">
403
+ <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
404
+ <span style="color:#f59e0b;font-size:14px">🎯</span>
405
+ <span style="font-size:13px;color:#e0e0e0;font-weight:600">精准商家检索条件</span>
406
+ <span style="font-size:11px;color:#666">(阈值参数 · jobs 表)</span>
407
+ </div>
408
+ <span id="preciseCriteriaArrow" style="color:#888;font-size:12px">▼</span>
409
+ </div>
410
+ <div id="preciseCriteriaBody" style="padding:2px 14px 14px">
411
+ <div style="font-size:11px;color:#888;margin-bottom:10px">以下阈值条件全部满足(AND),其余条件为固有规则:</div>
412
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px">
413
+ <div style="background:#14141d;border:1px solid #2a2a3a;border-radius:8px;padding:10px 12px">
414
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
415
+ <span style="font-size:12px;color:#60a5fa;font-weight:600">商品评论数</span>
416
+ <span style="font-size:10px;color:#666;font-family:monospace">product_review_count</span>
417
+ </div>
418
+ <div style="margin-top:6px;font-size:16px;color:#4ade80;font-weight:700">= 0</div>
419
+ <div style="margin-top:4px;font-size:11px;color:#888;line-height:1.5">商品评论数为 0</div>
420
+ </div>
421
+ <div style="background:#14141d;border:1px solid #2a2a3a;border-radius:8px;padding:10px 12px">
422
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
423
+ <span style="font-size:12px;color:#60a5fa;font-weight:600">商品销量</span>
424
+ <span style="font-size:10px;color:#666;font-family:monospace">product_sold_count</span>
425
+ </div>
426
+ <div style="margin-top:6px;font-size:16px;color:#4ade80;font-weight:700">&gt; 10</div>
427
+ <div style="margin-top:4px;font-size:11px;color:#888;line-height:1.5">商品销量大于 10</div>
428
+ </div>
429
+ <div style="background:#14141d;border:1px solid #2a2a3a;border-radius:8px;padding:10px 12px">
430
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
431
+ <span style="font-size:12px;color:#60a5fa;font-weight:600">EC 视频播放量</span>
432
+ <span style="font-size:10px;color:#666;font-family:monospace">playCount</span>
433
+ </div>
434
+ <div style="margin-top:6px;font-size:16px;color:#4ade80;font-weight:700">&gt; 1000</div>
435
+ <div style="margin-top:4px;font-size:11px;color:#888;line-height:1.5">最近 7 天发布的 EC 视频(isECVideo)中,存在播放量超过
436
+ 1000 的视频</div>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </div>
441
+ <div class="table-wrap">
442
+ <div class="precise-list-header">
443
+ <h3>精准商家列表</h3>
444
+ <div class="view-toggle">
445
+ <button id="preciseViewTableBtn" onclick="setPreciseView('table')">表格</button>
446
+ <button id="preciseViewVideoBtn" onclick="setPreciseView('video')" class="active">视频</button>
447
+ </div>
448
+ </div>
449
+ <div class="controls">
450
+ <input type="text" id="preciseSearchInput" placeholder="搜索精准商家用户名 / 昵称...">
451
+ <button onclick="clearPreciseFilters()">清空筛选</button>
452
+ </div>
453
+ <div id="preciseVideoWrap"></div>
454
+ <div class="table-scroll" id="preciseTableWrap" style="display:none">
455
+ <table>
456
+ <thead>
457
+ <tr>
458
+ <th style="width:48px">#</th>
459
+ <th>用户名</th>
460
+ <th>昵称</th>
461
+ <th>粉丝</th>
462
+ <th>视频</th>
463
+ <th>国家</th>
464
+ <th>销量</th>
465
+ <th>评论</th>
466
+ <th>评分</th>
467
+ <th>EC超1k视频数</th>
468
+ <th>最高EC播放</th>
469
+ <th>商品</th>
470
+ </tr>
471
+ </thead>
472
+ <tbody id="preciseTable"></tbody>
473
+ </table>
474
+ </div>
475
+ <div id="preciseMoreHint" onclick="loadPreciseUsersPage(true)"
476
+ style="text-align:center;padding:10px 8px;font-size:13px;color:#6b7280;cursor:default;user-select:none;transition:color 0.2s">
477
+ </div>
478
+ </div>
479
+ </div>
382
480
  <div id="tagsPage">
383
481
  <div class="stats" style="margin-bottom:16px">
384
482
  <div class="stat-card">