410 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// popup.js
document.addEventListener("DOMContentLoaded", async () => {
const panel = document.getElementById("draggablePanel");
const header = document.querySelector(".header");
if (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);
};
const isNewTab = new URLSearchParams(window.location.search).get("from") === "newtab";
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 = "<li>加载中...</li>";
}
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 = "<li>未找到相关文档</li>";
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 = `
<span class="es-filepath" title="${filepath}">${filepath}</span>
<span class="es-updatetime">${mtime ? "更新时间:" + mtime : ""}</span>
<span class="es-uploader">${mtime ? "上传人:" + uploader : ""}</span>
`;
li.appendChild(link);
li.appendChild(snippet);
li.appendChild(metaRow);
resultBox.appendChild(li);
});
renderPagination();
} catch (err) {
resultBox.innerHTML = "<li>搜索失败,请检查网络或服务状态。</li>";
}
}
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");
}