// popup.js document.addEventListener("DOMContentLoaded", async () => { const panel = document.getElementById("draggablePanel"); const header = document.querySelector(".header"); const isNewTab = new URLSearchParams(window.location.search).get("from") === "newtab"; if (isNewTab || (window.innerWidth > 900 && window.innerHeight > 700)) { document.body.classList.add("fullscreen"); if (panel) { panel.style.left = ""; panel.style.top = ""; panel.style.position = "fixed"; } } else if (panel && header) { let dragging = false, offsetX = 0, offsetY = 0; header.addEventListener("mousedown", (e) => { dragging = true; offsetX = e.clientX - panel.offsetLeft; offsetY = e.clientY - panel.offsetTop; document.body.style.userSelect = "none"; }); document.addEventListener("mousemove", (e) => { if (dragging) { panel.style.left = Math.max(0, e.clientX - offsetX) + "px"; panel.style.top = Math.max(0, e.clientY - offsetY) + "px"; } }); document.addEventListener("mouseup", () => { dragging = false; document.body.style.userSelect = ""; }); } const input = document.getElementById("searchInput"); const searchBtn = document.getElementById("searchBtn"); const clearBtn = document.getElementById("clearHistoryBtn"); const historyBox = document.getElementById("historyTags"); const historyWrapper = document.getElementById("historyWrapper"); const resultBox = document.getElementById("results"); const llmResultContainer = document.getElementById("llmResultContainer"); const llmResultContent = document.getElementById("llmResultContent"); const llmInput = document.getElementById("llmInput"); const llmSendBtn = document.getElementById("llmSendBtn"); const logoutBtn = document.getElementById("logoutBtn"); const userNameLabel = document.getElementById("userName"); const userAvatar = document.getElementById("userAvatar"); const userDropdown = document.getElementById("userDropdown"); const openTabBtn = document.getElementById("openTabBtn"); const settingsBtn = document.getElementById("settingsBtn"); const testBtn = document.getElementById("testBtn"); const openUploaderMenu = document.getElementById("openUploaderMenu"); const tabEs = document.getElementById("tabEs"); const tabAi = document.getElementById("tabAi"); const tabContentEs = document.getElementById("tabContentEs"); const tabContentAi = document.getElementById("tabContentAi"); tabEs.onclick = () => { tabEs.classList.add("active"); tabAi.classList.remove("active"); tabContentEs.classList.add("active"); tabContentAi.classList.remove("active"); }; tabAi.onclick = () => { tabEs.classList.remove("active"); tabAi.classList.add("active"); tabContentEs.classList.remove("active"); tabContentAi.classList.add("active"); setTimeout(() => { llmResultContent.scrollTop = llmResultContent.scrollHeight; }, 100); }; let token = null; let currentPage = 1; let totalPages = 1; let lastKeywords = []; let config = await loadMergedConfig(); const auth = await loadUserToken(); if (!auth) return handleLogout(auth); token = auth.token; userNameLabel.textContent = auth.username; async function triggerSearch() { const raw = input?.value?.trim(); if (!token || !raw) return; updateHistory(raw); renderHistoryTags(true); fetchResults(raw, 1); } function parseKeywordGroups(inputStr) { return inputStr .trim() .split("||") .map(group => { group = group.trim(); if (group.startsWith("(") && group.endsWith(")")) group = group.slice(1, -1); return group.split(/\s+/).filter(Boolean); }) .filter(g => g.length > 0); } function showLoading() { resultBox.innerHTML = "
  • 加载中...
  • "; } function clearResults() { resultBox.innerHTML = ""; llmResultContent.innerHTML = ""; llmResultContainer.classList.add("hidden"); historyWrapper.classList.add("hidden"); } function renderPagination() { const pagination = document.createElement("div"); pagination.className = "pagination"; // 首页按钮 const first = document.createElement("button"); first.textContent = "« 首页"; first.disabled = currentPage === 1; if (currentPage !== 1) first.onclick = () => fetchResults(lastKeywords, 1); pagination.appendChild(first); // 上一页按钮 const prev = document.createElement("button"); prev.textContent = "‹ 上一页"; prev.disabled = currentPage === 1; if (currentPage !== 1) prev.onclick = () => fetchResults(lastKeywords, currentPage - 1); pagination.appendChild(prev); // 计算滑动窗口 let startPage, endPage; if (totalPages <= 5) { startPage = 1; endPage = totalPages; } else { if (currentPage <= 3) { startPage = 1; endPage = 5; } else if (currentPage >= totalPages - 2) { startPage = totalPages - 4; endPage = totalPages; } else { startPage = currentPage - 2; endPage = currentPage + 2; } } // 左侧省略号 if (startPage > 1) { const dot = document.createElement("span"); dot.className = "ellipsis"; dot.textContent = "..."; pagination.appendChild(dot); } // 滑动窗口页码 for (let i = startPage; i <= endPage; i++) { const pageBtn = document.createElement("button"); pageBtn.textContent = i; if (i === currentPage) { pageBtn.className = "active-page"; pageBtn.disabled = true; } else { pageBtn.onclick = () => fetchResults(lastKeywords, i); } pagination.appendChild(pageBtn); } // 右侧省略号 if (endPage < totalPages) { const dot = document.createElement("span"); dot.className = "ellipsis"; dot.textContent = "..."; pagination.appendChild(dot); } // 下一页按钮 const next = document.createElement("button"); next.textContent = "下一页 ›"; next.disabled = currentPage === totalPages; if (currentPage !== totalPages) next.onclick = () => fetchResults(lastKeywords, currentPage + 1); pagination.appendChild(next); // 末页按钮 const last = document.createElement("button"); last.textContent = "末页 »"; last.disabled = currentPage === totalPages; if (currentPage !== totalPages) last.onclick = () => fetchResults(lastKeywords, totalPages); pagination.appendChild(last); resultBox.appendChild(pagination); } async function fetchResults(rawInput, page) { currentPage = page; lastKeywords = rawInput; showLoading(); const endpoint = config.apiConfig.esSearch; const [kwField, pageField, sizeField] = endpoint.requestParams.map(p => p.field); const [resultsField, totalField, sizeField2] = endpoint.responseParams.map(p => p.field); const keywordGroups = parseKeywordGroups(rawInput); const requestBody = { [kwField]: keywordGroups, [pageField]: page, [sizeField]: 10 }; try { const response = await fetch(endpoint.url, { method: "POST", headers: { "Authorization": `${token}`, "Content-Type": "application/json" }, body: JSON.stringify(requestBody) }); log("已使用前端接口配置:" + JSON.stringify(endpoint)); log("请求后端接口参数:" + JSON.stringify(requestBody)); if (response.status === 401) return handleLogout(); const data = await response.json(); const results = data[resultsField]; const total = data[totalField]; const pageSize = data[sizeField2] || 10; resultBox.innerHTML = ""; if (!results || results.length === 0) { resultBox.innerHTML = "
  • 未找到相关文档
  • "; return; } totalPages = Math.ceil(total / pageSize); const fileBaseUrl = config.apiConfig.materialPublicUrl.url; results.forEach(doc => { const li = document.createElement("li"); const filename = doc.filename || doc.title || "未知文件"; const filepath = doc.filepath?.replace(/^import-data\//, "").replace(/\.md$/, ".html") || ""; const mtime = doc.mtime ? formatLocalTime(doc.mtime) : ""; // 格式化时间 const uploader = doc.uploader ? doc.uploader : "管理员"; const link = document.createElement("a"); link.href = doc.url ? doc.url : `${fileBaseUrl}/${filepath || filename}`; link.target = "_blank"; link.textContent = filename; const snippet = document.createElement("div"); snippet.className = "summary"; snippet.innerHTML = doc.summary?.slice(0, 300) + "..."; // 元信息行 const metaRow = document.createElement("div"); metaRow.className = "es-meta-row"; metaRow.innerHTML = ` ${filepath} ${mtime ? "更新时间:" + mtime : ""} ${mtime ? "上传人:" + uploader : ""} `; li.appendChild(link); li.appendChild(snippet); li.appendChild(metaRow); resultBox.appendChild(li); }); renderPagination(); } catch (err) { resultBox.innerHTML = "
  • 搜索失败,请检查网络或服务状态。
  • "; } } function updateHistory(keyword) { chrome.storage.local.get(["searchHistory"], (res) => { let history = res.searchHistory || []; history = [keyword, ...history.filter(item => item !== keyword)]; if (history.length > 5) history = history.slice(0, 5); chrome.storage.local.set({ searchHistory: history }); }); } function renderHistoryTags(show = false) { chrome.storage.local.get(["searchHistory"], (res) => { const history = res.searchHistory || []; historyBox.innerHTML = ""; if (history.length === 0 || !show) { historyWrapper.classList.add("hidden"); return; } history.forEach(tagText => { const tag = document.createElement("span"); tag.className = "tag"; tag.textContent = tagText; tag.onclick = () => { input.value = tagText; triggerSearch(); }; historyBox.appendChild(tag); }); historyWrapper.classList.remove("hidden"); }); } searchBtn.addEventListener("click", triggerSearch); input.addEventListener("keydown", e => { if (e.key === "Enter") { e.preventDefault(); triggerSearch(); } }); input.addEventListener("focus", () => renderHistoryTags(true)); input.addEventListener("input", () => { if (!input.value.trim()) clearResults(); }); input.addEventListener("blur", () => setTimeout(() => historyWrapper.classList.add("hidden"), 200)); clearBtn.addEventListener("click", () => { chrome.storage.local.remove("searchHistory", () => renderHistoryTags()); }); logoutBtn?.addEventListener("click", () => handleLogout(auth)); userAvatar?.addEventListener("click", () => { userDropdown.classList.toggle("hidden"); }); settingsBtn?.addEventListener("click", () => { chrome.tabs.create({ url: chrome.runtime.getURL("pages/settings.html") }); }); testBtn?.addEventListener("click", () => { chrome.tabs.create({ url: chrome.runtime.getURL("pages/test.html") }); }); openUploaderMenu?.addEventListener("click", () => { const uploaderUrl = chrome.runtime ? chrome.runtime.getURL("pages/uploader.html") : "pages/uploader.html"; window.open(uploaderUrl, "_blank"); userDropdown.classList.add("hidden"); }); window.addEventListener("click", (e) => { if (!userDropdown.contains(e.target) && e.target !== userAvatar) { userDropdown.classList.add("hidden"); } }); // 智能助手发送(带高度自适应) llmSendBtn?.addEventListener("click", () => { const val = llmInput.value.trim(); if (!val) return; llmInput.value = ""; fetchLLMAnswer(val, llmResultContainer, llmResultContent); if (llmInput.tagName.toLowerCase() === 'textarea') { setTimeout(() => { llmInput.style.height = 'auto'; }, 10); } }); // 智能助手输入框:Enter发送,Ctrl/Shift+Enter换行 if (llmInput && llmInput.tagName.toLowerCase() === 'textarea') { function autoResizeTextarea(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; } llmInput.addEventListener("input", function() { autoResizeTextarea(llmInput); }); autoResizeTextarea(llmInput); llmInput.addEventListener("keydown", function(e) { if (e.key === "Enter" && !e.ctrlKey && !e.shiftKey) { e.preventDefault(); llmSendBtn.click(); } // 其余情况(含 Ctrl/Shift+Enter),浏览器原生换行 }); } openTabBtn.addEventListener("click", () => { const val = input?.value?.trim(); const encoded = encodeURIComponent(val || ""); const url = chrome.runtime.getURL("pages/popup.html?keywords=" + encoded + "&from=newtab"); chrome.tabs.create({ url }); }); const autoKeywords = new URLSearchParams(location.search).get("keywords"); if (autoKeywords) { input.value = decodeURIComponent(autoKeywords); triggerSearch(); } renderHistoryTags(); }); // 时间戳格式化辅助 function formatLocalTime(ts) { if (!ts) return ""; const date = new Date(Number(ts)); if (isNaN(date.getTime())) return ""; return date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") + " " + String(date.getHours()).padStart(2, "0") + ":" + String(date.getMinutes()).padStart(2, "0"); }