blog / src /pages /friends.astro
cacode's picture
Upload 434 files
96dd062 verified
---
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>