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 = '
正在搜索
'; 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 ``; }).join(""); sections.push(`

${label}

${rows}
`); }); 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 = `

${escapeHtml(title)}

${escapeHtml(hint)}
`; 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); }