96 lines
6.0 KiB
Plaintext
96 lines
6.0 KiB
Plaintext
---
|
|
import SkillCard from "@/components/SkillCard.astro";
|
|
import BaseLayout from "@/layouts/BaseLayout.astro";
|
|
import { getCatalogStats, loadCatalog } from "@/lib/catalog";
|
|
|
|
const entries = await loadCatalog();
|
|
const stats = getCatalogStats(entries);
|
|
const domains = [...new Set(entries.flatMap((entry) => entry.domains))].sort((a, b) => a.localeCompare(b, "zh-CN"));
|
|
---
|
|
|
|
<BaseLayout title="Skills 目录 — 仓颉 Skill" description={`浏览 ${stats.packs} 个 Skill Pack 和 ${stats.skills} 个经过蒸馏的原子 Skill。`}>
|
|
<section class="page-hero compact"><div class="shell"><p class="eyebrow">Registry</p><h1>Skills 目录</h1><p class="lede">当前收录 <strong>{stats.packs}</strong> 个 Skill Pack,共 <strong>{stats.skills}</strong> 个原子 Skill。搜索名称、领域或使用场景,快速找到合适的方法。</p></div></section>
|
|
|
|
<section class="catalog-section shell">
|
|
<div class="filters" role="search" aria-label="筛选 Skills">
|
|
<label class="search-field"><span>搜索</span><input id="skill-search" type="search" placeholder="例如:投资、文案、AI…" autocomplete="off" /></label>
|
|
<label><span>领域</span><select id="domain-filter"><option value="">全部领域</option>{domains.map((domain) => <option value={domain}>{domain}</option>)}</select></label>
|
|
<label><span>质量</span><select id="quality-filter"><option value="">全部质量</option><option value="verified">已验证</option><option value="community">社区收录</option><option value="experimental">实验性</option></select></label>
|
|
<label><span>来源</span><select id="source-filter"><option value="">全部来源</option><option value="github">GitHub 仓库</option><option value="bundled">仓库内置</option></select></label>
|
|
</div>
|
|
|
|
<div class="results-bar"><p id="results-count" aria-live="polite">显示全部 {entries.length} 个 Skill Pack</p><button id="reset-filters" type="button">重置筛选</button></div>
|
|
<div id="skills-grid" class="skill-grid">
|
|
{entries.map((entry) => <SkillCard entry={entry} filterable />)}
|
|
</div>
|
|
<div id="empty-state" class="empty-state" hidden><h2>没有找到匹配的 Skill</h2><p>换一个关键词,或清空筛选条件再试。</p><button class="button secondary" type="button" data-reset>清空筛选</button></div>
|
|
</section>
|
|
|
|
<script>
|
|
const search = document.querySelector<HTMLInputElement>("#skill-search");
|
|
const domain = document.querySelector<HTMLSelectElement>("#domain-filter");
|
|
const quality = document.querySelector<HTMLSelectElement>("#quality-filter");
|
|
const source = document.querySelector<HTMLSelectElement>("#source-filter");
|
|
const cards = [...document.querySelectorAll<HTMLElement>("[data-skill-card]")];
|
|
const count = document.querySelector<HTMLElement>("#results-count");
|
|
const empty = document.querySelector<HTMLElement>("#empty-state");
|
|
|
|
function applyFilters() {
|
|
const query = search?.value.trim().toLocaleLowerCase("zh-CN") ?? "";
|
|
let visible = 0;
|
|
for (const card of cards) {
|
|
const matches = (!query || card.dataset.search?.includes(query))
|
|
&& (!domain?.value || card.dataset.domains?.split("|").includes(domain.value))
|
|
&& (!quality?.value || card.dataset.quality === quality.value)
|
|
&& (!source?.value || card.dataset.source === source.value);
|
|
card.hidden = !matches;
|
|
if (matches) visible += 1;
|
|
}
|
|
if (count) count.textContent = visible === cards.length ? `显示全部 ${visible} 个 Skill Pack` : `找到 ${visible} 个 Skill Pack`;
|
|
if (empty) empty.hidden = visible !== 0;
|
|
const params = new URLSearchParams();
|
|
if (query) params.set("q", query);
|
|
if (domain?.value) params.set("domain", domain.value);
|
|
if (quality?.value) params.set("quality", quality.value);
|
|
history.replaceState(null, "", `${location.pathname}${params.size ? `?${params}` : ""}`);
|
|
}
|
|
|
|
function resetFilters() {
|
|
if (search) search.value = "";
|
|
if (domain) domain.value = "";
|
|
if (quality) quality.value = "";
|
|
if (source) source.value = "";
|
|
applyFilters();
|
|
search?.focus();
|
|
}
|
|
|
|
const params = new URLSearchParams(location.search);
|
|
if (search) search.value = params.get("q") ?? "";
|
|
if (domain) domain.value = params.get("domain") ?? "";
|
|
if (quality) quality.value = params.get("quality") ?? "";
|
|
applyFilters();
|
|
|
|
[search, domain, quality, source].forEach((control) => control?.addEventListener("input", applyFilters));
|
|
document.querySelectorAll("#reset-filters, [data-reset]").forEach((button) => button.addEventListener("click", resetFilters));
|
|
</script>
|
|
</BaseLayout>
|
|
|
|
<style>
|
|
.page-hero.compact { padding-bottom: 42px; }
|
|
.page-hero.compact h1 { font-size: clamp(42px, 6vw, 64px); }
|
|
.catalog-section { padding-top: 38px; }
|
|
.filters { display: grid; grid-template-columns: minmax(260px, 1.6fr) repeat(3, 1fr); gap: 10px; padding: 15px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--soft); }
|
|
.filters label { display: grid; gap: 6px; }
|
|
.filters label > span { color: var(--muted); font-size: 11px; font-weight: 700; }
|
|
.filters input, .filters select { width: 100%; height: 43px; padding: 0 12px; border: 1px solid var(--line); border-radius: 6px; background: white; color: var(--ink); outline: none; }
|
|
.filters input:focus, .filters select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
.results-bar { display: flex; justify-content: space-between; align-items: center; min-height: 67px; }
|
|
.results-bar p { color: var(--muted); font-size: 13px; }
|
|
.results-bar button { border: 0; background: transparent; color: var(--accent); font-size: 13px; }
|
|
.empty-state { padding: 80px 20px; text-align: center; }
|
|
.empty-state h2 { margin-bottom: 4px; }
|
|
.empty-state p { margin-top: 0; color: var(--muted); }
|
|
@media (max-width: 820px) { .filters { grid-template-columns: 1fr 1fr; } .search-field { grid-column: 1 / -1; } }
|
|
@media (max-width: 520px) { .filters { grid-template-columns: 1fr; } .search-field { grid-column: auto; } }
|
|
</style>
|