50 天计划第 1 天

今天是 50 天计划的第 1 天。今天的目标很简单,就是搭建环境,然后完成第 1 d个项目 Expanding Cards。

准备工作

这里使用 Revitesse-Lite 作为启动模板。Revitesse 基本集成了 React + Vite 的一些最佳实践,免去了配置环节。

$ npx degit flower-f/revitesse-lite 50days
$ cd 50days
$ git init
$ pnpm i
$ pnpm dev

因为 Revitesse 使用了动态路由,所以只需要修改目录结构就能自动生成路由。在 pages 目录下新建 day 目录,day 目录下再新建文件夹 1,并添加 index.tsx 文件。此时页面结构如下:

pages
  |-- index.tsx
  |-- day
      |-- 1
          |-- index.tsx

pages/day/1/index.tsx 中写入以下内容:

const ExpandingCardsPage = () => {
  return (
    <div>hello world</div>
  )
}

export default ExpandingCardsPage

此时访问路由 http://127.0.0.1:5173/day/1,可以看到一个 hello world。

项目开发

可以先看一下页面效果,对大体内容有个基本了解。

我们先完成一下页面的基本布局,借助 UnoCSS,我们可以很方便地完成布局。这里把 cardList 配置单独抽出来,是为了便于后续维护。

import img1 from '~/assets/1/1.png'
import img2 from '~/assets/1/2.png'
import img3 from '~/assets/1/3.png'
import img4 from '~/assets/1/4.png'
import img5 from '~/assets/1/5.png'

interface ItemType {
  title: string
  source: string
}

const originalCardList: ItemType[] = [
  {
    title: 'Explore The World',
    source: img1,
  },
  {
    title: 'Wild Forest',
    source: img2,
  },
  {
    title: 'Sunny Beach',
    source: img3,
  },
  {
    title: 'City on Winter',
    source: img4,
  },
  {
    title: 'Mountains - Clouds',
    source: img5,
  },
]

const ExpandingCardsPage = () => {
  return (
    <div flex items-center justify-center>
      <ul flex items-center className="w-90%">
        {
          originalCardList.map((cardItem, index) => (
            <li mx-2 relative key={index}>
              <button>
                <img src={cardItem.source} alt={cardItem.title} h-80vh object-cover rounded-50px />
                <h3 absolute left-20px bottom-40px text="xl left white" font-extrabold>
                  {cardItem.title}
                </h3>
              </button>
            </li>
          ))
        }
      </ul>
    </div>
  )
}

export default ExpandingCardsPage

现在布局效果已经基本出来了。我们首先做一件事情,把列表渲染的 item 部分抽出来。

const CardItem = ({ cardItem }: { cardItem: ItemType } & Partial<JSX.IntrinsicElements['button']>) => {
  return (
    <li relative mx-2>
      <button>
        <img src={cardItem.source} alt={cardItem.title} h-80vh object-cover rounded-50px />
        <h3 absolute left-20px bottom-40px text="xl left white" font-extrabold>
          {cardItem.title}
        </h3>
      </button>
    </li>
  )
}

const ExpandingCardsPage = () => {
  return (
    <div flex items-center justify-center>
      <ul flex items-center className="w-90%">
        {
          originalCardList.map((cardItem, index) => (
            <CardItem cardItem={cardItem} key={index} />
          ))
        }
      </ul>
    </div>
  )
}

下面我们开始梳理逻辑部分。折叠卡的具体表现形式是展开的时候卡片占据的比例不同,要实现这一效果,我们很容易想到通过 flex 布局实现。 只需要给展开的卡片设置 flex-grow 大于 1(我设置的是 5),缩着的卡片设置 flex-grow 小于 1 (我设置的是 0.6)即可。另外还需要设置 flex-basis 为 0。这里采取维护一个状态记录当前的展开项 index 是多少,从而识别当前卡片状态是展开还是收缩。

const CardItem = ({ cardItem, expand, onClick }: { cardItem: ItemType; expand: boolean } & Partial<JSX.IntrinsicElements['button']>) => {
  return (
    <li relative mx-2 basis-0 className={expand ? 'grow-5' : 'grow-0.6'}>
      <button onClick={onClick}>
        <img src={cardItem.source} alt={cardItem.title} h-80vh object-cover rounded-50px />
        <h3 absolute left-20px bottom-40px text="xl left white" font-extrabold>
          {cardItem.title}
        </h3>
      </button>
    </li>
  )
}

const ExpandingCardsPage = () => {
  const [expandIndex, setExpandIndex] = useState(0)

  return (
    <div flex items-center justify-center>
      <ul flex items-center className="w-90%">
        {
          originalCardList.map((cardItem, index) => (
            <CardItem
              cardItem={cardItem}
              key={index}
              expand={expandIndex === index}
              onClick={() => setExpandIndex(index)}
            />
          ))
        }
      </ul>
    </div>
  )
}

因为 UnoCSS 按需打包的特性,记得要在 uno.config.ts 中添加上对应的 safelist 配置,否则可能会得不到想要的效果。

export default defineConfig({
  // ...
  safelist: [
    'grow-0.6',
    'grow-5',
  ],
})

此时基本的效果已经实现,但是还有些细节需要调整,比如说文字部分,当卡片收缩的时候应该隐藏文字,这也很简单。给 h3 标签添加上 className={expand ? 'op100' : 'op0'} 即可,当然 op100 和 op0 也都需要添加到 safelist 中。

最后只需要把一些动画参数添加上去,以及处理一下响应式即可,具体的细节可以参考源码

©2022 Flower-F. All Rights Reserved.