| ---
|
| import { getEntry, render } from "astro:content";
|
| import Comment from "@components/comment/index.astro";
|
| import Markdown from "@components/common/Markdown.astro";
|
| import { Icon } from "astro-icon/components";
|
| import { friendsPageConfig, getEnabledFriends } from "@/config";
|
| import I18nKey from "@/i18n/i18nKey";
|
| import { i18n } from "@/i18n/translation";
|
| import MainGridLayout from "@/layouts/MainGridLayout.astro";
|
|
|
| const friendsPost = await getEntry("spec", "friends");
|
|
|
| if (!friendsPost) {
|
| throw new Error("friends page content not found");
|
| }
|
|
|
| const { Content } = await render(friendsPost);
|
|
|
|
|
| const items = getEnabledFriends();
|
| const allTags = [...new Set(items.flatMap((item) => item.tags || []))].sort();
|
|
|
|
|
| const title = i18n(I18nKey.friends);
|
| const description = i18n(I18nKey.friendsDescription);
|
| ---
|
|
|
| <MainGridLayout title={title} description={description}>
|
| <div
|
| class="flex w-full rounded-(--radius-large) overflow-hidden relative min-h-32"
|
| >
|
| <div class="card-base z-10 px-9 py-6 relative w-full">
|
| <!-- 页面标题和描述 -->
|
| <div class="mb-4">
|
| <div class="flex items-center gap-3 mb-3">
|
| <div
|
| class="h-8 w-8 rounded-lg bg-(--primary) flex items-center justify-center text-white dark:text-black/70"
|
| >
|
| <Icon name="material-symbols:group" class="text-[1.5rem]" />
|
| </div>
|
| <h1 class="text-3xl font-bold text-neutral-900 dark:text-neutral-100">
|
| {title}
|
| </h1>
|
| </div>
|
| {
|
| description && (
|
| <p class="text-base text-neutral-600 dark:text-neutral-400 leading-relaxed mb-4">
|
| {description}
|
| </p>
|
| )
|
| }
|
| </div>
|
|
|
| <!-- 标签筛选 -->
|
| {
|
| items.length > 0 && (
|
| <friend-filter class="flex flex-wrap gap-2 mb-6">
|
| <button
|
| data-tag="all"
|
| class="btn-regular px-3 py-1.5 rounded-lg bg-(--primary) text-white hover:bg-(--primary) transition-colors duration-200 text-sm font-medium"
|
| >
|
| {i18n(I18nKey.all)}
|
| </button>
|
| {allTags.map((tag) => (
|
| <button
|
| data-tag={tag}
|
| class="btn-regular px-3 py-1.5 rounded-lg bg-(--btn-regular-bg) text-(--btn-content) hover:bg-(--btn-regular-bg-hover) transition-colors duration-200 text-sm font-medium"
|
| >
|
| {tag}
|
| </button>
|
| ))}
|
| </friend-filter>
|
| )
|
| }
|
|
|
| <div class={`grid grid-cols-1 sm:grid-cols-2 ${friendsPageConfig.columns === 2 ? 'lg:grid-cols-2' : 'lg:grid-cols-3'} gap-3 my-4`}>
|
| {
|
| items.map((item) => (
|
| <a
|
| href={item.siteurl}
|
| target="_blank"
|
| rel="noopener noreferrer"
|
| data-tags={item.tags?.join(",")}
|
| class="friend-card group flex items-center gap-3 p-2.5 rounded-xl border border-(--line-divider) hover:border-(--primary) hover:bg-(--card-bg) transition-all duration-300 hover:shadow-lg relative overflow-hidden"
|
| >
|
| {}
|
| <div class="absolute inset-0 bg-(--primary) opacity-0 group-hover:opacity-5 transition-opacity duration-300 pointer-events-none" />
|
|
|
| {}
|
| <div class="relative w-16 h-16 shrink-0 rounded-xl overflow-hidden bg-zinc-100 dark:bg-zinc-800 border border-black/5 dark:border-white/5 group-hover:scale-105 transition-transform duration-300">
|
| <img
|
| src={item.imgurl}
|
| alt={item.title}
|
| class="w-full h-full object-cover"
|
| />
|
| </div>
|
|
|
| {}
|
| <div class="grow min-w-0 flex flex-col justify-center gap-0.5">
|
| <div class="flex items-center justify-between">
|
| <div class="font-bold text-base text-neutral-900 dark:text-neutral-100 group-hover:text-(--primary) transition-colors truncate pr-4">
|
| {item.title}
|
| </div>
|
| {}
|
| <Icon
|
| name="material-symbols:arrow-outward-rounded"
|
| class="text-(--primary) text-lg opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300"
|
| />
|
| </div>
|
|
|
| <div
|
| class="text-sm text-neutral-500 dark:text-neutral-400 line-clamp-1"
|
| title={item.desc}
|
| >
|
| {item.desc}
|
| </div>
|
|
|
| {}
|
| <div class="flex flex-wrap gap-1 mt-1">
|
| {item.tags && item.tags.length > 0
|
| ? item.tags
|
| .slice(0, 3)
|
| .map((tag) => (
|
| <span class="text-[0.65rem] px-1.5 py-0.5 rounded bg-neutral-100 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-400 transition-colors duration-300">
|
| {tag}
|
| </span>
|
| ))
|
| : null}
|
| </div>
|
| </div>
|
| </a>
|
| ))
|
| }
|
| </div>
|
| <Markdown class="mt-2">
|
| <Content />
|
| </Markdown>
|
| </div>
|
| </div>
|
|
|
| <!-- 评论区 -->
|
| <div class="mt-4">
|
| <Comment post={friendsPost} customPath="/friends/" />
|
| </div>
|
|
|
| <script slot="head" is:inline>
|
| if (!customElements.get("friend-filter")) {
|
| class FriendFilter extends HTMLElement {
|
| constructor() {
|
| super();
|
| }
|
|
|
| connectedCallback() {
|
| this.addEventListener("click", this.handleClick);
|
| }
|
|
|
| disconnectedCallback() {
|
| this.removeEventListener("click", this.handleClick);
|
| }
|
|
|
| handleClick(e) {
|
| const target = e.target;
|
| const button = target.closest("button");
|
| if (!button) return;
|
|
|
| const selectedTag = button.dataset.tag;
|
| const filters = this.querySelectorAll("button");
|
| const container = this.closest(".card-base");
|
| if (!container) return;
|
|
|
| const cards = container.querySelectorAll(".friend-card");
|
|
|
| filters.forEach((f) => {
|
| f.classList.remove("bg-(--primary)", "text-white");
|
| f.classList.remove("hover:bg-(--primary)");
|
| f.classList.add(
|
| "bg-(--btn-regular-bg)",
|
| "text-(--btn-content)"
|
| );
|
| f.classList.add("hover:bg-(--btn-regular-bg-hover)");
|
|
|
| if (f === button) {
|
| f.classList.remove(
|
| "bg-(--btn-regular-bg)",
|
| "text-(--btn-content)"
|
| );
|
| f.classList.remove("hover:bg-(--btn-regular-bg-hover)");
|
| f.classList.add("bg-(--primary)", "text-white");
|
| f.classList.add("hover:bg-(--primary)");
|
| }
|
| });
|
|
|
| cards.forEach((card) => {
|
| const cardEl = card;
|
| const tags = (cardEl.dataset.tags || "").split(",");
|
| if (
|
| selectedTag === "all" ||
|
| (selectedTag && tags.includes(selectedTag))
|
| ) {
|
| cardEl.style.display = "";
|
| cardEl.classList.add("animate-fade-in-up");
|
| } else {
|
| cardEl.style.display = "none";
|
| cardEl.classList.remove("animate-fade-in-up");
|
| }
|
| });
|
| }
|
| }
|
| customElements.define("friend-filter", FriendFilter);
|
| }
|
| </script>
|
| </MainGridLayout>
|
|
|