tt-help-cli-ycl 1.4.144 → 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.
@@ -608,6 +608,92 @@ function getTargetUsersByCountryFromDb(targetLocations = [], options = {}) {
608
608
  };
609
609
  }
610
610
 
611
+ /**
612
+ * 精确商家:目标商家(tt_seller=1 且 verified=0 且 目标国家)中,
613
+ * 最近 7 天有 isECVideo 播放 > 1000 的视频,且有商品、商品评论数=0、销量 > 10。
614
+ * 支持分页与搜索(unique_id/nickname),返回 total + users。
615
+ */
616
+ function getPreciseMerchantsFromDb(targetLocations = [], options = {}) {
617
+ if (!getDb()) return null;
618
+ if (!targetLocations.length) return { total: 0, users: [] };
619
+
620
+ const { limit, offset, search } = options;
621
+ const placeholders = targetLocations.map(() => "?").join(", ");
622
+
623
+ const where = `
624
+ tt_seller = 1
625
+ AND verified = 0
626
+ AND location_created IN (${placeholders})
627
+ AND top_video_seo_url IS NOT NULL AND top_video_seo_url != ''
628
+ AND product_review_count = 0
629
+ AND product_sold_count > 10
630
+ AND recent_ec_videos IS NOT NULL AND recent_ec_videos != ''
631
+ AND EXISTS (
632
+ SELECT 1 FROM json_each(recent_ec_videos)
633
+ WHERE CAST(json_extract(value, '$.playCount') AS INTEGER) > 1000
634
+ )
635
+ `;
636
+
637
+ let searchSql = "";
638
+ const searchParams = [];
639
+ if (search) {
640
+ searchSql = ` AND (unique_id LIKE ? OR nickname LIKE ?)`;
641
+ searchParams.push(`%${search}%`, `%${search}%`);
642
+ }
643
+
644
+ const totalRow = getDb()
645
+ .prepare(`SELECT COUNT(*) AS n FROM jobs WHERE ${where}${searchSql}`)
646
+ .get(...targetLocations, ...searchParams);
647
+ const total = totalRow?.n || 0;
648
+
649
+ const selectSql = `
650
+ SELECT
651
+ unique_id,
652
+ nickname,
653
+ sec_uid,
654
+ status,
655
+ follower_count,
656
+ video_count,
657
+ location_created,
658
+ latest_video_time,
659
+ refresh_time,
660
+ top_video_play_count,
661
+ top_video_href,
662
+ top_video_is_ad,
663
+ top_video_product_id,
664
+ top_video_product_title,
665
+ top_video_seo_url,
666
+ product_rating,
667
+ product_review_count,
668
+ product_sold_count,
669
+ product_name,
670
+ recent_ec_videos,
671
+ (SELECT COUNT(*) FROM json_each(recent_ec_videos)
672
+ WHERE CAST(json_extract(value, '$.playCount') AS INTEGER) > 1000) AS ecOver1k,
673
+ (SELECT MAX(CAST(json_extract(value, '$.playCount') AS INTEGER))
674
+ FROM json_each(recent_ec_videos)) AS maxEcPlay
675
+ FROM jobs
676
+ WHERE ${where}${searchSql}
677
+ ORDER BY product_sold_count DESC, unique_id ASC
678
+ `;
679
+
680
+ if (limit !== undefined) {
681
+ const safeLimit = Math.max(Math.min(Math.floor(limit), 1000), 1);
682
+ const safeOffset = Math.max(Math.floor(offset || 0), 0);
683
+ const rows = getDb()
684
+ .prepare(`${selectSql} LIMIT ? OFFSET ?`)
685
+ .all(...targetLocations, ...searchParams, safeLimit, safeOffset)
686
+ .map(mapJobRow);
687
+ return { total, limit: safeLimit, offset: safeOffset, users: rows };
688
+ }
689
+
690
+ const rows = getDb()
691
+ .prepare(selectSql)
692
+ .all(...targetLocations, ...searchParams)
693
+ .map(mapJobRow);
694
+ return { total, users: rows };
695
+ }
696
+
611
697
  /**
612
698
  * 查询已修改(modified_at IS NOT NULL)且 location_created 不在 targetLocations 中的目标商家,
613
699
  * 按国家分组返回。
@@ -3549,6 +3635,8 @@ export function createStore(filePath, options = {}) {
3549
3635
  getRawJobsPage: getRawJobsPageFromDb,
3550
3636
  getTargetUsers: getTargetUsersFromDb,
3551
3637
  getTargetUsersByCountry: getTargetUsersByCountryFromDb,
3638
+ getPreciseMerchants: (targetLocations, options) =>
3639
+ getPreciseMerchantsFromDb(targetLocations, options),
3552
3640
  getModifiedTargetUsersOutsideLocations: (loc) =>
3553
3641
  getModifiedTargetUsersOutsideLocations(loc),
3554
3642
  getStats,
@@ -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,10 @@ function navigateToTarget() {
1379
1390
  window.location.hash = "#target";
1380
1391
  }
1381
1392
 
1393
+ function navigateToPrecise() {
1394
+ window.location.hash = "#precise";
1395
+ }
1396
+
1382
1397
  function navigateToTags() {
1383
1398
  window.location.hash = "#tags";
1384
1399
  }
@@ -1399,6 +1414,7 @@ function hideAllPages() {
1399
1414
  document.getElementById("userUpdatePage").classList.remove("active");
1400
1415
  document.getElementById("rawPage").classList.remove("active");
1401
1416
  document.getElementById("targetPage").classList.remove("active");
1417
+ document.getElementById("precisePage").classList.remove("active");
1402
1418
  document.getElementById("tagsPage").classList.remove("active");
1403
1419
  document.getElementById("settingsPage").classList.remove("active");
1404
1420
  document.getElementById("watchdogPage").classList.remove("active");
@@ -1858,6 +1874,220 @@ async function submitAddTags() {
1858
1874
  }
1859
1875
  }
1860
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
+
1861
2091
  // ========== 关键词页面 ==========
1862
2092
 
1863
2093
  let currentTags = [];
@@ -1879,6 +2109,7 @@ function showTagsPage() {
1879
2109
  document.getElementById("userUpdatePage").classList.remove("active");
1880
2110
  document.getElementById("rawPage").classList.remove("active");
1881
2111
  document.getElementById("targetPage").classList.remove("active");
2112
+ document.getElementById("precisePage").classList.remove("active");
1882
2113
  document.getElementById("tagsPage").classList.add("active");
1883
2114
  document.getElementById("settingsPage").classList.remove("active");
1884
2115
  showLoading("正在加载关键词...");
@@ -2353,6 +2584,7 @@ function showTargetPage() {
2353
2584
  document.getElementById("userUpdatePage").classList.remove("active");
2354
2585
  document.getElementById("rawPage").classList.remove("active");
2355
2586
  document.getElementById("targetPage").classList.add("active");
2587
+ document.getElementById("precisePage").classList.remove("active");
2356
2588
  document.getElementById("tagsPage").classList.remove("active");
2357
2589
  document.getElementById("settingsPage").classList.remove("active");
2358
2590
  showLoading("正在加载目标商家数据...");
@@ -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">
@@ -112,6 +112,12 @@ body {
112
112
  color: #f472b6;
113
113
  }
114
114
 
115
+ /* 精准商家卡片文字改为红色 */
116
+ #statPreciseCard .label,
117
+ #statPreciseCard .value {
118
+ color: #ef4444;
119
+ }
120
+
115
121
  .stat-card .value-sub {
116
122
  font-size: 12px;
117
123
  font-weight: 400;
@@ -878,6 +884,14 @@ td.user-id:hover {
878
884
  display: block;
879
885
  }
880
886
 
887
+ #precisePage {
888
+ display: none;
889
+ }
890
+
891
+ #precisePage.active {
892
+ display: block;
893
+ }
894
+
881
895
  #backToTop {
882
896
  position: fixed;
883
897
  bottom: 32px;
@@ -1892,3 +1906,191 @@ td.user-id:hover {
1892
1906
  color: #888;
1893
1907
  font-weight: 600;
1894
1908
  }
1909
+
1910
+ /* ===== 精准商家:视频卡片视图 ===== */
1911
+ .precise-list-header {
1912
+ display: flex;
1913
+ align-items: center;
1914
+ justify-content: space-between;
1915
+ gap: 10px;
1916
+ margin-bottom: 8px;
1917
+ }
1918
+ .precise-list-header h3 {
1919
+ margin: 0;
1920
+ }
1921
+ .view-toggle {
1922
+ display: inline-flex;
1923
+ align-items: center;
1924
+ gap: 2px;
1925
+ border-bottom: 2px solid #2f2f3f;
1926
+ }
1927
+ .view-toggle button {
1928
+ padding: 7px 18px;
1929
+ border: none;
1930
+ border-bottom: 2px solid transparent;
1931
+ border-radius: 6px 6px 0 0;
1932
+ background: transparent;
1933
+ color: #9aa;
1934
+ font-size: 13px;
1935
+ cursor: pointer;
1936
+ transition: all 0.2s;
1937
+ }
1938
+ .view-toggle button:hover {
1939
+ color: #ccc;
1940
+ }
1941
+ .view-toggle button.active {
1942
+ color: #fff;
1943
+ border-bottom-color: #7c3aed;
1944
+ background: rgba(124, 58, 237, 0.14);
1945
+ font-weight: 600;
1946
+ }
1947
+
1948
+ .precise-video-wrap {
1949
+ display: flex;
1950
+ flex-direction: column;
1951
+ gap: 14px;
1952
+ }
1953
+ .precise-merchant-card {
1954
+ background: #1a1a24;
1955
+ border: 1px solid #2a2a3a;
1956
+ border-radius: 10px;
1957
+ padding: 12px 14px;
1958
+ }
1959
+ .precise-merchant-head {
1960
+ display: flex;
1961
+ flex-wrap: wrap;
1962
+ align-items: baseline;
1963
+ gap: 10px;
1964
+ margin-bottom: 10px;
1965
+ }
1966
+ .precise-merchant-head .pm-user {
1967
+ font-weight: 700;
1968
+ color: #60a5fa;
1969
+ text-decoration: none;
1970
+ font-size: 15px;
1971
+ }
1972
+ .precise-merchant-head .pm-user:hover {
1973
+ text-decoration: underline;
1974
+ }
1975
+ .precise-merchant-head .pm-nick {
1976
+ color: #ccc;
1977
+ font-size: 13px;
1978
+ }
1979
+ .precise-merchant-head .pm-country {
1980
+ font-size: 11px;
1981
+ background: #2a2a3a;
1982
+ color: #a78bfa;
1983
+ padding: 1px 7px;
1984
+ border-radius: 10px;
1985
+ }
1986
+ .precise-merchant-head .pm-stats {
1987
+ color: #888;
1988
+ font-size: 12px;
1989
+ margin-left: auto;
1990
+ }
1991
+
1992
+ .precise-video-grid {
1993
+ display: grid;
1994
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
1995
+ gap: 10px;
1996
+ }
1997
+ .precise-video-item {
1998
+ background: #20202e;
1999
+ border: 1px solid #2c2c3c;
2000
+ border-radius: 8px;
2001
+ overflow: hidden;
2002
+ position: relative;
2003
+ display: flex;
2004
+ flex-direction: column;
2005
+ }
2006
+ .precise-video-tile {
2007
+ display: block;
2008
+ position: relative;
2009
+ aspect-ratio: 9 / 14;
2010
+ background: linear-gradient(160deg, #2a2a3a 0%, #1c1c28 100%);
2011
+ color: #e0e0e0;
2012
+ text-decoration: none;
2013
+ overflow: hidden;
2014
+ }
2015
+ .precise-video-tile:hover {
2016
+ border-color: #7c3aed;
2017
+ }
2018
+ .precise-video-tile .pv-play {
2019
+ position: absolute;
2020
+ left: 50%;
2021
+ top: 50%;
2022
+ transform: translate(-50%, -50%);
2023
+ width: 42px;
2024
+ height: 42px;
2025
+ border-radius: 50%;
2026
+ background: rgba(0, 0, 0, 0.55);
2027
+ display: flex;
2028
+ align-items: center;
2029
+ justify-content: center;
2030
+ font-size: 20px;
2031
+ color: #fff;
2032
+ }
2033
+ .precise-video-tile .pv-count {
2034
+ position: absolute;
2035
+ left: 6px;
2036
+ top: 6px;
2037
+ font-size: 12px;
2038
+ color: #fff;
2039
+ text-shadow: 0 1px 2px #000;
2040
+ }
2041
+ /* 播放量最大的视频:播放量文字特殊高亮 */
2042
+ .precise-video-tile .pv-count-max {
2043
+ color: #1a1a24;
2044
+ font-weight: 800;
2045
+ background: #fbbf24;
2046
+ padding: 1px 5px;
2047
+ border-radius: 4px;
2048
+ text-shadow: none;
2049
+ box-shadow: 0 0 6px rgba(251, 191, 36, 0.8);
2050
+ }
2051
+ .precise-video-tile .pv-time {
2052
+ position: absolute;
2053
+ left: 6px;
2054
+ top: 24px;
2055
+ font-size: 11px;
2056
+ color: #cfcfcf;
2057
+ text-shadow: 0 1px 2px #000;
2058
+ }
2059
+ .precise-video-tile .pv-ad {
2060
+ position: absolute;
2061
+ right: 6px;
2062
+ top: 6px;
2063
+ font-size: 10px;
2064
+ background: rgba(254, 44, 85, 0.9);
2065
+ color: #fff;
2066
+ padding: 1px 5px;
2067
+ border-radius: 4px;
2068
+ }
2069
+ /* 商品名悬浮在图片上,最多 2 行、固定高度以容纳 2 行文字 */
2070
+ .precise-video-item .pv-product {
2071
+ position: absolute;
2072
+ left: 0;
2073
+ right: 0;
2074
+ bottom: 0;
2075
+ padding: 30px 8px 8px;
2076
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.92), rgba(0, 0, 0, 0));
2077
+ color: #fbbf24;
2078
+ font-size: 12px;
2079
+ line-height: 1.35;
2080
+ text-decoration: none;
2081
+ text-align: left;
2082
+ display: -webkit-box;
2083
+ -webkit-box-orient: vertical;
2084
+ -webkit-line-clamp: 2;
2085
+ line-clamp: 2;
2086
+ overflow: hidden;
2087
+ box-sizing: border-box;
2088
+ height: 71px;
2089
+ }
2090
+ .precise-video-item .pv-product:hover {
2091
+ color: #fff;
2092
+ text-decoration: underline;
2093
+ }
2094
+ .precise-video-item .pv-product .pv-product-icon {
2095
+ margin-right: 4px;
2096
+ }