mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 02:56:22 +00:00
feat: 添加 motion 动画支持,优化组件动画效果,调整动画持续时间
This commit is contained in:
@ -26,6 +26,7 @@
|
|||||||
"framer-motion": "^12.23.9",
|
"framer-motion": "^12.23.9",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
|
"motion": "^12.23.12",
|
||||||
"next": "15.4.1",
|
"next": "15.4.1",
|
||||||
"next-intl": "^4.3.4",
|
"next-intl": "^4.3.4",
|
||||||
"next-mdx-remote-client": "^2.1.3",
|
"next-mdx-remote-client": "^2.1.3",
|
||||||
|
55
web/pnpm-lock.yaml
generated
55
web/pnpm-lock.yaml
generated
@ -59,6 +59,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.525.0
|
specifier: ^0.525.0
|
||||||
version: 0.525.0(react@19.1.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:
|
next:
|
||||||
specifier: 15.4.1
|
specifier: 15.4.1
|
||||||
version: 15.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
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==}
|
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||||
engines: {node: '>= 6'}
|
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:
|
framer-motion@12.23.9:
|
||||||
resolution: {integrity: sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ==}
|
resolution: {integrity: sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2167,12 +2184,29 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
|
||||||
|
|
||||||
motion-dom@12.23.9:
|
motion-dom@12.23.9:
|
||||||
resolution: {integrity: sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==}
|
resolution: {integrity: sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==}
|
||||||
|
|
||||||
motion-utils@12.23.6:
|
motion-utils@12.23.6:
|
||||||
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
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:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@ -4399,6 +4433,15 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
mime-types: 2.1.35
|
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):
|
framer-motion@12.23.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-dom: 12.23.9
|
motion-dom: 12.23.9
|
||||||
@ -5173,12 +5216,24 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@3.0.1: {}
|
mkdirp@3.0.1: {}
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
|
||||||
motion-dom@12.23.9:
|
motion-dom@12.23.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-utils: 12.23.6
|
motion-utils: 12.23.6
|
||||||
|
|
||||||
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: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
@ -7,6 +7,8 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { getPostHref } from '@/utils/common/post'
|
import { getPostHref } from '@/utils/common/post'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { deceleration } from '@/motion/curve'
|
||||||
|
|
||||||
interface BlogCardProps {
|
interface BlogCardProps {
|
||||||
post: Post
|
post: Post
|
||||||
@ -29,34 +31,47 @@ export function BlogCard({ post, className }: BlogCardProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* 封面图片区域 */}
|
{/* 封面图片区域 */}
|
||||||
<div className="relative aspect-[16/9] overflow-hidden">
|
<div
|
||||||
|
className="relative aspect-[16/9] overflow-hidden"
|
||||||
|
>
|
||||||
{/* 自定义封面图片 */}
|
{/* 自定义封面图片 */}
|
||||||
{(post.cover || config.defaultCover) ? (
|
<motion.div
|
||||||
<Image
|
initial={{ scale: 1.2, opacity: 0 }}
|
||||||
src={post.cover || config.defaultCover}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
alt={post.title}
|
transition={{
|
||||||
fill
|
duration: config.animationDurationSecond,
|
||||||
className="object-cover w-full h-full transition-transform duration-300 group-hover:scale-105"
|
ease: deceleration,
|
||||||
sizes="(max-width: 768px) 100vw, 33vw"
|
}}
|
||||||
priority={false}
|
className="absolute inset-0 w-full h-full"
|
||||||
/>
|
>
|
||||||
) : (
|
{(post.cover || config.defaultCover) ? (
|
||||||
// 默认渐变背景 - 基于热度生成颜色
|
<Image
|
||||||
<div
|
src={post.cover || config.defaultCover}
|
||||||
className={cn(
|
alt={post.title}
|
||||||
'w-full h-full bg-gradient-to-br',
|
fill
|
||||||
post.heat > 80
|
className="object-cover w-full h-full transition-transform duration-300 group-hover:scale-105"
|
||||||
? 'from-red-400 via-pink-500 to-orange-500'
|
sizes="(max-width: 768px) 100vw, 33vw"
|
||||||
: post.heat > 60
|
priority={false}
|
||||||
? 'from-orange-400 via-yellow-500 to-red-500'
|
/>
|
||||||
: post.heat > 40
|
) : (
|
||||||
? 'from-blue-400 via-purple-500 to-pink-500'
|
// 默认渐变背景 - 基于热度生成颜色
|
||||||
: post.heat > 20
|
<div
|
||||||
? 'from-green-400 via-blue-500 to-purple-500'
|
className={cn(
|
||||||
: 'from-gray-400 via-slate-500 to-gray-600',
|
'w-full h-full bg-gradient-to-br',
|
||||||
)}
|
post.heat > 80
|
||||||
/>
|
? 'from-red-400 via-pink-500 to-orange-500'
|
||||||
)}
|
: post.heat > 60
|
||||||
|
? 'from-orange-400 via-yellow-500 to-red-500'
|
||||||
|
: post.heat > 40
|
||||||
|
? 'from-blue-400 via-purple-500 to-pink-500'
|
||||||
|
: post.heat > 20
|
||||||
|
? 'from-green-400 via-blue-500 to-purple-500'
|
||||||
|
: 'from-gray-400 via-slate-500 to-gray-600',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
|
||||||
{/* 覆盖层 */}
|
{/* 覆盖层 */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
<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>
|
</CardTitle>
|
||||||
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{/* Card Content - 主要内容 */}
|
{/* Card Content - 主要内容 */}
|
||||||
<CardContent className="flex-1">
|
<CardContent className="flex-1">
|
||||||
<CardDescription className="line-clamp-3 leading-relaxed">
|
<CardDescription className="line-clamp-3 leading-relaxed">
|
||||||
|
@ -6,6 +6,8 @@ import { isMobileByUA } from "@/utils/server/device";
|
|||||||
import { calculateReadingTime } from "@/utils/common/post";
|
import { calculateReadingTime } from "@/utils/common/post";
|
||||||
import { CommentSection } from "@/components/comment";
|
import { CommentSection } from "@/components/comment";
|
||||||
import { TargetType } from '@/models/types';
|
import { TargetType } from '@/models/types';
|
||||||
|
import * as motion from "motion/react-client"
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
function PostMeta({ post }: { post: Post }) {
|
function PostMeta({ post }: { post: Post }) {
|
||||||
return (
|
return (
|
||||||
@ -71,25 +73,33 @@ async function PostHeader({ post }: { post: Post }) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
style={{ zIndex: -1 }}
|
style={{ zIndex: -1 }}
|
||||||
/>
|
/>
|
||||||
{(post.labels || post.isOriginal) && (
|
<motion.div
|
||||||
<div className="flex flex-wrap gap-2 mb-4">
|
initial={{ opacity: 0, x: -50 }}
|
||||||
{post.isOriginal && (
|
animate={{ opacity: 1, x: 0 }}
|
||||||
<span className="bg-green-100 text-green-600 text-xs px-2 py-1 rounded">
|
transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}
|
||||||
原创
|
className="container mx-auto px-4"
|
||||||
</span>
|
>
|
||||||
)}
|
{(post.labels || post.isOriginal) && (
|
||||||
{(post.labels || []).map(label => (
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
<span key={label.id} className="bg-blue-100 text-blue-600 text-xs px-2 py-1 rounded">
|
{post.isOriginal && (
|
||||||
{label.key}
|
<span className="bg-green-100 text-green-600 text-xs px-2 py-1 rounded">
|
||||||
</span>
|
原创
|
||||||
))}
|
</span>
|
||||||
|
)}
|
||||||
|
{(post.labels || []).map(label => (
|
||||||
|
<span key={label.id} className="bg-blue-100 text-blue-600 text-xs px-2 py-1 rounded">
|
||||||
|
{label.key}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h1 className="text-5xl font-bold mb-2 text-primary-foreground">{post.title}</h1>
|
||||||
|
{/* 元数据区 */}
|
||||||
|
<div>
|
||||||
|
<PostMeta post={post} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</motion.div>
|
||||||
<h1 className="text-5xl font-bold mb-2 text-primary-foreground">{post.title}</h1>
|
|
||||||
{/* 元数据区 */}
|
|
||||||
<div>
|
|
||||||
<PostMeta post={post} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,79 +6,119 @@ import "highlight.js/styles/github.css"; // 你可以换成喜欢的主题
|
|||||||
import "highlight.js/styles/github-dark.css"; // 适用于暗黑模式
|
import "highlight.js/styles/github-dark.css"; // 适用于暗黑模式
|
||||||
import "highlight.js/styles/github-dark-dimmed.css"; // 适用于暗黑模式
|
import "highlight.js/styles/github-dark-dimmed.css"; // 适用于暗黑模式
|
||||||
import CodeBlock from "@/components/common/markdown-codeblock";
|
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 = {
|
export const markdownComponents = {
|
||||||
h1: (props: React.ComponentPropsWithoutRef<"h1">) => (
|
h1: (props: React.ComponentPropsWithoutRef<"h1">) => (
|
||||||
<h1
|
<MotionDiv>
|
||||||
className="scroll-m-20 text-4xl font-extrabold tracking-tight text-balance mt-10 mb-6"
|
<h1
|
||||||
{...props}
|
className="scroll-m-20 text-4xl font-extrabold tracking-tight text-balance mt-10 mb-6"
|
||||||
/>
|
{...props}
|
||||||
),
|
/>
|
||||||
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}
|
h2: (props: React.ComponentPropsWithoutRef<"h2">) => (
|
||||||
/>
|
<MotionDiv>
|
||||||
),
|
<h2
|
||||||
h3: (props: React.ComponentPropsWithoutRef<"h3">) => (
|
className="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0 mt-8 mb-4"
|
||||||
<h3
|
{...props}
|
||||||
className="scroll-m-20 text-2xl font-semibold tracking-tight mt-6 mb-3"
|
/>
|
||||||
{...props}
|
</MotionDiv>
|
||||||
/>
|
),
|
||||||
),
|
h3: (props: React.ComponentPropsWithoutRef<"h3">) => (
|
||||||
h4: (props: React.ComponentPropsWithoutRef<"h4">) => (
|
<MotionDiv>
|
||||||
<h4
|
<h3
|
||||||
className="scroll-m-20 text-xl font-semibold tracking-tight mt-5 mb-2"
|
className="scroll-m-20 text-2xl font-semibold tracking-tight mt-6 mb-3"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
</MotionDiv>
|
||||||
p: (props: React.ComponentPropsWithoutRef<"p">) => (
|
),
|
||||||
<p
|
h4: (props: React.ComponentPropsWithoutRef<"h4">) => (
|
||||||
className="leading-7 mt-4 mb-4"
|
<MotionDiv>
|
||||||
{...props}
|
<h4
|
||||||
/>
|
className="scroll-m-20 text-xl font-semibold tracking-tight mt-5 mb-2"
|
||||||
),
|
{...props}
|
||||||
blockquote: (props: React.ComponentPropsWithoutRef<"blockquote">) => (
|
/></MotionDiv>
|
||||||
<blockquote
|
),
|
||||||
className="border-l-4 border-blue-400 pl-4 italic my-6 py-2"
|
p: (props: React.ComponentPropsWithoutRef<"p">) => (
|
||||||
{...props}
|
<MotionDiv>
|
||||||
/>
|
<div className="leading-7 mt-4 mb-4">{props.children}</div>
|
||||||
),
|
</MotionDiv>
|
||||||
code: (props: React.ComponentPropsWithoutRef<"code">) => (
|
),
|
||||||
<code
|
blockquote: (props: React.ComponentPropsWithoutRef<"blockquote">) => (
|
||||||
className="bg-gray-100 dark:bg-gray-800 rounded px-1 py-0.5 text-sm font-mono"
|
<MotionDiv>
|
||||||
{...props}
|
<blockquote
|
||||||
/>
|
className="border-l-4 border-blue-400 pl-4 italic my-6 py-2"
|
||||||
),
|
{...props}
|
||||||
pre: ({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) => (
|
/>
|
||||||
<CodeBlock {...props}>{children}</CodeBlock>
|
</MotionDiv>
|
||||||
),
|
),
|
||||||
a: (props: React.ComponentPropsWithoutRef<"a">) => (
|
code: (props: React.ComponentPropsWithoutRef<"code">) => (
|
||||||
<a
|
<code
|
||||||
className="text-blue-600 hover:underline"
|
className="bg-gray-100 dark:bg-gray-800 rounded px-1 py-0.5 text-sm font-mono"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
pre: ({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) => (
|
||||||
|
<MotionDiv><CodeBlock {...props}>{children}</CodeBlock></MotionDiv>
|
||||||
|
),
|
||||||
|
a: (props: React.ComponentPropsWithoutRef<"a">) => (
|
||||||
|
<a
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RenderMarkdown(props: Omit<MDXRemoteProps, "components">) {
|
export function RenderMarkdown(props: Omit<MDXRemoteProps, "components">) {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>加载中...</div>}>
|
<Suspense fallback={<MarkdownSkeleton />}>
|
||||||
<MDXRemote {...props}
|
<MDXRemote {...props}
|
||||||
components={markdownComponents}
|
components={markdownComponents}
|
||||||
options={{
|
options={{
|
||||||
mdxOptions: {
|
mdxOptions: {
|
||||||
rehypePlugins: [[rehypeHighlight, { ignoreMissing: true }]],
|
rehypePlugins: [[rehypeHighlight, { ignoreMissing: true }]],
|
||||||
}
|
}
|
||||||
}} />
|
}} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenderMarkdownWithComponents(props: MDXRemoteProps) {
|
export function RenderMarkdownWithComponents(props: MDXRemoteProps) {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>加载中...</div>}>
|
<Suspense fallback={<MarkdownSkeleton />}>
|
||||||
<MDXRemote {...props} components={markdownComponents} />
|
<MDXRemote {...props} components={markdownComponents} />
|
||||||
</Suspense>
|
</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>
|
||||||
|
);
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ const config = {
|
|||||||
bodyWidthMobile: "100vw",
|
bodyWidthMobile: "100vw",
|
||||||
postsPerPage: 12,
|
postsPerPage: 12,
|
||||||
commentsPerPage: 8,
|
commentsPerPage: 8,
|
||||||
animationDurationSecond: 0.5,
|
animationDurationSecond: 0.618,
|
||||||
footer: {
|
footer: {
|
||||||
text: "Liteyuki ICP备 1145141919810",
|
text: "Liteyuki ICP备 1145141919810",
|
||||||
links: []
|
links: []
|
||||||
|
7
web/src/motion/curve.ts
Normal file
7
web/src/motion/curve.ts
Normal 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);
|
Reference in New Issue
Block a user