const HOTNESS_AUTH_TOKEN_KEY = "video-hotness-auth-token-v1";
const authGate = document.querySelector("#auth-gate");
const authForm = document.querySelector("#auth-form");
const authPassword = document.querySelector("#auth-password");
const authSubmit = document.querySelector("#auth-submit");
const authMessage = document.querySelector("#auth-message");
const form = document.querySelector("#collect-form");
const input = document.querySelector("#program-name");
const button = document.querySelector("#collect-button");
const exportLink = document.querySelector("#export-link");
const statusDot = document.querySelector("#status-dot");
const statusText = document.querySelector("#status-text");
const tableTitle = document.querySelector("#table-title");
const runCount = document.querySelector("#run-count");
const cards = document.querySelector("#cards");
const programList = document.querySelector("#program-list");
const networkLinks = document.querySelector("#network-links");
const collectPlatformBox = document.querySelector(".collect-platforms");
const mobileNote = document.querySelector("#mobile-note");
const mobileDeviceNameInput = document.querySelector("#mobile-device-name");
const saveDeviceNameButton = document.querySelector("#save-device-name-button");
const saveOfflineButton = document.querySelector("#save-offline-button");
const offlineCount = document.querySelector("#offline-count");
const offlineList = document.querySelector("#offline-list");
const clearOfflineButton = document.querySelector("#clear-offline-button");
const syncOfflineButton = document.querySelector("#sync-offline-button");
const mobileBatchText = document.querySelector("#mobile-batch-text");
const saveBatchOfflineButton = document.querySelector("#save-batch-offline-button");
const offlineStatus = document.querySelector("#offline-status");
const installHint = document.querySelector("#install-hint");
const installStatus = document.querySelector("#install-status");
const installAppButton = document.querySelector("#install-app-button");
const mobileServerUrlInput = document.querySelector("#mobile-server-url");
const saveMobileServerButton = document.querySelector("#save-mobile-server-button");
const testMobileServerButton = document.querySelector("#test-mobile-server-button");
const mobileBindingSummary = document.querySelector("#mobile-binding-summary");
const mobileAppState = document.querySelector("#mobile-app-state");
const MOBILE_DRAFTS_KEY = "video-hotness-mobile-drafts-v1";
const MOBILE_DEVICE_KEY = "video-hotness-mobile-device-v1";
const MOBILE_SERVER_KEY = "video-hotness-mobile-server-v1";
const platformOrder = ["tencent", "youku", "iqiyi", "mgtv"];
const platformLabels = {
tencent: "腾讯视频",
youku: "优酷",
iqiyi: "爱奇艺",
mgtv: "芒果TV",
};
const metricLabels = {
tencent: "热度值",
youku: "热度值",
iqiyi: "内容热度",
mgtv: "播放次数",
};
const urlInputs = {
tencent: document.querySelector("#url-tencent"),
youku: document.querySelector("#url-youku"),
iqiyi: document.querySelector("#url-iqiyi"),
mgtv: document.querySelector("#url-mgtv"),
};
let activeName = "";
let dirtyUrlInputs = new Set();
let deferredInstallPrompt = null;
let appStarted = false;
let authSubmitting = false;
authForm?.addEventListener("submit", async (event) => {
event.preventDefault();
await submitAccessPassword();
});
authSubmit?.addEventListener("click", async (event) => {
event.preventDefault();
await submitAccessPassword();
});
for (const [platform, element] of Object.entries(urlInputs)) {
element.addEventListener("input", () => {
dirtyUrlInputs.add(platform);
});
}
input.addEventListener("input", () => {
const name = input.value.trim();
if (activeName && name !== activeName) {
clearUrlInputs();
}
});
form.addEventListener("submit", async (event) => {
event.preventDefault();
const name = input.value.trim();
if (!name) return;
activeName = name;
const platforms = readCollectPlatforms();
if (platforms.length === 0) {
setStatus("error", "请至少选择一个采集平台");
return;
}
setBusy(true, `正在采集《${name}》`);
try {
const payload = await postJson("/api/collect", { name, urls: readUrlInputs(), platforms });
renderHistory(payload.history);
await refreshPrograms();
setStatus("ok", `已新增 ${formatTime(payload.collection.captured_at)} 这一列`);
} catch (error) {
setStatus("error", error.message);
} finally {
setBusy(false);
}
});
collectPlatformBox.addEventListener("change", (event) => {
if (!event.target.matches("input[type='checkbox']")) return;
updateCollectPlatformState();
});
saveOfflineButton.addEventListener("click", () => {
saveOfflineDraft();
});
saveDeviceNameButton.addEventListener("click", () => {
saveMobileDeviceName();
});
saveMobileServerButton.addEventListener("click", () => {
saveMobileServerUrl();
});
testMobileServerButton.addEventListener("click", () => {
testMobileServerConnection();
});
installAppButton.addEventListener("click", () => {
installMobileApp();
});
saveBatchOfflineButton.addEventListener("click", () => {
saveBatchOfflineDrafts();
});
syncOfflineButton.addEventListener("click", () => {
syncOfflineDrafts();
});
clearOfflineButton.addEventListener("click", () => {
const drafts = readOfflineDrafts();
if (drafts.length === 0) return;
if (!window.confirm(`确定清空 ${drafts.length} 条手机待同步记录吗?`)) return;
localStorage.setItem(MOBILE_DRAFTS_KEY, "[]");
renderOfflineDrafts();
setStatus("ok", "已清空手机待同步列表");
});
offlineList.addEventListener("click", (event) => {
const editButton = event.target.closest("[data-edit-draft]");
if (editButton) {
editOfflineDraft(editButton.dataset.editDraft);
return;
}
const deleteButton = event.target.closest("[data-delete-draft]");
if (deleteButton) {
deleteOfflineDraft(deleteButton.dataset.deleteDraft);
}
});
programList.addEventListener("click", async (event) => {
const item = event.target.closest("[data-name]");
if (!item) return;
activeName = item.dataset.name;
input.value = activeName;
await loadHistory(activeName);
});
window.addEventListener("online", updateOfflineStatus);
window.addEventListener("offline", updateOfflineStatus);
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
deferredInstallPrompt = event;
updateInstallPrompt("ready");
});
window.addEventListener("appinstalled", () => {
deferredInstallPrompt = null;
updateInstallPrompt("installed");
});
initializeApp();
async function initializeApp() {
if (!(await ensureAccessAuth())) return;
startApp();
}
async function startApp() {
if (appStarted) return;
appStarted = true;
updateCollectPlatformState();
mobileDeviceNameInput.value = mobileDeviceName();
mobileServerUrlInput.value = mobileServerBaseUrl();
registerMobileServiceWorker();
updateInstallPrompt(isStandaloneDisplay() ? "installed" : "manual");
updateOfflineStatus();
updateMobileBindingSummary();
renderOfflineDrafts();
await Promise.all([refreshPrograms(), loadNetworkLinks()]);
}
async function registerMobileServiceWorker() {
if (!("serviceWorker" in navigator)) {
if (installStatus) installStatus.textContent = "当前浏览器不支持离线缓存,可继续使用手机待同步列表。";
return;
}
try {
await navigator.serviceWorker.register("/mobile-sw.js");
} catch {
if (installStatus) installStatus.textContent = "离线缓存注册失败,可刷新后重试。";
}
}
async function installMobileApp() {
if (!deferredInstallPrompt) {
updateInstallPrompt("manual");
return;
}
installAppButton.disabled = true;
deferredInstallPrompt.prompt();
const choice = await deferredInstallPrompt.userChoice.catch(() => ({ outcome: "dismissed" }));
deferredInstallPrompt = null;
installAppButton.disabled = false;
updateInstallPrompt(choice.outcome === "accepted" ? "installed" : "manual");
}
function updateInstallPrompt(state) {
if (!installStatus || !installAppButton) return;
installHint.classList.toggle("install-ready", state === "ready");
if (state === "ready") {
installStatus.textContent = "当前浏览器支持直接安装,点击按钮后会添加到手机桌面。";
installAppButton.hidden = false;
return;
}
installAppButton.hidden = true;
installStatus.textContent = state === "installed"
? "已用 App 模式打开;离线录入和待同步列表可继续使用。"
: "如果没有安装按钮,请在手机浏览器菜单选择“添加到主屏幕”。";
}
function isStandaloneDisplay() {
return window.matchMedia?.("(display-mode: standalone)").matches || window.navigator.standalone === true;
}
function updateOfflineStatus() {
if (!offlineStatus) return;
const online = navigator.onLine;
const pendingDrafts = readOfflineDrafts().filter((draft) => draft.sync_status !== "synced");
if (mobileAppState) {
mobileAppState.textContent = online ? "在线" : "离线";
mobileAppState.classList.toggle("offline", !online);
}
offlineStatus.classList.toggle("offline", !online);
offlineStatus.innerHTML = online && pendingDrafts.length
? `有 ${pendingDrafts.length} 条可同步电脑可访问时点击“同步到电脑”,同步后会显示电脑已收到。`
: online
? `离线录入已准备首次打开后会缓存手机版;离开局域网时仍可保存待同步。`
: `当前离线可以继续录入并保存待同步;回到局域网后再同步到电脑。`;
}
async function loadHistory(name) {
setStatus("busy", `正在读取《${name}》历史`);
try {
const payload = await getJson(`/api/history?name=${encodeURIComponent(name)}`);
renderHistory(payload.history);
setStatus("ok", `已载入《${name}》`);
} catch (error) {
setStatus("error", error.message);
}
}
async function refreshPrograms() {
try {
const payload = await getJson("/api/programs");
renderPrograms(payload.programs || []);
} catch {
renderPrograms([]);
}
}
async function loadNetworkLinks() {
try {
const payload = await getJson("/api/network");
const urls = payload.urls || [];
networkLinks.innerHTML = urls.length
? urls.map((url) => `${escapeHtml(url)}`).join("
")
: "没有读取到局域网地址,可先用本机浏览器访问。";
} catch {
networkLinks.textContent = "局域网地址读取失败。";
}
}
function mobileServerBaseUrl() {
const saved = (localStorage.getItem(MOBILE_SERVER_KEY) || "").trim();
return saved || window.location.origin;
}
function normalizeServerUrl(value) {
const text = String(value || "").trim().replace(/\/+$/, "");
if (!text) return "";
try {
const parsed = new URL(text);
return parsed.origin;
} catch {
return "";
}
}
function apiUrl(path) {
return `${mobileServerBaseUrl()}${path}`;
}
function saveMobileServerUrl() {
const normalized = normalizeServerUrl(mobileServerUrlInput.value);
if (!normalized) {
setStatus("error", "请输入正确的电脑或 NAS 地址,例如 http://192.168.18.120:3001");
return;
}
localStorage.setItem(MOBILE_SERVER_KEY, normalized);
mobileServerUrlInput.value = normalized;
updateMobileBindingSummary();
setStatus("ok", `已绑定地址:${normalized}`);
}
async function testMobileServerConnection() {
const normalized = normalizeServerUrl(mobileServerUrlInput.value) || mobileServerBaseUrl();
if (normalized !== mobileServerBaseUrl()) {
localStorage.setItem(MOBILE_SERVER_KEY, normalized);
mobileServerUrlInput.value = normalized;
}
testMobileServerButton.disabled = true;
setStatus("busy", "正在测试电脑端连接");
try {
const payload = await getJson("/api/network");
updateMobileBindingSummary({ ok: true });
setStatus("ok", `连接正常,读取到 ${(payload.urls || []).length} 个手机访问地址`);
} catch (error) {
updateMobileBindingSummary({ ok: false, error: error.message });
setStatus("error", `连接失败:${error.message}`);
} finally {
testMobileServerButton.disabled = false;
}
}
function updateMobileBindingSummary(result = null) {
const base = mobileServerBaseUrl();
const pendingDrafts = readOfflineDrafts().filter((draft) => draft.sync_status !== "synced").length;
const resultText = result?.ok ? " · 连接正常" : result?.error ? ` · 连接失败:${result.error}` : "";
mobileBindingSummary.textContent = `当前绑定:${base} · 待同步 ${pendingDrafts} 条${resultText}`;
}
function renderPrograms(programs) {
if (programs.length === 0) {
programList.innerHTML = `
暂无历史
`;
return;
}
programList.innerHTML = programs.map((program) => `
`).join("");
}
function saveOfflineDraft() {
const name = input.value.trim();
if (!name) {
setStatus("error", "请先输入节目名");
return;
}
const draft = createOfflineDraft({
name,
note: mobileNote.value.trim(),
urls: readAllUrlInputs(),
platforms: readCollectPlatforms(),
});
const drafts = readOfflineDrafts();
drafts.unshift(draft);
localStorage.setItem(MOBILE_DRAFTS_KEY, JSON.stringify(drafts.slice(0, 200)));
renderOfflineDrafts();
updateOfflineStatus();
updateMobileBindingSummary();
setStatus("ok", `已保存《${name}》到手机待同步`);
}
function saveBatchOfflineDrafts() {
const names = parseMobileBatchNames(mobileBatchText.value);
if (names.length === 0) {
setStatus("error", "请先粘贴节目名单");
return;
}
const drafts = readOfflineDrafts();
const newDrafts = names.map((name) => createOfflineDraft({
name,
note: "批量离线录入",
urls: {},
platforms: readCollectPlatforms(),
}));
localStorage.setItem(MOBILE_DRAFTS_KEY, JSON.stringify([...newDrafts, ...drafts].slice(0, 200)));
mobileBatchText.value = "";
renderOfflineDrafts();
updateOfflineStatus();
updateMobileBindingSummary();
setStatus("ok", `已批量保存 ${newDrafts.length} 条待同步`);
}
function parseMobileBatchNames(text) {
const seen = new Set();
const names = [];
for (const line of String(text || "").split(/\r?\n/)) {
const name = line.split(/[,,\t]/)[0].trim();
if (!name || seen.has(name) || /节目|名称|片名/.test(name)) continue;
seen.add(name);
names.push(name);
}
return names;
}
function createOfflineDraft({ name, note = "", urls = {}, platforms = [] }) {
return {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
name,
note,
urls,
platforms,
device_name: mobileDeviceName(),
created_at: new Date().toISOString(),
sync_status: "pending",
};
}
function readOfflineDrafts() {
try {
const value = JSON.parse(localStorage.getItem(MOBILE_DRAFTS_KEY) || "[]");
return Array.isArray(value) ? value.filter((draft) => draft?.name) : [];
} catch {
return [];
}
}
function renderOfflineDrafts() {
const drafts = readOfflineDrafts();
const pendingDrafts = drafts.filter((draft) => draft.sync_status !== "synced");
offlineCount.textContent = String(drafts.length);
if (drafts.length === 0) {
offlineList.innerHTML = `暂无手机待同步记录
`;
clearOfflineButton.disabled = true;
syncOfflineButton.disabled = true;
return;
}
clearOfflineButton.disabled = false;
syncOfflineButton.disabled = pendingDrafts.length === 0;
offlineList.innerHTML = drafts.slice(0, 20).map((draft) => {
const urlCount = Object.values(draft.urls || {}).filter(Boolean).length;
const note = draft.note ? `${escapeHtml(draft.note)}
` : "";
const isSynced = draft.sync_status === "synced";
const syncLabel = isSynced ? "已同步" : "待同步";
return `
${escapeHtml(draft.name)}
${syncLabel}
${formatTime(draft.created_at)} · ${urlCount} 个链接 · ${escapeHtml((draft.platforms || []).map((platform) => platformLabels[platform] || platform).join("、") || "未选平台")}
${note}
`;
}).join("");
}
function editOfflineDraft(id) {
const drafts = readOfflineDrafts();
const draft = drafts.find((item) => item.id === id);
if (!draft) return;
const nextName = window.prompt("修改节目名", draft.name);
if (nextName === null) return;
const cleanName = nextName.trim();
if (!cleanName) {
setStatus("error", "节目名不能为空");
return;
}
const nextNote = window.prompt("修改备注", draft.note || "");
if (nextNote === null) return;
const updated = drafts.map((item) => item.id === id
? { ...item, name: cleanName, note: nextNote.trim(), sync_status: "pending", edited_at: new Date().toISOString() }
: item);
localStorage.setItem(MOBILE_DRAFTS_KEY, JSON.stringify(updated));
renderOfflineDrafts();
updateOfflineStatus();
updateMobileBindingSummary();
setStatus("ok", `已更新《${cleanName}》`);
}
function deleteOfflineDraft(id) {
const drafts = readOfflineDrafts();
const draft = drafts.find((item) => item.id === id);
if (!draft) return;
if (!window.confirm(`删除《${draft.name}》这条手机记录吗?`)) return;
localStorage.setItem(MOBILE_DRAFTS_KEY, JSON.stringify(drafts.filter((item) => item.id !== id)));
renderOfflineDrafts();
updateOfflineStatus();
updateMobileBindingSummary();
setStatus("ok", `已删除《${draft.name}》`);
}
async function syncOfflineDrafts() {
const drafts = readOfflineDrafts();
const pendingDrafts = drafts.filter((draft) => draft.sync_status !== "synced");
if (pendingDrafts.length === 0) {
setStatus("ok", "没有待同步的手机记录");
renderOfflineDrafts();
return;
}
syncOfflineButton.disabled = true;
setStatus("busy", `正在同步 ${pendingDrafts.length} 条到电脑`);
try {
const payload = await postJson("/api/mobile-sync", {
deviceName: mobileDeviceName(),
drafts: pendingDrafts,
});
const acceptedIds = new Set((payload.accepted || []).map((item) => item.id));
const syncedAt = new Date().toISOString();
const updatedDrafts = drafts.map((draft) => acceptedIds.has(draft.id)
? { ...draft, sync_status: "synced", synced_at: syncedAt }
: draft);
localStorage.setItem(MOBILE_DRAFTS_KEY, JSON.stringify(updatedDrafts));
renderOfflineDrafts();
updateOfflineStatus();
updateMobileBindingSummary();
setStatus("ok", `电脑已收到 ${acceptedIds.size} 条,已进入待处理`);
} catch (error) {
setStatus("error", `同步失败:${error.message}`);
} finally {
syncOfflineButton.disabled = false;
renderOfflineDrafts();
updateMobileBindingSummary();
}
}
function mobileDeviceName() {
let deviceName = (mobileDeviceNameInput?.value || localStorage.getItem(MOBILE_DEVICE_KEY) || "").trim();
if (!deviceName) {
deviceName = `mobile-${Math.random().toString(16).slice(2, 8)}`;
localStorage.setItem(MOBILE_DEVICE_KEY, deviceName);
}
return deviceName;
}
function saveMobileDeviceName() {
const deviceName = mobileDeviceNameInput.value.trim();
if (!deviceName) {
setStatus("error", "请输入这台手机或录入人的名称");
return;
}
localStorage.setItem(MOBILE_DEVICE_KEY, deviceName);
renderOfflineDrafts();
setStatus("ok", `已保存手机名称:${deviceName}`);
}
function renderHistory(history) {
const runs = history.runs || [];
tableTitle.textContent = history.name ? `《${history.name}》` : "还没有采集结果";
runCount.textContent = `${runs.length} 次`;
exportLink.href = history.name ? `/api/export?name=${encodeURIComponent(history.name)}` : "#";
exportLink.setAttribute("aria-disabled", history.name && runs.length > 0 ? "false" : "true");
syncUrlInputs(history);
if (runs.length === 0) {
cards.innerHTML = `暂无采集结果
`;
return;
}
cards.innerHTML = platformOrder.map((platform) => {
const row = history.platforms?.[platform] || { values: {} };
const latestRun = runs[runs.length - 1];
const latest = row.values?.[latestRun];
return renderPlatformCard(platform, row, latest, runs);
}).join("");
}
function renderPlatformCard(platform, row, latest, runs) {
const label = row.platform_label || platformLabels[platform] || platform;
const metric = row.metric_label || metricLabels[platform] || "指标值";
const url = row.url || latest?.url || "";
const latestHtml = renderLatest(latest);
const historyRows = runs.slice(-5).reverse().map((run) => {
const value = row.values?.[run];
const shown = value?.status === "ok" ? (value.raw || value.number || "") : statusLabel(value?.status);
return `
${formatTime(run)}
${escapeHtml(shown || "未采集")}
`;
}).join("");
return `
${latestHtml}
${historyRows}
`;
}
function renderLatest(value) {
if (!value) return `未采集
`;
if (value.status === "ok") {
const shown = value.raw || value.number || "";
const meta = value.number && String(value.number) !== String(value.raw) ? `标准化:${value.number}` : "";
const anomaly = value.anomaly ? `异常` : "";
const credibility = renderCredibilityBadge(value.credibility);
return `
${escapeHtml(shown)}${anomaly}
${credibility}
${meta ? `${escapeHtml(meta)}
` : ""}
${value.credibility?.reason ? `${escapeHtml(value.credibility.reason)}
` : ""}
${value.anomaly ? `${escapeHtml(value.anomaly.message || "")}
` : ""}
`;
}
const tone = value.status === "blocked" ? "warn" : "bad";
return `
${escapeHtml(statusLabel(value.status))}
${renderCredibilityBadge(value.credibility)}
${escapeHtml(value.error || "")}
`;
}
function renderCredibilityBadge(credibility) {
if (!credibility?.label) return "";
return `${escapeHtml(credibility.label)}`;
}
function syncUrlInputs(history) {
for (const platform of platformOrder) {
const input = urlInputs[platform];
if (!input) continue;
input.value = history.platforms?.[platform]?.url || "";
}
dirtyUrlInputs.clear();
}
function readUrlInputs() {
return Object.fromEntries(platformOrder
.filter((platform) => dirtyUrlInputs.has(platform))
.map((platform) => [
platform,
urlInputs[platform]?.value.trim() || "",
]));
}
function readAllUrlInputs() {
return Object.fromEntries(platformOrder.map((platform) => [
platform,
urlInputs[platform]?.value.trim() || "",
]));
}
function readCollectPlatforms() {
return [...collectPlatformBox.querySelectorAll("input[type='checkbox']:checked")]
.map((checkbox) => checkbox.value)
.filter((platform) => platformOrder.includes(platform));
}
function updateCollectPlatformState() {
for (const label of collectPlatformBox.querySelectorAll("label")) {
const checkbox = label.querySelector("input");
label.classList.toggle("active", checkbox.checked);
}
}
function clearUrlInputs() {
for (const input of Object.values(urlInputs)) {
input.value = "";
}
dirtyUrlInputs.clear();
}
async function getJson(url) {
const response = await fetch(apiUrl(url), { headers: authHeaders() });
const payload = await response.json();
if (handleAuthFailure(response, payload)) throw new Error(payload.error || "需要输入访问密码");
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
return payload;
}
async function postJson(url, body) {
const response = await fetch(apiUrl(url), {
method: "POST",
headers: { "content-type": "application/json", ...authHeaders() },
body: JSON.stringify(body),
});
const payload = await response.json();
if (handleAuthFailure(response, payload)) throw new Error(payload.error || "需要输入访问密码");
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
return payload;
}
async function ensureAccessAuth() {
try {
const response = await fetch(apiUrl("/api/auth/status"), { headers: authHeaders() });
const payload = await response.json();
if (!payload.enabled || payload.authorized) {
hideAuthGate();
return true;
}
} catch {
return true;
}
showAuthGate("");
return false;
}
async function submitAccessPassword() {
if (authSubmitting) return;
const password = authPassword?.value || "";
if (!password.trim()) {
showAuthGate("请输入访问密码");
return;
}
authSubmitting = true;
if (authSubmit) authSubmit.disabled = true;
setAuthMessage("正在验证...");
try {
const response = await fetch(apiUrl("/api/auth/login"), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ password }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "访问密码不正确");
if (payload.token) localStorage.setItem(HOTNESS_AUTH_TOKEN_KEY, payload.token);
if (authPassword) authPassword.value = "";
setAuthMessage("登录成功,正在进入...");
hideAuthGate();
await startApp();
} catch (error) {
showAuthGate(error.message || "访问密码不正确");
} finally {
authSubmitting = false;
if (authSubmit) authSubmit.disabled = false;
}
}
function authHeaders() {
const token = localStorage.getItem(HOTNESS_AUTH_TOKEN_KEY) || "";
return token ? { "x-hotness-auth-token": token } : {};
}
function handleAuthFailure(response, payload) {
if (response.status !== 401 || !payload?.requires_auth) return false;
localStorage.removeItem(HOTNESS_AUTH_TOKEN_KEY);
showAuthGate(payload.error || "需要输入访问密码");
return true;
}
function showAuthGate(message = "") {
if (!authGate) return;
authGate.hidden = false;
setAuthMessage(message);
requestAnimationFrame(() => authPassword?.focus());
}
function hideAuthGate() {
if (authGate) authGate.hidden = true;
setAuthMessage("");
}
function setAuthMessage(message) {
if (authMessage) authMessage.textContent = message || "";
}
function setBusy(isBusy, text = "") {
button.disabled = isBusy;
if (isBusy) setStatus("busy", text);
}
function setStatus(type, text) {
statusDot.className = `dot ${type}`;
statusText.textContent = text;
}
function statusLabel(status) {
return {
no_match: "未找到",
blocked: "被拦截",
error: "错误",
}[status] || status || "";
}
function formatTime(value) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(date);
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function escapeAttribute(value) {
return escapeHtml(value).replace(/`/g, "`");
}