Files
Eidolon/src/pages/search/index.astro
T

129 lines
7.3 KiB
Plaintext

---
import { Search as SearchIcon } from '@lucide/astro';
import BaseLayout from '@/layouts/BaseLayout.astro';
const query = Astro.url.searchParams.get('q')?.trim() ?? '';
---
<BaseLayout title={query ? `搜索:${query}` : '搜索'} description="搜索 Mirages 中的文章。" noindex>
<section class="mx-auto w-full px-[48px] pt-16 pb-14 max-[336px]:px-5 md:max-w-[720px] md:px-0 md:pt-20 min-[1302px]:max-w-[864px] min-[1600px]:max-w-[896px] min-[1800px]:max-w-[960px] min-[2000px]:max-w-[992px] min-[2400px]:max-w-[1024px]" data-search-page>
<header class="mb-8 border-b border-line pb-4">
<h1 class="m-0 text-xl leading-tight font-normal">搜索结果</h1>
<p class="mt-2 mb-0 text-sm text-subtle" data-search-query>{query ? `查询词:${query}` : '请输入查询词开始搜索。'}</p>
</header>
<form class="mb-8 flex items-center border-b border-line text-current" action={`${import.meta.env.BASE_URL}search/`} method="get" role="search">
<SearchIcon class="shrink-0 text-subtle" aria-hidden="true" size={17} />
<input class="min-w-0 flex-1 border-0 bg-transparent px-2 py-2 text-sm text-current outline-none focus:border-0 focus:outline-none focus:ring-0" type="search" name="q" value={query} placeholder="搜索文章" required aria-label="搜索文章" />
<button class="border-0 bg-transparent px-2 py-2 text-sm text-mirages-accent hover:bg-raised" type="submit">搜索</button>
</form>
<div data-search-page-results aria-live="polite">
{query ? <p class="px-3 py-4 text-sm text-subtle">正在搜索...</p> : <p class="px-3 py-4 text-sm text-subtle">请输入查询词开始搜索。</p>}
</div>
</section>
</BaseLayout>
<script>
interface PagefindResult { data: () => Promise<{ url: string; meta?: { title?: string; date?: string; category?: string; categories?: string; tags?: string; cover?: string } }>; }
interface PagefindModule { options: (options: { excerptLength: number }) => Promise<void>; search: (query: string) => Promise<{ results: PagefindResult[] }>; }
const cardBackgrounds = ['#547a82', '#806b76', '#657657', '#876d56', '#5f718d', '#796f54'];
const cardBackground = (url: string) => {
let hash = 0;
for (const character of url) hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
return cardBackgrounds[hash % cardBackgrounds.length];
};
let pagefindPromise: Promise<PagefindModule> | undefined;
const loadPagefind = () => {
pagefindPromise ??= (async () => {
const pagefindUrl = new URL(`${import.meta.env.BASE_URL}pagefind/pagefind.js`, document.baseURI).href;
const pagefind = await import(/* @vite-ignore */ pagefindUrl) as unknown as PagefindModule;
await pagefind.options({ excerptLength: 0 });
return pagefind;
})();
return pagefindPromise;
};
/* Client-side navigation reuses this module, so every run reads the page and query afresh. */
let runToken = 0;
const run = async () => {
const root = document.querySelector<HTMLElement>('[data-search-page]');
const results = root?.querySelector<HTMLElement>('[data-search-page-results]');
if (!root || !results) return;
const queryLabel = root.querySelector<HTMLElement>('[data-search-query]');
const input = root.querySelector<HTMLInputElement>('input[name="q"]');
const query = new URLSearchParams(window.location.search).get('q')?.trim() ?? '';
const token = ++runToken;
if (queryLabel) queryLabel.textContent = query ? `查询词:${query}` : '请输入查询词开始搜索。';
if (input && input.value.trim() !== query) input.value = query;
if (!query) {
results.innerHTML = '<p class="px-3 py-4 text-sm text-subtle">请输入查询词开始搜索。</p>';
return;
}
results.innerHTML = '<p class="px-3 py-4 text-sm text-subtle">正在搜索...</p>';
if (import.meta.env.DEV) { results.innerHTML = '<p class="px-3 py-4 text-sm text-subtle">搜索功能需在构建并预览后使用。</p>'; return; }
try {
const pagefind = await loadPagefind();
const search = await pagefind.search(query);
if (token !== runToken) return;
results.replaceChildren();
if (!search.results.length) {
results.innerHTML = '<p class="px-3 py-4 text-sm text-subtle">没有找到相关内容。</p>';
return;
}
for (const result of search.results) {
const data = await result.data();
if (token !== runToken) return;
const article = document.createElement('article');
article.className = 'py-[0.9375rem] md:py-[0.9375rem]';
const link = document.createElement('a');
link.className = 'group relative block h-50 overflow-hidden rounded-[0.3125rem] bg-[var(--card-background)] text-white no-underline shadow-sm transition-[transform,box-shadow] duration-200 ease-out hover:-translate-y-0.5 hover:scale-[1.005] hover:text-white hover:shadow-mirages max-[21rem]:h-42 md:h-62';
link.href = data.url;
link.style.setProperty('--card-background', cardBackground(data.url));
if (data.meta?.cover) {
const image = document.createElement('img');
image.className = 'absolute inset-0 size-full object-cover';
image.src = data.meta.cover;
image.alt = '';
image.loading = 'lazy';
link.append(image);
}
const mask = document.createElement('span');
mask.className = 'absolute inset-0 z-10 bg-black/25';
mask.setAttribute('aria-hidden', 'true');
link.append(mask);
const content = document.createElement('div');
content.className = 'absolute inset-0 z-20 flex flex-col items-center justify-center px-4 py-4 text-center md:px-6';
const title = document.createElement('h2');
title.className = 'm-0 max-w-[90%] [overflow-wrap:anywhere] font-sans text-[1.5625rem] leading-tight font-normal tracking-[0]';
title.textContent = data.meta?.title || data.url;
content.append(title);
const date = data.meta?.date;
const categories = data.meta?.categories ?? data.meta?.category;
const tags = data.meta?.tags;
if (date || categories || tags) {
const info = document.createElement('p');
info.className = 'mt-3 mb-0 max-w-[90%] [overflow-wrap:anywhere] text-[0.8125rem] leading-relaxed font-normal text-[#eee] [font-family:Consolas,Menlo,Monaco,"lucida_console","Liberation_Mono","Courier_New","andale_mono",monospaceX,monospace,sans-serif]';
const appendMeta = (value: string | undefined, className?: string) => {
if (!value) return;
if (info.childNodes.length > 0) info.append(' · ');
const item = document.createElement('span');
item.textContent = value;
if (className) item.className = className;
info.append(item);
};
appendMeta(date);
appendMeta(categories, '[font-family:var(--mirages-font-ui)]');
appendMeta(tags);
content.append(info);
}
link.append(content);
article.append(link);
results.append(article);
}
} catch { results.innerHTML = '<p class="px-3 py-4 text-sm text-subtle">搜索暂时不可用。</p>'; }
};
run();
document.addEventListener('astro:page-load', run);
</script>