410 lines
15 KiB
JavaScript
410 lines
15 KiB
JavaScript
// 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 = "<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");
|
||
}
|