feat: 添加移动端检测功能,优化组件动画效果
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 9s

This commit is contained in:
2025-09-11 00:03:03 +08:00
parent 29dc8bf27a
commit b3e8a5ef77
4 changed files with 29 additions and 40 deletions

View File

@ -15,6 +15,7 @@ import { listLabels } from "@/api/label";
import { POST_SORT_TYPE } from "@/localstore"; import { POST_SORT_TYPE } from "@/localstore";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useDevice } from "@/hooks/use-device"; import { useDevice } from "@/hooks/use-device";
import { checkIsMobile } from "@/utils/client/device";
// 定义排序类型 // 定义排序类型
type SortType = 'latest' | 'popular'; type SortType = 'latest' | 'popular';
@ -23,7 +24,6 @@ export default function BlogHome() {
const [labels, setLabels] = useState<Label[]>([]); const [labels, setLabels] = useState<Label[]>([]);
const [posts, setPosts] = useState<Post[]>([]); const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { isMobile } = useDevice();
const [sortType, setSortType, sortTypeLoaded] = useStoredState<SortType>(POST_SORT_TYPE, 'latest'); const [sortType, setSortType, sortTypeLoaded] = useStoredState<SortType>(POST_SORT_TYPE, 'latest');
useEffect(() => { useEffect(() => {
if (!sortTypeLoaded) return; if (!sortTypeLoaded) return;
@ -90,7 +90,7 @@ export default function BlogHome() {
{/* 主要内容区域 */} {/* 主要内容区域 */}
<motion.div <motion.div
className="lg:col-span-3 self-start" className="lg:col-span-3 self-start"
initial={{ y: isMobile ? 30 : 60, opacity: 0 }} initial={{ y: checkIsMobile() ? 30 : 60, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}> transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}>
{/* 文章列表标题 */} {/* 文章列表标题 */}
@ -155,9 +155,9 @@ export default function BlogHome() {
{/* 侧边栏 */} {/* 侧边栏 */}
<motion.div <motion.div
initial={isMobile ? { y: 30, opacity: 0 } : { x: 80, opacity: 0 }} initial={checkIsMobile() ? { y: 30, opacity: 0 } : { x: 80, opacity: 0 }}
animate={{ x: 0, y: 0, opacity: 1 }} animate={{ x: 0, y: 0, opacity: 1 }}
transition={{ duration: config.animationDurationSecond , ease: "easeOut" }} transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}
> >
<Sidebar <Sidebar
cards={[ cards={[

View File

@ -73,11 +73,8 @@ async function PostHeader({ post }: { post: Post }) {
aria-hidden="true" aria-hidden="true"
style={{ zIndex: -1 }} style={{ zIndex: -1 }}
/> />
<motion.div <div
initial={{ opacity: 0, x: -50 }} className={`container mx-auto px-4`}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}
className="container mx-auto px-4"
> >
{(post.labels || post.isOriginal) && ( {(post.labels || post.isOriginal) && (
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
@ -98,7 +95,7 @@ async function PostHeader({ post }: { post: Post }) {
<div> <div>
<PostMeta post={post} /> <PostMeta post={post} />
</div> </div>
</motion.div> </div>
</div> </div>
); );
@ -145,10 +142,23 @@ async function PostContent({ post }: { post: Post }) {
async function BlogPost({ post }: { post: Post }) { async function BlogPost({ post }: { post: Post }) {
return ( return (
<div className="h-full"> <div className="h-full"
>
{/* <ScrollToTop /> */} {/* <ScrollToTop /> */}
<motion.div
initial={{ opacity: 0, y: -30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}>
<PostHeader post={post} /> <PostHeader post={post} />
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: config.animationDurationSecond, ease: "easeOut" }}>
<PostContent post={post} /> <PostContent post={post} />
</motion.div>
<CommentSection targetType={TargetType.Post} targetId={post.id} totalCount={post.commentCount} /> <CommentSection targetType={TargetType.Post} targetId={post.id} totalCount={post.commentCount} />
</div> </div>
); );

View File

@ -6,67 +6,43 @@ 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"; 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">) => (
<MotionDiv>
<h1 <h1
className="scroll-m-20 text-4xl font-extrabold tracking-tight text-balance mt-10 mb-6" className="scroll-m-20 text-4xl font-extrabold tracking-tight text-balance mt-10 mb-6"
{...props} {...props}
/> />
</MotionDiv>
), ),
h2: (props: React.ComponentPropsWithoutRef<"h2">) => ( h2: (props: React.ComponentPropsWithoutRef<"h2">) => (
<MotionDiv>
<h2 <h2
className="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0 mt-8 mb-4" className="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0 mt-8 mb-4"
{...props} {...props}
/> />
</MotionDiv>
), ),
h3: (props: React.ComponentPropsWithoutRef<"h3">) => ( h3: (props: React.ComponentPropsWithoutRef<"h3">) => (
<MotionDiv>
<h3 <h3
className="scroll-m-20 text-2xl font-semibold tracking-tight mt-6 mb-3" className="scroll-m-20 text-2xl font-semibold tracking-tight mt-6 mb-3"
{...props} {...props}
/> />
</MotionDiv>
), ),
h4: (props: React.ComponentPropsWithoutRef<"h4">) => ( h4: (props: React.ComponentPropsWithoutRef<"h4">) => (
<MotionDiv>
<h4 <h4
className="scroll-m-20 text-xl font-semibold tracking-tight mt-5 mb-2" className="scroll-m-20 text-xl font-semibold tracking-tight mt-5 mb-2"
{...props} {...props}
/></MotionDiv> />
), ),
p: (props: React.ComponentPropsWithoutRef<"p">) => ( p: (props: React.ComponentPropsWithoutRef<"p">) => (
<MotionDiv>
<div className="leading-7 mt-4 mb-4">{props.children}</div> <div className="leading-7 mt-4 mb-4">{props.children}</div>
</MotionDiv>
), ),
blockquote: (props: React.ComponentPropsWithoutRef<"blockquote">) => ( blockquote: (props: React.ComponentPropsWithoutRef<"blockquote">) => (
<MotionDiv>
<blockquote <blockquote
className="border-l-4 border-blue-400 pl-4 italic my-6 py-2" className="border-l-4 border-blue-400 pl-4 italic my-6 py-2"
{...props} {...props}
/> />
</MotionDiv>
), ),
code: (props: React.ComponentPropsWithoutRef<"code">) => ( code: (props: React.ComponentPropsWithoutRef<"code">) => (
<code <code
@ -75,7 +51,7 @@ export const markdownComponents = {
/> />
), ),
pre: ({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) => ( pre: ({ children, ...props }: React.ComponentPropsWithoutRef<"pre">) => (
<MotionDiv><CodeBlock {...props}>{children}</CodeBlock></MotionDiv> <CodeBlock {...props}>{children}</CodeBlock>
), ),
a: (props: React.ComponentPropsWithoutRef<"a">) => ( a: (props: React.ComponentPropsWithoutRef<"a">) => (
<a <a

View File

@ -0,0 +1,3 @@
export function checkIsMobile() {
return typeof window !== "undefined" && window.innerWidth <= 768;
}