【山竹记账前端-vue】8.业务与轮子的关系


大纲链接 §

[toc]


1. 业务与轮子 ⇧

业务开发

  • 完成余下的页面
  • 在业务中寻找轮子来造
  • 学会造轮子,用造轮子的思维看懂业界源码
  • 可以基于业界轮子做二次封装满足公司要求

造轮子

造轮子2

  • <Button />
  • <FloatButton />
  • <Layout />
  • <Navbar />
  • <Overlay />
  • <Tabs />
  • <EmojiSelect />
  • <Form />

编程的阶段

  • 新手学习怎么用轮子(消费者)
  • 老手学习怎么造轮子(生产者)

造轮子的好处

什么样的轮子值得使用

  • 至少一个贡献者持续三年以上的

2. 创建 Button 组件 ⇧

@/components/BarButton.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineComponent } from 'vue'
import { cn } from 'cn'

interface BarButtonProps {onClick?: (e: MouseEvent) => void}

export const BarButton = defineComponent<BarButtonProps>({
  name: 'BarButton',
  components: {},
  setup(_props, _ctx) {
    return () => (
      <button
        type="button"
        class={cn(
          'bg-[var(--button-bg)] b-1 b-solid b-[var(--button-bg)] rounded-[var(--button-radius)]',
          'text-[var(--button-text-color)] font-size-[var(--button-text-size)]',
          'w-[100%] h-[var(--button-height)] p-x-1em p-y-[0.5em]',
        )}>
        {ctx.slots.default?.()}
      </button>
    )
  },
})
  • 除了 vue 自动绑定的 class 和 style 属性,需要额外声明组件外部参数的类型 XxxProps

在页面中引用,先创建开始记账路由 @/router/routes.ts

 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
import type { RouteRecordRaw } from 'vue-router'
import { WelcomeView } from '@/views/WelcomeView.tsx'
import { Welcome1stPage } from '@/modules/welcome/Welcome1stPage.tsx'
import { Welcome2ndPage } from '@/modules/welcome/Welcome2ndPage.tsx'
import { Welcome3rdPage } from '@/modules/welcome/Welcome3rdPage.tsx'
import { Welcome4thPage } from '@/modules/welcome/Welcome4thPage.tsx'
import { StartAccountsView } from '@/views/StartAccountsView.tsx'

export const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/welcome/1',
  },
  {
    path: '/welcome',
    name: 'welcome',
    component: WelcomeView,
    children: [
      // { path: '', name: 'default', component: WelCome1stPage },
      { path: '', name: 'welcomeDefault', redirect: '/welcome/1' },
      { path: '1', name: 'welcomePig', components: { main: Welcome1stPage } },
      { path: '2', name: 'welcomeClock', components: { main: Welcome2ndPage } },
      { path: '3', name: 'welcomeChart', components: { main: Welcome3rdPage } },
      { path: '4', name: 'welcomeCloud', components: { main: Welcome4thPage } },
    ],
  },
  {path: '/start', name: 'StartAccounts', component: StartAccountsView,},
]

页面组件 @/views/StartAccountsView.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
import { defineComponent } from 'vue'
import { BarButton } from '@/components/BarButton.tsx'
import { cn } from 'cn'

export const StartAccountsView = defineComponent({
  name: 'StartAccountsView',
  props: {},
  components: {},
  setup(/*props, ctx*/) {
    const onClick = (e: MouseEvent) => {
      console.log('_______________________')
      console.log("%c 4 --> e: ","color:#f00;", e);
      console.log('_______________________')
    }

    return () => (
      <div class="start-accounts-view">
        <div
          class={cn('p-x-1em')}>
          <BarButton onClick={onClick}>
            测试
          </BarButton>
        </div>
      </div>
    )
  },
})
  • 子组件声明好内部的 Props,父组件使用时可以得到明确的类型提示 onClick: ...

3. 创建 FloatButton 和 SvgIcon 组件 ⇧

复用已有的 SvgIcon 组件,添加Props类型 ⇧

iconfont 给设计师的图标绘制标准

  • 图标绘制
  • 需要设计师按照标准设计图标,尽量使用矢量图 SVG 格式
  • 注意下载时,根据是否需要自定义填充色,选择去除填充色,即 svg 中所有的 fill 属性

@/components/icons/SvgIcon.tsx

  • 已单独声明图标名称枚举类型 SvgIconNames

     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
    38
    39
    40
    41
    
    import { defineComponent, type PropType } from 'vue'
    import { cn } from 'cn'
    import { svgIconNames, type SvgIconNames } from '@/components/icons/svgIconNames.ts'
    
    interface SvgIconProps {
    name: SvgIconNames
    }
    
    /**
    * @Description: 图标组件
    * @Description: 默认使用插件 vite-plugin-svg-icons-ng
    * @Description: 替换使用插件 svgoStore
    * @FilePath: content/post/山竹记账/【山竹记账前端-vue】8.业务与轮子的关系.md
    */
    export const SvgIcon = defineComponent<SvgIconProps>({
    name: 'SvgIcon',
    props: {
    name: {
      type: String as PropType<SvgIconNames>,
      required: true,
      validator: (value: unknown) =>
        typeof value === 'string' && (svgIconNames as readonly string[]).includes(value),
    },
    },
    setup(props, _ctx) {
    // 添加验证
    if (!props.name || !(svgIconNames as readonly string[]).includes(props.name)) {
      console.error(`Invalid icon name: ${props.name}`)
    }
    
    return () => (
      <svg
        class={cn(
          'where:(h-1em w-1em align-[-0.15em] fill-[currentColor] overflow-clip)',
        )}
        aria-hidden="true">
        <use xlinkHref={`#icon-${props.name}`}/>
      </svg>
    )
    },
    })
    

封装 FloatButton 组件 ⇧

  • 添加了一个已去色的图标 add.svg 作为按钮图标

@/components/FloatButton.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
import { defineComponent } from 'vue'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { cn } from 'cn'

interface FloatButtonProps {
  onClick?: (e: MouseEvent) => void,
}

export const FloatButton = defineComponent<FloatButtonProps>({
  name: 'FloatButton',
  setup(/*props, ctx*/) {
    return () => (
      <div class={cn(
        'w-56px h-56px flex justify-center items-center',
        'bg-[var(--float-button-bg)] rounded-[var(--float-button-radius)]',
      )}>
        <SvgIcon
          name="add"
          class={cn(
            'w-2.5em h-2.5em',
            'fill-[var(--float-button-text-color)]',
            'translate-y-[-2px]'
          )}/>
      </div>
    )
  },
})

在 @/views/StartAccountsView.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
import { defineComponent } from 'vue'
import { BarButton } from '@/components/BarButton.tsx'
import { cn } from 'cn'
import { FloatButton } from '@/components/FloatButton.tsx'

export const StartAccountsView = defineComponent({
  name: 'StartAccountsView',
  setup(/*props, ctx*/) {
    const onClick = (e: MouseEvent) => {
      console.log('_______________________')
      console.log("%c 4 --> e: ","color:#f00;", e);
      console.log('_______________________')
    }

    return () => (
      <div class="start-accounts-view w-full h-full position-relative">
        <div
          class={cn('p-x-1em')}>
          <BarButton onClick={onClick}>
            测试
          </BarButton>

          <FloatButton class="fixed bottom-1em right-1em" />
        </div>
      </div>
    )
  },
})

完成 StartAccountsView 页面组件 ⇧

支持 FloatButton.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
import { defineComponent, type PropType } from 'vue'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { cn } from 'cn'
import type { SvgIconNames } from '@/components/icons/SvgIcon.tsx'

interface FloatButtonProps {
  onClick?: (e: MouseEvent) => void,
  iconName: SvgIconNames
}

export const FloatButton = defineComponent<FloatButtonProps>({
  name: 'FloatButton',
  props: {
    iconName: {
      type: String as PropType<SvgIconNames>,
      required: true,
    },
  },
  setup(props, _ctx) {
    return () => (
      <div class={cn(
        'w-56px h-56px flex justify-center items-center',
        'bg-[var(--float-button-bg)] rounded-[var(--float-button-radius)]',
      )}>
        <SvgIcon
          name={props.iconName}
          class={cn(
            'w-2.5em h-2.5em',
            'fill-[var(--float-button-text-color)]',
            'translate-y-[-2px]'
          )}/>
      </div>
    )
  },
})

@/views/StartAccountsView.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
import { defineComponent } from 'vue'
import { BarButton } from '@/components/BarButton.tsx'
import { cn } from 'cn'
import { FloatButton } from '@/components/FloatButton.tsx'

export const StartAccountsView = defineComponent({
  name: 'StartAccountsView',
  setup(/*props, ctx*/) {
    const onClick = (e: MouseEvent) => {}
    return () => (
      <div class="start-accounts-view w-full h-full position-relative">
        <div
          class={cn('p-x-1em')}>
          <BarButton onClick={onClick}>
            测试
          </BarButton>

          <FloatButton
            iconName="add"
            class="fixed bottom-1em right-1em" />
        </div>
      </div>
    )
  },
})

继续 Navbar 与 Overlay 组件

4. 制作 Center 组件 ⇧

新增 PlaceCenter 居中布局组件 @/components/icons/PlaceCenter.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
import { defineComponent, type PropType } from 'vue'
import { cn } from 'cn'

type DirectionType =  'horizontal' | 'vertical' | '|' | '-'
interface PlaceCenterProps {
  direction?: DirectionType
}

export const PlaceCenter = defineComponent<PlaceCenterProps>({
  name: 'PlaceCenter',
  props: {
    direction: {
      type: String as PropType<PlaceCenterProps['direction']>,
      required: false,
      default: 'horizontal',
    }
  },
  setup( _props, _ctx ) {
    const direction = _props.direction ?? 'horizontal'
    const flexDirection = cn(['horizontal', '-'].includes(direction) ? 'flex-row' : 'flex-col')
    return () => (
      <section class={cn(
        'flex place-items-center place-content-center',
        flexDirection,
      )}>
        {_ctx.slots?.default?.()}
      </section>
    )
  },
})

添加居中图标组件 @/views/StartAccountsView.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
38
import { defineComponent } from 'vue'
import { cn } from 'cn'
import { BarButton } from '@/components/BarButton.tsx'
import { FloatButton } from '@/components/FloatButton.tsx'
import { PlaceCenter } from '@/components/icons/PlaceCenter.tsx'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'

export const StartAccountsView = defineComponent({
  name: 'StartAccountsView',
  setup(/*props, ctx*/) {
    const onClick = (e: MouseEvent) => {
      console.log('_______________________')
      console.log("%c 4 --> e: ","color:#f00;", e);
      console.log('_______________________')
    }

    return () => (
      <div class="start-accounts-view w-full h-full position-relative">
        <nav>menu</nav>

        <PlaceCenter class="p-y-10em">
          <SvgIcon class="w-8em h-8em" name="pig"/>
        </PlaceCenter>

        <div
          class={cn('p-x-1em')}>
          <BarButton onClick={onClick}>
            测试
          </BarButton>
        </div>

        <FloatButton
          iconName="add"
          class="fixed bottom-1em right-1em" />
      </div>
    )
  },
})

5. 制作 Navbar 组件 ⇧

@/components/NavBar.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// import ...
export const NavBar = defineComponent({
  name: 'NavBar',
  setup( _props, _ctx ) {
    const {slots} = _ctx

    return () => (
      <header class={cn(
        s.navbar,
        'text-[var(--navbar-text-color)]'
      )}>
        <span class={s.icon_wrapper}>{slots?.icon?.()}</span>
        <span class={s.title_wrapper}>{slots?.default?.()}</span>
      </header>
    )
  },
})

@/components/NavBar.module.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.navbar {
  // for unocss: 'bg-gradient-to-b from-[var(--navbar-bg-start)] to-[var(--navbar-bg-end)]'
  background-image: linear-gradient(to bottom, var(--navbar-bg-start), var(--navbar-bg-end));
  display: flex;
  justify-content: flex-start;
  align-items: center;
  padding: 40px 16px 12px;

  & > .icon_wrapper {
    width: 36px;
    height: 36px;
    font-weight: bold;
    display: flex;
    place-items: center;
    justify-content: center;
  }

  & > .title_wrapper {
    font-size: 1.5em;
    font-weight: bold;
    margin-left: 12px;
  }
}

@src/views/StartAccountsView.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
38
39
40
41
42
43
import { defineComponent } from 'vue'
import { cn } from 'cn'
import { BarButton } from '@/components/BarButton.tsx'
import { FloatButton } from '@/components/FloatButton.tsx'
import { PlaceCenter } from '@/components/PlaceCenter.tsx'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { NavBar } from '@/components/NavBar.tsx'
import '@/views/StartAccountsView.scoped.scss'

export const StartAccountsView = defineComponent({
  name: 'StartAccountsView',
  setup(/*props, ctx*/) {
    const onClick = (e: MouseEvent) => {/* ... */}

    return () => (
      <div class={cn(
        'start-accounts-view w-full h-full position-relative',
      )}>
        <NavBar class="navbar bg-gradient-to-b from-[var(--navbar-bg-start)] to-[var(--navbar-bg-end)]">{
          {
            default: '山竹记账',
            icon: () => <SvgIcon class="w-2em h-2em" name="menu" />,
          }
        }</NavBar>

        <PlaceCenter class="p-y-10em">
          <SvgIcon class="w-8em h-8em" name="pig"/>
        </PlaceCenter>

        <div
          class={cn('p-x-1em')}>
          <BarButton onClick={onClick}>
            开始记账
          </BarButton>
        </div>

        <FloatButton
          iconName="add"
          class="fixed bottom-1em right-1em" />
      </div>
    )
  },
})
  • 使用具名插槽传参 icon 和头部标题文字

6. 制作 Overlay 组件 ⇧

点击菜单按钮,左侧划出浮层

4. 完善 Overlay 样式 ⇧


·未完待续·

参考文章

相关文章

  • 无

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