132 lines
5.3 KiB
JavaScript
132 lines
5.3 KiB
JavaScript
let globalSearchTimer = null;
|
|
|
|
function openGlobalSearch() {
|
|
if (!state.user) return;
|
|
toggleHeaderCommandMenu(false);
|
|
openModalDialog(elements.globalSearchDialog);
|
|
requestAnimationFrame(() => {
|
|
elements.globalSearchInput.focus();
|
|
elements.globalSearchInput.select();
|
|
});
|
|
}
|
|
function handleGlobalSearchShortcut(event) {
|
|
if (!event.ctrlKey || event.altKey || event.shiftKey || event.key.toLowerCase() !== "k") return;
|
|
if (!state.user) return;
|
|
if (event.defaultPrevented) {
|
|
showToast("Ctrl+K 已被其他功能占用,请点击顶部搜索按钮");
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
openGlobalSearch();
|
|
}
|
|
|
|
function closeGlobalSearch() {
|
|
clearTimeout(globalSearchTimer);
|
|
if (elements.globalSearchDialog.open) elements.globalSearchDialog.close();
|
|
}
|
|
|
|
function scheduleGlobalSearch() {
|
|
clearTimeout(globalSearchTimer);
|
|
const query = elements.globalSearchInput.value.trim();
|
|
state.globalSearchActiveIndex = -1;
|
|
if (!query) {
|
|
state.globalSearchResults = [];
|
|
renderGlobalSearchEmpty("输入名称或代码开始搜索", "使用方向键选择,回车打开详情", "corner-down-left");
|
|
return;
|
|
}
|
|
elements.globalSearchResults.innerHTML = '<div class="global-search-loading"><span class="spinner" aria-hidden="true"></span><span>正在搜索</span></div>';
|
|
globalSearchTimer = setTimeout(() => runGlobalSearch(query), 160);
|
|
}
|
|
|
|
async function runGlobalSearch(query) {
|
|
const requestSequence = ++state.globalSearchRequestSequence;
|
|
const params = new URLSearchParams({ q: query, trade_date: elements.tradeDate.value });
|
|
try {
|
|
const payload = await apiRequest(`/api/search?${params}`);
|
|
if (requestSequence !== state.globalSearchRequestSequence || elements.globalSearchInput.value.trim() !== query) return;
|
|
renderGlobalSearchResults(payload.groups || {});
|
|
} catch (error) {
|
|
if (requestSequence !== state.globalSearchRequestSequence) return;
|
|
state.globalSearchResults = [];
|
|
renderGlobalSearchEmpty(error.message || "搜索失败", "请稍后重试", "circle-alert");
|
|
}
|
|
}
|
|
|
|
function renderGlobalSearchResults(groups) {
|
|
const definitions = [
|
|
["stocks", "股票"],
|
|
["sectors", "板块"],
|
|
["themes", "题材"],
|
|
["indices", "指数"],
|
|
];
|
|
const iconNames = { stock: "chart-candlestick", sector: "layout-grid", theme: "lightbulb", index: "chart-line" };
|
|
const flattened = [];
|
|
const sections = [];
|
|
definitions.forEach(([key, label]) => {
|
|
const items = Array.isArray(groups[key]) ? groups[key] : [];
|
|
if (!items.length) return;
|
|
const rows = items.map((item) => {
|
|
const index = flattened.length;
|
|
flattened.push(item);
|
|
return `<button class="global-search-result" type="button" role="option" aria-selected="false" data-search-result-index="${index}">
|
|
<span class="global-search-result-icon"><i data-lucide="${iconNames[item.type] || "search"}"></i></span>
|
|
<span class="global-search-result-copy"><strong>${escapeHtml(item.name || "--")}</strong><span>${escapeHtml(item.subtitle || item.type_label || label)}</span></span>
|
|
<span class="global-search-result-code">${escapeHtml(item.code || "")}</span>
|
|
</button>`;
|
|
}).join("");
|
|
sections.push(`<section class="global-search-group" aria-label="${label}"><h3 class="global-search-group-title">${label}</h3>${rows}</section>`);
|
|
});
|
|
state.globalSearchResults = flattened;
|
|
state.globalSearchActiveIndex = flattened.length ? 0 : -1;
|
|
if (!flattened.length) {
|
|
renderGlobalSearchEmpty("没有找到相关结果", "可尝试输入完整名称或六位股票代码", "search-x");
|
|
return;
|
|
}
|
|
elements.globalSearchResults.innerHTML = sections.join("");
|
|
updateGlobalSearchSelection(false);
|
|
refreshIcons();
|
|
}
|
|
|
|
function renderGlobalSearchEmpty(title, hint, iconName) {
|
|
elements.globalSearchResults.innerHTML = `<div class="global-search-empty"><i data-lucide="${iconName}"></i><p>${escapeHtml(title)}</p><span>${escapeHtml(hint)}</span></div>`;
|
|
refreshIcons();
|
|
}
|
|
|
|
function handleGlobalSearchInputKeydown(event) {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
closeGlobalSearch();
|
|
return;
|
|
}
|
|
if (!["ArrowDown", "ArrowUp", "Enter"].includes(event.key)) return;
|
|
if (!state.globalSearchResults.length) return;
|
|
event.preventDefault();
|
|
if (event.key === "Enter") {
|
|
openGlobalSearchResult(state.globalSearchActiveIndex);
|
|
return;
|
|
}
|
|
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
state.globalSearchActiveIndex = (state.globalSearchActiveIndex + direction + state.globalSearchResults.length) % state.globalSearchResults.length;
|
|
updateGlobalSearchSelection(true);
|
|
}
|
|
|
|
function updateGlobalSearchSelection(scrollIntoView) {
|
|
elements.globalSearchResults.querySelectorAll("[data-search-result-index]").forEach((item) => {
|
|
const selected = number(item.dataset.searchResultIndex) === state.globalSearchActiveIndex;
|
|
item.classList.toggle("is-active", selected);
|
|
item.setAttribute("aria-selected", String(selected));
|
|
if (selected && scrollIntoView) item.scrollIntoView({ block: "nearest" });
|
|
});
|
|
}
|
|
|
|
function openGlobalSearchResult(index) {
|
|
const item = state.globalSearchResults[index];
|
|
if (!item) return;
|
|
closeGlobalSearch();
|
|
if (item.type === "stock") {
|
|
openStock(item.id, { code: item.code, name: item.name, sector: item.industry || "其他" });
|
|
return;
|
|
}
|
|
openEntityDetail(item);
|
|
}
|