0

我不知道如何<AnimateSharedLayout />通过 framer-motion 使用组件为路由设置动画。基本上在这里的代码中,我想显示一个图像列表,当点击它们时,我想导航到/images/[image_id]并显示它。我在本地没有图像,所以我必须获取它们。这里有一些问题:

  1. 第一次在索引页面上获取相同的元素,然后在详细信息页面上重新获取(如果有办法将它带到详细信息页面会很好)
  2. 更改路线前,页面自动滚动到顶部,破坏动画;
  3. 如果用户在慢速网络上,动画会滞后甚至无法启动;
  4. 返回上一条路线不会重播动画(我可能必须缓存 api 调用以避免重新获取它们?)

这是代码_app.tsx

import '../styles/globals.css';
import 'bulma/css/bulma.css';
import { AppProps } from "next/app";
import { AnimateSharedLayout, AnimatePresence } from 'framer-motion';

const MyApp: React.FC<AppProps> = ({ Component, pageProps, router }) => {
    return (
        <AnimateSharedLayout>
            <AnimatePresence>
                <Component {...pageProps} key={router.route} />
            </AnimatePresence>
        </AnimateSharedLayout>
    );
}

export default MyApp;

这里是pages/index.tsx

import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import Picture, { PictureProps } from "./components/Picture";

export const base = 'https://jsonplaceholder.typicode.com';

const Home: React.FC = () => {
    const [pics, setPics] = useState<PictureProps[]>([]);
    useEffect(() => {
        fetch(`${base}/photos?_start=0&_limit=15`)
            .then(res => res.json())
            .then(res => setPics(res as PictureProps[]))
            .catch(err => console.log("fetching error", err));
    }, []);
    return (
        <div className="container pt-4">
            <div className="columns is-multiline">
                {
                    pics.map(pic => (
                        <div key={pic.id} className="column is-one-third-desktop is-half-tablet is-full-mobile">
                            <Picture {...pic} />
                        </div>
                    ))
                }
            </div>
        </div>
    );
}

export default Home;

这里是pages/images/[image].tsx

import { motion } from "framer-motion";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { PictureProps } from "../components/Picture";
import { base } from '../index';

const Image: React.FC = () => {
    const router = useRouter();
    const { image } = router.query;
    const [pic, setPic] = useState<PictureProps>({} as PictureProps);
    useEffect(() => {
        image && fetch(`${base}/photos/${image}`)
            .then(res => res.json())
            .then(res => setPic(res))
            .catch(err => console.log(err));
    }, [image]);
    return (
        <motion.div>   
            {
                image &&
                <motion.figure layoutId={`img-${image}`} className='image'>
                    <img src={pic.url} alt={pic.title} />
                </motion.figure>
            }
        </motion.div>
    );
}

export default Image;

这是components/Picture.tsx

import { motion, Variants } from "framer-motion";
import Link from "next/link";

export type PictureProps = {
    albumId: number,
    id: number,
    title: string,
    url: string,
    thumbnailUrl: string,
};

const cardVariants: Variants = {
    unloaded: {
        y: -100,
        opacity: 0,
        transition: {
            when: "afterChildren",
        },
    },
    loaded: {
        y: 0,
        opacity: 1,
        transition: {
            when: "beforeChildren",
        },
    }
};

const textVariants: Variants = {
    unloaded: {
        scale: .7,
        opacity: 0,
    },
    loaded: {
        scale: 1,
        opacity: 1,
    },
};

const Picture: React.FC<PictureProps> = ({
    id, title, url,
    albumId, thumbnailUrl
}) => {
    console.log(`img-${id}`);
    return (
        <motion.div className='card' variants={cardVariants} initial="unloaded" animate="loaded" exit="exit">
            <div className="card-image">
                <motion.figure 
                    className="image"
                    layoutId={`img-${id}`}
                >
                    <img src={url} alt={title} />
                </motion.figure>
            </div>
            <motion.div className="card-content">
                <div className="media">
                    <div className="media-left">
                        <Link href={`images/${id}`}>
                            <a>
                                <figure className="image is-50by50">
                                    <img src={thumbnailUrl} alt="thumbnail" />
                                </figure>
                            </a>
                        </Link>
                    </div>
                    <motion.div variants={textVariants} className="media-content">
                        <p className="title is-6">{title}</p>
                    </motion.div>
                </div>
                <motion.div variants={textVariants} className='content'>
                    Some random text Some random text 
                    Some random text Some random text
                    <br />
                    <strong>Album id</strong>:&nbsp;<em>{albumId}</em>
                </motion.div>
            </motion.div>
        </motion.div>
    );
}

export default Picture;

4

1 回答 1

4

想通了,这是新代码

pages/_app.tsx

import '../styles/globals.css';
import 'bulma/css/bulma.css';
import { AppProps } from "next/app";
import { AnimateSharedLayout, AnimatePresence } from 'framer-motion';

const MyApp: React.FC<AppProps> = ({ Component, pageProps, router }) => {
    return (
        <AnimateSharedLayout type='crossfade'>
            <Component {...pageProps} />
        </AnimateSharedLayout>
    );
}

export default MyApp;

pages/index.tsx

import Picture, { PictureProps } from "../components/Picture";
import { NextPage, GetStaticProps } from "next"

export const base = 'https://jsonplaceholder.typicode.com';

type IndexProps = {
    pictures: PictureProps[],
}

const Home: NextPage<IndexProps> = ({ pictures }) => {
    return (
        <div className="container pt-4">
            <div className="columns is-multiline">
                {
                    pictures.map(pic => (
                        <div key={pic.id} className="column is-one-third-desktop is-half-tablet is-full-mobile">
                            <Picture {...pic} />
                        </div>
                    ))
                }
            </div>
        </div>
    );
}

export const getStaticProps: GetStaticProps = async () => {
    const pics = await fetch(`${base}/photos?_start=0&_limit=15`)
        .then(res => res.json())
        .then(res => res)
        .catch(err => console.log("fetching error", err));
    return {
        props: {
            pictures: pics,
        },
    };
}

export default Home;

pages/images/[image].tsx

import { motion } from "framer-motion";
import { PictureProps } from "../../components/Picture";
import { base } from '../index';
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
import { useRouter } from 'next/router';

type ImagePageProps = {
    image: PictureProps,
}

const Image: NextPage<ImagePageProps> = ({ image }) => {
    const { isFallback } = useRouter();
    return isFallback ? <div>loading...</div> 
        : (
            <div>   
                {
                    image &&
                    <motion.div layoutId={`img-${image.id}`} style={{
                        backgroundImage: `url('${image.url}')`, backgroundPosition: 'center',
                        backgroundRepeat: 'no-repeat', backgroundSize: 'cover',
                        height: '70vh', position: 'relative', top: 0, left: 0, width: '100%',
                    }}>
                    </motion.div>
                }
            </div>
        );
}

export const getStaticProps: GetStaticProps = async ctx => {
    const { image } = ctx.params;
    const pic = await fetch(`${base}/photos/${image}`)
        .then(res => res.json())
        .then(res => res)
        .catch(err => console.log(err));
    return {
        props: {
            image: pic,
        },
    };
}

export const getStaticPaths: GetStaticPaths = async () => {
    const ids = await fetch(`${base}/photos?_start=0&_limit=15`)
        .then(res => res.json())
        .then(res => res.map(el => el.id.toString()))
        .catch(err => console.log(err));
    return {
        paths: ids.map((id: string) => ({
            params: {
                image: id,
            },
        })),
        fallback: true,
    };
};

export default Image;

components/Picture.tsx

import { motion, Variants } from "framer-motion";
import Link from "next/link";

export type PictureProps = {
    albumId: number,
    id: number,
    title: string,
    url: string,
    thumbnailUrl: string,
};

const cardVariants: Variants = {
    initial: {
        scale: 0.9,
        transition: {
            when: 'afterChildren',
        },
    },
    enter: {
        scale: 1,
        transition: {
            when: 'beforeChildren',
            duration: .3,
        },
    },
};

const textVariants: Variants = {
    initial: {
        opacity: 0,
    },
    enter: {
        opacity: 1,
    },
};

const Picture: React.FC<PictureProps> = ({
    id, title, url,
    albumId, thumbnailUrl
}) => {
    return (
        <motion.div className='card' variants={cardVariants} initial="initial" animate="enter">
            <div className="card-image">
                <motion.figure 
                    className="image"
                    layoutId={`img-${id}`}
                >
                    <img src={url} alt={title} />
                </motion.figure>
            </div>
            <motion.div variants={textVariants} className="card-content">
                <div className="media">
                    <div className="media-left">
                        <Link href={`/images/${id}`}>
                            <a>
                                <figure className="image is-50by50">
                                    <img src={thumbnailUrl} alt="thumbnail" />
                                </figure>
                            </a>
                        </Link>
                    </div>
                    <motion.div variants={textVariants} className="media-content">
                        <p className="title is-6">{title}</p>
                    </motion.div>
                </div>
                <motion.div variants={textVariants} className='content'>
                    Some random text Some random text 
                    Some random text Some random text
                    <br />
                    <strong>Album id</strong>:&nbsp;<em>{albumId}</em>
                </motion.div>
            </motion.div>
        </motion.div>
    );
}

export default Picture;

于 2020-12-01T22:44:37.323 回答