tt-help-cli-ycl 1.4.17 → 1.4.19
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/src/cli/comments.js +6 -4
- package/src/cli/tag.js +5 -7
- package/src/cli/watch.js +15 -4
- package/src/lib/args.js +22 -7
- package/src/lib/constants.js +12 -2
- package/src/watch/data-store.js +206 -8
- package/src/watch/db-crud.js +16 -1
- package/src/watch/db-schema.js +5 -0
- package/src/watch/db-stats.js +2 -6
- package/src/watch/db-tags.js +106 -6
- package/src/watch/public/app.js +554 -11
- package/src/watch/public/index.html +4 -1
- package/src/watch/public/style.css +41 -0
- package/src/watch/server.js +273 -7
- package/src/watch/tag-service.js +317 -10
package/src/watch/public/app.js
CHANGED
|
@@ -21,6 +21,7 @@ let currentRawLocation = "";
|
|
|
21
21
|
let currentSort = { key: null, asc: true };
|
|
22
22
|
let prevStatValues = {};
|
|
23
23
|
let prevUserMap = {};
|
|
24
|
+
let prevTargetUsers = 0;
|
|
24
25
|
|
|
25
26
|
async function fetchStats() {
|
|
26
27
|
try {
|
|
@@ -164,6 +165,13 @@ function renderStats() {
|
|
|
164
165
|
statJobsBase.textContent = `jobs_base: ${formatStatNum(d.jobsBaseTotal || 0)}`;
|
|
165
166
|
flashEl("statError", d.errorUsers);
|
|
166
167
|
flashEl("statTarget", d.targetUsers, { full: true });
|
|
168
|
+
// 检测目标商家新增,放烟花(仅首页)
|
|
169
|
+
const hash = window.location.hash.replace(/^#/, "");
|
|
170
|
+
if (!hash && prevTargetUsers > 0 && d.targetUsers > prevTargetUsers) {
|
|
171
|
+
const diff = d.targetUsers - prevTargetUsers;
|
|
172
|
+
triggerFirework(diff);
|
|
173
|
+
}
|
|
174
|
+
prevTargetUsers = d.targetUsers || 0;
|
|
167
175
|
flashEl("statUserUpdateTasks", d.userUpdateTasks || 0, { full: true });
|
|
168
176
|
flashEl("statRawJobs", d.rawJobs || 0);
|
|
169
177
|
flashEl("statCommentTask", d.commentTaskCount || 0, { full: true });
|
|
@@ -256,13 +264,21 @@ function renderActiveClients(clients) {
|
|
|
256
264
|
const tbody = document.getElementById("activeClientsBody");
|
|
257
265
|
if (!section || !bar) return;
|
|
258
266
|
|
|
259
|
-
const types = [
|
|
267
|
+
const types = [
|
|
268
|
+
"explore",
|
|
269
|
+
"refresh",
|
|
270
|
+
"attach",
|
|
271
|
+
"comments",
|
|
272
|
+
"scoring",
|
|
273
|
+
"tag-refresh",
|
|
274
|
+
];
|
|
260
275
|
const labels = {
|
|
261
276
|
explore: "Explore",
|
|
262
277
|
refresh: "Refresh",
|
|
263
278
|
attach: "Attach",
|
|
264
279
|
comments: "Comments",
|
|
265
280
|
scoring: "Scoring",
|
|
281
|
+
"tag-refresh": "TagRefresh",
|
|
266
282
|
};
|
|
267
283
|
const grouped = {};
|
|
268
284
|
for (const c of clients) {
|
|
@@ -352,7 +368,7 @@ function renderCountryChart(countries) {
|
|
|
352
368
|
const isTarget = targetLocations.includes(c.country);
|
|
353
369
|
const targetBadge =
|
|
354
370
|
c.targetCount > 0
|
|
355
|
-
? `<span class="target-badge">🎯 ${c.targetCount}</span>`
|
|
371
|
+
? `<span class="target-badge${isTarget ? "" : " target-badge-other"}">🎯 ${c.targetCount}</span>`
|
|
356
372
|
: `<span class="target-badge" style="visibility:hidden"> </span>`;
|
|
357
373
|
return `
|
|
358
374
|
<div class="bar-row${isTarget ? " is-target" : ""}">
|
|
@@ -796,6 +812,55 @@ async function confirmNonSeller(uniqueId) {
|
|
|
796
812
|
}
|
|
797
813
|
}
|
|
798
814
|
|
|
815
|
+
async function moveToBase(uniqueId) {
|
|
816
|
+
if (
|
|
817
|
+
!confirm(
|
|
818
|
+
`确定要将 @${uniqueId} 移到 jobs_base 并重走流程吗?\n这会将用户从 jobs 表移到 jobs_base,完全从头开始采集。`,
|
|
819
|
+
)
|
|
820
|
+
) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
showLoading("正在移动...");
|
|
824
|
+
try {
|
|
825
|
+
const res = await fetch(
|
|
826
|
+
`/api/user-move-to-base/${encodeURIComponent(uniqueId)}`,
|
|
827
|
+
{
|
|
828
|
+
method: "PUT",
|
|
829
|
+
headers: { "Content-Type": "application/json" },
|
|
830
|
+
},
|
|
831
|
+
);
|
|
832
|
+
const data = await res.json();
|
|
833
|
+
if (data.error) {
|
|
834
|
+
showToast(data.error, true);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
showToast(`@${uniqueId} 已移到 jobs_base,将重走流程`);
|
|
838
|
+
|
|
839
|
+
// 从当前列表中移除该用户
|
|
840
|
+
currentTargetUsers = currentTargetUsers.filter(
|
|
841
|
+
(u) => u.uniqueId !== uniqueId,
|
|
842
|
+
);
|
|
843
|
+
currentTargetTotal = Math.max(0, currentTargetTotal - 1);
|
|
844
|
+
renderTargetTable();
|
|
845
|
+
|
|
846
|
+
// 更新统计
|
|
847
|
+
const statEl = document.getElementById("targetPageStatTotal");
|
|
848
|
+
if (statEl)
|
|
849
|
+
statEl.textContent = formatStatNum(currentTargetTotal, { full: true });
|
|
850
|
+
if (currentStats) {
|
|
851
|
+
currentStats.targetUsers = Math.max(
|
|
852
|
+
0,
|
|
853
|
+
(currentStats.targetUsers || 0) - 1,
|
|
854
|
+
);
|
|
855
|
+
flashEl("statTarget", currentStats.targetUsers, { full: true });
|
|
856
|
+
}
|
|
857
|
+
} catch (e) {
|
|
858
|
+
showToast("移动失败: " + e.message, true);
|
|
859
|
+
} finally {
|
|
860
|
+
hideLoading();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
799
864
|
async function submitAddUsers() {
|
|
800
865
|
const ta = document.getElementById("modalUserInput");
|
|
801
866
|
const raw = ta.value.trim();
|
|
@@ -1051,6 +1116,121 @@ function scrollToTop() {
|
|
|
1051
1116
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
1052
1117
|
}
|
|
1053
1118
|
|
|
1119
|
+
// ── 回顶部按钮拖拽 ──
|
|
1120
|
+
(function initBackToTopDrag() {
|
|
1121
|
+
const btn = document.getElementById("backToTop");
|
|
1122
|
+
if (!btn) return;
|
|
1123
|
+
|
|
1124
|
+
let startX,
|
|
1125
|
+
startY,
|
|
1126
|
+
startLeft,
|
|
1127
|
+
startTop,
|
|
1128
|
+
dragging = false,
|
|
1129
|
+
moved = false;
|
|
1130
|
+
const DRAG_THRESHOLD = 5;
|
|
1131
|
+
const STORAGE_KEY = "backToTopPos";
|
|
1132
|
+
|
|
1133
|
+
// 恢复保存的位置
|
|
1134
|
+
const saved = (function () {
|
|
1135
|
+
try {
|
|
1136
|
+
return JSON.parse(localStorage.getItem(STORAGE_KEY));
|
|
1137
|
+
} catch (_) {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
})();
|
|
1141
|
+
if (
|
|
1142
|
+
saved &&
|
|
1143
|
+
typeof saved.left === "number" &&
|
|
1144
|
+
typeof saved.top === "number"
|
|
1145
|
+
) {
|
|
1146
|
+
btn.style.left = saved.left + "px";
|
|
1147
|
+
btn.style.top = saved.top + "px";
|
|
1148
|
+
btn.style.right = "auto";
|
|
1149
|
+
btn.style.bottom = "auto";
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function clamp(val, min, max) {
|
|
1153
|
+
return Math.min(Math.max(val, min), max);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function getPos(e) {
|
|
1157
|
+
if (e.touches) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
1158
|
+
return { x: e.clientX, y: e.clientY };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function onStart(e) {
|
|
1162
|
+
if (e.button !== undefined && e.button !== 0) return; // 只响应左键
|
|
1163
|
+
const pos = getPos(e);
|
|
1164
|
+
startX = pos.x;
|
|
1165
|
+
startY = pos.y;
|
|
1166
|
+
const rect = btn.getBoundingClientRect();
|
|
1167
|
+
startLeft = rect.left;
|
|
1168
|
+
startTop = rect.top;
|
|
1169
|
+
moved = false;
|
|
1170
|
+
dragging = true;
|
|
1171
|
+
btn.classList.add("dragging");
|
|
1172
|
+
btn.style.right = "auto";
|
|
1173
|
+
btn.style.bottom = "auto";
|
|
1174
|
+
btn.style.left = startLeft + "px";
|
|
1175
|
+
btn.style.top = startTop + "px";
|
|
1176
|
+
e.preventDefault();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function onMove(e) {
|
|
1180
|
+
if (!dragging) return;
|
|
1181
|
+
const pos = getPos(e);
|
|
1182
|
+
const dx = pos.x - startX;
|
|
1183
|
+
const dy = pos.y - startY;
|
|
1184
|
+
if (
|
|
1185
|
+
!moved &&
|
|
1186
|
+
Math.abs(dx) < DRAG_THRESHOLD &&
|
|
1187
|
+
Math.abs(dy) < DRAG_THRESHOLD
|
|
1188
|
+
)
|
|
1189
|
+
return;
|
|
1190
|
+
moved = true;
|
|
1191
|
+
const newLeft = clamp(
|
|
1192
|
+
startLeft + dx,
|
|
1193
|
+
0,
|
|
1194
|
+
window.innerWidth - btn.offsetWidth,
|
|
1195
|
+
);
|
|
1196
|
+
const newTop = clamp(
|
|
1197
|
+
startTop + dy,
|
|
1198
|
+
0,
|
|
1199
|
+
window.innerHeight - btn.offsetHeight,
|
|
1200
|
+
);
|
|
1201
|
+
btn.style.left = newLeft + "px";
|
|
1202
|
+
btn.style.top = newTop + "px";
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function onEnd() {
|
|
1206
|
+
if (!dragging) return;
|
|
1207
|
+
dragging = false;
|
|
1208
|
+
btn.classList.remove("dragging");
|
|
1209
|
+
if (moved) {
|
|
1210
|
+
// 保存位置
|
|
1211
|
+
try {
|
|
1212
|
+
localStorage.setItem(
|
|
1213
|
+
STORAGE_KEY,
|
|
1214
|
+
JSON.stringify({
|
|
1215
|
+
left: parseInt(btn.style.left),
|
|
1216
|
+
top: parseInt(btn.style.top),
|
|
1217
|
+
}),
|
|
1218
|
+
);
|
|
1219
|
+
} catch (_) {}
|
|
1220
|
+
} else {
|
|
1221
|
+
// 未拖动则视为点击
|
|
1222
|
+
scrollToTop();
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
btn.addEventListener("mousedown", onStart);
|
|
1227
|
+
btn.addEventListener("touchstart", onStart, { passive: false });
|
|
1228
|
+
window.addEventListener("mousemove", onMove);
|
|
1229
|
+
window.addEventListener("touchmove", onMove, { passive: false });
|
|
1230
|
+
window.addEventListener("mouseup", onEnd);
|
|
1231
|
+
window.addEventListener("touchend", onEnd);
|
|
1232
|
+
})();
|
|
1233
|
+
|
|
1054
1234
|
function handleRoute() {
|
|
1055
1235
|
const hash = window.location.hash;
|
|
1056
1236
|
if (hash === "#pending") {
|
|
@@ -1291,6 +1471,8 @@ let currentTagsAllLoaded = false;
|
|
|
1291
1471
|
let currentTagsSeq = 0;
|
|
1292
1472
|
const TAGS_PAGE_SIZE = 200;
|
|
1293
1473
|
let cachedTagCountries = [];
|
|
1474
|
+
let currentRefreshHours = 24;
|
|
1475
|
+
let currentTargetRefreshHours = 24;
|
|
1294
1476
|
|
|
1295
1477
|
function showTagsPage() {
|
|
1296
1478
|
document.getElementById("mainPage").classList.add("hidden");
|
|
@@ -1300,7 +1482,9 @@ function showTagsPage() {
|
|
|
1300
1482
|
document.getElementById("targetPage").classList.remove("active");
|
|
1301
1483
|
document.getElementById("tagsPage").classList.add("active");
|
|
1302
1484
|
showLoading("正在加载关键词...");
|
|
1303
|
-
Promise.all([loadTagStats(), loadTagsPage()]).finally(
|
|
1485
|
+
Promise.all([loadTagStats(), loadTagsPage(), loadRefreshStats()]).finally(
|
|
1486
|
+
hideLoading,
|
|
1487
|
+
);
|
|
1304
1488
|
}
|
|
1305
1489
|
|
|
1306
1490
|
function setTagsFilter(filter) {
|
|
@@ -1325,13 +1509,94 @@ async function loadTagStats() {
|
|
|
1325
1509
|
flashEl("tagsPageStatDead", stats.dead || 0);
|
|
1326
1510
|
flashEl("tagsPageStatNew", (stats.new || 0) + (stats.scoring || 0));
|
|
1327
1511
|
cachedTagCountries = stats.countries || [];
|
|
1328
|
-
// 初始化国家下拉
|
|
1329
1512
|
renderTagsCountryFilter();
|
|
1330
1513
|
} catch (e) {
|
|
1331
1514
|
console.error("获取关键词统计失败:", e);
|
|
1332
1515
|
}
|
|
1333
1516
|
}
|
|
1334
1517
|
|
|
1518
|
+
async function loadRefreshStats() {
|
|
1519
|
+
try {
|
|
1520
|
+
const res = await fetch(
|
|
1521
|
+
`/api/tags/refresh-stats?hours=${currentRefreshHours}`,
|
|
1522
|
+
);
|
|
1523
|
+
const data = await res.json();
|
|
1524
|
+
renderRefreshStats(data);
|
|
1525
|
+
} catch (e) {
|
|
1526
|
+
console.error("获取刷新统计失败:", e);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function renderRefreshStats(data) {
|
|
1531
|
+
const el = document.getElementById("tagsRefreshStats");
|
|
1532
|
+
if (!el || !data || !data.countries || data.countries.length === 0) {
|
|
1533
|
+
if (el) el.innerHTML = "";
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
const pct =
|
|
1537
|
+
data.total > 0 ? Math.round((data.refreshed / data.total) * 100) : 0;
|
|
1538
|
+
const barW = Math.min(pct, 100);
|
|
1539
|
+
|
|
1540
|
+
const HOUR_OPTIONS = [12, 24, 36, 48, 72];
|
|
1541
|
+
let html = `<div style="background:#1a1a2e;border:1px solid #2a2a3a;border-radius:10px;padding:16px">
|
|
1542
|
+
<div id="refreshStatsHeader" style="display:flex;align-items:center;gap:16px;cursor:pointer;user-select:none">
|
|
1543
|
+
<span style="font-size:14px;font-weight:600;color:#e0e0e0;position:relative">
|
|
1544
|
+
📊 最近 <span id="refreshHoursLabel" style="color:#60a5fa;cursor:pointer;text-decoration:underline dotted" onclick="event.stopPropagation();toggleHoursDropdown(event)">${currentRefreshHours}h</span> 刷新进度
|
|
1545
|
+
<div id="refreshHoursDropdown" style="display:none;position:absolute;top:100%;left:0;margin-top:4px;background:#1e1e3a;border:1px solid #3b82f6;border-radius:8px;padding:4px 0;z-index:100;box-shadow:0 4px 16px rgba(0,0,0,0.5);min-width:60px">
|
|
1546
|
+
${HOUR_OPTIONS.map((h) => `<div class="hours-option" data-hours="${h}" style="padding:6px 14px;cursor:pointer;font-size:13px;color:${h === currentRefreshHours ? "#60a5fa" : "#ccc"};font-weight:${h === currentRefreshHours ? "600" : "400"};white-space:nowrap" onclick="event.stopPropagation();selectRefreshHours(${h})">${h}h</div>`).join("")}
|
|
1547
|
+
</div>
|
|
1548
|
+
</span>
|
|
1549
|
+
<span style="font-size:13px;color:#9ca3af">${data.refreshed} / ${data.total}</span>
|
|
1550
|
+
<div style="flex:1;max-width:200px;height:8px;background:#2a2a3a;border-radius:4px;overflow:hidden">
|
|
1551
|
+
<div style="height:100%;width:${barW}%;background:${pct >= 50 ? "#22c55e" : "#facc15"};border-radius:4px;transition:width 0.3s"></div>
|
|
1552
|
+
</div>
|
|
1553
|
+
<span style="font-size:14px;font-weight:700;color:${pct >= 50 ? "#22c55e" : "#facc15"}">${pct}%</span>
|
|
1554
|
+
<span id="refreshStatsArrow" style="color:#888;font-size:12px">▶</span>
|
|
1555
|
+
</div>
|
|
1556
|
+
<div id="refreshStatsBody" style="display:none;margin-top:12px">
|
|
1557
|
+
<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
1558
|
+
<thead><tr style="color:#888;text-align:left">
|
|
1559
|
+
<th style="padding:4px 8px">国家</th>
|
|
1560
|
+
<th style="padding:4px 8px;text-align:right">需刷新</th>
|
|
1561
|
+
<th style="padding:4px 8px;text-align:right">${currentRefreshHours}h已刷</th>
|
|
1562
|
+
<th style="padding:4px 8px;width:120px">进度</th>
|
|
1563
|
+
</tr></thead>
|
|
1564
|
+
<tbody>`;
|
|
1565
|
+
|
|
1566
|
+
for (const c of data.countries) {
|
|
1567
|
+
const cpct = c.total > 0 ? Math.round((c.refreshed / c.total) * 100) : 0;
|
|
1568
|
+
const cBarW = Math.min(cpct, 100);
|
|
1569
|
+
html += `<tr style="color:#ccc;border-top:1px solid #222">
|
|
1570
|
+
<td style="padding:4px 8px">${c.country}</td>
|
|
1571
|
+
<td style="padding:4px 8px;text-align:right">${c.total}</td>
|
|
1572
|
+
<td style="padding:4px 8px;text-align:right;color:${cpct >= 50 ? "#22c55e" : "#facc15"}">${c.refreshed}</td>
|
|
1573
|
+
<td style="padding:4px 8px">
|
|
1574
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
1575
|
+
<div style="flex:1;height:6px;background:#2a2a3a;border-radius:3px;overflow:hidden">
|
|
1576
|
+
<div style="height:100%;width:${cBarW}%;background:${cpct >= 50 ? "#22c55e" : "#facc15"};border-radius:3px"></div>
|
|
1577
|
+
</div>
|
|
1578
|
+
<span style="font-size:11px;color:#9ca3af;min-width:28px;text-align:right">${cpct}%</span>
|
|
1579
|
+
</div>
|
|
1580
|
+
</td>
|
|
1581
|
+
</tr>`;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
html += `</tbody></table></div></div>`;
|
|
1585
|
+
el.innerHTML = html;
|
|
1586
|
+
|
|
1587
|
+
// 绑定展开/折叠
|
|
1588
|
+
const header = document.getElementById("refreshStatsHeader");
|
|
1589
|
+
const body = document.getElementById("refreshStatsBody");
|
|
1590
|
+
const arrow = document.getElementById("refreshStatsArrow");
|
|
1591
|
+
if (header && body && arrow) {
|
|
1592
|
+
header.addEventListener("click", () => {
|
|
1593
|
+
const open = body.style.display !== "none";
|
|
1594
|
+
body.style.display = open ? "none" : "";
|
|
1595
|
+
arrow.textContent = open ? "▶" : "▼";
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1335
1600
|
function renderTagsCountryFilter() {
|
|
1336
1601
|
const sel = document.getElementById("tagsCountryFilter");
|
|
1337
1602
|
if (!sel) return;
|
|
@@ -1441,7 +1706,7 @@ function renderTagsTable() {
|
|
|
1441
1706
|
const matchedAuthors = t.matched_authors ?? "-";
|
|
1442
1707
|
const pushedUsers = t.pushed_users ?? "-";
|
|
1443
1708
|
const refreshedAt = t.refreshed_at
|
|
1444
|
-
? t.refreshed_at
|
|
1709
|
+
? formatTime(new Date(t.refreshed_at).getTime())
|
|
1445
1710
|
: "-";
|
|
1446
1711
|
|
|
1447
1712
|
return `<tr>
|
|
@@ -1486,7 +1751,7 @@ document
|
|
|
1486
1751
|
.addEventListener("click", async () => {
|
|
1487
1752
|
showLoading("正在刷新...");
|
|
1488
1753
|
try {
|
|
1489
|
-
await Promise.all([loadTagStats(), loadTagsPage()]);
|
|
1754
|
+
await Promise.all([loadTagStats(), loadTagsPage(), loadRefreshStats()]);
|
|
1490
1755
|
showToast("刷新完成");
|
|
1491
1756
|
} catch (e) {
|
|
1492
1757
|
showToast("刷新失败: " + e.message, true);
|
|
@@ -1519,6 +1784,151 @@ function loadMoreTags() {
|
|
|
1519
1784
|
loadTagsPage(true);
|
|
1520
1785
|
}
|
|
1521
1786
|
|
|
1787
|
+
// ====== 刷新统计时间切换 ======
|
|
1788
|
+
|
|
1789
|
+
function toggleHoursDropdown(event) {
|
|
1790
|
+
const dropdown = document.getElementById("refreshHoursDropdown");
|
|
1791
|
+
if (!dropdown) return;
|
|
1792
|
+
const isOpen = dropdown.style.display !== "none";
|
|
1793
|
+
dropdown.style.display = isOpen ? "none" : "block";
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
async function selectRefreshHours(hours) {
|
|
1797
|
+
const dropdown = document.getElementById("refreshHoursDropdown");
|
|
1798
|
+
if (dropdown) dropdown.style.display = "none";
|
|
1799
|
+
if (hours === currentRefreshHours) return;
|
|
1800
|
+
currentRefreshHours = hours;
|
|
1801
|
+
showLoading("正在加载...");
|
|
1802
|
+
try {
|
|
1803
|
+
await loadRefreshStats();
|
|
1804
|
+
const label = document.getElementById("refreshHoursLabel");
|
|
1805
|
+
if (label) label.textContent = hours + "h";
|
|
1806
|
+
} catch (e) {
|
|
1807
|
+
console.error("刷新统计失败:", e);
|
|
1808
|
+
} finally {
|
|
1809
|
+
hideLoading();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// ===== 目标商家刷新进度(复用 tags 页面的 UI 模式)=====
|
|
1814
|
+
|
|
1815
|
+
async function loadTargetRefreshStats() {
|
|
1816
|
+
try {
|
|
1817
|
+
const res = await fetch(
|
|
1818
|
+
`/api/targets/refresh-stats?hours=${currentTargetRefreshHours}`,
|
|
1819
|
+
);
|
|
1820
|
+
const data = await res.json();
|
|
1821
|
+
renderTargetRefreshStats(data);
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
console.error("获取目标商家刷新统计失败:", e);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function renderTargetRefreshStats(data) {
|
|
1828
|
+
const el = document.getElementById("targetRefreshStats");
|
|
1829
|
+
if (!el || !data || !data.countries || data.countries.length === 0) {
|
|
1830
|
+
if (el) el.innerHTML = "";
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const pct =
|
|
1834
|
+
data.total > 0 ? Math.round((data.refreshed / data.total) * 100) : 0;
|
|
1835
|
+
const barW = Math.min(pct, 100);
|
|
1836
|
+
|
|
1837
|
+
const HOUR_OPTIONS = [12, 24, 36, 48, 72];
|
|
1838
|
+
let html = `<div style="background:#1a1a2e;border:1px solid #2a2a3a;border-radius:10px;padding:16px">
|
|
1839
|
+
<div id="targetRefreshStatsHeader" style="display:flex;align-items:center;gap:16px;cursor:pointer;user-select:none">
|
|
1840
|
+
<span style="font-size:14px;font-weight:600;color:#e0e0e0;position:relative">
|
|
1841
|
+
📊 最近 <span id="targetRefreshHoursLabel" style="color:#60a5fa;cursor:pointer;text-decoration:underline dotted" onclick="event.stopPropagation();toggleTargetHoursDropdown(event)">${currentTargetRefreshHours}h</span> 刷新进度
|
|
1842
|
+
<div id="targetRefreshHoursDropdown" style="display:none;position:absolute;top:100%;left:0;margin-top:4px;background:#1e1e3a;border:1px solid #3b82f6;border-radius:8px;padding:4px 0;z-index:100;box-shadow:0 4px 16px rgba(0,0,0,0.5);min-width:60px">
|
|
1843
|
+
${HOUR_OPTIONS.map((h) => `<div class="hours-option" data-hours="${h}" style="padding:6px 14px;cursor:pointer;font-size:13px;color:${h === currentTargetRefreshHours ? "#60a5fa" : "#ccc"};font-weight:${h === currentTargetRefreshHours ? "600" : "400"};white-space:nowrap" onclick="event.stopPropagation();selectTargetRefreshHours(${h})">${h}h</div>`).join("")}
|
|
1844
|
+
</div>
|
|
1845
|
+
</span>
|
|
1846
|
+
<span style="font-size:13px;color:#9ca3af">${data.refreshed} / ${data.total}</span>
|
|
1847
|
+
<div style="flex:1;max-width:200px;height:8px;background:#2a2a3a;border-radius:4px;overflow:hidden">
|
|
1848
|
+
<div style="height:100%;width:${barW}%;background:${pct >= 50 ? "#22c55e" : "#facc15"};border-radius:4px;transition:width 0.3s"></div>
|
|
1849
|
+
</div>
|
|
1850
|
+
<span style="font-size:14px;font-weight:700;color:${pct >= 50 ? "#22c55e" : "#facc15"}">${pct}%</span>
|
|
1851
|
+
<span id="targetRefreshStatsArrow" style="color:#888;font-size:12px">▶</span>
|
|
1852
|
+
</div>
|
|
1853
|
+
<div id="targetRefreshStatsBody" style="display:none;margin-top:12px">
|
|
1854
|
+
<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
1855
|
+
<thead><tr style="color:#888;text-align:left">
|
|
1856
|
+
<th style="padding:4px 8px">国家</th>
|
|
1857
|
+
<th style="padding:4px 8px;text-align:right">商家数</th>
|
|
1858
|
+
<th style="padding:4px 8px;text-align:right">${currentTargetRefreshHours}h已刷</th>
|
|
1859
|
+
<th style="padding:4px 8px;width:120px">进度</th>
|
|
1860
|
+
</tr></thead>
|
|
1861
|
+
<tbody>`;
|
|
1862
|
+
|
|
1863
|
+
for (const c of data.countries) {
|
|
1864
|
+
const cpct = c.total > 0 ? Math.round((c.refreshed / c.total) * 100) : 0;
|
|
1865
|
+
const cBarW = Math.min(cpct, 100);
|
|
1866
|
+
html += `<tr style="color:#ccc;border-top:1px solid #222">
|
|
1867
|
+
<td style="padding:4px 8px">${c.country}</td>
|
|
1868
|
+
<td style="padding:4px 8px;text-align:right">${c.total}</td>
|
|
1869
|
+
<td style="padding:4px 8px;text-align:right;color:${cpct >= 50 ? "#22c55e" : "#facc15"}">${c.refreshed}</td>
|
|
1870
|
+
<td style="padding:4px 8px">
|
|
1871
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
1872
|
+
<div style="flex:1;height:6px;background:#2a2a3a;border-radius:3px;overflow:hidden">
|
|
1873
|
+
<div style="height:100%;width:${cBarW}%;background:${cpct >= 50 ? "#22c55e" : "#facc15"};border-radius:3px"></div>
|
|
1874
|
+
</div>
|
|
1875
|
+
<span style="font-size:11px;color:#9ca3af;min-width:28px;text-align:right">${cpct}%</span>
|
|
1876
|
+
</div>
|
|
1877
|
+
</td>
|
|
1878
|
+
</tr>`;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
html += `</tbody></table></div></div>`;
|
|
1882
|
+
el.innerHTML = html;
|
|
1883
|
+
|
|
1884
|
+
// 绑定展开/折叠
|
|
1885
|
+
const header = document.getElementById("targetRefreshStatsHeader");
|
|
1886
|
+
const body = document.getElementById("targetRefreshStatsBody");
|
|
1887
|
+
const arrow = document.getElementById("targetRefreshStatsArrow");
|
|
1888
|
+
if (header && body && arrow) {
|
|
1889
|
+
header.addEventListener("click", () => {
|
|
1890
|
+
const open = body.style.display !== "none";
|
|
1891
|
+
body.style.display = open ? "none" : "";
|
|
1892
|
+
arrow.textContent = open ? "▶" : "▼";
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function toggleTargetHoursDropdown(event) {
|
|
1898
|
+
const dropdown = document.getElementById("targetRefreshHoursDropdown");
|
|
1899
|
+
if (!dropdown) return;
|
|
1900
|
+
const isOpen = dropdown.style.display !== "none";
|
|
1901
|
+
dropdown.style.display = isOpen ? "none" : "block";
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
async function selectTargetRefreshHours(hours) {
|
|
1905
|
+
const dropdown = document.getElementById("targetRefreshHoursDropdown");
|
|
1906
|
+
if (dropdown) dropdown.style.display = "none";
|
|
1907
|
+
if (hours === currentTargetRefreshHours) return;
|
|
1908
|
+
currentTargetRefreshHours = hours;
|
|
1909
|
+
try {
|
|
1910
|
+
await loadTargetRefreshStats();
|
|
1911
|
+
const label = document.getElementById("targetRefreshHoursLabel");
|
|
1912
|
+
if (label) label.textContent = hours + "h";
|
|
1913
|
+
} catch (e) {
|
|
1914
|
+
console.error("目标商家刷新统计失败:", e);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// 点击空白处关闭下拉
|
|
1919
|
+
document.addEventListener("click", (e) => {
|
|
1920
|
+
const dropdown = document.getElementById("refreshHoursDropdown");
|
|
1921
|
+
const label = document.getElementById("refreshHoursLabel");
|
|
1922
|
+
if (
|
|
1923
|
+
dropdown &&
|
|
1924
|
+
dropdown.style.display !== "none" &&
|
|
1925
|
+
!label?.contains(e.target) &&
|
|
1926
|
+
!dropdown.contains(e.target)
|
|
1927
|
+
) {
|
|
1928
|
+
dropdown.style.display = "none";
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1522
1932
|
// 排序
|
|
1523
1933
|
document.querySelectorAll(".sortable-tag").forEach((th) => {
|
|
1524
1934
|
th.addEventListener("click", () => {
|
|
@@ -1548,6 +1958,7 @@ function showTargetPage() {
|
|
|
1548
1958
|
showLoading("正在加载目标商家数据...");
|
|
1549
1959
|
fetchTargetByCountry();
|
|
1550
1960
|
fetchTargetOtherCountries();
|
|
1961
|
+
loadTargetRefreshStats();
|
|
1551
1962
|
// 同步统计
|
|
1552
1963
|
if (currentStats) {
|
|
1553
1964
|
document.getElementById("targetPageStatTotal").textContent = formatStatNum(
|
|
@@ -1564,7 +1975,7 @@ let currentTargetLoading = false;
|
|
|
1564
1975
|
let currentTargetAllLoaded = false;
|
|
1565
1976
|
let currentTargetSeq = 0;
|
|
1566
1977
|
let currentTargetSort = { key: null, asc: true };
|
|
1567
|
-
const TARGET_PAGE_SIZE =
|
|
1978
|
+
const TARGET_PAGE_SIZE = 500;
|
|
1568
1979
|
|
|
1569
1980
|
async function fetchTargetOtherCountries() {
|
|
1570
1981
|
try {
|
|
@@ -1816,7 +2227,7 @@ function renderTargetTable() {
|
|
|
1816
2227
|
<td data-label="最近发布" style="font-size:11px;color:#888">${latestVideo}</td>
|
|
1817
2228
|
<td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
|
|
1818
2229
|
<td data-label="操作">
|
|
1819
|
-
<button class="btn-
|
|
2230
|
+
<button class="btn-pending" onclick="moveToBase('${escapeJsString(u.uniqueId)}')" title="移到 jobs_base 重走流程">待确认</button>
|
|
1820
2231
|
</td>
|
|
1821
2232
|
</tr>`;
|
|
1822
2233
|
})
|
|
@@ -1960,6 +2371,7 @@ document
|
|
|
1960
2371
|
const btn = document.getElementById("targetReprocessBtn");
|
|
1961
2372
|
if (btn) btn.style.display = "none";
|
|
1962
2373
|
await fetchTargetByCountry();
|
|
2374
|
+
await loadTargetRefreshStats();
|
|
1963
2375
|
showToast("刷新完成");
|
|
1964
2376
|
} catch (e) {
|
|
1965
2377
|
showToast("刷新失败: " + e.message, true);
|
|
@@ -2580,6 +2992,136 @@ function initTargetTableSorting() {
|
|
|
2580
2992
|
});
|
|
2581
2993
|
}
|
|
2582
2994
|
|
|
2995
|
+
// ========== 烟花/鞭炮动画 ==========
|
|
2996
|
+
function triggerFirework(newCount) {
|
|
2997
|
+
const canvas = document.getElementById("fireworkCanvas");
|
|
2998
|
+
if (!canvas) return;
|
|
2999
|
+
const ctx = canvas.getContext("2d");
|
|
3000
|
+
|
|
3001
|
+
canvas.width = window.innerWidth;
|
|
3002
|
+
canvas.height = window.innerHeight;
|
|
3003
|
+
|
|
3004
|
+
const colors = [
|
|
3005
|
+
"#fe2c55",
|
|
3006
|
+
"#ff6b6b",
|
|
3007
|
+
"#ffd93d",
|
|
3008
|
+
"#6bcb77",
|
|
3009
|
+
"#4d96ff",
|
|
3010
|
+
"#a78bfa",
|
|
3011
|
+
"#ff922b",
|
|
3012
|
+
"#20c997",
|
|
3013
|
+
"#f06595",
|
|
3014
|
+
"#ffe066",
|
|
3015
|
+
"#5c7cfa",
|
|
3016
|
+
"#ff8787",
|
|
3017
|
+
];
|
|
3018
|
+
|
|
3019
|
+
const particles = [];
|
|
3020
|
+
const burstCount = Math.min(newCount, 5) + 2; // 最多 7 朵烟花
|
|
3021
|
+
|
|
3022
|
+
// 创建多朵烟花
|
|
3023
|
+
for (let b = 0; b < burstCount; b++) {
|
|
3024
|
+
const cx = Math.random() * canvas.width * 0.8 + canvas.width * 0.1;
|
|
3025
|
+
const cy = Math.random() * canvas.height * 0.5 + canvas.height * 0.1;
|
|
3026
|
+
const hue = Math.random() * 360;
|
|
3027
|
+
const count = 80 + Math.floor(Math.random() * 60);
|
|
3028
|
+
|
|
3029
|
+
for (let i = 0; i < count; i++) {
|
|
3030
|
+
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.3;
|
|
3031
|
+
const speed = 2 + Math.random() * 6;
|
|
3032
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
3033
|
+
particles.push({
|
|
3034
|
+
x: cx,
|
|
3035
|
+
y: cy,
|
|
3036
|
+
vx: Math.cos(angle) * speed,
|
|
3037
|
+
vy: Math.sin(angle) * speed,
|
|
3038
|
+
life: 1,
|
|
3039
|
+
decay: 0.008 + Math.random() * 0.02,
|
|
3040
|
+
size: 1.5 + Math.random() * 2.5,
|
|
3041
|
+
color,
|
|
3042
|
+
gravity: 0.04,
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
let animFrame;
|
|
3048
|
+
function animate() {
|
|
3049
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
3050
|
+
|
|
3051
|
+
let alive = false;
|
|
3052
|
+
for (const p of particles) {
|
|
3053
|
+
p.x += p.vx;
|
|
3054
|
+
p.y += p.vy;
|
|
3055
|
+
p.vy += p.gravity;
|
|
3056
|
+
p.life -= p.decay;
|
|
3057
|
+
|
|
3058
|
+
if (p.life > 0) {
|
|
3059
|
+
alive = true;
|
|
3060
|
+
ctx.save();
|
|
3061
|
+
ctx.globalAlpha = p.life;
|
|
3062
|
+
ctx.fillStyle = p.color;
|
|
3063
|
+
ctx.shadowColor = p.color;
|
|
3064
|
+
ctx.shadowBlur = 6;
|
|
3065
|
+
ctx.beginPath();
|
|
3066
|
+
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
3067
|
+
ctx.fill();
|
|
3068
|
+
ctx.restore();
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
if (alive) {
|
|
3073
|
+
animFrame = requestAnimationFrame(animate);
|
|
3074
|
+
} else {
|
|
3075
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
if (animFrame) cancelAnimationFrame(animFrame);
|
|
3080
|
+
animate();
|
|
3081
|
+
|
|
3082
|
+
// 显示提示文字
|
|
3083
|
+
showFireworkToast(newCount);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
function showFireworkToast(count) {
|
|
3087
|
+
let toast = document.getElementById("firework-toast");
|
|
3088
|
+
if (!toast) {
|
|
3089
|
+
toast = document.createElement("div");
|
|
3090
|
+
toast.id = "firework-toast";
|
|
3091
|
+
toast.style.cssText = `
|
|
3092
|
+
position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
|
|
3093
|
+
font-size:48px;font-weight:900;z-index:10000;pointer-events:none;
|
|
3094
|
+
color:#ffd93d;text-shadow:0 0 30px #fe2c55,0 0 60px #fe2c55,0 0 90px #ff6b6b;
|
|
3095
|
+
animation:fireworkToast 2.5s ease-out forwards;
|
|
3096
|
+
white-space:nowrap;
|
|
3097
|
+
`;
|
|
3098
|
+
document.body.appendChild(toast);
|
|
3099
|
+
// 注入动画 keyframes
|
|
3100
|
+
if (!document.getElementById("fw-style")) {
|
|
3101
|
+
const style = document.createElement("style");
|
|
3102
|
+
style.id = "fw-style";
|
|
3103
|
+
style.textContent = `
|
|
3104
|
+
@keyframes fireworkToast {
|
|
3105
|
+
0% { opacity:0; transform:translate(-50%,-50%) scale(0.3); }
|
|
3106
|
+
15% { opacity:1; transform:translate(-50%,-50%) scale(1.2); }
|
|
3107
|
+
25% { opacity:1; transform:translate(-50%,-50%) scale(1); }
|
|
3108
|
+
80% { opacity:1; transform:translate(-50%,-50%) scale(1); }
|
|
3109
|
+
100% { opacity:0; transform:translate(-50%,-60%) scale(1.5); }
|
|
3110
|
+
}
|
|
3111
|
+
`;
|
|
3112
|
+
document.head.appendChild(style);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
toast.textContent = `🎆 +${count} 目标商家!`;
|
|
3116
|
+
toast.style.animation = "none";
|
|
3117
|
+
void toast.offsetWidth;
|
|
3118
|
+
toast.style.animation = "fireworkToast 2.5s ease-out forwards";
|
|
3119
|
+
|
|
3120
|
+
setTimeout(() => {
|
|
3121
|
+
if (toast.parentNode) toast.remove();
|
|
3122
|
+
}, 2600);
|
|
3123
|
+
}
|
|
3124
|
+
|
|
2583
3125
|
// 初始化
|
|
2584
3126
|
fetchStats();
|
|
2585
3127
|
fetchUsers();
|
|
@@ -2587,7 +3129,8 @@ fetchClientErrors();
|
|
|
2587
3129
|
initTableSorting();
|
|
2588
3130
|
initTargetTableSorting();
|
|
2589
3131
|
|
|
3132
|
+
// 错峰轮询,避免同时发 4 个请求阻塞服务端
|
|
2590
3133
|
setInterval(fetchStats, 10000);
|
|
2591
|
-
setInterval(fetchUsers, 10000);
|
|
2592
|
-
setInterval(fetchClientErrors, 10000);
|
|
2593
|
-
setInterval(refreshUserUpdateIfActive, 10000);
|
|
3134
|
+
setTimeout(() => setInterval(fetchUsers, 10000), 2500);
|
|
3135
|
+
setTimeout(() => setInterval(fetchClientErrors, 10000), 5000);
|
|
3136
|
+
setTimeout(() => setInterval(refreshUserUpdateIfActive, 10000), 7500);
|