tt-help-cli-ycl 1.4.149 → 1.4.151
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/add-claim-index.mjs +28 -0
- package/scripts/checkpoint-wal.mjs +20 -0
- package/scripts/test-photo-post.mjs +156 -0
- package/scripts/test-precheck-perf.mjs +270 -0
- package/scripts/test-product-fill-cdp.mjs +44 -0
- package/scripts/test-product-fill-db.mjs +43 -0
- package/scripts/test-seller-any-country.mjs +167 -0
- package/src/cli/product-fill.js +13 -1
- package/src/lib/api-interceptor.js +10 -1
- package/src/lib/app-config.js +15 -1
- package/src/lib/browser/cdp.js +2 -0
- package/src/lib/browser/page.js +14 -0
- package/src/lib/shop-scraper.mjs +79 -0
- package/src/lib/target-locations.js +8 -5
- package/src/scraper/explore-core.js +8 -2
- package/src/watch/data-store.js +136 -130
- package/src/watch/db-columns.js +1 -0
- package/src/watch/db-schema.js +17 -5
- package/src/watch/db-tags.js +15 -5
- package/src/watch/public/app.js +285 -50
- package/src/watch/public/index.html +66 -46
- package/src/watch/public/style.css +267 -0
- package/src/watch/server.js +16 -2
package/src/watch/public/app.js
CHANGED
|
@@ -182,7 +182,7 @@ function renderStats() {
|
|
|
182
182
|
flashEl("statTotal", d.totalUsers);
|
|
183
183
|
flashEl("statProcessing", d.processingUsers || 0);
|
|
184
184
|
flashEl("statDone", d.processedUsers);
|
|
185
|
-
flashEl("statPending", d.pendingUsers);
|
|
185
|
+
flashEl("statPending", d.pendingUsers, { full: true });
|
|
186
186
|
flashEl("statError", d.errorUsers);
|
|
187
187
|
flashEl("statTarget", d.targetUsers, { full: true });
|
|
188
188
|
flashEl("statPrecise", d.preciseMerchants, { full: true });
|
|
@@ -206,7 +206,8 @@ function renderStats() {
|
|
|
206
206
|
const pendingTotal = document.getElementById("pendingStatTotal");
|
|
207
207
|
if (pendingTotal) pendingTotal.textContent = formatStatNum(d.totalUsers);
|
|
208
208
|
const pendingCount = document.getElementById("pendingStatPending");
|
|
209
|
-
if (pendingCount)
|
|
209
|
+
if (pendingCount)
|
|
210
|
+
pendingCount.textContent = formatStatNum(d.pendingUsers, { full: true });
|
|
210
211
|
const pendingUserUpdate = document.getElementById(
|
|
211
212
|
"pendingStatUserUpdateTasks",
|
|
212
213
|
);
|
|
@@ -222,7 +223,9 @@ function renderStats() {
|
|
|
222
223
|
userUpdateTotal.textContent = formatStatNum(d.totalUsers);
|
|
223
224
|
const userUpdatePending = document.getElementById("userUpdateStatPending");
|
|
224
225
|
if (userUpdatePending)
|
|
225
|
-
userUpdatePending.textContent = formatStatNum(d.pendingUsers
|
|
226
|
+
userUpdatePending.textContent = formatStatNum(d.pendingUsers, {
|
|
227
|
+
full: true,
|
|
228
|
+
});
|
|
226
229
|
const userUpdateTasks = document.getElementById(
|
|
227
230
|
"userUpdateStatUserUpdateTasks",
|
|
228
231
|
);
|
|
@@ -238,7 +241,9 @@ function renderStats() {
|
|
|
238
241
|
rawPageRawJobs.textContent = formatStatNum(d.rawJobs || 0);
|
|
239
242
|
const rawPagePending = document.getElementById("rawPageStatPending");
|
|
240
243
|
if (rawPagePending)
|
|
241
|
-
rawPagePending.textContent = formatStatNum(d.pendingUsers || 0
|
|
244
|
+
rawPagePending.textContent = formatStatNum(d.pendingUsers || 0, {
|
|
245
|
+
full: true,
|
|
246
|
+
});
|
|
242
247
|
const rawPageUserUpdate = document.getElementById(
|
|
243
248
|
"rawPageStatUserUpdateTasks",
|
|
244
249
|
);
|
|
@@ -247,11 +252,11 @@ function renderStats() {
|
|
|
247
252
|
document.getElementById("lastUpdate").textContent =
|
|
248
253
|
"更新于 " + new Date().toLocaleTimeString();
|
|
249
254
|
document.getElementById("fileMeta").textContent =
|
|
250
|
-
formatStatNum(d.processingUsers || 0) +
|
|
255
|
+
formatStatNum(d.processingUsers || 0, { full: true }) +
|
|
251
256
|
" 处理中, " +
|
|
252
|
-
formatStatNum(d.totalUsers) +
|
|
257
|
+
formatStatNum(d.totalUsers, { full: true }) +
|
|
253
258
|
" 个用户, 待处理 " +
|
|
254
|
-
formatStatNum(d.pendingUsers || 0);
|
|
259
|
+
formatStatNum(d.pendingUsers || 0, { full: true });
|
|
255
260
|
|
|
256
261
|
renderCountryChart(d.countryStats);
|
|
257
262
|
renderSourceChart(d.sourceStats);
|
|
@@ -700,6 +705,8 @@ const DEFAULT_LOCATION_OPTIONS = [
|
|
|
700
705
|
"PT",
|
|
701
706
|
"TH",
|
|
702
707
|
"US",
|
|
708
|
+
"MY",
|
|
709
|
+
"VN",
|
|
703
710
|
];
|
|
704
711
|
let locationOptions = [...DEFAULT_LOCATION_OPTIONS];
|
|
705
712
|
|
|
@@ -989,7 +996,10 @@ function escapeJsString(str) {
|
|
|
989
996
|
}
|
|
990
997
|
|
|
991
998
|
document.addEventListener("keydown", (e) => {
|
|
992
|
-
if (e.key === "Escape")
|
|
999
|
+
if (e.key === "Escape") {
|
|
1000
|
+
closeAddModal();
|
|
1001
|
+
closeSkuModal();
|
|
1002
|
+
}
|
|
993
1003
|
});
|
|
994
1004
|
|
|
995
1005
|
document.getElementById("userTable").addEventListener("click", (e) => {
|
|
@@ -1299,8 +1309,12 @@ function handleRoute() {
|
|
|
1299
1309
|
showPrecisePage();
|
|
1300
1310
|
} else if (hash === "#tags") {
|
|
1301
1311
|
showTagsPage();
|
|
1302
|
-
} else if (hash === "#settings") {
|
|
1312
|
+
} else if (hash === "#settings" || hash.startsWith("#settings/")) {
|
|
1303
1313
|
showSettingsPage();
|
|
1314
|
+
const sub = hash.startsWith("#settings/")
|
|
1315
|
+
? hash.slice("#settings/".length)
|
|
1316
|
+
: "";
|
|
1317
|
+
switchSettingsPanel(sub || "llm", false);
|
|
1304
1318
|
} else if (hash === "#watchdog") {
|
|
1305
1319
|
showWatchdogPage();
|
|
1306
1320
|
} else {
|
|
@@ -1437,6 +1451,24 @@ function showSettingsPage() {
|
|
|
1437
1451
|
loadTargetLocations();
|
|
1438
1452
|
}
|
|
1439
1453
|
|
|
1454
|
+
// 设置页左侧分类切换:panel 对应 settings-nav-item 的 data-panel 与 settingsPanel-<panel> 容器
|
|
1455
|
+
function switchSettingsPanel(panel, updateHash = true) {
|
|
1456
|
+
document
|
|
1457
|
+
.querySelectorAll("#settingsNav .settings-nav-item")
|
|
1458
|
+
.forEach((btn) =>
|
|
1459
|
+
btn.classList.toggle("active", btn.dataset.panel === panel),
|
|
1460
|
+
);
|
|
1461
|
+
document
|
|
1462
|
+
.querySelectorAll(".settings-panel")
|
|
1463
|
+
.forEach((el) =>
|
|
1464
|
+
el.classList.toggle("active", el.id === "settingsPanel-" + panel),
|
|
1465
|
+
);
|
|
1466
|
+
if (updateHash) {
|
|
1467
|
+
const target = panel === "llm" ? "#settings" : "#settings/" + panel;
|
|
1468
|
+
if (window.location.hash !== target) window.location.hash = target;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1440
1472
|
// ========== 目标国家配置(通用 app-config,落盘 ~/.tt-help-config.json) ==========
|
|
1441
1473
|
|
|
1442
1474
|
const AVAILABLE_COUNTRIES = [
|
|
@@ -1455,6 +1487,8 @@ const AVAILABLE_COUNTRIES = [
|
|
|
1455
1487
|
"PT",
|
|
1456
1488
|
"TH",
|
|
1457
1489
|
"US",
|
|
1490
|
+
"MY",
|
|
1491
|
+
"VN",
|
|
1458
1492
|
"GB",
|
|
1459
1493
|
"SE",
|
|
1460
1494
|
"NO",
|
|
@@ -1476,6 +1510,11 @@ const AVAILABLE_COUNTRIES = [
|
|
|
1476
1510
|
"CO",
|
|
1477
1511
|
];
|
|
1478
1512
|
|
|
1513
|
+
// ========== 目标国家配置(宫格 + 拖拽排序,位置即领取优先级) ==========
|
|
1514
|
+
|
|
1515
|
+
let _targetLocations = []; // 顺序即领取优先级,拖拽/增删后需点保存才落盘
|
|
1516
|
+
let _targetLocationsSaved = []; // 最近一次加载/保存的快照,用于脏状态提示
|
|
1517
|
+
|
|
1479
1518
|
function normalizeCountryInput(str) {
|
|
1480
1519
|
return [
|
|
1481
1520
|
...new Set(
|
|
@@ -1487,20 +1526,39 @@ function normalizeCountryInput(str) {
|
|
|
1487
1526
|
];
|
|
1488
1527
|
}
|
|
1489
1528
|
|
|
1529
|
+
/** 两位国家代码 → 国旗 emoji */
|
|
1530
|
+
function countryFlag(code) {
|
|
1531
|
+
if (!/^[A-Za-z]{2}$/.test(code)) return "🏳️";
|
|
1532
|
+
return String.fromCodePoint(
|
|
1533
|
+
...[...code.toUpperCase()].map((c) => 0x1f1e6 + c.charCodeAt(0) - 65),
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function targetLocationsDirty() {
|
|
1538
|
+
return _targetLocations.join(",") !== _targetLocationsSaved.join(",");
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function updateTargetLocationsStatus() {
|
|
1542
|
+
const status = document.getElementById("targetLocationsStatus");
|
|
1543
|
+
if (!status) return;
|
|
1544
|
+
if (targetLocationsDirty()) {
|
|
1545
|
+
status.textContent = `⛔ 有未保存的修改(当前 ${_targetLocations.length} 国,顺序: ${_targetLocations.join(" → ")}),点“保存目标国家”生效`;
|
|
1546
|
+
status.className = "settings-status warn";
|
|
1547
|
+
} else {
|
|
1548
|
+
status.textContent = `✅ 已生效:${_targetLocations.length} 国,位置即领取优先级(越靠前越先领)`;
|
|
1549
|
+
status.className = "settings-status ok";
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1490
1553
|
async function loadTargetLocations() {
|
|
1491
|
-
const input = document.getElementById("targetLocationsInput");
|
|
1492
1554
|
const status = document.getElementById("targetLocationsStatus");
|
|
1493
|
-
if (!input) return;
|
|
1494
1555
|
try {
|
|
1495
1556
|
const res = await fetch("/api/settings/target-locations");
|
|
1496
1557
|
const data = await res.json();
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
status.textContent = `当前 ${locations.length} 个国家: ${locations.join(", ")}`;
|
|
1502
|
-
status.className = "settings-status ok";
|
|
1503
|
-
}
|
|
1558
|
+
_targetLocations = (data.locations || []).map(String);
|
|
1559
|
+
_targetLocationsSaved = [..._targetLocations];
|
|
1560
|
+
renderTargetLocationsGrid();
|
|
1561
|
+
updateTargetLocationsStatus();
|
|
1504
1562
|
} catch (e) {
|
|
1505
1563
|
console.error("loadTargetLocations error:", e);
|
|
1506
1564
|
if (status) {
|
|
@@ -1510,47 +1568,151 @@ async function loadTargetLocations() {
|
|
|
1510
1568
|
}
|
|
1511
1569
|
}
|
|
1512
1570
|
|
|
1513
|
-
function
|
|
1514
|
-
const grid = document.getElementById("
|
|
1571
|
+
function renderTargetLocationsGrid() {
|
|
1572
|
+
const grid = document.getElementById("targetLocationsGrid");
|
|
1515
1573
|
if (!grid) return;
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1574
|
+
const chips = _targetLocations
|
|
1575
|
+
.map(
|
|
1576
|
+
(loc, i) => `
|
|
1577
|
+
<div class="loc-chip" draggable="true" data-loc="${loc}" data-idx="${i}" title="拖动排序(位置即优先级)">
|
|
1578
|
+
<span class="loc-chip-idx">${i + 1}</span>
|
|
1579
|
+
<span class="loc-chip-flag">${countryFlag(loc)}</span>
|
|
1580
|
+
<span class="loc-chip-code">${loc}</span>
|
|
1581
|
+
<button type="button" class="loc-chip-del" onclick="removeTargetLocation('${loc}')" title="移除 ${loc}">×</button>
|
|
1582
|
+
</div>`,
|
|
1583
|
+
)
|
|
1584
|
+
.join("");
|
|
1585
|
+
grid.innerHTML =
|
|
1586
|
+
chips +
|
|
1587
|
+
`<button type="button" class="loc-chip loc-chip-add" onclick="toggleCountryPicker()" title="添加国家">+</button>`;
|
|
1588
|
+
bindLocChipDrag(grid);
|
|
1589
|
+
renderCountryPicker();
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// ---- 拖拽排序(HTML5 DnD) ----
|
|
1593
|
+
let _dragLoc = null;
|
|
1594
|
+
|
|
1595
|
+
function bindLocChipDrag(grid) {
|
|
1596
|
+
grid.querySelectorAll(".loc-chip[draggable]").forEach((chip) => {
|
|
1597
|
+
chip.addEventListener("dragstart", (e) => {
|
|
1598
|
+
_dragLoc = chip.dataset.loc;
|
|
1599
|
+
chip.classList.add("dragging");
|
|
1600
|
+
e.dataTransfer.effectAllowed = "move";
|
|
1601
|
+
e.dataTransfer.setData("text/plain", _dragLoc);
|
|
1602
|
+
});
|
|
1603
|
+
chip.addEventListener("dragend", () => {
|
|
1604
|
+
_dragLoc = null;
|
|
1605
|
+
chip.classList.remove("dragging");
|
|
1606
|
+
grid
|
|
1607
|
+
.querySelectorAll(".drop-before,.drop-after")
|
|
1608
|
+
.forEach((el) => el.classList.remove("drop-before", "drop-after"));
|
|
1609
|
+
});
|
|
1610
|
+
chip.addEventListener("dragover", (e) => {
|
|
1611
|
+
e.preventDefault();
|
|
1612
|
+
e.dataTransfer.dropEffect = "move";
|
|
1613
|
+
if (_dragLoc == null || chip.dataset.loc === _dragLoc) return;
|
|
1614
|
+
const rect = chip.getBoundingClientRect();
|
|
1615
|
+
const before = e.clientX < rect.left + rect.width / 2;
|
|
1616
|
+
chip.classList.toggle("drop-before", before);
|
|
1617
|
+
chip.classList.toggle("drop-after", !before);
|
|
1618
|
+
});
|
|
1619
|
+
chip.addEventListener("dragleave", () => {
|
|
1620
|
+
chip.classList.remove("drop-before", "drop-after");
|
|
1621
|
+
});
|
|
1622
|
+
chip.addEventListener("drop", (e) => {
|
|
1623
|
+
e.preventDefault();
|
|
1624
|
+
if (_dragLoc == null || chip.dataset.loc === _dragLoc) return;
|
|
1625
|
+
const rect = chip.getBoundingClientRect();
|
|
1626
|
+
const before = e.clientX < rect.left + rect.width / 2;
|
|
1627
|
+
moveTargetLocation(_dragLoc, Number(chip.dataset.idx) + (before ? 0 : 1));
|
|
1628
|
+
});
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/** 把 fromLoc 移动到目标插入位(insertAt 为移除前语义下的目标下标) */
|
|
1633
|
+
function moveTargetLocation(fromLoc, insertAt) {
|
|
1634
|
+
const fromIdx = _targetLocations.indexOf(fromLoc);
|
|
1635
|
+
if (fromIdx === -1) return;
|
|
1636
|
+
_targetLocations.splice(fromIdx, 1);
|
|
1637
|
+
const target = Math.max(0, Math.min(insertAt, _targetLocations.length));
|
|
1638
|
+
_targetLocations.splice(target, 0, fromLoc);
|
|
1639
|
+
renderTargetLocationsGrid();
|
|
1640
|
+
updateTargetLocationsStatus();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// ---- 添加 / 删除 ----
|
|
1644
|
+
function removeTargetLocation(loc) {
|
|
1645
|
+
_targetLocations = _targetLocations.filter((l) => l !== loc);
|
|
1646
|
+
renderTargetLocationsGrid();
|
|
1647
|
+
updateTargetLocationsStatus();
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function addTargetLocation(locRaw) {
|
|
1651
|
+
const loc = String(locRaw || "")
|
|
1652
|
+
.trim()
|
|
1653
|
+
.toUpperCase();
|
|
1654
|
+
if (!/^[A-Z]{2}$/.test(loc)) return false;
|
|
1655
|
+
if (_targetLocations.includes(loc)) return false;
|
|
1656
|
+
_targetLocations.push(loc); // 新增排最后(优先级最低),可拖动调整
|
|
1657
|
+
renderTargetLocationsGrid();
|
|
1658
|
+
updateTargetLocationsStatus();
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ---- 添加面板(+展开) ----
|
|
1663
|
+
function toggleCountryPicker() {
|
|
1664
|
+
const picker = document.getElementById("countryPicker");
|
|
1665
|
+
if (!picker) return;
|
|
1666
|
+
picker.style.display = picker.style.display === "none" ? "block" : "none";
|
|
1667
|
+
renderCountryPicker();
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function renderCountryPicker() {
|
|
1671
|
+
const picker = document.getElementById("countryPicker");
|
|
1672
|
+
if (!picker || picker.style.display === "none") return;
|
|
1673
|
+
const selected = new Set(_targetLocations);
|
|
1674
|
+
const grid = document.getElementById("countryPickerGrid");
|
|
1675
|
+
if (grid) {
|
|
1676
|
+
grid.innerHTML = AVAILABLE_COUNTRIES.filter((loc) => !selected.has(loc))
|
|
1677
|
+
.map(
|
|
1678
|
+
(loc) =>
|
|
1679
|
+
`<button type="button" class="loc-option" onclick="addTargetLocation('${loc}')">${countryFlag(loc)} ${loc}</button>`,
|
|
1680
|
+
)
|
|
1681
|
+
.join("");
|
|
1682
|
+
}
|
|
1521
1683
|
}
|
|
1522
1684
|
|
|
1523
|
-
function
|
|
1524
|
-
const input = document.getElementById("
|
|
1685
|
+
function addCustomCountry() {
|
|
1686
|
+
const input = document.getElementById("customCountryInput");
|
|
1525
1687
|
if (!input) return;
|
|
1526
|
-
|
|
1527
|
-
if (
|
|
1528
|
-
|
|
1688
|
+
const ok = addTargetLocation(input.value);
|
|
1689
|
+
if (ok) {
|
|
1690
|
+
input.value = "";
|
|
1691
|
+
input.focus();
|
|
1529
1692
|
} else {
|
|
1530
|
-
|
|
1693
|
+
input.placeholder = "无效或已存在(两位大写字母)";
|
|
1694
|
+
setTimeout(() => (input.placeholder = "其他代码,如 GB"), 2000);
|
|
1531
1695
|
}
|
|
1532
|
-
|
|
1533
|
-
renderTargetCountryGrid(current);
|
|
1696
|
+
renderCountryPicker();
|
|
1534
1697
|
}
|
|
1535
1698
|
|
|
1699
|
+
// ---- 保存 / 恢复默认 ----
|
|
1536
1700
|
async function saveTargetLocations() {
|
|
1537
|
-
const input = document.getElementById("targetLocationsInput");
|
|
1538
1701
|
const status = document.getElementById("targetLocationsStatus");
|
|
1539
|
-
if (!
|
|
1540
|
-
const locations = normalizeCountryInput(input.value);
|
|
1702
|
+
if (!status) return;
|
|
1541
1703
|
status.textContent = "保存中...";
|
|
1542
1704
|
status.className = "settings-status loading";
|
|
1543
1705
|
try {
|
|
1544
1706
|
const res = await fetch("/api/settings/target-locations", {
|
|
1545
1707
|
method: "POST",
|
|
1546
1708
|
headers: { "Content-Type": "application/json" },
|
|
1547
|
-
body: JSON.stringify({ locations }),
|
|
1709
|
+
body: JSON.stringify({ locations: _targetLocations }),
|
|
1548
1710
|
});
|
|
1549
1711
|
const data = await res.json();
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1712
|
+
_targetLocations = (data.locations || []).map(String);
|
|
1713
|
+
_targetLocationsSaved = [..._targetLocations];
|
|
1714
|
+
renderTargetLocationsGrid();
|
|
1715
|
+
updateTargetLocationsStatus();
|
|
1554
1716
|
} catch (e) {
|
|
1555
1717
|
console.error("saveTargetLocations error:", e);
|
|
1556
1718
|
status.textContent = "保存失败";
|
|
@@ -1571,13 +1733,10 @@ async function resetTargetLocations() {
|
|
|
1571
1733
|
body: JSON.stringify({ locations: [] }),
|
|
1572
1734
|
});
|
|
1573
1735
|
const data = await res.json();
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
status.textContent = `已恢复默认 ${(data.locations || []).length} 个国家`;
|
|
1579
|
-
status.className = "settings-status ok";
|
|
1580
|
-
}
|
|
1736
|
+
_targetLocations = (data.locations || []).map(String);
|
|
1737
|
+
_targetLocationsSaved = [..._targetLocations];
|
|
1738
|
+
renderTargetLocationsGrid();
|
|
1739
|
+
updateTargetLocationsStatus();
|
|
1581
1740
|
} catch (e) {
|
|
1582
1741
|
console.error("resetTargetLocations error:", e);
|
|
1583
1742
|
if (status) {
|
|
@@ -1994,7 +2153,7 @@ function renderPreciseTable() {
|
|
|
1994
2153
|
if (!el) return;
|
|
1995
2154
|
if (!currentPreciseUsers.length) {
|
|
1996
2155
|
el.innerHTML =
|
|
1997
|
-
'<tr><td colspan="
|
|
2156
|
+
'<tr><td colspan="13" style="text-align:center;color:#666;padding:20px">暂无精准商家</td></tr>';
|
|
1998
2157
|
return;
|
|
1999
2158
|
}
|
|
2000
2159
|
el.innerHTML = currentPreciseUsers
|
|
@@ -2003,6 +2162,10 @@ function renderPreciseTable() {
|
|
|
2003
2162
|
const ec = u.ecOver1k ?? 0;
|
|
2004
2163
|
const maxEc = u.maxEcPlay ?? 0;
|
|
2005
2164
|
const title = (u.productName || "").replace(/</g, "<");
|
|
2165
|
+
const skus = Array.isArray(u.productSkus) ? u.productSkus : [];
|
|
2166
|
+
const skuCell = skus.length
|
|
2167
|
+
? `<span class="sku-cell" onclick="openSkuModal(${i})" title="点击查看 ${skus.length} 个 SKU 明细">📦 ${skus.length} · 库${formatNum(u.skuTotalStock || 0)}</span>`
|
|
2168
|
+
: '<span style="color:#555">-</span>';
|
|
2006
2169
|
return `<tr>
|
|
2007
2170
|
<td style="text-align:right;color:#666">${i + 1}</td>
|
|
2008
2171
|
<td><a href="https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}" target="_blank">@${escapeHtml(u.uniqueId)}</a></td>
|
|
@@ -2013,6 +2176,7 @@ function renderPreciseTable() {
|
|
|
2013
2176
|
<td style="color:#22c55e">${formatNum(sold)}</td>
|
|
2014
2177
|
<td style="color:#f87171">${u.productReviewCount ?? 0}</td>
|
|
2015
2178
|
<td>${u.productRating != null ? u.productRating : "-"}</td>
|
|
2179
|
+
<td>${skuCell}</td>
|
|
2016
2180
|
<td>${ec}</td>
|
|
2017
2181
|
<td>${formatNum(maxEc)}</td>
|
|
2018
2182
|
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${title}">${title || "-"}</td>
|
|
@@ -2021,6 +2185,72 @@ function renderPreciseTable() {
|
|
|
2021
2185
|
.join("");
|
|
2022
2186
|
}
|
|
2023
2187
|
|
|
2188
|
+
// ── SKU 明细弹窗(精准商家页)──
|
|
2189
|
+
function openSkuModal(idx) {
|
|
2190
|
+
const u = currentPreciseUsers[idx];
|
|
2191
|
+
if (!u) return;
|
|
2192
|
+
const skus = Array.isArray(u.productSkus) ? u.productSkus : [];
|
|
2193
|
+
if (!skus.length) return;
|
|
2194
|
+
let overlay = document.getElementById("skuModalOverlay");
|
|
2195
|
+
if (overlay) overlay.remove();
|
|
2196
|
+
overlay = document.createElement("div");
|
|
2197
|
+
overlay.id = "skuModalOverlay";
|
|
2198
|
+
overlay.className = "modal-overlay";
|
|
2199
|
+
const totalStock = skus.reduce((s, x) => s + (x.stock || 0), 0);
|
|
2200
|
+
const rows = skus
|
|
2201
|
+
.map((s) => {
|
|
2202
|
+
const stock = s.stock ?? null;
|
|
2203
|
+
const stockCls =
|
|
2204
|
+
stock === null
|
|
2205
|
+
? ""
|
|
2206
|
+
: stock === 0 || s.stockStatus === 2
|
|
2207
|
+
? "sku-oos"
|
|
2208
|
+
: stock <= 20
|
|
2209
|
+
? "sku-low"
|
|
2210
|
+
: "sku-ok";
|
|
2211
|
+
const stockText =
|
|
2212
|
+
stock === null
|
|
2213
|
+
? "-"
|
|
2214
|
+
: stock === 0 || s.stockStatus === 2
|
|
2215
|
+
? "售罄"
|
|
2216
|
+
: formatNum(stock);
|
|
2217
|
+
return `<tr>
|
|
2218
|
+
<td class="sku-spec" title="${escapeAttr(s.specs || "")}">${escapeHtml(s.specs || "-")}</td>
|
|
2219
|
+
<td class="sku-id" title="${escapeAttr(s.skuId || "")}">${escapeHtml(String(s.skuId || "-").slice(-6))}</td>
|
|
2220
|
+
<td class="${stockCls}">${stockText}</td>
|
|
2221
|
+
<td>${s.stockStatus ?? "-"}</td>
|
|
2222
|
+
</tr>`;
|
|
2223
|
+
})
|
|
2224
|
+
.join("");
|
|
2225
|
+
overlay.innerHTML = `
|
|
2226
|
+
<div class="modal sku-modal">
|
|
2227
|
+
<h3>📦 @${escapeHtml(u.uniqueId)} 的 SKU(${skus.length} 个)</h3>
|
|
2228
|
+
<div class="sku-modal-meta">
|
|
2229
|
+
${escapeHtml(u.productName || "") ? `<div class="sku-modal-name">${escapeHtml(u.productName)}</div>` : ""}
|
|
2230
|
+
<div>总库存 <b>${formatNum(totalStock)}</b> · 销量 <b style="color:#22c55e">${formatNum(u.productSoldCount ?? 0)}</b> · 评分 <b>${u.productRating ?? "-"}</b></div>
|
|
2231
|
+
${u.topVideoSeoUrl ? `<a href="${escapeAttr(u.topVideoSeoUrl)}" target="_blank" class="sku-modal-link">🛒 打开商品页</a>` : ""}
|
|
2232
|
+
</div>
|
|
2233
|
+
<div class="sku-table-wrap">
|
|
2234
|
+
<table class="sku-table">
|
|
2235
|
+
<thead><tr><th>规格</th><th>SKU尾号</th><th>库存</th><th>状态</th></tr></thead>
|
|
2236
|
+
<tbody>${rows}</tbody>
|
|
2237
|
+
</table>
|
|
2238
|
+
</div>
|
|
2239
|
+
<div class="btn-row" style="margin-top:14px">
|
|
2240
|
+
<button class="btn-cancel" onclick="closeSkuModal()">关闭</button>
|
|
2241
|
+
</div>
|
|
2242
|
+
</div>`;
|
|
2243
|
+
document.body.appendChild(overlay);
|
|
2244
|
+
overlay.addEventListener("click", (e) => {
|
|
2245
|
+
if (e.target === overlay) closeSkuModal();
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function closeSkuModal() {
|
|
2250
|
+
const overlay = document.getElementById("skuModalOverlay");
|
|
2251
|
+
if (overlay) overlay.remove();
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2024
2254
|
function renderPreciseVideos() {
|
|
2025
2255
|
const wrap = document.getElementById("preciseVideoWrap");
|
|
2026
2256
|
if (!wrap) return;
|
|
@@ -2037,6 +2267,11 @@ function renderPreciseVideos() {
|
|
|
2037
2267
|
const review = u.productReviewCount ?? 0;
|
|
2038
2268
|
const rating = u.productRating != null ? u.productRating : "-";
|
|
2039
2269
|
const userLink = `https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}`;
|
|
2270
|
+
const skus = Array.isArray(u.productSkus) ? u.productSkus : [];
|
|
2271
|
+
const idx = currentPreciseUsers.indexOf(u);
|
|
2272
|
+
const skuBadge = skus.length
|
|
2273
|
+
? ` · <span class="sku-cell" onclick="event.stopPropagation();openSkuModal(${idx})" title="点击查看 ${skus.length} 个 SKU 明细">📦 SKU ${skus.length} · 库${formatNum(u.skuTotalStock || skus.reduce((s, x) => s + (x.stock || 0), 0))}</span>`
|
|
2274
|
+
: "";
|
|
2040
2275
|
const tiles = vids
|
|
2041
2276
|
.reduce((acc, v) => {
|
|
2042
2277
|
const play = v.playCount || 0;
|
|
@@ -2062,7 +2297,7 @@ function renderPreciseVideos() {
|
|
|
2062
2297
|
<a class="pm-user" href="${userLink}" target="_blank">@${escapeHtml(u.uniqueId)}</a>
|
|
2063
2298
|
<span class="pm-nick">${escapeHtml(u.nickname || "-")}</span>
|
|
2064
2299
|
<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>
|
|
2300
|
+
<span class="pm-stats">粉丝 ${formatNum(u.followerCount || 0)} · 视频 ${u.videoCount || 0} · 销量 ${formatNum(sold)} · 评论 ${review} · 评分 ${rating} · EC视频 ${vids.length}${skuBadge}</span>
|
|
2066
2301
|
</div>
|
|
2067
2302
|
${tiles ? `<div class="precise-video-grid">${tiles}</div>` : '<div style="color:#666;font-size:12px;padding:6px 0">该商家暂无 EC 视频数据</div>'}
|
|
2068
2303
|
</div>`;
|
|
@@ -464,6 +464,7 @@
|
|
|
464
464
|
<th>销量</th>
|
|
465
465
|
<th>评论</th>
|
|
466
466
|
<th>评分</th>
|
|
467
|
+
<th>SKU</th>
|
|
467
468
|
<th>EC超1k视频数</th>
|
|
468
469
|
<th>最高EC播放</th>
|
|
469
470
|
<th>商品</th>
|
|
@@ -555,54 +556,73 @@
|
|
|
555
556
|
<div class="label">← 返回主页面</div>
|
|
556
557
|
</div>
|
|
557
558
|
</div>
|
|
558
|
-
<div class="settings-
|
|
559
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
<
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
<div class="settings-
|
|
567
|
-
<
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
559
|
+
<div class="settings-layout">
|
|
560
|
+
<div class="settings-nav" id="settingsNav">
|
|
561
|
+
<button type="button" class="settings-nav-item active" data-panel="llm" onclick="switchSettingsPanel('llm')">⚙️
|
|
562
|
+
LLM 模型</button>
|
|
563
|
+
<button type="button" class="settings-nav-item" data-panel="countries"
|
|
564
|
+
onclick="switchSettingsPanel('countries')">🎯 目标国家</button>
|
|
565
|
+
</div>
|
|
566
|
+
<div class="settings-panels">
|
|
567
|
+
<div class="settings-panel active" id="settingsPanel-llm">
|
|
568
|
+
<div class="settings-card">
|
|
569
|
+
<h3>⚙️ LLM 模型配置</h3>
|
|
570
|
+
<p class="settings-hint">切换后立即生效,无需重启服务。配置保存在数据库中。</p>
|
|
571
|
+
<div class="settings-form">
|
|
572
|
+
<div class="settings-field">
|
|
573
|
+
<label>服务商</label>
|
|
574
|
+
<select id="settingsProvider" onchange="onProviderChange()"></select>
|
|
575
|
+
</div>
|
|
576
|
+
<div class="settings-field">
|
|
577
|
+
<label>模型</label>
|
|
578
|
+
<select id="settingsModel"></select>
|
|
579
|
+
</div>
|
|
580
|
+
<div class="settings-field">
|
|
581
|
+
<label>API 地址</label>
|
|
582
|
+
<input type="text" id="settingsUrl" placeholder="https://..." readonly>
|
|
583
|
+
<span class="settings-note">选服务商后自动填入,也可手动修改</span>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="settings-field">
|
|
586
|
+
<label>API Key</label>
|
|
587
|
+
<div class="settings-key-row">
|
|
588
|
+
<input type="password" id="settingsApiKey" placeholder="sk-...">
|
|
589
|
+
<button id="settingsToggleKey" onclick="toggleApiKeyVisibility()" title="显示/隐藏">👁</button>
|
|
590
|
+
</div>
|
|
591
|
+
<span class="settings-note" id="settingsKeyHint"></span>
|
|
592
|
+
</div>
|
|
593
|
+
<div class="settings-status" id="settingsStatus"></div>
|
|
594
|
+
<div class="btn-row">
|
|
595
|
+
<button class="btn-submit" onclick="saveLlmConfig()">保存配置</button>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
580
598
|
</div>
|
|
581
|
-
<span class="settings-note" id="settingsKeyHint"></span>
|
|
582
|
-
</div>
|
|
583
|
-
<div class="settings-status" id="settingsStatus"></div>
|
|
584
|
-
<div class="btn-row">
|
|
585
|
-
<button class="btn-submit" onclick="saveLlmConfig()">保存配置</button>
|
|
586
599
|
</div>
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
600
|
+
<div class="settings-panel" id="settingsPanel-countries">
|
|
601
|
+
<div class="settings-card">
|
|
602
|
+
<h3>🎯 目标国家</h3>
|
|
603
|
+
<p class="settings-hint">设置采集的目标国家列表及领取优先级,保存后写入通用配置文件(~/.tt-help-config.json),立即生效。列表顺序即任务领取优先级:越靠前越先领取。
|
|
604
|
+
</p>
|
|
605
|
+
<div class="settings-form">
|
|
606
|
+
<div class="settings-field">
|
|
607
|
+
<label>目标国家(拖动卡片排序,位置即领取优先级:越靠前越先领取)</label>
|
|
608
|
+
<div class="loc-chips" id="targetLocationsGrid"></div>
|
|
609
|
+
<div class="country-picker" id="countryPicker" style="display:none">
|
|
610
|
+
<div class="country-picker-title">选择要添加的国家(新加的排最后,可拖动调整优先级)</div>
|
|
611
|
+
<div class="loc-grid" id="countryPickerGrid"></div>
|
|
612
|
+
<div class="custom-loc-row">
|
|
613
|
+
<input type="text" id="customCountryInput" class="custom-loc-input" placeholder="其他代码,如 GB"
|
|
614
|
+
maxlength="2" onkeydown="if(event.key==='Enter')addCustomCountry()">
|
|
615
|
+
<button type="button" class="btn-submit" onclick="addCustomCountry()">添加</button>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="settings-status" id="targetLocationsStatus"></div>
|
|
620
|
+
<div class="btn-row">
|
|
621
|
+
<button class="btn-submit" onclick="saveTargetLocations()">保存目标国家</button>
|
|
622
|
+
<button class="btn-cancel" onclick="resetTargetLocations()">恢复默认</button>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
606
626
|
</div>
|
|
607
627
|
</div>
|
|
608
628
|
</div>
|