diff --git a/web/package.json b/web/package.json
index 24457c7..6f99963 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 290d6a0..77dfef8 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -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: {}
diff --git a/web/src/components/blog-home/blog-home-card.tsx b/web/src/components/blog-home/blog-home-card.tsx
index a1da558..dc687ef 100644
--- a/web/src/components/blog-home/blog-home-card.tsx
+++ b/web/src/components/blog-home/blog-home-card.tsx
@@ -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,34 +31,47 @@ export function BlogCard({ post, className }: BlogCardProps) {
)}
>
{/* 封面图片区域 */}
-
+
{/* 自定义封面图片 */}
- {(post.cover || config.defaultCover) ? (
-
- ) : (
- // 默认渐变背景 - 基于热度生成颜色
-
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',
- )}
- />
- )}
+
+ {(post.cover || config.defaultCover) ? (
+
+ ) : (
+ // 默认渐变背景 - 基于热度生成颜色
+ 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',
+ )}
+ />
+ )}
+
+
{/* 覆盖层 */}
@@ -117,6 +132,7 @@ export function BlogCard({ post, className }: BlogCardProps) {
+
{/* Card Content - 主要内容 */}
diff --git a/web/src/components/blog-post/blog-post.tsx b/web/src/components/blog-post/blog-post.tsx
index 3d220d6..a95ee80 100644
--- a/web/src/components/blog-post/blog-post.tsx
+++ b/web/src/components/blog-post/blog-post.tsx
@@ -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,25 +73,33 @@ async function PostHeader({ post }: { post: Post }) {
aria-hidden="true"
style={{ zIndex: -1 }}
/>
- {(post.labels || post.isOriginal) && (
-
- {post.isOriginal && (
-
- 原创
-
- )}
- {(post.labels || []).map(label => (
-
- {label.key}
-
- ))}
+
+ {(post.labels || post.isOriginal) && (
+
+ {post.isOriginal && (
+
+ 原创
+
+ )}
+ {(post.labels || []).map(label => (
+
+ {label.key}
+
+ ))}
+
+ )}
+ {post.title}
+ {/* 元数据区 */}
+
- )}
- {post.title}
- {/* 元数据区 */}
-
+
+
);
}
diff --git a/web/src/components/common/markdown.tsx b/web/src/components/common/markdown.tsx
index f1a573c..d671260 100644
--- a/web/src/components/common/markdown.tsx
+++ b/web/src/components/common/markdown.tsx
@@ -6,79 +6,119 @@ 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 (
+ {props.children}
+
+ );
+}
export const markdownComponents = {
- h1: (props: React.ComponentPropsWithoutRef<"h1">) => (
-
- ),
- h2: (props: React.ComponentPropsWithoutRef<"h2">) => (
-
- ),
- h3: (props: React.ComponentPropsWithoutRef<"h3">) => (
-
- ),
- h4: (props: React.ComponentPropsWithoutRef<"h4">) => (
-
- ),
- p: (props: React.ComponentPropsWithoutRef<"p">) => (
-
- ),
- blockquote: (props: React.ComponentPropsWithoutRef<"blockquote">) => (
-
- ),
- code: (props: React.ComponentPropsWithoutRef<"code">) => (
-
- ),
- pre: ({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) => (
- {children}
- ),
- a: (props: React.ComponentPropsWithoutRef<"a">) => (
-
- ),
+ h1: (props: React.ComponentPropsWithoutRef<"h1">) => (
+
+
+
+
+ ),
+ h2: (props: React.ComponentPropsWithoutRef<"h2">) => (
+
+
+
+ ),
+ h3: (props: React.ComponentPropsWithoutRef<"h3">) => (
+
+
+
+ ),
+ h4: (props: React.ComponentPropsWithoutRef<"h4">) => (
+
+
+ ),
+ p: (props: React.ComponentPropsWithoutRef<"p">) => (
+
+ {props.children}
+
+ ),
+ blockquote: (props: React.ComponentPropsWithoutRef<"blockquote">) => (
+
+
+
+ ),
+ code: (props: React.ComponentPropsWithoutRef<"code">) => (
+
+ ),
+ pre: ({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) => (
+ {children}
+ ),
+ a: (props: React.ComponentPropsWithoutRef<"a">) => (
+
+ ),
};
export function RenderMarkdown(props: Omit) {
- return (
- 加载中... }>
-
-
- );
+ return (
+ }>
+
+
+ );
}
export function RenderMarkdownWithComponents(props: MDXRemoteProps) {
- return (
- 加载中...}>
-
-
- );
+ return (
+
}>
+
+
+ );
+}
+
+export function MarkdownSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
}
\ No newline at end of file
diff --git a/web/src/config.ts b/web/src/config.ts
index b2babdd..1627c7e 100644
--- a/web/src/config.ts
+++ b/web/src/config.ts
@@ -16,7 +16,7 @@ const config = {
bodyWidthMobile: "100vw",
postsPerPage: 12,
commentsPerPage: 8,
- animationDurationSecond: 0.5,
+ animationDurationSecond: 0.618,
footer: {
text: "Liteyuki ICP备 1145141919810",
links: []
diff --git a/web/src/motion/curve.ts b/web/src/motion/curve.ts
new file mode 100644
index 0000000..caf5276
--- /dev/null
+++ b/web/src/motion/curve.ts
@@ -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);