tt-help-cli-ycl 1.4.17 → 1.4.18
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 +10 -0
- package/src/lib/constants.js +12 -2
- package/src/watch/data-store.js +89 -0
- 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 +307 -10
- package/src/watch/public/index.html +2 -1
- package/src/watch/public/style.css +30 -0
- package/src/watch/server.js +222 -9
- package/src/watch/tag-service.js +317 -10
package/src/watch/public/app.js
CHANGED
|
@@ -256,13 +256,21 @@ function renderActiveClients(clients) {
|
|
|
256
256
|
const tbody = document.getElementById("activeClientsBody");
|
|
257
257
|
if (!section || !bar) return;
|
|
258
258
|
|
|
259
|
-
const types = [
|
|
259
|
+
const types = [
|
|
260
|
+
"explore",
|
|
261
|
+
"refresh",
|
|
262
|
+
"attach",
|
|
263
|
+
"comments",
|
|
264
|
+
"scoring",
|
|
265
|
+
"tag-refresh",
|
|
266
|
+
];
|
|
260
267
|
const labels = {
|
|
261
268
|
explore: "Explore",
|
|
262
269
|
refresh: "Refresh",
|
|
263
270
|
attach: "Attach",
|
|
264
271
|
comments: "Comments",
|
|
265
272
|
scoring: "Scoring",
|
|
273
|
+
"tag-refresh": "TagRefresh",
|
|
266
274
|
};
|
|
267
275
|
const grouped = {};
|
|
268
276
|
for (const c of clients) {
|
|
@@ -352,7 +360,7 @@ function renderCountryChart(countries) {
|
|
|
352
360
|
const isTarget = targetLocations.includes(c.country);
|
|
353
361
|
const targetBadge =
|
|
354
362
|
c.targetCount > 0
|
|
355
|
-
? `<span class="target-badge">🎯 ${c.targetCount}</span>`
|
|
363
|
+
? `<span class="target-badge${isTarget ? "" : " target-badge-other"}">🎯 ${c.targetCount}</span>`
|
|
356
364
|
: `<span class="target-badge" style="visibility:hidden"> </span>`;
|
|
357
365
|
return `
|
|
358
366
|
<div class="bar-row${isTarget ? " is-target" : ""}">
|
|
@@ -796,6 +804,55 @@ async function confirmNonSeller(uniqueId) {
|
|
|
796
804
|
}
|
|
797
805
|
}
|
|
798
806
|
|
|
807
|
+
async function moveToBase(uniqueId) {
|
|
808
|
+
if (
|
|
809
|
+
!confirm(
|
|
810
|
+
`确定要将 @${uniqueId} 移到 jobs_base 并重走流程吗?\n这会将用户从 jobs 表移到 jobs_base,完全从头开始采集。`,
|
|
811
|
+
)
|
|
812
|
+
) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
showLoading("正在移动...");
|
|
816
|
+
try {
|
|
817
|
+
const res = await fetch(
|
|
818
|
+
`/api/user-move-to-base/${encodeURIComponent(uniqueId)}`,
|
|
819
|
+
{
|
|
820
|
+
method: "PUT",
|
|
821
|
+
headers: { "Content-Type": "application/json" },
|
|
822
|
+
},
|
|
823
|
+
);
|
|
824
|
+
const data = await res.json();
|
|
825
|
+
if (data.error) {
|
|
826
|
+
showToast(data.error, true);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
showToast(`@${uniqueId} 已移到 jobs_base,将重走流程`);
|
|
830
|
+
|
|
831
|
+
// 从当前列表中移除该用户
|
|
832
|
+
currentTargetUsers = currentTargetUsers.filter(
|
|
833
|
+
(u) => u.uniqueId !== uniqueId,
|
|
834
|
+
);
|
|
835
|
+
currentTargetTotal = Math.max(0, currentTargetTotal - 1);
|
|
836
|
+
renderTargetTable();
|
|
837
|
+
|
|
838
|
+
// 更新统计
|
|
839
|
+
const statEl = document.getElementById("targetPageStatTotal");
|
|
840
|
+
if (statEl)
|
|
841
|
+
statEl.textContent = formatStatNum(currentTargetTotal, { full: true });
|
|
842
|
+
if (currentStats) {
|
|
843
|
+
currentStats.targetUsers = Math.max(
|
|
844
|
+
0,
|
|
845
|
+
(currentStats.targetUsers || 0) - 1,
|
|
846
|
+
);
|
|
847
|
+
flashEl("statTarget", currentStats.targetUsers, { full: true });
|
|
848
|
+
}
|
|
849
|
+
} catch (e) {
|
|
850
|
+
showToast("移动失败: " + e.message, true);
|
|
851
|
+
} finally {
|
|
852
|
+
hideLoading();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
799
856
|
async function submitAddUsers() {
|
|
800
857
|
const ta = document.getElementById("modalUserInput");
|
|
801
858
|
const raw = ta.value.trim();
|
|
@@ -1051,6 +1108,121 @@ function scrollToTop() {
|
|
|
1051
1108
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
1052
1109
|
}
|
|
1053
1110
|
|
|
1111
|
+
// ── 回顶部按钮拖拽 ──
|
|
1112
|
+
(function initBackToTopDrag() {
|
|
1113
|
+
const btn = document.getElementById("backToTop");
|
|
1114
|
+
if (!btn) return;
|
|
1115
|
+
|
|
1116
|
+
let startX,
|
|
1117
|
+
startY,
|
|
1118
|
+
startLeft,
|
|
1119
|
+
startTop,
|
|
1120
|
+
dragging = false,
|
|
1121
|
+
moved = false;
|
|
1122
|
+
const DRAG_THRESHOLD = 5;
|
|
1123
|
+
const STORAGE_KEY = "backToTopPos";
|
|
1124
|
+
|
|
1125
|
+
// 恢复保存的位置
|
|
1126
|
+
const saved = (function () {
|
|
1127
|
+
try {
|
|
1128
|
+
return JSON.parse(localStorage.getItem(STORAGE_KEY));
|
|
1129
|
+
} catch (_) {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
})();
|
|
1133
|
+
if (
|
|
1134
|
+
saved &&
|
|
1135
|
+
typeof saved.left === "number" &&
|
|
1136
|
+
typeof saved.top === "number"
|
|
1137
|
+
) {
|
|
1138
|
+
btn.style.left = saved.left + "px";
|
|
1139
|
+
btn.style.top = saved.top + "px";
|
|
1140
|
+
btn.style.right = "auto";
|
|
1141
|
+
btn.style.bottom = "auto";
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function clamp(val, min, max) {
|
|
1145
|
+
return Math.min(Math.max(val, min), max);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function getPos(e) {
|
|
1149
|
+
if (e.touches) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
1150
|
+
return { x: e.clientX, y: e.clientY };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function onStart(e) {
|
|
1154
|
+
if (e.button !== undefined && e.button !== 0) return; // 只响应左键
|
|
1155
|
+
const pos = getPos(e);
|
|
1156
|
+
startX = pos.x;
|
|
1157
|
+
startY = pos.y;
|
|
1158
|
+
const rect = btn.getBoundingClientRect();
|
|
1159
|
+
startLeft = rect.left;
|
|
1160
|
+
startTop = rect.top;
|
|
1161
|
+
moved = false;
|
|
1162
|
+
dragging = true;
|
|
1163
|
+
btn.classList.add("dragging");
|
|
1164
|
+
btn.style.right = "auto";
|
|
1165
|
+
btn.style.bottom = "auto";
|
|
1166
|
+
btn.style.left = startLeft + "px";
|
|
1167
|
+
btn.style.top = startTop + "px";
|
|
1168
|
+
e.preventDefault();
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function onMove(e) {
|
|
1172
|
+
if (!dragging) return;
|
|
1173
|
+
const pos = getPos(e);
|
|
1174
|
+
const dx = pos.x - startX;
|
|
1175
|
+
const dy = pos.y - startY;
|
|
1176
|
+
if (
|
|
1177
|
+
!moved &&
|
|
1178
|
+
Math.abs(dx) < DRAG_THRESHOLD &&
|
|
1179
|
+
Math.abs(dy) < DRAG_THRESHOLD
|
|
1180
|
+
)
|
|
1181
|
+
return;
|
|
1182
|
+
moved = true;
|
|
1183
|
+
const newLeft = clamp(
|
|
1184
|
+
startLeft + dx,
|
|
1185
|
+
0,
|
|
1186
|
+
window.innerWidth - btn.offsetWidth,
|
|
1187
|
+
);
|
|
1188
|
+
const newTop = clamp(
|
|
1189
|
+
startTop + dy,
|
|
1190
|
+
0,
|
|
1191
|
+
window.innerHeight - btn.offsetHeight,
|
|
1192
|
+
);
|
|
1193
|
+
btn.style.left = newLeft + "px";
|
|
1194
|
+
btn.style.top = newTop + "px";
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function onEnd() {
|
|
1198
|
+
if (!dragging) return;
|
|
1199
|
+
dragging = false;
|
|
1200
|
+
btn.classList.remove("dragging");
|
|
1201
|
+
if (moved) {
|
|
1202
|
+
// 保存位置
|
|
1203
|
+
try {
|
|
1204
|
+
localStorage.setItem(
|
|
1205
|
+
STORAGE_KEY,
|
|
1206
|
+
JSON.stringify({
|
|
1207
|
+
left: parseInt(btn.style.left),
|
|
1208
|
+
top: parseInt(btn.style.top),
|
|
1209
|
+
}),
|
|
1210
|
+
);
|
|
1211
|
+
} catch (_) {}
|
|
1212
|
+
} else {
|
|
1213
|
+
// 未拖动则视为点击
|
|
1214
|
+
scrollToTop();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
btn.addEventListener("mousedown", onStart);
|
|
1219
|
+
btn.addEventListener("touchstart", onStart, { passive: false });
|
|
1220
|
+
window.addEventListener("mousemove", onMove);
|
|
1221
|
+
window.addEventListener("touchmove", onMove, { passive: false });
|
|
1222
|
+
window.addEventListener("mouseup", onEnd);
|
|
1223
|
+
window.addEventListener("touchend", onEnd);
|
|
1224
|
+
})();
|
|
1225
|
+
|
|
1054
1226
|
function handleRoute() {
|
|
1055
1227
|
const hash = window.location.hash;
|
|
1056
1228
|
if (hash === "#pending") {
|
|
@@ -1291,6 +1463,7 @@ let currentTagsAllLoaded = false;
|
|
|
1291
1463
|
let currentTagsSeq = 0;
|
|
1292
1464
|
const TAGS_PAGE_SIZE = 200;
|
|
1293
1465
|
let cachedTagCountries = [];
|
|
1466
|
+
let currentRefreshHours = 24;
|
|
1294
1467
|
|
|
1295
1468
|
function showTagsPage() {
|
|
1296
1469
|
document.getElementById("mainPage").classList.add("hidden");
|
|
@@ -1300,7 +1473,9 @@ function showTagsPage() {
|
|
|
1300
1473
|
document.getElementById("targetPage").classList.remove("active");
|
|
1301
1474
|
document.getElementById("tagsPage").classList.add("active");
|
|
1302
1475
|
showLoading("正在加载关键词...");
|
|
1303
|
-
Promise.all([loadTagStats(), loadTagsPage()]).finally(
|
|
1476
|
+
Promise.all([loadTagStats(), loadTagsPage(), loadRefreshStats()]).finally(
|
|
1477
|
+
hideLoading,
|
|
1478
|
+
);
|
|
1304
1479
|
}
|
|
1305
1480
|
|
|
1306
1481
|
function setTagsFilter(filter) {
|
|
@@ -1325,13 +1500,94 @@ async function loadTagStats() {
|
|
|
1325
1500
|
flashEl("tagsPageStatDead", stats.dead || 0);
|
|
1326
1501
|
flashEl("tagsPageStatNew", (stats.new || 0) + (stats.scoring || 0));
|
|
1327
1502
|
cachedTagCountries = stats.countries || [];
|
|
1328
|
-
// 初始化国家下拉
|
|
1329
1503
|
renderTagsCountryFilter();
|
|
1330
1504
|
} catch (e) {
|
|
1331
1505
|
console.error("获取关键词统计失败:", e);
|
|
1332
1506
|
}
|
|
1333
1507
|
}
|
|
1334
1508
|
|
|
1509
|
+
async function loadRefreshStats() {
|
|
1510
|
+
try {
|
|
1511
|
+
const res = await fetch(
|
|
1512
|
+
`/api/tags/refresh-stats?hours=${currentRefreshHours}`,
|
|
1513
|
+
);
|
|
1514
|
+
const data = await res.json();
|
|
1515
|
+
renderRefreshStats(data);
|
|
1516
|
+
} catch (e) {
|
|
1517
|
+
console.error("获取刷新统计失败:", e);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function renderRefreshStats(data) {
|
|
1522
|
+
const el = document.getElementById("tagsRefreshStats");
|
|
1523
|
+
if (!el || !data || !data.countries || data.countries.length === 0) {
|
|
1524
|
+
if (el) el.innerHTML = "";
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const pct =
|
|
1528
|
+
data.total > 0 ? Math.round((data.refreshed / data.total) * 100) : 0;
|
|
1529
|
+
const barW = Math.min(pct, 100);
|
|
1530
|
+
|
|
1531
|
+
const HOUR_OPTIONS = [12, 24, 36, 48, 72];
|
|
1532
|
+
let html = `<div style="background:#1a1a2e;border:1px solid #2a2a3a;border-radius:10px;padding:16px">
|
|
1533
|
+
<div id="refreshStatsHeader" style="display:flex;align-items:center;gap:16px;cursor:pointer;user-select:none">
|
|
1534
|
+
<span style="font-size:14px;font-weight:600;color:#e0e0e0;position:relative">
|
|
1535
|
+
📊 最近 <span id="refreshHoursLabel" style="color:#60a5fa;cursor:pointer;text-decoration:underline dotted" onclick="event.stopPropagation();toggleHoursDropdown(event)">${currentRefreshHours}h</span> 刷新进度
|
|
1536
|
+
<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">
|
|
1537
|
+
${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("")}
|
|
1538
|
+
</div>
|
|
1539
|
+
</span>
|
|
1540
|
+
<span style="font-size:13px;color:#9ca3af">${data.refreshed} / ${data.total}</span>
|
|
1541
|
+
<div style="flex:1;max-width:200px;height:8px;background:#2a2a3a;border-radius:4px;overflow:hidden">
|
|
1542
|
+
<div style="height:100%;width:${barW}%;background:${pct >= 50 ? "#22c55e" : "#facc15"};border-radius:4px;transition:width 0.3s"></div>
|
|
1543
|
+
</div>
|
|
1544
|
+
<span style="font-size:14px;font-weight:700;color:${pct >= 50 ? "#22c55e" : "#facc15"}">${pct}%</span>
|
|
1545
|
+
<span id="refreshStatsArrow" style="color:#888;font-size:12px">▶</span>
|
|
1546
|
+
</div>
|
|
1547
|
+
<div id="refreshStatsBody" style="display:none;margin-top:12px">
|
|
1548
|
+
<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
1549
|
+
<thead><tr style="color:#888;text-align:left">
|
|
1550
|
+
<th style="padding:4px 8px">国家</th>
|
|
1551
|
+
<th style="padding:4px 8px;text-align:right">需刷新</th>
|
|
1552
|
+
<th style="padding:4px 8px;text-align:right">${currentRefreshHours}h已刷</th>
|
|
1553
|
+
<th style="padding:4px 8px;width:120px">进度</th>
|
|
1554
|
+
</tr></thead>
|
|
1555
|
+
<tbody>`;
|
|
1556
|
+
|
|
1557
|
+
for (const c of data.countries) {
|
|
1558
|
+
const cpct = c.total > 0 ? Math.round((c.refreshed / c.total) * 100) : 0;
|
|
1559
|
+
const cBarW = Math.min(cpct, 100);
|
|
1560
|
+
html += `<tr style="color:#ccc;border-top:1px solid #222">
|
|
1561
|
+
<td style="padding:4px 8px">${c.country}</td>
|
|
1562
|
+
<td style="padding:4px 8px;text-align:right">${c.total}</td>
|
|
1563
|
+
<td style="padding:4px 8px;text-align:right;color:${cpct >= 50 ? "#22c55e" : "#facc15"}">${c.refreshed}</td>
|
|
1564
|
+
<td style="padding:4px 8px">
|
|
1565
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
1566
|
+
<div style="flex:1;height:6px;background:#2a2a3a;border-radius:3px;overflow:hidden">
|
|
1567
|
+
<div style="height:100%;width:${cBarW}%;background:${cpct >= 50 ? "#22c55e" : "#facc15"};border-radius:3px"></div>
|
|
1568
|
+
</div>
|
|
1569
|
+
<span style="font-size:11px;color:#9ca3af;min-width:28px;text-align:right">${cpct}%</span>
|
|
1570
|
+
</div>
|
|
1571
|
+
</td>
|
|
1572
|
+
</tr>`;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
html += `</tbody></table></div></div>`;
|
|
1576
|
+
el.innerHTML = html;
|
|
1577
|
+
|
|
1578
|
+
// 绑定展开/折叠
|
|
1579
|
+
const header = document.getElementById("refreshStatsHeader");
|
|
1580
|
+
const body = document.getElementById("refreshStatsBody");
|
|
1581
|
+
const arrow = document.getElementById("refreshStatsArrow");
|
|
1582
|
+
if (header && body && arrow) {
|
|
1583
|
+
header.addEventListener("click", () => {
|
|
1584
|
+
const open = body.style.display !== "none";
|
|
1585
|
+
body.style.display = open ? "none" : "";
|
|
1586
|
+
arrow.textContent = open ? "▶" : "▼";
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1335
1591
|
function renderTagsCountryFilter() {
|
|
1336
1592
|
const sel = document.getElementById("tagsCountryFilter");
|
|
1337
1593
|
if (!sel) return;
|
|
@@ -1441,7 +1697,7 @@ function renderTagsTable() {
|
|
|
1441
1697
|
const matchedAuthors = t.matched_authors ?? "-";
|
|
1442
1698
|
const pushedUsers = t.pushed_users ?? "-";
|
|
1443
1699
|
const refreshedAt = t.refreshed_at
|
|
1444
|
-
? t.refreshed_at
|
|
1700
|
+
? formatTime(new Date(t.refreshed_at).getTime())
|
|
1445
1701
|
: "-";
|
|
1446
1702
|
|
|
1447
1703
|
return `<tr>
|
|
@@ -1486,7 +1742,7 @@ document
|
|
|
1486
1742
|
.addEventListener("click", async () => {
|
|
1487
1743
|
showLoading("正在刷新...");
|
|
1488
1744
|
try {
|
|
1489
|
-
await Promise.all([loadTagStats(), loadTagsPage()]);
|
|
1745
|
+
await Promise.all([loadTagStats(), loadTagsPage(), loadRefreshStats()]);
|
|
1490
1746
|
showToast("刷新完成");
|
|
1491
1747
|
} catch (e) {
|
|
1492
1748
|
showToast("刷新失败: " + e.message, true);
|
|
@@ -1519,6 +1775,46 @@ function loadMoreTags() {
|
|
|
1519
1775
|
loadTagsPage(true);
|
|
1520
1776
|
}
|
|
1521
1777
|
|
|
1778
|
+
// ====== 刷新统计时间切换 ======
|
|
1779
|
+
|
|
1780
|
+
function toggleHoursDropdown(event) {
|
|
1781
|
+
const dropdown = document.getElementById("refreshHoursDropdown");
|
|
1782
|
+
if (!dropdown) return;
|
|
1783
|
+
const isOpen = dropdown.style.display !== "none";
|
|
1784
|
+
dropdown.style.display = isOpen ? "none" : "block";
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
async function selectRefreshHours(hours) {
|
|
1788
|
+
const dropdown = document.getElementById("refreshHoursDropdown");
|
|
1789
|
+
if (dropdown) dropdown.style.display = "none";
|
|
1790
|
+
if (hours === currentRefreshHours) return;
|
|
1791
|
+
currentRefreshHours = hours;
|
|
1792
|
+
showLoading("正在加载...");
|
|
1793
|
+
try {
|
|
1794
|
+
await loadRefreshStats();
|
|
1795
|
+
const label = document.getElementById("refreshHoursLabel");
|
|
1796
|
+
if (label) label.textContent = hours + "h";
|
|
1797
|
+
} catch (e) {
|
|
1798
|
+
console.error("刷新统计失败:", e);
|
|
1799
|
+
} finally {
|
|
1800
|
+
hideLoading();
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// 点击空白处关闭下拉
|
|
1805
|
+
document.addEventListener("click", (e) => {
|
|
1806
|
+
const dropdown = document.getElementById("refreshHoursDropdown");
|
|
1807
|
+
const label = document.getElementById("refreshHoursLabel");
|
|
1808
|
+
if (
|
|
1809
|
+
dropdown &&
|
|
1810
|
+
dropdown.style.display !== "none" &&
|
|
1811
|
+
!label?.contains(e.target) &&
|
|
1812
|
+
!dropdown.contains(e.target)
|
|
1813
|
+
) {
|
|
1814
|
+
dropdown.style.display = "none";
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1522
1818
|
// 排序
|
|
1523
1819
|
document.querySelectorAll(".sortable-tag").forEach((th) => {
|
|
1524
1820
|
th.addEventListener("click", () => {
|
|
@@ -1816,7 +2112,7 @@ function renderTargetTable() {
|
|
|
1816
2112
|
<td data-label="最近发布" style="font-size:11px;color:#888">${latestVideo}</td>
|
|
1817
2113
|
<td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
|
|
1818
2114
|
<td data-label="操作">
|
|
1819
|
-
<button class="btn-
|
|
2115
|
+
<button class="btn-pending" onclick="moveToBase('${escapeJsString(u.uniqueId)}')" title="移到 jobs_base 重走流程">待确认</button>
|
|
1820
2116
|
</td>
|
|
1821
2117
|
</tr>`;
|
|
1822
2118
|
})
|
|
@@ -2587,7 +2883,8 @@ fetchClientErrors();
|
|
|
2587
2883
|
initTableSorting();
|
|
2588
2884
|
initTargetTableSorting();
|
|
2589
2885
|
|
|
2886
|
+
// 错峰轮询,避免同时发 4 个请求阻塞服务端
|
|
2590
2887
|
setInterval(fetchStats, 10000);
|
|
2591
|
-
setInterval(fetchUsers, 10000);
|
|
2592
|
-
setInterval(fetchClientErrors, 10000);
|
|
2593
|
-
setInterval(refreshUserUpdateIfActive, 10000);
|
|
2888
|
+
setTimeout(() => setInterval(fetchUsers, 10000), 2500);
|
|
2889
|
+
setTimeout(() => setInterval(fetchClientErrors, 10000), 5000);
|
|
2890
|
+
setTimeout(() => setInterval(refreshUserUpdateIfActive, 10000), 7500);
|
|
@@ -370,7 +370,7 @@
|
|
|
370
370
|
</div>
|
|
371
371
|
</div>
|
|
372
372
|
</div>
|
|
373
|
-
<div id="backToTop" title="
|
|
373
|
+
<div id="backToTop" title="回到顶部(可拖动)">↑</div>
|
|
374
374
|
</div>
|
|
375
375
|
<div id="tagsPage">
|
|
376
376
|
<div class="stats" style="margin-bottom:16px">
|
|
@@ -397,6 +397,7 @@
|
|
|
397
397
|
<div class="label">🔄 刷新</div>
|
|
398
398
|
</div>
|
|
399
399
|
</div>
|
|
400
|
+
<div id="tagsRefreshStats" style="margin-bottom:16px"></div>
|
|
400
401
|
<div class="tags-layout">
|
|
401
402
|
<div class="tags-table-card">
|
|
402
403
|
<h3>关键词列表</h3>
|
|
@@ -306,6 +306,10 @@ body {
|
|
|
306
306
|
font-weight: 600;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
.target-badge-other {
|
|
310
|
+
color: #666 !important;
|
|
311
|
+
}
|
|
312
|
+
|
|
309
313
|
.source-row {
|
|
310
314
|
display: flex;
|
|
311
315
|
align-items: center;
|
|
@@ -869,6 +873,12 @@ td.user-id:hover {
|
|
|
869
873
|
display: flex;
|
|
870
874
|
}
|
|
871
875
|
|
|
876
|
+
#backToTop.dragging {
|
|
877
|
+
transition: none;
|
|
878
|
+
opacity: 0.9;
|
|
879
|
+
cursor: grabbing;
|
|
880
|
+
}
|
|
881
|
+
|
|
872
882
|
#mainPage.hidden {
|
|
873
883
|
display: none;
|
|
874
884
|
}
|
|
@@ -1483,6 +1493,26 @@ td.user-id:hover {
|
|
|
1483
1493
|
color: #ef4444;
|
|
1484
1494
|
}
|
|
1485
1495
|
|
|
1496
|
+
/* 待确认按钮 */
|
|
1497
|
+
.btn-pending {
|
|
1498
|
+
padding: 4px 10px;
|
|
1499
|
+
border: 1px solid #fbbf24;
|
|
1500
|
+
border-radius: 4px;
|
|
1501
|
+
background: transparent;
|
|
1502
|
+
color: #fbbf24;
|
|
1503
|
+
font-size: 11px;
|
|
1504
|
+
font-weight: 600;
|
|
1505
|
+
cursor: pointer;
|
|
1506
|
+
transition: all 0.15s;
|
|
1507
|
+
white-space: nowrap;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
.btn-pending:hover {
|
|
1511
|
+
background: rgba(251, 191, 36, 0.12);
|
|
1512
|
+
border-color: #f59e0b;
|
|
1513
|
+
color: #f59e0b;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1486
1516
|
/* ===== 关键词页面 ===== */
|
|
1487
1517
|
#tagsPage {
|
|
1488
1518
|
display: none;
|