/**
* ============================================
* 🔍 增强版搜索模块 (search.js)
* 功能:智能搜索、结果展示、链接验证、自动回退
* 最后更新:2025-01-XX (v2.0 - 支持自动回退到 JavaScript 搜索)
* ============================================
*/
import { config } from './config.js';
const DEFAULT_TIMEOUT = 4000; // 请求超时:4 秒
async function fetchWithTimeout(url, options = {}, timeout = DEFAULT_TIMEOUT) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
return response;
} finally {
clearTimeout(timer);
}
}
// 工具函数:防抖
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// 注意:fetchGitHubData 函数已不再需要
// GitHub 数据现在由 Go 后端获取
export class SearchModule {
constructor() {
this.cache = new Map();
this.goApiFailedCount = 0; // Go API 失败计数
this.goApiDisabled = false; // Go API 是否已禁用
this.goApiCheckTime = 0; // 上次检查 Go API 的时间
this.initElements();
this.setupEvents();
}
initElements() {
this.elements = {
input: document.getElementById('searchInput'),
results: document.getElementById('searchResults'),
container: document.querySelector('.search-container')
};
if (!this.elements.input || !this.elements.results) {
throw new Error('搜索模块初始化失败:未找到必要的DOM元素');
}
}
setupEvents() {
const { input } = this.elements;
input.addEventListener('input', debounce(this.handleSearch.bind(this), 300));
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.hideResults();
}
});
this.setupShortcuts();
document.addEventListener('click', (e) => {
if (!this.elements.container?.contains(e.target)) {
this.hideResults();
}
});
}
setupShortcuts() {
const shortcut = document.getElementById('searchShortcut');
const isApple = /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent);
if (shortcut) {
shortcut.textContent = isApple ? '⌘K' : 'Ctrl K';
shortcut.setAttribute('aria-label', isApple ? '快捷键 Command K' : '快捷键 Control K');
}
document.addEventListener('keydown', (e) => {
const target = e.target;
const isTyping = target?.matches?.('input, textarea, select, [contenteditable="true"]');
const isCommandK = e.key?.toLowerCase() === 'k' && (e.metaKey || e.ctrlKey);
const isSlash = e.key === '/' && !e.metaKey && !e.ctrlKey && !e.altKey;
const isSearchShortcut = isCommandK || (isSlash && !isTyping);
if (!isSearchShortcut || (isTyping && target !== this.elements.input)) return;
e.preventDefault();
this.elements.input.focus({ preventScroll: true });
this.elements.input.select();
this.elements.container?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}
async handleSearch(e) {
const query = e.target.value.trim();
if (!query) {
this.hideResults();
return;
}
// 最小查询长度检查
if (query.length < 2) {
const { results: resultsContainer } = this.elements;
resultsContainer.innerHTML = `
`;
this.showResults();
return;
}
this.showLoading();
try {
const results = await this.search(query);
this.displayResults(results);
} catch (error) {
console.error('[Search] 搜索失败:', error);
// 显示更友好的错误信息
const { results: resultsContainer } = this.elements;
let errorMessage = error.message || '未知错误';
// 根据错误类型提供更具体的提示
if (errorMessage.includes('400') || errorMessage.includes('请求失败')) {
errorMessage = '搜索关键词太短,请至少输入 2 个字符';
} else if (errorMessage.includes('网络') || errorMessage.includes('fetch')) {
errorMessage = '网络连接失败,请检查网络后重试';
}
resultsContainer.innerHTML = `
搜索失败:${errorMessage}
请尝试:
• 输入至少 2 个字符
• 使用热门库名称(如:bootstrap, jquery)
• 刷新页面后重试
`;
this.showResults();
}
}
displayResults(results) {
const { results: resultsContainer } = this.elements;
if (!results || results.length === 0) {
resultsContainer.innerHTML = `
`;
return;
}
resultsContainer.innerHTML = results
.map(result => this.renderSearchResult(result))
.join('');
this.bindResultEvents();
this.showResults();
}
async search(query) {
const cacheKey = query.toLowerCase();
// 检查缓存
if (this.cache.has(cacheKey)) {
const { timestamp, data } = this.cache.get(cacheKey);
if (Date.now() - timestamp < 5 * 60 * 1000) {
return data;
}
}
// 检查 Go API 是否可用(如果连续失败3次,禁用30分钟)
const now = Date.now();
const goApiRecentlyDisabled = this.goApiDisabled && (now - this.goApiCheckTime < 30 * 60 * 1000);
if (!goApiRecentlyDisabled) {
// Go API 可用,尝试使用
try {
const currentCDN = config.getCurrentCDN();
const apiUrl = `${config.searchApiUrl}?q=${encodeURIComponent(query)}&limit=20&type=all&cdn=${config.cdnSource}`;
// 创建超时控制器
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3秒超时
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
credentials: 'same-origin',
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
if (data.success && data.results) {
console.log('[Search] ✅ Go API 成功,返回', data.results.length, '个结果');
// 重置失败计数
this.goApiFailedCount = 0;
this.goApiDisabled = false;
// 缓存结果
this.cache.set(cacheKey, {
timestamp: Date.now(),
data: data.results
});
return data.results;
}
}
}
// Go API 返回非 200 状态码
this.goApiFailedCount++;
if (this.goApiFailedCount >= 3) {
this.goApiDisabled = true;
this.goApiCheckTime = Date.now();
console.warn('[Search] ⚠️ Go API 连续失败 3 次,禁用 30 分钟,直接使用 JavaScript 搜索');
} else {
console.warn('[Search] ⚠️ Go API 返回', response.status, `(失败 ${this.goApiFailedCount}/3)`);
}
} catch (error) {
// Go API 请求失败(网络错误、404、超时等)
this.goApiFailedCount++;
if (this.goApiFailedCount >= 3) {
this.goApiDisabled = true;
this.goApiCheckTime = Date.now();
console.warn('[Search] ⚠️ Go API 连续失败 3 次,禁用 30 分钟,直接使用 JavaScript 搜索');
} else {
const errorMsg = error.name === 'AbortError' ? '请求超时' : error.message;
console.warn('[Search] ⚠️ Go API 请求失败:', errorMsg, `(失败 ${this.goApiFailedCount}/3)`);
}
}
} else {
console.log('[Search] Go API 已禁用,直接使用 JavaScript 搜索');
}
// 使用改进的 JavaScript 搜索(Go API 失败或禁用时)
console.log('[Search] 🔄 使用 JavaScript 搜索模式');
try {
return await this.searchWithJavaScript(query);
} catch (jsError) {
console.error('[Search] ❌ JavaScript 搜索也失败:', jsError);
throw jsError;
}
}
/**
* 使用 JavaScript 直接搜索(优化版 - 更快、更准确)
*/
async searchWithJavaScript(query) {
const cacheKey = 'js_' + query.toLowerCase();
// 检查缓存
if (this.cache.has(cacheKey)) {
const { timestamp, data } = this.cache.get(cacheKey);
if (Date.now() - timestamp < 5 * 60 * 1000) {
return data;
}
}
try {
// 预处理查询:支持中文关键词和别名
const processedQuery = this.preprocessQuery(query);
// 首先检查热门库匹配(快速路径)
const popularResults = this.searchPopularLibraries(query, processedQuery);
if (popularResults.length > 0) {
console.log('[Search] 找到热门库匹配,优先返回');
return popularResults.slice(0, 10);
}
// 最小查询长度检查
if (processedQuery.length < 2) {
console.warn('[Search] 查询太短,至少需要 2 个字符');
return [];
}
// 从 npm 搜索(限制数量以提高速度)
const response = await fetchWithTimeout(
`${config.npmRegistryApi}/-/v1/search?text=${encodeURIComponent(processedQuery)}&size=15`,
{},
5000
);
if (!response.ok) {
// 处理 400 错误(查询太短)
if (response.status === 400) {
console.warn('[Search] npm API 返回 400,查询可能太短');
return [];
}
throw new Error(`npm API 请求失败: ${response.status}`);
}
const data = await response.json();
const { objects } = data || { objects: [] };
if (!objects || objects.length === 0) {
return [];
}
// 快速过滤:先计算相关性,只处理高相关性结果
const scoredPackages = objects
.map(({ package: pkg }) => ({
pkg,
relevance: this.calculateRelevance(pkg, query, processedQuery)
}))
.filter(item => item.relevance >= 0.3) // 提高阈值,只处理高相关性
.sort((a, b) => b.relevance - a.relevance)
.slice(0, 10); // 只处理前10个最相关的
// 并行处理包信息(使用 Promise.allSettled 以容忍单个包失败)
// 限制并发数为 5,避免压垮服务器
const packagePromises = scoredPackages.map(async ({ pkg, relevance }) => {
try {
// 并行获取版本和文件列表(带超时控制)
const [versionData, files] = await Promise.all([
this.getPackageVersion(pkg.name).catch(() => null),
this.getPackageFiles(pkg.name, 'latest').catch(() => []) // 先尝试 latest
]);
if (!versionData) {
return null;
}
// 如果使用 latest 失败,再获取具体版本
let actualFiles = files;
if ((!files || files.length === 0) && versionData.version && versionData.version !== 'latest') {
try {
actualFiles = await this.getPackageFiles(pkg.name, versionData.version);
} catch (err) {
// jsDelivr 404 错误,包可能不存在或版本问题
console.warn(`[Search] 包 ${pkg.name}@${versionData.version} 文件获取失败:`, err.message);
return null;
}
}
if (!actualFiles || actualFiles.length === 0) {
return null;
}
// 快速过滤 CSS/JS 文件
const cssFiles = actualFiles.filter(f =>
/\.(min\.)?css$/i.test(f.name)
);
const jsFiles = actualFiles.filter(f =>
/\.(min\.)?js$/i.test(f.name)
);
// 优先选择 .min 文件
const bestCssFile = cssFiles.find(f => f.name.includes('.min.css')) || cssFiles[0];
const bestJsFile = jsFiles.find(f => f.name.includes('.min.js')) || jsFiles[0];
const results = [];
// 只添加最佳文件(每个包最多1个CSS和1个JS)
if (bestCssFile) {
const cdnUrl = this.generateCdnUrl(pkg.name, versionData.version, bestCssFile.name);
results.push({
type: 'css',
name: pkg.name,
version: versionData.version,
description: pkg.description || '暂无描述',
fileName: bestCssFile.name,
url: cdnUrl,
code: ``,
relevance: relevance
});
}
if (bestJsFile) {
const cdnUrl = this.generateCdnUrl(pkg.name, versionData.version, bestJsFile.name);
results.push({
type: 'js',
name: pkg.name,
version: versionData.version,
description: pkg.description || '暂无描述',
fileName: bestJsFile.name,
url: cdnUrl,
code: ``,
relevance: relevance
});
}
return results;
} catch (err) {
console.warn(`处理包 ${pkg.name} 失败:`, err);
return null;
}
});
// 并发执行,但限制并发数
const maxConcurrency = 5;
const results = [];
for (let i = 0; i < packagePromises.length; i += maxConcurrency) {
const batch = packagePromises.slice(i, i + maxConcurrency);
const batchResults = await Promise.allSettled(batch);
batchResults.forEach(result => {
if (result.status === 'fulfilled' && result.value) {
results.push(...result.value);
}
});
}
// 排序结果
results.sort((a, b) => (b.relevance || 0) - (a.relevance || 0));
// 限制结果数量
const finalResults = results.slice(0, 20);
// 缓存结果
this.cache.set(cacheKey, {
timestamp: Date.now(),
data: finalResults
});
return finalResults;
} catch (error) {
console.error('[Search] JavaScript 搜索失败:', error);
throw new Error('搜索服务暂不可用,请稍后重试');
}
}
/**
* 搜索热门库(快速匹配)
*/
searchPopularLibraries(query, processedQuery) {
const popularLibraries = [
{ name: 'bootstrap', package: 'bootstrap', file: 'dist/css/bootstrap.min.css', aliases: ['bs', 'bt', '引导'], startsWith: ['b'] },
{ name: 'tailwindcss', package: 'tailwindcss', file: 'dist/tailwind.min.css', aliases: ['tw', 'tailwind', '尾风'], startsWith: ['t', 'ta', 'tai'] },
{ name: 'fontawesome', package: '@fortawesome/fontawesome-free', file: 'css/all.min.css', aliases: ['fa', 'fontawesome', '字体图标'], startsWith: ['f', 'fo', 'fon'] },
{ name: 'Google Sans', package: 'Google Sans', file: 'font', aliases: ['google sans', '谷歌字体', '字体'], startsWith: ['g', 'go', 'goo'] },
{ name: 'Google Sans Code VF', package: 'Google Sans Code', file: 'font', aliases: ['google sans code', 'code font', '代码字体', '等宽代码', '等宽字体'], startsWith: ['g', 'go', 'goo'] },
{ name: 'animate.css', package: 'animate.css', file: 'animate.min.css', aliases: ['animate', '动画'], startsWith: ['a', 'an', 'ani'] },
{ name: 'jquery', package: 'jquery', file: 'dist/jquery.min.js', aliases: ['jq', 'jquery'], startsWith: ['j', 'jq', 'jqu'] },
{ name: 'bulma', package: 'bulma', file: 'css/bulma.min.css', aliases: ['bulma'], startsWith: ['b', 'bu', 'bul'] },
{ name: 'uikit', package: 'uikit', file: 'dist/css/uikit.min.css', aliases: ['ui', 'uikit'], startsWith: ['u', 'ui', 'uik'] },
];
const lowerQuery = query.toLowerCase().trim();
const lowerProcessed = processedQuery.toLowerCase().trim();
// 如果查询太短(1个字符),只匹配开头
if (lowerQuery.length === 1) {
for (const lib of popularLibraries) {
if (lib.startsWith && lib.startsWith.includes(lowerQuery)) {
const cdnUrl = this.generateCdnUrl(lib.package, 'latest', lib.file);
const isFont = lib.file === 'font';
const fileType = isFont || lib.file.endsWith('.css') ? 'css' : 'js';
const fontUrl = lib.name === 'Google Sans Code VF'
? 'https://fonts.bluecdn.com/css2?family=Google+Sans+Code:wght@400..700&display=swap'
: 'https://fonts.bluecdn.com/css2?family=Google+Sans:wght@400;500;700&display=swap';
const finalUrl = isFont ? fontUrl : cdnUrl;
return [{
type: fileType,
name: lib.package,
version: isFont ? (lib.name === 'Google Sans Code VF' ? '400..700' : '400 / 500 / 700') : 'latest',
description: `${lib.name} - 热门库(输入 "${lib.name}" 获取完整结果)`,
fileName: lib.file,
url: finalUrl,
code: fileType === 'css'
? ``
: ``,
relevance: 0.9 // 单字符匹配降低相关性
}];
}
}
return [];
}
// 正常匹配(2个字符以上)
for (const lib of popularLibraries) {
if (lib.name === lowerQuery ||
lib.name === lowerProcessed ||
lib.package === lowerQuery ||
lib.package === lowerProcessed ||
lib.name.startsWith(lowerQuery) ||
lib.name.startsWith(lowerProcessed) ||
lib.aliases.some(alias => {
const lowerAlias = alias.toLowerCase();
return lowerAlias === lowerQuery ||
lowerAlias === lowerProcessed ||
lowerAlias.startsWith(lowerQuery) ||
lowerAlias.startsWith(lowerProcessed);
})) {
const cdnUrl = this.generateCdnUrl(lib.package, 'latest', lib.file);
const isFont = lib.file === 'font';
const fileType = isFont || lib.file.endsWith('.css') ? 'css' : 'js';
const fontUrl = lib.name === 'Google Sans Code VF'
? 'https://fonts.bluecdn.com/css2?family=Google+Sans+Code:wght@400..700&display=swap'
: 'https://fonts.bluecdn.com/css2?family=Google+Sans:wght@400;500;700&display=swap';
const finalUrl = isFont ? fontUrl : cdnUrl;
return [{
type: fileType,
name: lib.package,
version: isFont ? (lib.name === 'Google Sans Code VF' ? '400..700' : '400 / 500 / 700') : 'latest',
description: isFont ? `${lib.name} - 字体资源` : `${lib.name} - 热门库`,
fileName: lib.file,
url: finalUrl,
code: fileType === 'css'
? ``
: ``,
relevance: 1.0
}];
}
}
return [];
}
/**
* 预处理查询关键词
*/
preprocessQuery(query) {
const lowerQuery = query.toLowerCase().trim();
// 中文关键词映射
const chineseKeywords = {
'样式': 'css',
'样式表': 'css',
'框架': 'framework',
'UI框架': 'ui framework',
'引导': 'bootstrap',
'尾风': 'tailwind',
'动画': 'animate',
'动画库': 'animate.css',
'字体': 'font',
'字体库': 'font',
'字体资源': 'font',
'代码字体': 'Google Sans Code',
'等宽字体': 'Google Sans Code',
'图标': 'icon',
'图标库': 'icon library',
'字体图标': 'fontawesome',
};
if (chineseKeywords[lowerQuery]) {
return chineseKeywords[lowerQuery];
}
// 拼音和别名映射
const aliases = {
'bs': 'bootstrap',
'bt': 'bootstrap',
'tw': 'tailwindcss',
'fa': 'fontawesome',
'ui': 'uikit',
};
if (aliases[lowerQuery]) {
return aliases[lowerQuery];
}
return query;
}
/**
* 计算相关性分数(优化版 - 更准确的匹配)
*/
calculateRelevance(pkg, originalQuery, processedQuery) {
let score = 0;
const name = pkg.name.toLowerCase();
const description = (pkg.description || '').toLowerCase();
const keywords = (pkg.keywords || []).map(k => k.toLowerCase());
const query = processedQuery.toLowerCase();
const original = originalQuery.toLowerCase();
// 名称完全匹配(最高权重)
if (name === query || name === original) {
score += 100;
}
// 名称以查询开头(高权重)
else if (name.startsWith(query) || name.startsWith(original)) {
score += 85;
}
// 名称包含查询(中等权重)
else if (name.includes(query) || name.includes(original)) {
// 如果查询词在名称中间,降低权重
const position = name.indexOf(query);
if (position === 0) {
score += 60; // 开头位置
} else if (position > 0 && position < name.length - query.length) {
score += 40; // 中间位置
} else {
score += 50; // 其他位置
}
}
// 部分匹配(单词边界匹配)
else {
const words = name.split(/[-_]/);
for (const word of words) {
if (word === query || word === original) {
score += 45; // 单词完全匹配
} else if (word.startsWith(query) || word.startsWith(original)) {
score += 30; // 单词开头匹配
} else if (word.includes(query) || word.includes(original)) {
score += 20; // 单词包含匹配
}
}
}
// 描述匹配(权重较低)
if (description.includes(query) || description.includes(original)) {
score += 15;
}
// 关键词匹配(权重中等)
for (const keyword of keywords) {
if (keyword === query || keyword === original) {
score += 35; // 关键词完全匹配
} else if (keyword.startsWith(query) || keyword.startsWith(original)) {
score += 25; // 关键词开头匹配
} else if (keyword.includes(query) || keyword.includes(original)) {
score += 12; // 关键词包含匹配
}
}
// 热门库加权(如果匹配)
const popularLibs = ['bootstrap', 'tailwindcss', 'jquery', 'fontawesome', 'animate.css', 'bulma', 'uikit'];
if (popularLibs.some(lib => name.includes(lib))) {
score += 10;
}
// 归一化到 0-1
return Math.min(score / 100, 1.0);
}
/**
* 获取包版本
*/
async getPackageVersion(packageName) {
try {
const response = await fetchWithTimeout(`${config.npmRegistryApi}/${packageName}`, {}, 5000);
if (!response.ok) return null;
const data = await response.json();
return {
version: data['dist-tags']?.latest || 'latest',
updatedAt: data.time?.modified || null
};
} catch (error) {
console.warn(`[Search] 获取 ${packageName} 版本失败:`, error.message);
return null;
}
}
/**
* 获取包文件列表
*/
async getPackageFiles(packageName, version) {
try {
// 清理版本号(移除特殊字符)
const cleanVersion = (version || 'latest').replace(/[^a-zA-Z0-9.-]/g, '');
const url = `https://data.jsdelivr.com/v1/package/npm/${packageName}@${cleanVersion}/flat`;
const response = await fetchWithTimeout(url, {}, 8000);
// 处理 404 错误(包不存在或版本问题)
if (response.status === 404) {
console.warn(`[Search] 包 ${packageName}@${cleanVersion} 不存在`);
return [];
}
if (!response.ok) {
console.warn(`[Search] jsDelivr API 错误: ${response.status}`);
return [];
}
const data = await response.json();
return data.files || [];
} catch (error) {
// 静默处理错误,返回空数组
const errorMsg = error.name === 'AbortError' ? '请求超时' : error.message;
console.warn(`[Search] 获取包文件失败 ${packageName}:`, errorMsg);
return [];
}
}
/**
* 生成 CDN URL(根据不同CDN源动态生成)
*/
generateCdnUrl(packageName, version, fileName) {
const cleanFile = fileName
.replace(/^[.\/\\]+/, '')
.replace(/\/+/g, '/');
const cdnHost = config.getCurrentCDN().domain;
return `https://${cdnHost}/npm/${packageName}/${version}/${cleanFile}`;
}
// 注意:processPackageResults 方法已不再需要
// 所有处理现在在 PHP 后端完成
renderSearchResult(result) {
const fileIcon = result.type === 'css' ?
'' :
'';
const githubStats = result.github ? `
` : '';
return `
${this.escapeHtml(result.description)}
`;
}
formatNumber(num) {
if (!num) return '0';
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
/**
* 下载文件
*/
async downloadFile(url, filename) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
}
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
// 清理
document.body.removeChild(a);
window.URL.revokeObjectURL(blobUrl);
return true;
} catch (error) {
console.error('下载失败:', error);
throw error;
}
}
bindResultEvents() {
const { results } = this.elements;
// 处理复制按钮
results.querySelectorAll('.search-copy-btn:not(.search-download-btn)').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
try {
await navigator.clipboard.writeText(btn.dataset.copy);
const originalHTML = btn.innerHTML;
btn.innerHTML = '已复制';
btn.classList.add('copy-success');
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove('copy-success');
}, 2000);
} catch (error) {
console.error('复制失败:', error);
}
});
});
// 处理下载按钮
results.querySelectorAll('.search-download-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '下载中...';
try {
const url = btn.dataset.download;
const filename = btn.dataset.filename || 'download.css';
await this.downloadFile(url, filename);
btn.innerHTML = '已下载';
btn.classList.add('copy-success');
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove('copy-success');
btn.disabled = false;
}, 2000);
} catch (error) {
btn.innerHTML = '失败';
btn.classList.add('copy-error');
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove('copy-error');
btn.disabled = false;
}, 2000);
console.error('下载失败:', error);
}
});
});
}
showLoading() {
const { results } = this.elements;
results.style.display = 'block';
results.innerHTML = `
搜索中...
`;
}
showResults() {
const { results, container } = this.elements;
results.style.display = 'block';
container?.classList.add('active');
}
hideResults() {
const { results, container } = this.elements;
results.style.display = 'none';
container?.classList.remove('active');
}
showError() {
const { results } = this.elements;
results.innerHTML = `
`;
}
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
}
export function initSearch() {
try {
return new SearchModule();
} catch (error) {
console.error('搜索模块初始化失败:', error);
return null;
}
}