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) ? ( - {post.title} - ) : ( - // 默认渐变背景 - 基于热度生成颜色 -
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) ? ( + {post.title} + ) : ( + // 默认渐变背景 - 基于热度生成颜色 +
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);