增加搜索框

This commit is contained in:
lsy 2025-03-28 19:16:12 +08:00
parent 8d6e7a3502
commit e356212737
2 changed files with 325 additions and 1 deletions

View File

@ -25,6 +25,30 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
<!-- 导航链接 --> <!-- 导航链接 -->
<div class="hidden md:flex md:items-center md:space-x-8"> <div class="hidden md:flex md:items-center md:space-x-8">
<!-- 桌面端搜索框 -->
<div class="relative">
<input
type="text"
id="desktop-search"
class="w-48 pl-10 pr-4 py-1.5 rounded-full text-sm text-gray-700 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 bg-gray-50/80 dark:bg-gray-800/60 border border-gray-200/60 dark:border-gray-700/40 focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-500 focus:bg-white dark:focus:bg-gray-800 focus:border-primary-300 dark:focus:border-primary-600 transition-all duration-300"
placeholder="搜索文章..."
/>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<!-- 搜索结果容器(默认隐藏) -->
<div id="desktop-search-results" class="absolute top-full left-0 right-0 mt-2 max-h-80 overflow-y-auto rounded-lg bg-white/95 dark:bg-gray-800/95 shadow-md border border-gray-200/70 dark:border-gray-700/70 backdrop-blur-sm z-50 hidden">
<!-- 结果将通过JS动态填充 -->
<div class="p-4 text-center text-gray-500 dark:text-gray-400" id="desktop-search-message">
<p>输入关键词开始搜索</p>
</div>
<ul class="divide-y divide-gray-200/70 dark:divide-gray-700/70" id="desktop-search-list"></ul>
</div>
</div>
{NAV_LINKS.map(link => ( {NAV_LINKS.map(link => (
<a <a
href={link.href} href={link.href}
@ -43,6 +67,19 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
<!-- 移动端菜单按钮 --> <!-- 移动端菜单按钮 -->
<div class="flex items-center md:hidden"> <div class="flex items-center md:hidden">
<!-- 移动端搜索按钮 -->
<button
type="button"
id="mobile-search-button"
class="inline-flex items-center justify-center p-2 rounded-md text-secondary-400 dark:text-secondary-500 hover:text-secondary-500 dark:hover:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-dark-card focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500 mr-2"
aria-expanded="false"
>
<span class="sr-only">搜索</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button <button
type="button" type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-secondary-400 dark:text-secondary-500 hover:text-secondary-500 dark:hover:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-dark-card focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500" class="inline-flex items-center justify-center p-2 rounded-md text-secondary-400 dark:text-secondary-500 hover:text-secondary-500 dark:hover:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-dark-card focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500"
@ -61,6 +98,39 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
</div> </div>
</div> </div>
<!-- 移动端搜索面板 -->
<div id="mobile-search-panel" class="hidden md:hidden fixed inset-x-0 top-16 p-4 bg-white dark:bg-gray-800 shadow-md z-50 border-t border-gray-200 dark:border-gray-700">
<div class="relative">
<input
type="text"
id="mobile-search"
class="w-full pl-10 pr-10 py-2 rounded-full text-sm text-gray-700 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 bg-gray-50/80 dark:bg-gray-800/60 border border-gray-200/60 dark:border-gray-700/40 focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-500 focus:bg-white dark:focus:bg-gray-800 focus:border-primary-300 dark:focus:border-primary-600 transition-all duration-300"
placeholder="搜索文章..."
/>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<button
id="mobile-search-close"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- 移动端搜索结果 -->
<div id="mobile-search-results" class="mt-3 max-h-80 overflow-y-auto rounded-lg bg-white/95 dark:bg-gray-800/95 shadow-md border border-gray-200/70 dark:border-gray-700/70 backdrop-blur-sm hidden">
<!-- 结果将通过JS动态填充 -->
<div class="p-4 text-center text-gray-500 dark:text-gray-400" id="mobile-search-message">
<p>输入关键词开始搜索</p>
</div>
<ul class="divide-y divide-gray-200/70 dark:divide-gray-700/70" id="mobile-search-list"></ul>
</div>
</div>
<!-- 移动端菜单 --> <!-- 移动端菜单 -->
<div class="hidden md:hidden fixed inset-x-0 top-16 z-40" id="mobile-menu"> <div class="hidden md:hidden fixed inset-x-0 top-16 z-40" id="mobile-menu">
<div id="mobile-menu-bg"> <div id="mobile-menu-bg">
@ -137,11 +207,26 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
background: rgba(15, 23, 42, 0.8); background: rgba(15, 23, 42, 0.8);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
} }
/* 搜索面板动画 */
#mobile-search-panel.show {
animation: slide-down 0.2s ease-out forwards;
}
@keyframes slide-down {
0% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
</style> </style>
<script> <script>
const header = document.getElementById('header-bg'); const header = document.getElementById('header-bg');
const mobileMenuBg = document.getElementById('mobile-menu-bg');
const scrollThreshold = 50; const scrollThreshold = 50;
function updateHeaderBackground() { function updateHeaderBackground() {
@ -184,5 +269,206 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
menuCloseIcon.classList.toggle('hidden'); menuCloseIcon.classList.toggle('hidden');
}); });
} }
// 搜索功能逻辑
document.addEventListener('DOMContentLoaded', () => {
// 搜索节流函数
function debounce<T extends (...args: any[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | undefined;
return function(this: any, ...args: Parameters<T>): void {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// 获取DOM元素
const desktopSearch = document.getElementById('desktop-search');
const desktopResults = document.getElementById('desktop-search-results');
const desktopList = document.getElementById('desktop-search-list');
const desktopMessage = document.getElementById('desktop-search-message');
const mobileSearchButton = document.getElementById('mobile-search-button');
const mobileSearchPanel = document.getElementById('mobile-search-panel');
const mobileSearch = document.getElementById('mobile-search');
const mobileResults = document.getElementById('mobile-search-results');
const mobileList = document.getElementById('mobile-search-list');
const mobileMessage = document.getElementById('mobile-search-message');
const mobileSearchClose = document.getElementById('mobile-search-close');
// 文章对象的接口定义
interface Article {
id: string;
title: string;
date: string | Date;
summary?: string;
tags?: string[];
image?: string;
}
let articles: Article[] = [];
// 获取文章数据
async function fetchArticles() {
try {
const response = await fetch('/api/search');
if (!response.ok) {
throw new Error('获取文章数据失败');
}
articles = await response.json();
} catch (error) {
console.error('获取文章失败:', error);
}
}
// 搜索文章
function searchArticles(query: string, resultsList: HTMLElement, resultsMessage: HTMLElement) {
if (!query.trim()) {
resultsList.innerHTML = '';
resultsMessage.textContent = '输入关键词开始搜索';
resultsMessage.style.display = 'block';
return;
}
if (articles.length === 0) {
resultsMessage.textContent = '正在加载数据...';
resultsMessage.style.display = 'block';
return;
}
const lowerQuery = query.toLowerCase();
// 过滤并排序结果
const filteredArticles = articles
.filter(article => {
const title = article.title.toLowerCase();
const tags = article.tags ? article.tags.map(tag => tag.toLowerCase()) : [];
const summary = article.summary ? article.summary.toLowerCase() : '';
return title.includes(lowerQuery) ||
tags.some(tag => tag.includes(lowerQuery)) ||
summary.includes(lowerQuery);
})
.sort((a, b) => {
// 标题匹配优先
const aTitle = a.title.toLowerCase();
const bTitle = b.title.toLowerCase();
if (aTitle.includes(lowerQuery) && !bTitle.includes(lowerQuery)) {
return -1;
}
if (!aTitle.includes(lowerQuery) && bTitle.includes(lowerQuery)) {
return 1;
}
// 日期排序
return new Date(b.date).getTime() - new Date(a.date).getTime();
})
.slice(0, 10); // 限制结果数量
if (filteredArticles.length === 0) {
resultsList.innerHTML = '';
resultsMessage.textContent = '没有找到相关内容';
resultsMessage.style.display = 'block';
return;
}
// 显示结果
resultsMessage.style.display = 'none';
resultsList.innerHTML = filteredArticles.map(article => `
<li>
<a href="/articles/${article.id}" class="block px-4 py-3 hover:bg-gray-50/70 dark:hover:bg-gray-700/70 transition-colors duration-200">
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">${article.title}</h3>
${article.summary ? `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">${article.summary}</p>` : ''}
${article.tags && article.tags.length > 0 ? `
<div class="flex flex-wrap gap-1 mt-1.5">
${article.tags.slice(0, 3).map(tag => `
<span class="inline-block text-xs bg-primary-50/50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400 py-0.5 px-1.5 rounded-full">#${tag}</span>
`).join('')}
${article.tags.length > 3 ? `<span class="text-xs text-gray-400 dark:text-gray-500">+${article.tags.length - 3}</span>` : ''}
</div>
` : ''}
</a>
</li>
`).join('');
}
// 节流搜索
const debouncedDesktopSearch = debounce((value: string) => {
if (desktopList && desktopMessage) {
searchArticles(value, desktopList as HTMLElement, desktopMessage as HTMLElement);
}
}, 300);
const debouncedMobileSearch = debounce((value: string) => {
if (mobileList && mobileMessage) {
searchArticles(value, mobileList as HTMLElement, mobileMessage as HTMLElement);
}
}, 300);
// 桌面端搜索逻辑
if (desktopSearch && desktopResults) {
desktopSearch.addEventListener('focus', () => {
desktopResults.classList.remove('hidden');
if (!articles.length) fetchArticles();
});
desktopSearch.addEventListener('input', (e: Event) => {
const target = e.target as HTMLInputElement;
if (target && target.value !== undefined) {
debouncedDesktopSearch(target.value);
}
});
// 点击外部关闭结果
document.addEventListener('click', (e: MouseEvent) => {
const target = e.target as Node;
if (desktopSearch && !desktopSearch.contains(target) && !desktopResults.contains(target)) {
desktopResults.classList.add('hidden');
}
});
// ESC键关闭结果
desktopSearch.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
desktopResults.classList.add('hidden');
}
});
}
// 移动端搜索逻辑
if (mobileSearchButton && mobileSearchPanel) {
mobileSearchButton.addEventListener('click', () => {
mobileSearchPanel.classList.remove('hidden');
mobileSearchPanel.classList.add('show');
if (mobileSearch) mobileSearch.focus();
if (!articles.length) fetchArticles();
});
if (mobileSearchClose) {
mobileSearchClose.addEventListener('click', () => {
mobileSearchPanel.classList.add('hidden');
mobileSearchPanel.classList.remove('show');
});
}
if (mobileSearch && mobileResults) {
mobileSearch.addEventListener('input', (e: Event) => {
mobileResults.classList.remove('hidden');
const target = e.target as HTMLInputElement;
if (target && target.value !== undefined) {
debouncedMobileSearch(target.value);
}
});
// ESC键关闭搜索面板
mobileSearch.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
mobileSearchPanel.classList.add('hidden');
mobileSearchPanel.classList.remove('show');
}
});
}
}
});
</script> </script>

38
src/pages/api/search.ts Normal file
View File

@ -0,0 +1,38 @@
import { getCollection } from 'astro:content';
export async function GET() {
try {
// 获取所有文章
const articles = await getCollection('articles');
// 过滤掉草稿文章,并转换为简化的数据结构
const formattedArticles = articles
.filter(article => !article.data.draft) // 过滤掉草稿
.map(article => ({
id: article.id,
title: article.data.title,
date: article.data.date,
summary: article.data.summary || '',
tags: article.data.tags || [],
image: article.data.image || ''
}))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // 按日期排序
return new Response(JSON.stringify(formattedArticles), {
status: 200,
headers: {
'Content-Type': 'application/json',
// 添加缓存头缓存1小时
'Cache-Control': 'public, max-age=3600'
}
});
} catch (error) {
console.error('获取文章数据失败:', error);
return new Response(JSON.stringify({ error: '获取文章数据失败' }), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
});
}
}