【山竹记账前端-react】17.Grid + Flex 布局实战


大纲链接 §

[toc]


1. 记账页面 ⇧

记账页面

  • 包括收入、支出tab切换
  • tab内容中的标签列表,添加标签按钮
  • 输入日期、金额显示,数字按钮键盘

2. Tabs组件与泛型组件 ⇧

添加路由,新建页面组件 ⇧

@/route/routes.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { Root } from '@/components/Root'
import { Home } from '@/pages/Home'
import { ItemCreatePage } from '@/pages/ItemCreatePage';
import { ItemsPage } from '@/pages/ItemsPage'
import { SignInPage } from '@/pages/SignInPage'
import { welcomeRouter } from '@/route/welcomeRouter'
import type { RouteObject } from 'react-router-dom'

export const routes: RouteObject[] = [
  { path: '/', element: <Root />, },
  { path: '/home', element: <Home title="首页" />, },
  welcomeRouter,
  { path: '/items', element: <ItemsPage />, },
  { path: '/items/create', element: <ItemCreatePage />, },
  { path: '/sign_in', element: <SignInPage />},
  { path: '/statistics', element: <div>统计图表</div>},
  { path: '/export', element: <div>敬请期待</div>},
  { path: '/notify', element: <div>敬请期待</div>},
  { path: '/tags', element: <div>标签</div>},
  { path: '/tech_stack', element: <div>敬请期待</div>},
]

@/pages/ItemCreatePage.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { GradientBanner } from "@/components/GradientBanner";
import { SvgIcon } from "@/components/SvgIcon";
import { TopNav } from "@/components/TopNav";
import { cn } from "cn";

interface Props {
  className?: string
}

export const ItemCreatePage: React.FC<Props> = ({ /* className, */ }) => {
  const Icon = (<SvgIcon name="back-arrow" className="size-[24px]" />)

  return (
    <div className={cn('container w-screen h-screen relative')}>
      <GradientBanner>
        <TopNav icon={Icon} title="记一笔" />
      </GradientBanner>
    </div>
  )
}

封装 SwitchTabs 泛型组件 ⇧

@/components/SwitchTabs.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { cn } from "cn";

interface Props<T> {
  className?: string
  selectedTabKey: T
  tabItems: Readonly<Array<{ key: T; text: string }>>
  onSwitchTab: (key: T) => void
}

export const SwitchTabs = <T extends string>({ className, tabItems, selectedTabKey, onSwitchTab, }: Props<T>) => {
  const selectedClass = (key: string) => cn(
    'relative',
    key === selectedTabKey
      ? 'after:(content-[""] block absolute bottom-0 left-0 w-full h-4px bg-[var(--tabs-indicator-bg)])'
      : ''
  )

  return (
    <nav className={className}>
      <menu className={cn(
        'flex text-white',
        'children:(px-24px py-12px cursor-pointer)',
      )}>
        {
          tabItems.map(item => (
            <li
              key={item.key}
              onClick={() => onSwitchTab(item.key)}
              className={selectedClass(item.key)}>
              {item.text}
            </li>
          ))
        }
      </menu>
    </nav>
  )
}
  • 去除函数组建声明 : React.FC<Props>
  • 参数前声明泛型 <T extends string>
    • 泛型参数 T 用于限制 tabItems 中的 key 类型为 string
  • 参数后声明类型 { className, tabItems, selectedTabKey, onSwitchTab, }: Props<T>)
  • 声明外部参数 interface Props<T> 传类型参数 T
    • 声明 selectedTabKey: T
    • 声明 tabItems: Readonly<Array<{ key: T; text: string }>>
    • 声明 onSwitchTab: (key: T) => void

重构 @/components/DateRangePicker.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { SwitchTabs } from "@/components/SwitchTabs";

export type DateTabRangeStr = 'thisMonth' | 'lastMonth' | 'thisYear' | 'customDate'

interface Props {
  selectedId: DateTabRangeStr
  onSelect: (key: DateTabRangeStr) => void
}

type DateRange = {
  key: DateTabRangeStr
  text: string
}

const dateRanges: Readonly<DateRange[]> = [
  { key: 'thisMonth', text: '本月', },
  { key: 'lastMonth', text: '上月', },
  { key: 'thisYear', text: '今年', },
  { key: 'customDate', text: '去自定义日期', },
]

export const DateRangePicker: React.FC<Props> = ({
  selectedId = 'thisMonth',
  onSelect: onSelected,
}) => {
  return (
    <SwitchTabs
      selectedTabKey={selectedId}
      tabItems={dateRanges}
      onSwitchTab={onSelected} />
  )
}

重构 @/pages/ItemsPage.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// import ...

export const ItemsPage: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState<DateTabRangeStr>('thisMonth')
  const { visible, setVisible, } = useMenuStore()
  const onClickMenu = () => {
    setVisible(!visible)
  }
  const Icon = (<SvgIcon name="menu" className="size-[24px]" onClick={onClickMenu} />)

  return (
    <div className={cn('container w-screen h-screen relative')}>
      <GradientBanner>
        <TopNav icon={Icon} title="山竹记账" />
        <DateRangePicker selectedId={selectedDate} onSelect={setSelectedDate} />
      </GradientBanner>

      <main className="max-h-[calc(100svh-112px)]">
        <div className="relative max-h-[calc(100svh-112px)] flex flex-col">
          <ItemsSummary startDate={selectedDate} endDate={selectedDate} />

          <ItemsList startDate={selectedDate} endDate={selectedDate} />
        </div>

        <AddItemFloatButton />

        <FloatingOverlayMenu />
      </main>
    </div>
  )
}

3. 完成顶部导航栏 ⇧


4. Tabs内容与classPrefix ⇧


5. Grid布局展示所有标签 ⇧


6. 完善样式细节 ⇧


7. 这个布局写得好 ⇧


8. 使用 vh-check 解决全屏 bug ⇧


9. 消除 key 警告 ⇧


·未完待续·

参考文章

相关文章

  • 无

  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名