feat: 添加 motion 动画支持,优化组件动画效果,调整动画持续时间

This commit is contained in:
2025-09-10 23:33:55 +08:00
parent 7a1af795ef
commit 77eaa7a612
7 changed files with 242 additions and 113 deletions

View File

@ -26,6 +26,7 @@
"framer-motion": "^12.23.9",
"highlight.js": "^11.11.1",
"lucide-react": "^0.525.0",
"motion": "^12.23.12",
"next": "15.4.1",
"next-intl": "^4.3.4",
"next-mdx-remote-client": "^2.1.3",

55
web/pnpm-lock.yaml generated
View File

@ -59,6 +59,9 @@ importers:
lucide-react:
specifier: ^0.525.0
version: 0.525.0(react@19.1.0)
motion:
specifier: ^12.23.12
version: 12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next:
specifier: 15.4.1
version: 15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@ -1602,6 +1605,20 @@ packages:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
framer-motion@12.23.9:
resolution: {integrity: sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ==}
peerDependencies:
@ -2167,12 +2184,29 @@ packages:
engines: {node: '>=10'}
hasBin: true
motion-dom@12.23.12:
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
motion-dom@12.23.9:
resolution: {integrity: sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==}
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
motion@12.23.12:
resolution: {integrity: sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -4399,6 +4433,15 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
framer-motion@12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
motion-dom: 12.23.12
motion-utils: 12.23.6
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
framer-motion@12.23.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
motion-dom: 12.23.9
@ -5173,12 +5216,24 @@ snapshots:
mkdirp@3.0.1: {}
motion-dom@12.23.12:
dependencies:
motion-utils: 12.23.6
motion-dom@12.23.9:
dependencies:
motion-utils: 12.23.6
motion-utils@12.23.6: {}
motion@12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
framer-motion: 12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
ms@2.1.3: {}
nanoid@3.3.11: {}

View File

@ -7,6 +7,8 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import config from '@/config'
import { cn } from '@/lib/utils'
import { getPostHref } from '@/utils/common/post'
import { motion } from 'framer-motion'
import { deceleration } from '@/motion/curve'
interface BlogCardProps {
post: Post
@ -29,8 +31,19 @@ export function BlogCard({ post, className }: BlogCardProps) {
)}
>
{/* 封面图片区域 */}
<div className="relative aspect-[16/9] overflow-hidden">
<div
className="relative aspect-[16/9] overflow-hidden"
>
{/* 自定义封面图片 */}
<motion.div
initial={{ scale: 1.2, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{
duration: config.animationDurationSecond,
ease: deceleration,
}}
className="absolute inset-0 w-full h-full"
>
{(post.cover || config.defaultCover) ? (
<Image
src={post.cover || config.defaultCover}
@ -57,6 +70,8 @@ export function BlogCard({ post, className }: BlogCardProps) {
)}
/>
)}
</motion.div>
{/* 覆盖层 */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
@ -117,6 +132,7 @@ export function BlogCard({ post, className }: BlogCardProps) {
</CardTitle>
</CardHeader>
{/* Card Content - 主要内容 */}
<CardContent className="flex-1">
<CardDescription className="line-clamp-3 leading-relaxed">

View File

@ -6,6 +6,8 @@ import { isMobileByUA } from "@/utils/server/device";
import { calculateReadingTime } from "@/utils/common/post";
import { CommentSection } from "@/components/comment";
import { TargetType } from '@/models/types';
import * as motion from "motion/react-client"
import config from "@/config";
function PostMeta({ post }: { post: Post }) {
return (
@ -71,6 +73,12 @@ async function PostHeader({ post }: { post: Post }) {
aria-hidden="true"
style={{ zIndex: -1 }}
/>
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}
className="container mx-auto px-4"
>
{(post.labels || post.isOriginal) && (
<div className="flex flex-wrap gap-2 mb-4">
{post.isOriginal && (
@ -90,6 +98,8 @@ async function PostHeader({ post }: { post: Post }) {
<div>
<PostMeta post={post} />
</div>
</motion.div>
</div>
);
}

View File

@ -6,43 +6,67 @@ import "highlight.js/styles/github.css"; // 你可以换成喜欢的主题
import "highlight.js/styles/github-dark.css"; // 适用于暗黑模式
import "highlight.js/styles/github-dark-dimmed.css"; // 适用于暗黑模式
import CodeBlock from "@/components/common/markdown-codeblock";
import * as motion from "motion/react-client"
import config from "@/config";
import { Skeleton } from "@/components/ui/skeleton";
export function MotionDiv(props: React.ComponentPropsWithoutRef<"div">) {
return (
<motion.div
initial={{ opacity: 0.3, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}
>{props.children}
</motion.div>
);
}
export const markdownComponents = {
h1: (props: React.ComponentPropsWithoutRef<"h1">) => (
<MotionDiv>
<h1
className="scroll-m-20 text-4xl font-extrabold tracking-tight text-balance mt-10 mb-6"
{...props}
/>
</MotionDiv>
),
h2: (props: React.ComponentPropsWithoutRef<"h2">) => (
<MotionDiv>
<h2
className="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0 mt-8 mb-4"
{...props}
/>
</MotionDiv>
),
h3: (props: React.ComponentPropsWithoutRef<"h3">) => (
<MotionDiv>
<h3
className="scroll-m-20 text-2xl font-semibold tracking-tight mt-6 mb-3"
{...props}
/>
</MotionDiv>
),
h4: (props: React.ComponentPropsWithoutRef<"h4">) => (
<MotionDiv>
<h4
className="scroll-m-20 text-xl font-semibold tracking-tight mt-5 mb-2"
{...props}
/>
/></MotionDiv>
),
p: (props: React.ComponentPropsWithoutRef<"p">) => (
<p
className="leading-7 mt-4 mb-4"
{...props}
/>
<MotionDiv>
<div className="leading-7 mt-4 mb-4">{props.children}</div>
</MotionDiv>
),
blockquote: (props: React.ComponentPropsWithoutRef<"blockquote">) => (
<MotionDiv>
<blockquote
className="border-l-4 border-blue-400 pl-4 italic my-6 py-2"
{...props}
/>
</MotionDiv>
),
code: (props: React.ComponentPropsWithoutRef<"code">) => (
<code
@ -51,7 +75,7 @@ export const markdownComponents = {
/>
),
pre: ({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) => (
<CodeBlock {...props}>{children}</CodeBlock>
<MotionDiv><CodeBlock {...props}>{children}</CodeBlock></MotionDiv>
),
a: (props: React.ComponentPropsWithoutRef<"a">) => (
<a
@ -63,7 +87,7 @@ export const markdownComponents = {
export function RenderMarkdown(props: Omit<MDXRemoteProps, "components">) {
return (
<Suspense fallback={<div>...</div>}>
<Suspense fallback={<MarkdownSkeleton />}>
<MDXRemote {...props}
components={markdownComponents}
options={{
@ -77,8 +101,24 @@ export function RenderMarkdown(props: Omit<MDXRemoteProps, "components">) {
export function RenderMarkdownWithComponents(props: MDXRemoteProps) {
return (
<Suspense fallback={<div>...</div>}>
<Suspense fallback={<MarkdownSkeleton />}>
<MDXRemote {...props} components={markdownComponents} />
</Suspense>
);
}
export function MarkdownSkeleton() {
return (
<div className="space-y-4 mt-8">
<Skeleton className="h-10 w-2/3" />
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-1/3" />
</div>
);
}

View File

@ -16,7 +16,7 @@ const config = {
bodyWidthMobile: "100vw",
postsPerPage: 12,
commentsPerPage: 8,
animationDurationSecond: 0.5,
animationDurationSecond: 0.618,
footer: {
text: "Liteyuki ICP备 1145141919810",
links: []

7
web/src/motion/curve.ts Normal file
View File

@ -0,0 +1,7 @@
// 定义一系列motion动画用的曲线
export const liner = (t: number) => t;
export const acceleration = (t: number) => t * t;
export const deceleration = (t: number) => 1 - (1 - t) * (1 - t);