【山竹记账前端-vue】13.制作两个页面:记一笔 ItemCreate、TagEdit


大纲链接 §

[toc]


1. 创建列表 ⇧

创建列表数据结构 ⇧

  • 记账项目数据格式 {id: 1, name: 'xx', sign: '💰', category: 'incomes'}
    • 类型尽量不用 type 作为属性,它是很多编程语言的内置关键字

@/views/ItemCreate.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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
import { defineComponent, ref } from 'vue'
import { MainLayout } from '@/components/layout/MainLayout.tsx'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { SwitchTabs } from '@/components/tab/SwitchTabs.tsx'
import { ItemTab } from '@/components/tab/ItemTab.tsx'
import { InputPad } from '@/modules/items/InputPad.tsx'

export const ItemCreate = defineComponent({
  name: 'ItemCreate',
  setup(/* _props, _ctx */) {
    const kindRef = ref('支出')
    const refExpensesTags = ref([
      { id: 1, name: '餐费', sign: '¥', category: 'expenses' },
      { id: 2, name: '打车', sign: '¥', category: 'expenses' },
      { id: 3, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 4, name: '打车', sign: '¥', category: 'expenses' },
      { id: 5, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 6, name: '打车', sign: '¥', category: 'expenses' },
      { id: 7, name: '聚餐', sign: '¥', category: 'expenses' },
    ])
    const refIncomeTags = ref([
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
    ])

    return () => (
      <MainLayout>
        {{
          title: () => '记一笔',
          icon: () => <SvgIcon name="back-arrow" class="w-2em h-2em" />,
          default: () => (
            <>
              <SwitchTabs selected={kindRef.value} v-model:selected={kindRef.value}>
                <ItemTab tabId="expenses" tabName="支出">
                  支出 icon 列表
                  {refExpensesTags.value.map(tag =>
                    <div class="">
                      <div class="">
                        {tag.sign}
                      </div>
                      <div class="">
                        {tag.name}
                      </div>
                    </div>
                  )}
                </ItemTab>
                <ItemTab tabId="incomes" tabName="收入">
                  收入 icon 列表
                  {refIncomeTags.value.map(tag =>
                    <div class="">
                      <div class="">
                        {tag.sign}
                      </div>
                      <div class="">
                        {tag.name}
                      </div>
                    </div>
                  )}
                </ItemTab>
              </SwitchTabs>

              <section class="fixed bottom-0 left-0 w-full">
                <InputPad />
              </section>
            </>
          ),
        }}
      </MainLayout>
    )
  },
})

完善 TagList 页面样式 ⇧

@/views/ItemCreate.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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { defineComponent, ref } from 'vue'
import { MainLayout } from '@/components/layout/MainLayout.tsx'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { SwitchTabs } from '@/components/tab/SwitchTabs.tsx'
import { ItemTab } from '@/components/tab/ItemTab.tsx'
import { InputPad } from '@/modules/items/InputPad.tsx'

export const ItemCreate = defineComponent({
  name: 'ItemCreate',
  setup(/* _props, _ctx */) {
    const kindRef = ref('支出')
    const refExpensesTags = ref([
      { id: 1, name: '餐费', sign: '¥', category: 'expenses' },
      { id: 2, name: '打车', sign: '¥', category: 'expenses' },
      { id: 3, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 4, name: '打车', sign: '¥', category: 'expenses' },
      { id: 5, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 6, name: '打车', sign: '¥', category: 'expenses' },
      { id: 7, name: '聚餐', sign: '¥', category: 'expenses' },
    ])
    const refIncomeTags = ref([
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
    ])

    return () => (
      <MainLayout>
        {{
          title: () => '记一笔',
          icon: () => <SvgIcon name="back-arrow" class="w-2em h-2em" />,
          default: () => (
            <div class="h-full flex flex-col">
              <SwitchTabs
                selected={kindRef.value}
                v-model:selected={kindRef.value}>
                <ItemTab tabId="expenses" tabName="支出" class="flex flex-wrap pt-1em">
                  <div class="relative w-20vw h-[calc(20vw*1.14)] flex flex-col items-center">
                    <div class="bg-[var(--tag-bg)] b-1 b-solid b-[var(--tag-bg)] w-12vw h-12vw rounded-[50%] flex justify-center items-center">
                      <SvgIcon name="add" class="fill-[var(--main)]" />
                    </div>
                    <div class="text-12px mt-.25em">
                      新增
                    </div>
                  </div>
                  {refExpensesTags.value.map(tag =>
                    <div class="relative w-20vw h-[calc(20vw*1.14)] flex flex-col items-center">
                      <div class="bg-[var(--tag-bg)] b-1 b-solid b-[var(--tag-bg)] w-12vw h-12vw rounded-[50%] flex justify-center items-center">
                        {tag.sign}
                      </div>
                      <div class="text-12px mt-.25em">
                        {tag.name}
                      </div>
                    </div>
                  )}
                </ItemTab>
                <ItemTab tabId="incomes" tabName="收入" class="flex flex-wrap pt-1em">
                  <div class="relative w-20vw h-[calc(20vw*1.14)] flex flex-col items-center">
                    <div class="bg-[var(--tag-bg)] b-1 b-solid b-[var(--tag-bg)] w-12vw h-12vw rounded-[50%] flex justify-center items-center">
                      <SvgIcon name="add" class="fill-[var(--main)]" />
                    </div>
                    <div class="text-12px mt-.25em">
                      新增
                    </div>
                  </div>
                  {refIncomeTags.value.map(tag =>
                    <div class="relative w-20vw h-[calc(20vw*1.14)] flex flex-col items-center">
                      <div class="bg-[var(--tag-bg)] b-1 b-solid b-[var(--tag-bg)] w-12vw h-12vw rounded-[50%] flex justify-center items-center">
                        {tag.sign}
                      </div>
                      <div class="text-12px mt-.25em">
                        {tag.name}
                      </div>
                    </div>
                  )}
                </ItemTab>
              </SwitchTabs>

              <section class="w-full grow-0 shrink-0">
                <InputPad />
              </section>
            </div>
          ),
        }}
      </MainLayout>
    )
  },
})
  • 添加tags标签数量,查看是否样式重叠

解决重叠问题 ⇧

@/components/tab/SwitchTabs.tsx

  • 添加根标签
  • 使用黏着定位 <nav class="sticky left-0 top-0 z-1">
  • 父元素添加样式 <div class="grow-1 shrink-1 overflow-auto scrollbar-hide relative">

     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
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    
    import { defineComponent, type PropType } from 'vue'
    import { ItemTab } from '@/components/tab/ItemTab.tsx'
    import { cn } from 'cn'
    
    export const SwitchTabs = defineComponent({
    name: 'SwitchTabs',
    props: {
    selected: {
      type: String as PropType<string>,
    },
    },
    setup(_props, _ctx) {
    const selectedClass = (tabName: string) =>
      _props.selected === tabName
        ? cn(
            'relative',
            `after:(content-[""] block absolute bottom-0 left-0 w-full h-4px bg-[var(--tabs-indicator-bg)])`,
          )
        : ''
    
    return () => {
      const defaultSlotTabs = _ctx.slots?.default?.() ?? []
      const ifAllItemTab = defaultSlotTabs.every(
        (slot) => slot.type === ItemTab && !!slot.props?.tabName,
      )
      if (!ifAllItemTab) {
        throw new Error(`SwitchTabs: 子组件必须是 ItemTab,且必须有 tabName 属性`)
      }
    
      return (
        <div class="grow-1 shrink-1 overflow-auto scrollbar-hide relative">
          {/* 切换标签 */}
          <nav class="sticky left-0 top-0 z-1">
            <menu class="flex items-center text-color-[var(--navbar-text-color)]">
              {defaultSlotTabs.map((slot, index) => (
                <li
                  key={index + slot?.props?.tabName}
                  class={cn(
                    'grow-1 shrink-0 text-center',
                    'p-y-12px bg-[var(--navbar-bg-end)]',
                    selectedClass(slot?.props?.tabName),
                  )}
                  onClick={() => {
                    _ctx.emit('update:selected', slot?.props?.tabName)
                  }}>
                  {slot?.props?.tabName}
                </li>
              ))}
            </menu>
          </nav>
    
          {/* 标签内容-插槽-数组 */}
          <section>
            {defaultSlotTabs.find((slot) => slot.props?.tabName === _props.selected)}
          </section>
        </div>
      )
    }
    },
    })
    

封装记账图标标签组件组件 ⇧

@src/modules/tags/IconTag.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
import { defineComponent, type PropType} from 'vue'

export const IconTag = defineComponent({
  name: 'IconTag',
  props: {
    tagName: { type: String as PropType<string>, default: '', },
    tagSign: { type: String as PropType<string>, default: '😃', },
  },
  setup( _props, _ctx ) {
    return () => (
      <div class="relative w-20vw h-[calc(20vw*1.14)] flex flex-col items-center">
        <div class="bg-[var(--tag-bg)] b-1 b-solid b-[var(--tag-bg)] w-12vw h-12vw rounded-[50%] flex justify-center items-center">
          {/* 兼容插槽,可放图标组件 */}
          {
            _ctx.slots?.default?.()
            ? _ctx.slots?.default?.()
            : _props.tagSign
          }
        </div>
        <div class="text-12px mt-.25em">
          {_props.tagName}
        </div>
      </div>
    )
  },
})

重构创建标签页面 @/views/ItemCreate.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import { computed, defineComponent, ref } from 'vue'
import { MainLayout } from '@/components/layout/MainLayout.tsx'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { SwitchTabs } from '@/components/tab/SwitchTabs.tsx'
import { ItemTab } from '@/components/tab/ItemTab.tsx'
import { InputPad } from '@/modules/items/InputPad.tsx'
import { IconTag } from '@/modules/tags/IconTag.tsx'

export const ItemCreate = defineComponent({
  name: 'ItemCreate',
  setup(/* _props, _ctx */) {
    const kindRef = ref('支出')
    const refExpensesTags = ref([
      { id: 1, name: '餐费', sign: '¥', category: 'expenses' },
      { id: 2, name: '打车', sign: '¥', category: 'expenses' },
      { id: 3, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 4, name: '打车', sign: '¥', category: 'expenses' },
      { id: 5, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 6, name: '打车', sign: '¥', category: 'expenses' },
      { id: 7, name: '聚餐', sign: '¥', category: 'expenses' },
    ])
    const refIncomeTags = ref([
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      // ...
    ])

    return () => (
      <MainLayout>
        {{
          title: () => '记一笔',
          icon: () => <SvgIcon name="back-arrow" class="w-2em h-2em" />,
          default: () => (
            <div class="h-full flex flex-col">
              <SwitchTabs
                selected={kindRef.value}
                v-model:selected={kindRef.value}>
                <ItemTab tabId="expenses" tabName="支出" class="flex flex-wrap pt-1em">
                  <IconTag tagName="新增">
                    <SvgIcon name="add" class="fill-[var(--main)]" />
                  </IconTag>
                  {refExpensesTags.value.map(tag =>
                    <IconTag tagName={tag.name} tagSign={tag.sign}/>
                  )}
                </ItemTab>
                
                <ItemTab tabId="incomes" tabName="收入" class="flex flex-wrap pt-1em">
                  <IconTag tagName="新增">
                    <SvgIcon name="add" class="fill-[var(--main)]" />
                  </IconTag>
                  {refIncomeTags.value.map(tag =>
                    <IconTag tagName={tag.name} tagSign={tag.sign}/>
                  )}
                </ItemTab>

              </SwitchTabs>

              <section class="w-full grow-0 shrink-0">
                <InputPad />
              </section>
            </div>
          ),
        }}
      </MainLayout>
    )
  },
})

@/views/ItemCreate.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
export const ItemCreate = defineComponent({
  name: 'ItemCreate',
  setup(/* _props, _ctx */) {
    const kindRef = ref('支出')
    const refExpensesTags = ref([
      { id: 1, name: '餐费', sign: '¥', category: 'expenses' },
      { id: 2, name: '打车', sign: '¥', category: 'expenses' },
      { id: 3, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 4, name: '打车', sign: '¥', category: 'expenses' },
      { id: 5, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 6, name: '打车', sign: '¥', category: 'expenses' },
      { id: 7, name: '聚餐', sign: '¥', category: 'expenses' },
    ])
    const refIncomeTags = ref([
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      // ...
    ])
    
    const itemTabList = computed(() => ([
      {
        tabId: 'expenses2',
        tabName: '支出',
        tagList: refExpensesTags.value,
      },
      {
        tabId: 'incomes2',
        tabName: '收入',
        tagList: refIncomeTags.value,
      },
    ]))

    return () => (
      <MainLayout>
        {{
          title: () => '记一笔',
          icon: () => <SvgIcon name="back-arrow" class="w-2em h-2em" />,
          default: () => (
            <div class="h-full flex flex-col">
              <SwitchTabs
                selected={kindRef.value}
                v-model:selected={kindRef.value}>
                {
                  itemTabList.value
                    .map(itemTab => (
                      <ItemTab
                        key={itemTab.tabId}
                        tabId={itemTab.tabId}
                        tabName={itemTab.tabName}
                        class="flex flex-wrap pt-1em">
                        <IconTag tagName="新增">
                          <SvgIcon name="add" class="fill-[var(--main)]" />
                        </IconTag>
                        {
                          itemTab.tagList
                            .map(tag =>
                              <IconTag tagName={tag.name} tagSign={tag.sign}/>
                            )
                        }
                      </ItemTab>
                    ))
                }

              </SwitchTabs>

              <section class="w-full grow-0 shrink-0">
                <InputPad />
              </section>
            </div>
          ),
        }}
      </MainLayout>
    )
  },
})
  • 报错 SwitchTabs: 子组件必须是 ItemTab,且必须有 tabName 属性
  • 原因是使用数组循环遍历的节点都是包一层 fragment,内部不是直接 ItemTab 组件了
  • 在 SwitchTabs 组件内部需要额外判断是否为文档片段,是则返回拍平后的 VNode[]

Symbol(v-fgt)


添加生成默认插槽标签函数,兼容可遍历的文档片段 ⇧

@/views/genDefaultSlotTabs.ts

1
2
3
4
5
6
7
8
import type { VNode } from 'vue'

export const genDefaultSlotTabs = (slots: Array<VNode>): VNode[] =>
  slots.flatMap((slot) =>
    typeof slot?.type === 'symbol' && Symbol.keyFor(slot?.type as symbol) === 'v-fgt'
      ? genDefaultSlotTabs(slot.children as VNode[])
      : slot,
  )
  • 使用 Symbol.keyFor(slot?.type as symbol) === 'v-fgt' 判断是否为文档片段
  • 递归判断节点是否为文档片段,并返回拍平后的节点数组

重构 @/components/tab/SwitchTabs.tsx

  • 处理报错
  • 原组件内部必须使用默认插槽接收 ItemTab 组件,ItemTab 组件必须有 tabName 属性
  • 拿到默认插槽的子组件数组,遍历判断是否为 ItemTab 或是 v-fgt,递归拍平返回子组件数组并渲染 defaultSlotTabs

     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
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    
    import { defineComponent, type PropType, } from 'vue'
    import { ItemTab } from '@/components/tab/ItemTab.tsx'
    import { cn } from 'cn'
    import { genDefaultSlotTabs } from '@/views/genDefaultSlotTabs.ts'
    
    export const SwitchTabs = defineComponent({
    name: 'SwitchTabs',
    props: {
    selected: {
      type: String as PropType<string>,
    },
    },
    setup(_props, _ctx) {
    const selectedClass = (tabName: string) =>
      _props.selected === tabName
        ? cn(
            'relative',
            `after:(content-[""] block absolute bottom-0 left-0 w-full h-4px bg-[var(--tabs-indicator-bg)])`,
          )
        : ''
    
    return () => {
      const defaultSlotTabs = genDefaultSlotTabs(_ctx.slots?.default?.() ?? [])
      const ifAllItemTab = defaultSlotTabs
        .every((slot) => slot.type === ItemTab && !!slot.props?.tabName)
    
      if (!ifAllItemTab) {
        throw new Error(`SwitchTabs: 子组件必须是 ItemTab,且必须有 tabName 属性`)
      }
    
      console.log('SwitchTabs_______________________')
      console.log(_ctx.slots?.default?.())
      console.log(defaultSlotTabs)
      console.log('_______________________SwitchTabs')
    
      return (
        <div class="grow-1 shrink-1 overflow-auto scrollbar-hide relative">
          {/* 切换标签 */}
          <nav class="sticky left-0 top-0 z-1">
            <menu class="flex items-center text-color-[var(--navbar-text-color)]">
              {defaultSlotTabs.map((slot, index) => (
                <li
                  key={index + slot?.props?.tabName}
                  class={cn(
                    'grow-1 shrink-0 text-center',
                    'p-y-12px bg-[var(--navbar-bg-end)]',
                    selectedClass(slot?.props?.tabName),
                  )}
                  onClick={() => {
                    _ctx.emit('update:selected', slot?.props?.tabName)
                  }}>
                  {slot?.props?.tabName}
                </li>
              ))}
            </menu>
          </nav>
    
          {/* 标签内容-插槽-数组 */}
          <section>
            {defaultSlotTabs.find((slot) => slot.props?.tabName === _props.selected)}
          </section>
        </div>
      )
    }
    },
    })
    

重构创建标签页面 @/views/ItemCreate.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// import ...

export const ItemCreate = defineComponent({
  name: 'ItemCreate',
  setup(/* _props, _ctx */) {
    const kindRef = ref('支出')
    const refExpensesTags = ref([
      { id: 1, name: '餐费', sign: '¥', category: 'expenses' },
      { id: 2, name: '打车', sign: '¥', category: 'expenses' },
      { id: 3, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 4, name: '打车', sign: '¥', category: 'expenses' },
      { id: 5, name: '聚餐', sign: '¥', category: 'expenses' },
      { id: 6, name: '打车', sign: '¥', category: 'expenses' },
      { id: 7, name: '聚餐', sign: '¥', category: 'expenses' },
    ])
    const refIncomeTags = ref([
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 6, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 11, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 18, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 17, name: '彩票', sign: '¥', category: 'incomes' },
      { id: 19, name: '滴滴', sign: '¥', category: 'incomes' },
      { id: 4, name: '工资', sign: '¥', category: 'incomes' },
      { id: 5, name: '彩票', sign: '¥', category: 'incomes' },
      // ...
    ])
    const itemTabList = computed(() => ([
      { tabId: 'expenses', tabName: '支出', tagList: refExpensesTags.value, },
      { tabId: 'incomes', tabName: '收入', tagList: refIncomeTags.value, },
    ]))
    return () => (
      <MainLayout>
        {{
          title: () => '记一笔',
          icon: () => <SvgIcon name="back-arrow" class="w-2em h-2em" />,
          default: () => (
            <div class="h-full flex flex-col">
              <SwitchTabs
                selected={kindRef.value}
                v-model:selected={kindRef.value}>
                {
                  itemTabList.value
                    .map(itemTab => (
                      <ItemTab
                        key={itemTab.tabId}
                        tabId={itemTab.tabId}
                        tabName={itemTab.tabName}
                        class="flex flex-wrap pt-1em">
                        <IconTag tagName="新增">
                          <SvgIcon name="add" class="fill-[var(--main)]" />
                        </IconTag>
                        {
                          itemTab.tagList
                            .map(tag =>
                              <IconTag tagName={tag.name} tagSign={tag.sign}/>
                            )
                        }
                      </ItemTab>
                    ))
                }
              </SwitchTabs>

              <section class="w-full grow-0 shrink-0">
                <InputPad />
              </section>
            </div>
          ),
        }}
      </MainLayout>
    )
  },
})

微信、手机屏幕样式测试


2. 制作 TagEdit 页面 ⇧

完成 TagDetailEditView 组件样式结构 ⇧

@/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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// import type { RouteRecordRaw } from 'vue-router'...

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 },
  /* 记账项目相关:项目列表页、创建页 */
  {
    path: '/items',
    name: 'items',
    component: ItemsView,
    children: [
      {
        path: '',
        name: 'itemList',
        component: ItemList,
      },
      {
        path: 'create',
        component: ItemCreate,
      },
    ],
  },
  /* 标签项目相关:标签详细页、创建页 */
  {
    path: '/tags',
    name: 'tags',
    component: TagView,
    children: [
      { path: '', name: 'tagDefault', redirect: '/tags/create', },
      { path: 'create', name: 'tagCreate', component: TagCreateView },
      { path: 'edit/:id', name: 'tagEdit', component: TagDetailEditView },
    ],
  },
]

@/views/TagDetailEditView.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
import { defineComponent, } from 'vue'
import { MainLayout } from '@/components/layout/MainLayout.tsx'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { TagForm } from '@/modules/tags/TagForm.tsx'

export const TagDetailEditView = defineComponent({
  name: 'TagDetailEditView',
  setup(/* _props, _ctx */) {
    return () => (
      <MainLayout>
        {{
          title: () => '编辑标签',
          icon: () => <SvgIcon name="back-arrow" size="1.5em" onClick={() => {}} />,
          default: () => (
            <>
              <TagForm />
              <div class="p-1em">
                <BarButton level="danger" onClick={() => {}}>删除标签</BarButton>
              </div>
            </>
          ),
        }}
      </MainLayout>
    )
  },
})
  • 页面样式基本与创建页面一直,已抽取组件 @/modules/tags/TagForm.tsx 复用,需要添加插槽
  • 逻辑上需要一开始就获取数据,暂时不做
  • 异同点
    • 数据源不同
    • 错误验证展示逻辑相同
    • 提交逻辑不同
    • 创建的api 与 更新的api
  • TagForm.tsx 仅做验证逻辑,展示验证错误结;
    • 仅当编辑tag时,有表单默认数据,通过 props 传入,暂时不做,现在只完成样式;
    • 提交逻辑抛出事件,暂时不做,现在只完成样式;

为 BarButton 组件添加 level 属性支持多风格样式 ⇧

@/assets/styles/vars.scss

 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
44
45
46
47
48
49
50
51
52
53
54
55
/* color palette from <https://github.com/vuejs/theme> */
:root {
  --main: #5c33be;
  --error-color: #e10505;
  // welcome page
  --welcome-card-bg-color: white;
  --welcome-text: white;
  --title-text: #d4d4ee;
  --welcome-card-start: var(--main);
  --welcome-card-end: #8f4cd7;
  --welcome-text-primary-color: #6035bf;
  --transition-duration: 0.8s;
  --button-bg: var(--main);
  --button-bg-primary: var(--button-bg);
  --button-bg-success: #67C23A;
  --button-bg-warning: #E6A23C;
  --button-bg-danger: var(--error-color);
  --button-text-color: white;
  --button-text-size: 18px;
  --button-radius: 8px;
  --button-height: 48px;
  --float-button-bg: var(--button-bg);
  --float-button-text-color: var(--button-text-color);
  --float-button-radius: 50%;
  --navbar-bg-start: var(--welcome-card-start);
  --navbar-bg-end: var(--welcome-card-end);
  --navbar-text-color: var(--button-text-color);
  --navbar-min-height: 88px;
  --z-index-button-nav: 10;
  // 划出浮层 遮罩层
  --z-index-float-overlay: 20;
  --overlay-mask-bg: rgba(0, 0, 0, 0.5);
  --overlay-bg: white;
  --overlay-user-bg: var(--button-bg);
  --overlay-user-text: var(--button-text-color);
  --top-safe-area-height: 24px;
  // tabs
  --tabs-indicator-bg: rgba(255, 255, 255, 0.5);
  // input pad
  --pad-border-color: #ddd;
  --pad-button-bg-color: white;
  --pad-submit-button-bg-color: var(--button-bg);
  --pad-submit-button-text-color: var(--button-text-color);
  --amount-text-color: #53a867;
  --date-text-color: #999;
  // input emoji
  --input-min-height: 48px;
  --input-border-color: var(--main);
  --input-radius: var(--button-radius);
  --input-shadow: rgba(0, 0, 0, 0.25);
  --emoji-nav-text: #999;
  --emoji-nav-text-selected: #333;
  // tag
  --tag-bg: #EFEFEF;
}

@/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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { defineComponent, type PropType } from 'vue'
import { cn } from 'cn'

type ButtonFeatType = 'button' | 'submit' | 'reset'
type ButtonLevelType = 'primary' | 'success' | 'warning' | 'danger'

interface BarButtonProps {
  onClick?: (e: MouseEvent) => void
  type?: ButtonFeatType
  level?: ButtonLevelType
}

export const BarButton = defineComponent<BarButtonProps>({
  name: 'BarButton',
  props: {
    type: {type: String as PropType<ButtonFeatType>, default: 'button' as const,},
    level: {type: String as PropType<ButtonLevelType>, default: 'primary' as const,},
  },
  setup(_props, ctx) {
    const levelHashClass: Record<ButtonLevelType, string> = {
      primary: cn('bg-[var(--button-bg-primary)] b-[var(--button-bg-primary)]'),
      success: cn('bg-[var(--button-bg-success)] b-[var(--button-bg-success)]'),
      warning: cn('bg-[var(--button-bg-warning)] b-[var(--button-bg-warning)]'),
      danger: cn('bg-[var(--button-bg-danger)] b-[var(--button-bg-danger)]'),
    }

    return () => (
      <button
        type={_props.type}
        class={cn(
          `${levelHashClass[_props.level ?? 'primary']}`,
          'b-1 b-solid 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>
    )
  },
})

3. 修改SwitchTabs内部子项样式,使用 :where() 降低优先级 ⇧

@/components/tab/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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// import ...

export const SwitchTabs = defineComponent({
  name: 'SwitchTabs',
  props: {
    selected: {
      type: String as PropType<string>,
    },
  },
  setup(_props, _ctx) {
    const selectedClass = (tabName: string) =>
      _props.selected === tabName
        ? cn(/* ... */)
        : ''

    return () => {
      const defaultSlotTabs = genDefaultSlotTabs(_ctx.slots?.default?.() ?? [])
      const ifAllItemTab = defaultSlotTabs
        .every((slot) => slot.type === ItemTab && !!slot.props?.tabName)
      if (!ifAllItemTab) {
        throw new Error(`SwitchTabs: 子组件必须是 ItemTab,且必须有 tabName 属性`)
      }

      return (
        <div class="grow-1 shrink-1 overflow-auto scrollbar-hide relative">
          {/* 切换标签 */}
          <nav class="sticky left-0 top-0 z-1 bg-[var(--navbar-bg-end)]">
            <menu class="flex justify-start items-center text-color-[var(--navbar-text-color)]">
              {defaultSlotTabs.map((slot, index) => (
                <li
                  key={index + slot?.props?.tabName}
                  class={cn(
                    'where:(text-center grow-1 shrink-0 px-1em py-12px)',
                    selectedClass(slot?.props?.tabName),
                  )}
                  onClick={() => {
                    _ctx.emit('update:selected', slot?.props?.tabName)
                  }}>
                  {slot?.props?.tabName}
                </li>
              ))}
            </menu>
          </nav>

          {/* 标签内容-插槽-数组 */}
          <section>
            {defaultSlotTabs.find((slot) => slot.props?.tabName === _props.selected)}
          </section>
        </div>
      )
    }
  },
})
  • 默认子项样式优先级降为0 'where:(text-center grow-1 shrink-0 px-1em py-12px)'

@/views/ItemList.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
import { defineComponent, ref } from 'vue'
import { MainLayout } from '@/components/layout/MainLayout.tsx'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { SwitchTabs } from '@/components/tab/SwitchTabs.tsx'
import { ItemTab } from '@/components/tab/ItemTab.tsx'

export const ItemList = defineComponent({
  name: 'ItemList',
  setup(/* _props, _ctx */) {
    const selectedDateRef = ref('本月')
    return () => (
      <MainLayout>
        {{
          title: () => '山竹记账',
          icon: () => <SvgIcon name="menu" class="w-2em h-2em" />,
          default: () => (
            <div class="h-full flex flex-col">
              <SwitchTabs
                selected={selectedDateRef.value}
                v-model:selected={selectedDateRef.value}
                class="[&>nav>menu>li]:(text-start grow-0)">
                <ItemTab tabName="本月" tabId="thisMonth">本月</ItemTab>
                <ItemTab tabName="上月" tabId="lastMonth">上月</ItemTab>
                <ItemTab tabName="今年" tabId="thisYear">今年</ItemTab>
                <ItemTab tabName="自定义范围" tabId="customRange">自定义范围</ItemTab>
              </SwitchTabs>
            </div>
          ),
        }}
      </MainLayout>
    )
  },
})
  • 使用父级样式覆盖 '[&>nav>menu>li]:(text-start grow-0)'

参考文章

相关文章

  • 无

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