mirror of
https://github.com/LiuYuYang01/ThriveX-Blog.git
synced 2026-09-03 06:24:24 +08:00
refactor(components, config): 更新组件样式和配置以提升一致性和性能
- 在 AGENTS.md 中新增 Next.js 代理规则说明,强调破坏性更改。 - 在 next.config.mjs 中启用 Turbopack 文件系统缓存以加快开发构建速度,并更新图片质量设置。 - 重构多个组件(如 RandomArticle、CateHero、Waterfall 等),优化图片渲染逻辑,提升样式一致性和用户体验。 - 在 Empty 组件中将 img 标签替换为 Image 组件以支持更好的图像处理。
This commit is contained in:
@@ -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 -->
|
||||
|
||||
|
||||
# 项目规则
|
||||
|
||||
## 核心技术栈
|
||||
|
||||
@@ -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: '**',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user