refactor(components, config): 更新组件样式和配置以提升一致性和性能

- 在 AGENTS.md 中新增 Next.js 代理规则说明,强调破坏性更改。
- 在 next.config.mjs 中启用 Turbopack 文件系统缓存以加快开发构建速度,并更新图片质量设置。
- 重构多个组件(如 RandomArticle、CateHero、Waterfall 等),优化图片渲染逻辑,提升样式一致性和用户体验。
- 在 Empty 组件中将 img 标签替换为 Image 组件以支持更好的图像处理。
This commit is contained in:
刘宇阳
2026-07-07 17:03:23 +08:00
parent 98155f8c8a
commit c4c0f9543d
10 changed files with 78 additions and 103 deletions

View File

@@ -1,3 +1,10 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
# 项目规则
## 核心技术栈

View File

@@ -3,21 +3,14 @@ const nextConfig = {
// 关闭严格模式
reactStrictMode: false,
output: 'standalone',
// 启用 Turbopack 文件系统缓存,加快开发时候的构建速度
experimental: {
turbopackFileSystemCacheForDev: true,
},
// 配置图片来源
images: {
qualities: [75, 80, 90, 100],
remotePatterns: [
{
protocol: 'https',
hostname: 'res.liuyuyang.net',
},
{
protocol: 'https',
hostname: '**.liuyuyang.net',
},
{
protocol: 'https',
hostname: 'liuyuyang.net',
},
{
protocol: 'https',
hostname: '**',

View File

@@ -53,16 +53,7 @@ const RandomArticle = () => {
className="relative w-full h-40 bg-cover bg-center bg-no-repeat"
style={coverUrl ? { backgroundImage: `url(${coverUrl})` } : undefined}
>
{coverUrl ? (
<Image
src={coverUrl}
alt={item.title}
fill
className="object-cover transition duration-300 ease-out group-hover:scale-105"
/>
) : (
<div className="absolute inset-0 bg-slate-400/50 dark:bg-slate-600/50" />
)}
{!coverUrl && <div className="absolute inset-0 bg-slate-400/50 dark:bg-slate-600/50" />}
<div
className="absolute inset-0 bg-linear-to-t from-black/70 via-transparent to-transparent pointer-events-none"
aria-hidden

View File

@@ -1,6 +1,5 @@
'use client';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { LuChevronDown } from 'react-icons/lu';
@@ -29,20 +28,14 @@ export default function CateHero({ children, image }: Props) {
className="relative flex flex-col items-center justify-center overflow-hidden px-4 pt-16 pb-10 text-center transition-opacity duration-500 ease-out max-md:h-[min(48vh,20rem)] max-md:min-h-70 md:min-h-dvh md:px-6 md:pb-24 md:pt-28"
style={{ opacity }}
>
<div aria-hidden className="absolute inset-0">
<Image
src={image}
alt=""
fill
priority
className="object-cover object-[center_45%] max-md:object-[center_32%] sm:object-center"
sizes="100vw"
quality={80}
/>
<div className="absolute inset-0 bg-linear-to-r from-black/45 via-black/25 to-black/10" />
<div className="absolute inset-0 bg-linear-to-b from-black/35 via-black/10 via-45% to-transparent md:via-55%" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[58%] bg-linear-to-t from-background from-5% via-background/75 to-transparent dark:from-black-a dark:via-black-a/75 max-md:h-[68%] md:h-[42%]" />
</div>
<div
aria-hidden
className="absolute inset-0 bg-cover bg-no-repeat object-[center_45%] max-md:object-[center_32%] sm:object-center"
style={{ backgroundImage: `url(${image})`, backgroundPosition: 'center 45%' }}
/>
<div aria-hidden className="absolute inset-0 bg-linear-to-r from-black/45 via-black/25 to-black/10" />
<div aria-hidden className="absolute inset-0 bg-linear-to-b from-black/35 via-black/10 via-45% to-transparent md:via-55%" />
<div aria-hidden className="pointer-events-none absolute inset-x-0 bottom-0 h-[58%] bg-linear-to-t from-background from-5% via-background/75 to-transparent dark:from-black-a dark:via-black-a/75 max-md:h-[68%] md:h-[42%]" />
<div className="relative z-10 flex w-full max-w-4xl flex-col items-center justify-center px-2 sm:px-6">
{children}

View File

@@ -190,7 +190,7 @@ const FriendCard = ({ item, type, index }: { item: Web; type: string; index: num
<img
src={item.image}
alt={item.title}
className={`relative w-14 h-14 rounded-full object-cover bg-gray-100 dark:bg-gray-800 border-2 border-white dark:border-gray-700 shadow-md transition-[scale,rotate] duration-500 group-hover:scale-110 group-hover:rotate-3`}
className="relative w-14 h-14 rounded-full object-cover bg-gray-100 dark:bg-gray-800 border-2 border-white dark:border-gray-700 shadow-md transition-[scale,rotate] duration-500 group-hover:scale-110 group-hover:rotate-3"
onError={(e) => {
const el = e.target as HTMLImageElement;
if (el.src !== DEFAULT_AVATAR) el.src = DEFAULT_AVATAR;

View File

@@ -6,7 +6,6 @@ import ArticleMeta from '@/components/ArticleLayout/components/ArticleMeta';
import Empty from '@/components/Empty';
import Show from '@/components/Show';
interface CardProps {
data: Paginate<Article[]>;
}
@@ -14,13 +13,11 @@ interface CardProps {
const Card = async ({ data }: CardProps) => {
const covers = await getThemeCovers();
// 生成文章摘要取前100个字
const genArticleInfo = (data: Article) => {
if (data.description?.trim()?.length) {
return data.description;
} else {
return data.content.slice(0, 100);
}
return data.content.slice(0, 100);
};
return (
@@ -31,7 +28,6 @@ const Card = async ({ data }: CardProps) => {
<Link href={`/article/${item.id}`} className="flex flex-col justify-between h-full text-center sm:text-start">
<h3 className="overflow-hidden relative w-full my-2.5 text_shadow text-white hover:text-primary text-center text-lg md:text-xl lg:text-[22px] xl:text-2xl line-clamp-1">{item.title}</h3>
<p className="text-center text-[#cecece] text-sm sm:text-[15px] leading-7 sm:indent-8 line-clamp-2 xl:line-clamp-3">{genArticleInfo(item)}</p>
<ArticleMeta article={item} className="justify-center sm:justify-start" />
</Link>
</div>

View File

@@ -6,7 +6,6 @@ import ArticleMeta from '@/components/ArticleLayout/components/ArticleMeta';
import Empty from '@/components/Empty';
import Show from '@/components/Show';
interface ClassicsProps {
data: Paginate<Article[]>;
}
@@ -14,13 +13,11 @@ interface ClassicsProps {
const Classics = async ({ data }: ClassicsProps) => {
const covers = await getThemeCovers();
// 生成文章摘要取前100个字
const genArticleInfo = (data: Article) => {
if (data.description?.trim()?.length) {
return data.description;
} else {
return data.content.slice(0, 100);
}
return data.content.slice(0, 100);
};
return (
@@ -29,47 +26,46 @@ const Classics = async ({ data }: ClassicsProps) => {
const cover = getRandomImage(item.cover, covers);
return (
<div key={item.id} className="panel relative overflow-hidden flex h-[190px] md:h-60 lg:h-52 xl:h-60 bg-black-b">
{index % 2 === 0 && (
<div
className="hidden sm:block relative min-w-[45%] bg-cover bg-no-repeat bg-center scale-100 hover:scale-125 z-10 transition-[scale] duration-300 ease-out"
style={{
clipPath: 'polygon(0 0, 100% 0, 90% 100%, 0 100%)',
backgroundImage: `url(${cover})`,
}}
/>
)}
<div className="relative w-full sm:w-[65%] py-5 px-5 sm:px-10 lg:px-5 xl:px-10 z-20">
<Link href={`/article/${item.id}`} className="flex flex-col justify-between h-full text-center sm:text-start">
<h3 className="overflow-hidden relative w-full my-2.5 text-white hover:text-primary text-lg md:text-xl lg:text-[22px] xl:text-2xl line-clamp-1">{item.title}</h3>
<p className="text-[#cecece] text-sm sm:text-[15px] leading-7 sm:indent-8 line-clamp-2 xl:line-clamp-3">{genArticleInfo(item)}</p>
<ArticleMeta
article={item}
className={`justify-center ${index % 2 === 0 ? ' sm:justify-end' : 'sm:justify-start'}`}
<div key={item.id} className="panel relative overflow-hidden flex h-[190px] md:h-60 lg:h-52 xl:h-60 bg-black-b">
{index % 2 === 0 && (
<div
className="hidden sm:block relative min-w-[45%] bg-cover bg-no-repeat bg-center scale-100 hover:scale-125 z-10 transition-[scale] duration-300 ease-out"
style={{
clipPath: 'polygon(0 0, 100% 0, 90% 100%, 0 100%)',
backgroundImage: `url(${cover})`,
}}
/>
</Link>
</div>
)}
<div
className="absolute w-full h-60 bg-cover bg-center"
style={{
filter: 'blur(2.5rem) brightness(0.6)',
backgroundImage: `url(${cover})`,
}}
/>
<div className="relative w-full sm:w-[65%] py-5 px-5 sm:px-10 lg:px-5 xl:px-10 z-20">
<Link href={`/article/${item.id}`} className="flex flex-col justify-between h-full text-center sm:text-start">
<h3 className="overflow-hidden relative w-full my-2.5 text-white hover:text-primary text-lg md:text-xl lg:text-[22px] xl:text-2xl line-clamp-1">{item.title}</h3>
<p className="text-[#cecece] text-sm sm:text-[15px] leading-7 sm:indent-8 line-clamp-2 xl:line-clamp-3">{genArticleInfo(item)}</p>
<ArticleMeta
article={item}
className={`justify-center ${index % 2 === 0 ? ' sm:justify-end' : 'sm:justify-start'}`}
/>
</Link>
</div>
{index % 2 !== 0 && (
<div
className="relative min-w-[45%] bg-cover bg-no-repeat bg-center scale-100 z-10 hover:scale-125 transition-[scale] duration-300 ease-out hidden sm:block"
className="absolute w-full h-60 bg-cover bg-center"
style={{
clipPath: 'polygon(10% 0, 100% 0, 100% 100%, 0 100%)',
filter: 'blur(2.5rem) brightness(0.6)',
backgroundImage: `url(${cover})`,
}}
/>
)}
</div>
{index % 2 !== 0 && (
<div
className="relative min-w-[45%] bg-cover bg-no-repeat bg-center scale-100 z-10 hover:scale-125 transition-[scale] duration-300 ease-out hidden sm:block"
style={{
clipPath: 'polygon(10% 0, 100% 0, 100% 100%, 0 100%)',
backgroundImage: `url(${cover})`,
}}
/>
)}
</div>
);
})}

View File

@@ -20,24 +20,23 @@ export default ({ data }: WaterfallProps) => {
const { theme } = useConfigStore();
return (
<>
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid mb-12" columnClassName="masonry-grid_column">
{data.result.map((item) => (
<div key={item.id} className="group overflow-hidden mt-2.5 rounded-xl bg-white dark:bg-black-b border dark:border-black-b hover:shadow-[0_10px_20px_1px_rgb(83,157,253,.1)] cursor-pointer">
<Link href={`/article/${item.id}`}>
<div className="overflow-hidden h-32">
<div className="relative h-full bg-cover bg-no-repeat bg-center scale-100 hover:scale-125 z-10 transition-[scale] duration-300 ease-out" style={{ backgroundImage: `url(${getRandomImage(item.cover, theme.covers)})` }} />
</div>
<div className="py-2 px-4">
<h1 className="mb-2 text-black dark:text-white group-hover:text-primary line-clamp-2 ">{item.title}</h1>
<div className="text-sm text-gray-500 dark:text-[#8c9ab1] line-clamp-4">{item.description}</div>
</div>
</Link>
</div>
))}
</Masonry>
</>
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid mb-12" columnClassName="masonry-grid_column">
{data.result.map((item) => (
<div key={item.id} className="group overflow-hidden mt-2.5 rounded-xl bg-white dark:bg-black-b border dark:border-black-b hover:shadow-[0_10px_20px_1px_rgb(83,157,253,.1)] cursor-pointer">
<Link href={`/article/${item.id}`}>
<div className="overflow-hidden h-32">
<div
className="relative h-full bg-cover bg-no-repeat bg-center scale-100 hover:scale-125 z-10 transition-[scale] duration-300 ease-out"
style={{ backgroundImage: `url(${getRandomImage(item.cover, theme.covers)})` }}
/>
</div>
<div className="py-2 px-4">
<h1 className="mb-2 text-black dark:text-white group-hover:text-primary line-clamp-2 ">{item.title}</h1>
<div className="text-sm text-gray-500 dark:text-[#8c9ab1] line-clamp-4">{item.description}</div>
</div>
</Link>
</div>
))}
</Masonry>
);
};

View File

@@ -1,9 +1,10 @@
import Image from 'next/image';
import EmptySvg from '@/assets/svg/other/empty.svg';
const Empty = ({ info }: { info: string }) => {
return (
<div className="w-52 mx-auto py-12 mt-5">
<img src={EmptySvg.src} alt="空状态" width={208} height={144} />
<Image src={EmptySvg} alt="空状态" width={208} height={144} />
<div className="pt-5 text-center text-gray-700 dark:text-white">{info}</div>
</div>
);

View File

@@ -6,9 +6,8 @@ import { getRandomImage } from '@/utils';
import { useConfigStore } from '@/stores';
interface Props {
src?: string; // 图片列表
isRipple?: boolean; // 是否显示波浪
/** 按 16:9 比例完整展示背景图,避免固定高度裁切 */
src?: string;
isRipple?: boolean;
fullImage?: boolean;
children?: ReactNode;
}