【山竹记账前端-vue】14.制作 ItemList 页面


大纲链接 §

[toc]


1. 重构 time ⇧

改用 Class Time ⇧

期望实现更多其他api:firstDayOfMonth、firstDayOfYear、lastDayOfMonth、lastDayOfYear、add、subtract

  • 需要改用原型链,节省内存

@/shared/time.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
export class Time {
  #date: Date
  constructor(date = new Date()) {
    this.#date = date
  }
  format(pattern: string = 'YYYY-MM-DD') {
    // YYYY
    const year = this.#date.getFullYear()
    // MM
    const month = this.#date.getMonth() + 1
    // DD
    const day = this.#date.getDate()
    // HH
    const hour = this.#date.getHours()
    // mm
    const minute = this.#date.getMinutes()
    // ss
    const second = this.#date.getSeconds()
    // sss
    const millisecond = this.#date.getMilliseconds()
    return (
      pattern
        .replace(/YYYY/g, year.toString())
        // 使用 padStart 长度不足则补零
        .replace(/MM/g, month.toString().padStart(2, '0'))
        .replace(/DD/g, day.toString().padStart(2, '0'))
        .replace(/HH/g, hour.toString().padStart(2, '0'))
        .replace(/mm/g, minute.toString().padStart(2, '0'))
        .replace(/ss/g, second.toString().padStart(2, '0'))
        .replace(/sss/g, millisecond.toString().padStart(3, '0'))
    )
  }
}
  • 使用 new Time(new Date('2027-01-31')).format('YYYY-MM-DD HH:mm:ss.sss')

实现 add(1, 'month') ⇧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
add(amount: number, unit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond') {
  const date = new Date(this.#date.getTime())
  switch (unit) {
    case 'year':
      date.setFullYear(date.getFullYear() + amount)
      break
    case 'month':
      date.setMonth(date.getMonth() + amount)
      break
    case 'week':
      date.setDate(date.getDate() + amount * 7)
      break
    case 'day':
      date.setDate(date.getDate() + amount)
      break
    default:
      throw new Error(`Invalid unit: ${unit}`)
  }
  • 当前月份第一天的日期
    • 使用 this.date.setDate(1) 会改当前调用的日期,不符合预期,并不想修改日期,而是创建一个新的
    • 改用 new Time(new Date()) 来构造:new Time(new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0,0,0))
    • 当前月的最后一天就是下月的0号
  • 暂不考虑闰秒的情况
  • add 需要考虑月份的特殊情况 date.setMonth(date.getMonth() + amount)
    • const d1 = new Date(2000, 0, 31) 闰年1月份加上一个月的日期
    • d1 为 Mon Jan 31 2000 00:00:00 GMT+0800 (中国标准时间) 符合预期
    • d1.setMonth(d1.getMonth() + 1),再查看
    • d1 变为 Thu Mar 02 2000 00:00:00 GMT+0800 (中国标准时间) 不符合预期
    • 加上的是当前月份的天数:一月加一个月就是加上31天,二月加一个月就是加上28或者29天
    • 1月31号加29天应该是29号,因为2000年是闰年
    • 需要改为人类容易理解的判断方式

@/shared/dateTime.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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
export class DateTime {
  #date: Date
  constructor(date = new Date()) {
    this.#date = date
  }
  format(pattern: string = 'YYYY-MM-DD') {
    // YYYY
    const year = this.#date.getFullYear()
    // MM
    const month = this.#date.getMonth() + 1
    // DD
    const day = this.#date.getDate()
    // HH
    const hour = this.#date.getHours()
    // mm
    const minute = this.#date.getMinutes()
    // ss
    const second = this.#date.getSeconds()
    // sss
    const millisecond = this.#date.getMilliseconds()
    return (
      pattern
        .replace(/YYYY/g, year.toString())
        // 使用 padStart 长度不足则补零
        .replace(/MM/g, month.toString().padStart(2, '0'))
        .replace(/DD/g, day.toString().padStart(2, '0'))
        .replace(/HH/g, hour.toString().padStart(2, '0'))
        .replace(/mm/g, minute.toString().padStart(2, '0'))
        .replace(/ss/g, second.toString().padStart(2, '0'))
        .replace(/sss/g, millisecond.toString().padStart(3, '0'))
    )
  }
  add(amount: number, unit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond') {
    const date = new Date(this.#date.getTime())
    switch (unit) {
      case 'year':
        date.setFullYear(date.getFullYear() + amount)
        break
      case 'month':
        // date.setMonth(date.getMonth() + amount)
        // 01-31 + 1m = 02-28/29
        const currentDay = date.getDate() // 2000-01-31
        date.setDate(1) // 设置为 01-01,再加31天就不会跳到3月份去了,保证月份正确
        date.setMonth(date.getMonth() + amount) // 02-01
        // 获取 2000-03-00 00:00:00 的日期数字,其中 monthIndex 为 2
        const lastDayOfMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0,0,0,0,0).getDate()
        date.setDate(Math.min(currentDay, lastDayOfMonth))
        break
      case 'week':
        date.setDate(date.getDate() + amount * 7)
        break
      case 'day':
        date.setDate(date.getDate() + amount)
        break
      case 'hour':
        date.setHours(date.getHours() + amount)
        break
      case 'minute':
        date.setMinutes(date.getMinutes() + amount)
        break
      case 'second':
        date.setSeconds(date.getSeconds() + amount)
        break
      case 'millisecond':
        date.setMilliseconds(date.getMilliseconds() + amount)
        break
      default:
        throw new Error(`Invalid unit: ${unit}`)
    }
  }
}
  • 测试使用
    • const t = new DateTime(new Date(2000, 0, 31, 0, 0, 0,))
    • console.log( t.add(1, 'month').getRawDate() )

@/modules/items/DateInput.tsx

1
2
3
4
// ...
const formattedDateStr = computed(() =>
  (new DateTime(new Date(currentDateRef.value.join('-')))).format(),
)

2. 打算实现 ItemSummary,完成 ItemList 页面基本结构样式 ⇧

  • <ItemSummary startDate="2026-11-01" endDate="2026-12-31" />
    • 日期区间为左开右闭,因为包含时间 2026-11-01 00:00:00 开始,不包含 2027-01-01 00:00:00,表示 2026-12-31 12:59:59

@/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
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
import { defineComponent, reactive, 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 { ItemSummary } from '@/modules/items/ItemSummary.tsx'
import { DateTime } from '@/shared/dateTime.ts'

export const ItemList = defineComponent({
  name: 'ItemList',
  setup(/* _props, _ctx */) {
    const selectedDateRef = ref('本月')
    const currentDate = new DateTime()
    const customDate = reactive({start: new DateTime(), end: new DateTime()})
    const dateList = [
      {start: currentDate.firstDayOfMonth(), end: currentDate.lastDayOfMonth()},
      {
        start: currentDate.subtract(1, 'month').firstDayOfMonth(),
        end: currentDate.subtract(1, 'month').lastDayOfMonth()
      },
      {start: currentDate.firstDayOfYear(), end: currentDate.lastDayOfYear()},
    ] as const
    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">
                  <ItemSummary
                    startDate={dateList[0].start.format()}
                    endDate={dateList[0].end.format()} />
                </ItemTab>
                <ItemTab tabName="上月" tabId="lastMonth">
                  <ItemSummary
                    startDate={dateList[1].start.format()}
                    endDate={dateList[1].end.format()} />
                </ItemTab>
                <ItemTab tabName="今年" tabId="thisYear">
                  <ItemSummary
                    startDate={dateList[2].start.format()}
                    endDate={dateList[2].end.format()} />
                </ItemTab>
                <ItemTab tabName="自定义范围" tabId="customRange">
                  <ItemSummary
                    startDate={customDate.start.format()}
                    endDate={customDate.end.format()} />
                </ItemTab>
              </SwitchTabs>
            </div>
          ),
        }}
      </MainLayout>
    )
  },
})

@/modules/items/ItemSummary.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
import { defineComponent, type PropType } from 'vue'
import { cn } from 'cn'

export const ItemSummary = defineComponent({
  name: 'ItemSummary',
  props: {
    startDate: {
      type: String as PropType<string>,
      required: true,
    },
    endDate: {
      type: String as PropType<string>,
      required: true,
    },
  },
  setup(/* _props, _ctx */) {
    return () => (
      <div class="flex flex-col h-[calc(100vh-136px)] scrollbar-hide relative">
        <ul class={cn(
          'bg-[var(--total-bg)] text-[var(--total-text)]',
          'grow-0 shrink-0 flex justify-between',
          'm-1em py-0 px-1em rounded-[var(--button-radius)]',
        )}>
          <li class="p-1em flex flex-col text-center color-[var(--total-income)]"><span>收入</span><span>¥ 128.00</span></li>
          <li class="p-1em flex flex-col text-center color-[var(--total-expenses)]"><span>支出</span><span>¥ 99.00</span></li>
          <li class="p-1em flex flex-col text-center color-[var(--total-text)]"><span>净收入</span><span>¥ 39.00</span></li>
        </ul>
        <div class="max-h-[calc(100%-112px)] grow-1 shrink-1 overflow-auto scrollbar-hide">
          <ul class="[&>li]:[--sibling-idx:calc(1*sibling-index())] [&>li>div.sign]:bg-[oklch(70%_0.15_calc(var(--sibling-idx)*70))]">
            <li class="flex justify-between items-start py-12px px-1em b-b-1px b-b-[var(--button-border-color)]">
              <div class="sign grow-0 shrink-0 size-48px rounded-[50%] flex justify-center items-center my-0 mx-.5em">
                <span>😀</span>
              </div>
              <div class="mx-.25em grow-1 shrink-1">
                <div class="flex justify-between">
                  <span class="text-[var(--list-tag-text)]">旅行</span>
                  <span class="text-[var(--list-amount-text)]">¥1234.00</span>
                </div>
                <div class="text-[var(--list-time-text)] mt-.25em">
                  2027-01-01 12:00:00
                </div>
              </div>
            </li>
            {/* 可以临时复制多个li查看效果 */}
          </ul>

          <div class="pt-2em mb-2em text-center">向下滑动加载更多</div>
        </div>
      </div>
    )
  },
})
  • [&>li]:[--sibling-idx:calc(1*sibling-index())] [&>li>div.sign]:bg-[oklch(70%_0.15_calc(var(--sibling-idx)*70))]
    • 使用 sibling-index() 计算相邻元素下标;再使用 oklch(70%_0.15_calc(var(--sibling-idx)*70)) 动态计算颜色
    • 需要让父元素强制锁死 sibling-index() 这个值,而不是默认会原封不动地拿到当前的子元素节点上再进行计算,都算成 1
    • 必须借助 @property(显式注册整型变量)或 @container style()(容器样式查询)
    • 需要在 unocss.config.ts 中配置 preflight

uno.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { defineConfig } from 'unocss'

export default defineConfig({
  preflights: [
    {
      getCSS: () => `
        @property --sibling-idx {
          syntax: '<integer>'; /* 核心:限制为整数类型,迫使浏览器立即求值 */
          inherits: true;      /* 允许向下继承计算后的结果 */
          initial-value: 1;
        }
      `
    }
  ]
})

之后可以改为使用 v-for 或 .map() 循环遍历下标 index 来计算


3. 给 ItemList 页面添加对话框,使用 Vant 的 Overlay 组件实现 ⇧

@/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
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
import { defineComponent, reactive, ref, toRaw, watchEffect } 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 { ItemSummary } from '@/modules/items/ItemSummary.tsx'
import { DateTime } from '@/shared/dateTime.ts'
import { FloatButton } from '@/components/FloatButton.tsx'
import { Overlay } from 'vant'

export const ItemList = defineComponent({
  name: 'ItemList',
  setup(/* _props, _ctx */) {
    const selectedDateRef = ref('本月')
    const currentDate = new DateTime()
    // 调用 api 时需要 toRaw 转换访问私有变量
    const customDate = reactive({
      start: currentDate.getOriginDate(),
      end: currentDate.getOriginDate()
    })

    const dateList = [
      {start: currentDate.firstDayOfMonth(), end: currentDate.lastDayOfMonth()},
      {
        start: currentDate.subtract(1, 'month').firstDayOfMonth(),
        end: currentDate.subtract(1, 'month').lastDayOfMonth()
      },
      {start: currentDate.firstDayOfYear(), end: currentDate.lastDayOfYear()},
    ] as const

    const refOverlayVisible = ref(false)
    watchEffect(() => {
      refOverlayVisible.value = selectedDateRef.value === '自定义日期范围'
    })
    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">
                  <ItemSummary
                    startDate={dateList[0].start.format()}
                    endDate={dateList[0].end.format()} />
                </ItemTab>
                <ItemTab tabName="上月" tabId="lastMonth">
                  <ItemSummary
                    startDate={dateList[1].start.format()}
                    endDate={dateList[1].end.format()} />
                </ItemTab>
                <ItemTab tabName="今年" tabId="thisYear">
                  <ItemSummary
                    startDate={dateList[2].start.format()}
                    endDate={dateList[2].end.format()} />
                </ItemTab>
                <ItemTab tabName="自定义日期范围" tabId="customDateRange">
                  <ItemSummary
                    startDate={toRaw(customDate.start)?.format()}
                    endDate={toRaw(customDate.end)?.format()} />
                </ItemTab>
              </SwitchTabs>

              {/* 自定义日期范围弹出层 */}
              <Overlay
                show={refOverlayVisible.value}
                onClick={() => (refOverlayVisible.value = false)}
                class="flex items-center justify-center b-3 b-solid b-red bg-[var(--overlay-mask-bg)]" >
                <div
                  onClick={(e: MouseEvent) => e.stopPropagation()}
                  class="bg-[var(--overlay-bg)] w-80vw">
                  <header class="bg-[var(--main)] text-[var(--dialog-header-text)] py-12px px-1em">
                    请选择时间
                  </header>
                  <main>
                    <form>
                      <div>

                      </div>
                      <div>

                      </div>
                    </form>
                  </main>
                </div>
              </Overlay>

              <FloatButton class="fixed bottom-1em right-1em" iconName="add" />
            </div>
          ),
        }}
      </MainLayout>
    )
  },
})
  • 在嵌套内容添加阻止事件传播 onClick={(e: MouseEvent) => e.stopPropagation()}

4. 封装 From 和 FormItem 组件 ⇧

可以统一封装 Form 结构:标题、内容/穿餐,伪代码:

1
2
3
4
5
6
7
8
9
<Form onSubmit={() => {}}>
  <FormItem 
    label="标题"
    v-model={x.value}
    type="text | emojiSelector | date"
    errors={errors[name]}>
    {Xxx}
  </FormItem>
</FormItem>
  • 业务轮子只需考虑满足所有业务需求即可
  • 支持 text、emojiSelector、date 等类型
  • 实现 FormItem 的 v-model 双向绑定与错误提示显示
  • 添加表单标签、冒号后缀及额外提示信息渲染
  • 支持通过插槽自定义表单项内容,例如 文本、按钮等

@/components/form/FormItem.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
import { computed, defineComponent, type PropType, ref, type UnwrapRef } from 'vue'
import { EmojiSelector } from '@/modules/tags/EmojiSelector.tsx'
import { cn } from 'cn'
import type { FormDataType } from '@/shared/validator/formValidate.ts'

export type FormItemType = 'text' | 'emojiSelector' | 'date'

interface FormItemProps {
  uniqId?: string
  modelValue?: UnwrapRef<FormDataType[string]>
  type?: FormItemType
  error?: string
  hasErrorPlaceholder?: boolean
  label?: string
  hasLabelColon?: boolean
}

export const FormItem = defineComponent<FormItemProps>({
  name: 'FormItem',
  props: {
    // 使用 useId 生成唯一 id,可以区分类型相同的表单项,并且在 label 标签上的 for 属性上
    uniqId: { type: String as PropType<string>, default: '', },
    label: { type: String as PropType<string>, default: '', },
    hasLabelColon: { type: Boolean as PropType<boolean>, default: true, },
    type: { type: String as PropType<FormItemType>, },
    modelValue: {
      type: [String, Number, null, undefined,] as PropType<UnwrapRef<FormDataType[string]>>,
      default: '',
    },
    error: { type: String as PropType<string>, },
    hasErrorPlaceholder: { type: Boolean as PropType<boolean>, default: false, },
  },
  emit: ['update:modelValue',],
  setup( _props, _ctx ) {
    const extraHint = ref('')
    const itemContentCommonClass = cn('grow-1 min-h-[var(--input-min-height)] max-w-100%')
    const inputClass = cn(
      'p-x-1em text-18px',
      'b-1 rounded-[var(--input-radius)] shadow-[inset_0_0_3px_var(--input-shadow)]',
      _props.error ? 'b-[var(--error-color)]' : 'b-[var(--input-border-color)]',
    )
    const getModelValue = computed({
      get() {return _props.modelValue?.toString()},
      set(value) {_ctx.emit('update:modelValue', value)}
    })
    const itemContent = computed(() => {
      switch (_props.type) {
        case 'text':
          return (
            <input
              id={`form-item-${_props.type}-${_props.uniqId}`}
              v-model={getModelValue.value}
              class={inputClass} />
          )
        case 'emojiSelector':
          return (
            <EmojiSelector
              v-model={getModelValue.value}
              hasError={!!_props.error}
              v-model:extraHint={extraHint.value} />
          )
        case 'date':
          return <input />
        case undefined:
          return _ctx.slots.default?.()
      }
    })
    return () => (
      <div class="mt-.5em">
        <label for={`form-item-${_props.type}-${_props.uniqId}`} class="block">
          {/* 标题 */}
          {_props.label &&
            <span class="label_name">
              {_props.label}{_props.hasLabelColon ? ':' : ''}{extraHint.value}
            </span>
          }
          {/* 内容 */}
          <div class={cn(itemContentCommonClass, 'content-value flex mt-0.25em children:grow-1')}>
            {itemContent.value}
          </div>
          {/* 错误 */}
          {
            (_props.error || _props.hasErrorPlaceholder)  &&
            <div class={cn('error-hint min-h-18px m-t-.25em text-12px color-[var(--error-color)]')}>
              <span>{_props.error}</span>
            </div>
          }
        </label>
      </div>
    )
  },
})
  • 父组件可使用 useId 生成唯一 id,传参 uniqId,可以区分类型相同的表单项,并且在 label 标签上的 for 属性上
    • 但不可放在 key 属性上,否则每次输入时都会重新渲染组件
  • 使用计算属性作为 v-model 的双向绑定值 const getModelValue = computed({ get() {...}, set(value) {...}, })
  • 在 itemContent 后续可继续扩展其他类型的表单项,例如 date
  • hasErrorPlaceholder 用来给存在错误提示的表单项作为占位,防止高度抖动

@/components/form/FormContainer.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, type PropType } from 'vue'
import { cn } from 'cn'

export const FormContainer = defineComponent({
  name: 'FormContainer',
  props: {
    onSubmit: {
      type: Function as PropType<(e: SubmitEvent) => void>,
    },
  },
  setup(_props, _ctx) {
    return () => (
      <form
        onSubmit={_props.onSubmit}
        class={cn(
          'where:(p-1em min-h-720px)',
        )}>
        {_ctx.slots?.default?.()}
      </form>
    )
  },
})

@src/modules/tags/TagForm.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
import { defineComponent, reactive, toRaw, useId } from 'vue'
import { BarButton } from '@/components/BarButton.tsx'
import {
  type FormDataType,
  type FormRules,
  formValidate,
} from '@/shared/validator/formValidate.ts'
import { FormContainer } from '@/components/form/FormContainer.tsx'
import { FormItem } from '@/components/form/FormItem.tsx'

export const TagForm = defineComponent({
  name: 'TagForm',
  props: {
    /* 仅当编辑tag时,有表单默认数据,通过 props 传入;暂时占位 */
  },
  setup(/* _props, _ctx */) {
    const formData = reactive<FormDataType>({
      name: '',
      sign: '',
    })
    const errors = reactive<{[k in keyof FormDataType]?: string[]}>({})
    const onSubmit = (e: SubmitEvent) => {
      e.preventDefault()
      const rules = [
        { key: 'name', type: 'required', required: true, message: '必填', },
        { key: 'name', type: 'pattern', regexp: /^.{1,4}$/, message: '只能填1到4个字符', },
        { key: 'sign', type: 'required', required: true, message: '必填', },
      ] satisfies FormRules<typeof formData>
      // 每次先清空上一次报错
      Object.assign(errors, {name: undefined, sign: undefined,} )
      // reactive 无法重复赋值,只能覆盖key
      Object.assign(errors, formValidate(toRaw(formData), rules))
    }
    return () => (
      <FormContainer
        onSubmit={onSubmit}>
        <FormItem
          uniqId={useId()}
          type="text"
          label="标签名称"
          v-model={formData.name}
          hasErrorPlaceholder
          error={errors.name?.join(', ')}/>
        <FormItem
          uniqId={useId()}
          type="emojiSelector"
          label="符号"
          v-model={formData.sign} hasErrorPlaceholder
          error={errors.sign?.join(', ') }/>
        <FormItem>
          <p class="tips text-center py-1em px-0 min-h-[var(--input-min-height)]">
            记账时长按即可进行编辑
          </p>
        </FormItem>
        <FormItem>
          <BarButton
            type="submit"
            onClick={() => {}}
            class="formItem button formItem_value">确定</BarButton>
        </FormItem>
      </FormContainer>
    )
  },
})

对比之前的 @src/modules/tags/TagForm.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
import { defineComponent, reactive, toRaw, useId } from 'vue'
import { BarButton } from '@/components/BarButton.tsx'
import {
  type FormDataType,
  type FormRules,
  formValidate,
} from '@/shared/validator/formValidate.ts'

export const TagForm = defineComponent({
  name: 'TagForm',
  setup(/* _props, _ctx */) {
    const formData = reactive<FormDataType>({
      name: '',
      sign: '',
    })
    const errors = reactive<{[k in keyof FormDataType]?: string[]}>({})
    const onSubmit = (e: SubmitEvent) => {/* ... */}
    return () => (
      <form
        onSubmit={onSubmit}
        class="p-1em min-h-720px">
        <div class="mt-0.5em flex-grow-0">
          <label for="tag-name">
            <span>标签名称</span>

            <div class="flex mt-0.25em">
              <input
                id="tag-name"
                v-model={formData.name}
                class={cn(
                  'grow-1 min-h-[var(--input-min-height)] max-w-100%',
                  'b-1 rounded-[var(--input-radius)]',
                  'text-18px p-x-1em shadow-[inset_0_0_3px_var(--input-shadow)]',
                  errors.name ? 'b-[var(--error-color)]' : 'b-[var(--input-border-color)]',
                )}
              />
            </div>

            <div class="min-h-18px m-t-.25em text-12px color-[var(--error-color)]">
              <span>{errors.name?.join(', ')}</span>
            </div>
          </label>
        </div>

        <div class="mt-0.5em">
          <label class="flex flex-col">
            <span>符号 {emojiHint.value}</span>

            <div class="flex mt-0.25em">
              <EmojiSelector
                hasError={!!errors.sign?.join(', ')}
                v-model={formData.sign}
                v-model:extraHint={emojiHint.value}/>
            </div>

            <div class="min-h-18px m-t-.25em text-12px color-[var(--error-color)]">
              <span>{errors.sign?.join(', ')}</span>
            </div>
          </label>
        </div>

        <p class="tips flex-grow-0 text-center py-1em px-0 min-h-[var(--input-min-height)]">
          记账时长按即可进行编辑
        </p>

        <div class="flex-grow-0 mt-0.5em min-h-[var(--input-min-height)]">
          <div class="mt-0.25em">
            <BarButton
              type="submit"
              onClick={() => {}}
              class="formItem button formItem_value">确定</BarButton>
          </div>
        </div>
      </form>
    )
  },
})

5. 完成自定义时间弹出框 ⇧

封装 DatePickerPopup.tsx 组件,改造 DateInput.tsx ⇧

封装 今日日期数组 工具函数 @/const/genDateArrayOfToday.ts

1
2
3
4
export const genDateArrayOfToday = () => {
  const nowDate = new Date()
  return [`${nowDate.getFullYear()}`, `${nowDate.getMonth() + 1}`, `${nowDate.getDate()}`]
}

@/modules/items/DatePickerPopup.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
import { computed, defineComponent, type PropType, ref } from 'vue'
import { DatePicker, Popup } from 'vant'
import { DateTime } from '@/shared/dateTime.ts'
import { genDateArrayOfToday } from '@/const/genDateArrayOfToday.ts'

export const DatePickerPopup = defineComponent({
  name: 'DatePickerPopup',
  props: {
    popupVisible: { type: Boolean as PropType<boolean>, default: false, },
    dateStrArray: { type: Array as PropType<string[]>, default: () => genDateArrayOfToday(), }
  },
  emits: ['update:popupVisible', 'update:dateStrArray'],
  setup( _props, _ctx ) {
    const lastDateRef = ref<string[]>(_props.dateStrArray ?? genDateArrayOfToday())
    const [minDate, maxDate,] = [
      new DateTime(new Date()).add(-1, 'year').getRawDate(),
      new DateTime(new Date()).add(1, 'year').getRawDate(),
    ]
    const getPopupVisible = computed({
      get: () => _props.popupVisible,
      set: (value) => _ctx.emit('update:popupVisible', value),
    })
    const getDateStrArray = computed({
      get: () => _props.dateStrArray,
      set: (value) => _ctx.emit('update:dateStrArray', value),
    })
    const confirmDate = ({ selectedValues }: { selectedValues: string[] }) => {
      lastDateRef.value = selectedValues
      getDateStrArray.value = selectedValues
      getPopupVisible.value = false
    }
    const cancelDate = () => {
      getDateStrArray.value = lastDateRef.value
      getPopupVisible.value = false
    }
    return () => (
      <Popup
        position="bottom"
        v-model:show={getPopupVisible.value}
        onClose={cancelDate}>
        <DatePicker
          title="选择年月日"
          v-model={getDateStrArray.value}
          minDate={minDate}
          maxDate={maxDate}
          onConfirm={confirmDate}
          onCancel={cancelDate}/>
      </Popup>
    )
  },
})

@/modules/items/DateInput.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 { computed, defineComponent, ref } from 'vue'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { DateTime } from '@/shared/dateTime.ts'
import { DatePickerPopup } from '@/modules/items/DatePickerPopup.tsx'

export const DateInput = defineComponent({
  name: 'DateInput',
  setup(/* _props, _ctx */) {
    const datePickerVisibleRef = ref(false)
    const nowDate = new Date()
    const jsonDateStr = JSON.stringify([`${nowDate.getFullYear()}`, `${nowDate.getMonth() + 1}`, `${nowDate.getDate()}`])
    const currentDateRef = ref<string[]>(JSON.parse(jsonDateStr))
    const formattedDateStr = computed(() =>
      (new DateTime(new Date(currentDateRef.value.join('-')))).format(),
    )
    const toggleDatePicker = () => {
      datePickerVisibleRef.value = !datePickerVisibleRef.value
    }

    return () => (
      <span class="flex justify-start items-center">
        <SvgIcon
          onClick={toggleDatePicker}
          name="date"
          class="size-2em mx-1 fill-[var(--button-main)]"
        />
        <span class="animation-rainbow text-14px font-mono color-[var(--vp-c-brand-next)]">
          <span onClick={toggleDatePicker}>{formattedDateStr.value}</span>
          <DatePickerPopup
            v-model:popupVisible={datePickerVisibleRef.value}
            v-model:dateStrArray={currentDateRef.value}
          />
        </span>
      </span>
    )
  },
})

对比原来 DateInput.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
import { computed, defineComponent, ref } from 'vue'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { DateTime } from '@/shared/dateTime.ts'
import { DatePicker, Popup } from 'vant'

export const DateInput = defineComponent({
  name: 'DateInput',
  setup(/* _props, _ctx */) {
    const datePickerVisibleRef = ref(false)
    const nowDate = new Date()
    const jsonDateStr = JSON.stringify([`${nowDate.getFullYear()}`, `${nowDate.getMonth() + 1}`, `${nowDate.getDate()}`])
    const lastDateRef = ref<string[]>(JSON.parse(jsonDateStr))
    const currentDateRef = ref<string[]>(JSON.parse(jsonDateStr))
    const formattedDateStr = computed(() =>
      (new DateTime(new Date(currentDateRef.value.join('-')))).format(),
    )
    const toggleDatePicker = () => {
      datePickerVisibleRef.value = !datePickerVisibleRef.value
    }
    const [minDate, maxDate,] = [
      new DateTime(new Date()).add(-1, 'year').getRawDate(),
      new DateTime(new Date()).add(1, 'year').getRawDate(),
    ]
    const confirmDate = ({ selectedValues }: { selectedValues: string[] }) => {
      lastDateRef.value = selectedValues
      currentDateRef.value = selectedValues
      datePickerVisibleRef.value = false
    }
    const cancelDate = () => {
      currentDateRef.value = lastDateRef.value
      datePickerVisibleRef.value = false
    }
    return () => (
      <span class="flex justify-start items-center">
        <SvgIcon
          onClick={toggleDatePicker}
          name="date"
          class="size-2em mx-1 fill-[var(--button-main)]" />
        <span class="animation-rainbow text-14px font-mono color-[var(--vp-c-brand-next)]">
          <span onClick={toggleDatePicker}>{formattedDateStr.value}</span>
          <Popup
            position="bottom"
            v-model:show={datePickerVisibleRef.value}
            onClose={cancelDate}>
            <DatePicker
              title="选择年月日"
              v-model={currentDateRef.value}
              minDate={minDate}
              maxDate={maxDate}
              onConfirm={confirmDate}
              onCancel={cancelDate} />
          </Popup>
        </span>
      </span>
    )
  },
})

处理日期格式字符串 Vant 的 DatePicker 绑定值互相转换的问题 ⇧

重构 @/const/genDateArrayOfToday.ts 为 @/const/genDateStr.ts 生成通用日期数组

1
2
3
4
5
6
export const genDateStr = (defaultDateValue: Date = new Date()): string =>
  [
    `${defaultDateValue.getFullYear()}`,
    `${defaultDateValue.getMonth() + 1}`,
    `${defaultDateValue.getDate()}`,
  ].join('-')

@/components/form/DatePickerPopup.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 { computed, defineComponent, type PropType, ref } from 'vue'
import { DatePicker, Popup } from 'vant'
import { DateTime } from '@/shared/dateTime.ts'
import { genDateStr } from '@/const/genDateStr.ts'

export const DatePickerPopup = defineComponent({
  name: 'DatePickerPopup',
  props: {
    popupVisible: { type: Boolean as PropType<boolean>, default: false, },
    dateStr: { type: String as PropType<string>, default: () => genDateStr(), }
  },
  emits: ['update:popupVisible', 'update:dateStr',],
  setup( _props, _ctx ) {
    const lastDateStrRef = ref<string>(_props.dateStr ?? genDateStr())
    const [minDate, maxDate,] = [
      new DateTime(new Date()).add(-1, 'year').getRawDate(),
      new DateTime(new Date()).add(1, 'year').getRawDate(),
    ]
    const getPopupVisible = computed({
      get: () => _props.popupVisible,
      set: (value) => _ctx.emit('update:popupVisible', value),
    })
    const getDateStrArrayTransfer = computed({
      // '2026-10-22' -> [2026, 10, 22]
      get: () => _props.dateStr.split('-'),
      // [2026, 10, 22] -> '2026-10-22'
      set: (value: string[]) => _ctx.emit('update:dateStr', value.join('-')),
    })
    const confirmDate = ({ selectedValues }: { selectedValues: string[] }) => {
      lastDateStrRef.value = selectedValues.join('-') // '2026-10-22'
      getDateStrArrayTransfer.value = selectedValues // [2026, 10, 22]
      getPopupVisible.value = false
    }
    const cancelDate = () => {
      getDateStrArrayTransfer.value = lastDateStrRef.value.split('-') // [2026, 10, 22]
      getPopupVisible.value = false
    }
    return () => (
      <Popup
        position="bottom"
        v-model:show={getPopupVisible.value}
        onClose={cancelDate}>
        <DatePicker
          title="选择年月日"
          v-model={getDateStrArrayTransfer.value}
          minDate={minDate}
          maxDate={maxDate}
          onConfirm={confirmDate}
          onCancel={cancelDate}/>
      </Popup>
    )
  },
})
  • 外部传参改为 dateStr
  • 发布事件名称改为 'update:dateStr'
  • 引用该组件的双向绑定需要改为 v-model:dateStr={}
  • 可写的计算属性 getDateStrArrayTransfer 需要在 '2026-10-22' <-> [2026, 10, 22] 格式之间转换
    • 返回值使用 dateStr.split('-') 转换 '2026-10-22' -> [2026, 10, 22]
    • 设置值使用 value.join('-') 转换 [2026, 10, 22] -> '2026-10-22'
  • 其他相关数据涉及同样修改
    • lastDateStrRef
    • confirmDate
    • cancelDate

@/modules/items/DateInput.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
import { defineComponent, ref } from 'vue'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'
import { DatePickerPopup } from '@/components/form/DatePickerPopup.tsx'
import { genDateStr } from '@/const/genDateStr.ts'

export const DateInput = defineComponent({
  name: 'DateInput',
  setup(/* _props, _ctx */) {
    const datePickerVisibleRef = ref(false)
    const currentDateStrRef = ref<string>(genDateStr())
    const toggleDatePicker = () => {
      datePickerVisibleRef.value = !datePickerVisibleRef.value
    }
    return () => (
      <span class="flex justify-start items-center">
        <SvgIcon
          onClick={toggleDatePicker}
          name="date"
          class="size-2em mx-1 fill-[var(--main)]" />
        <span class="animation-rainbow text-14px font-mono color-[var(--vp-c-brand-next)]">
          <span onClick={toggleDatePicker}>{currentDateStrRef.value}</span>
          <DatePickerPopup
            v-model:popupVisible={datePickerVisibleRef.value}
            v-model:dateStr={currentDateStrRef.value} />
        </span>
      </span>
    )
  },
})

为 FormItem.tsx 添加 弹出日期选择器组件 的类型选项 ⇧

@/components/form/FormItem.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
121
122
import { computed, defineComponent, type PropType, ref, type UnwrapRef, useId } from 'vue'
import { EmojiSelector } from '@/modules/tags/EmojiSelector.tsx'
import { cn } from 'cn'
import type { FormDataType } from '@/shared/validator/formValidate.ts'
import { BarButton } from '@/components/BarButton.tsx'
import { LabelOrDivWrapper } from '@/components/form/LabelOrDivWrapper.tsx'
import { DatePickerPopup } from '@/components/form/DatePickerPopup.tsx'

export type FormItemType = 'text' | 'emojiSelector' | 'date' | 'submit-button'

interface FormItemProps {
  modelValue?: UnwrapRef<FormDataType[string]>
  type?: FormItemType
  error?: string
  hasErrorPlaceholder?: boolean
  label?: string
  hasLabelColon?: boolean
}

export const FormItem = defineComponent<FormItemProps>({
  name: 'FormItem',
  props: {
    label: { type: String as PropType<string>, default: '', },
    hasLabelColon: { type: Boolean as PropType<boolean>, default: true, },
    type: { type: String as PropType<FormItemType>, },
    modelValue: {
      type: [String, Number, null, undefined,] as PropType<UnwrapRef<FormDataType[string]>>,
      default: '',
    },
    error: { type: String as PropType<string>, },
    hasErrorPlaceholder: { type: Boolean as PropType<boolean>, default: false, },
  },
  emit: ['update:modelValue',],
  setup( _props, _ctx ) {
    const extraHint = ref('')
    const getModelValue = computed({
      get() {return _props.modelValue?.toString()},
      set(value) {_ctx.emit('update:modelValue', value)}
    })
    const getIfNeedLabelFor = computed(() =>
      ['text', 'submit-button'].includes(_props?.type ?? ''))
    const getLabelFor = computed(() => getIfNeedLabelFor.value
        ? `form-item-${_props.type}-${useId()}`
        : '')

    const dateVisibleRef = ref(false)
    const itemContent = computed(() => {
      switch (_props.type) {
        case 'text':
          return (
            <input
              id={getLabelFor.value}
              type="date"
              v-model={getModelValue.value}
              class={
                cn(
                  'p-x-1em text-18px',
                  'b-1 rounded-[var(--input-radius)] shadow-[inset_0_0_3px_var(--input-shadow)]',
                  _props.error ? 'b-[var(--error-color)]' : 'b-[var(--input-border-color)]',
                )
              }/>
          )
        case 'emojiSelector':
          return (
            <EmojiSelector
              v-model={getModelValue.value}
              hasError={!!_props.error}
              v-model:extraHint={extraHint.value} />
          )
        case 'submit-button':
          return <BarButton id={getLabelFor.value} type="submit">确定</BarButton>
        case 'date':
          return (
            <>
              <input
                id={getLabelFor.value}
                readonly={true}
                value={getModelValue.value}
                onClick={() => {dateVisibleRef.value = true}}
                class={
                  cn(
                    'p-x-1em text-18px',
                    'b-1 rounded-[var(--input-radius)] shadow-[inset_0_0_3px_var(--input-shadow)]',
                    _props.error ? 'b-[var(--error-color)]' : 'b-[var(--input-border-color)]',
                  )
                }/>
              <DatePickerPopup
                v-model:popupVisible={dateVisibleRef.value}
                v-model:dateStr={getModelValue.value} />
            </>
          )
        case undefined:
          return _ctx.slots.default?.()
      }
    })
    return () => (
      <div class="mt-.5em">
        <LabelOrDivWrapper
          ifUseLabelTag={getIfNeedLabelFor.value}
          labelFor={getLabelFor.value}>
          {/* 标题 */}
          {_props.label &&
            <span class="label_name">
              {_props.label}{_props.hasLabelColon ? ':' : ''}{extraHint.value}
            </span>
          }
          {/* 内容 */}
          <div class={cn('content-value flex mt-0.25em grow-1 children:grow-1 min-h-[var(--input-min-height)] max-w-100%')}>
            {itemContent.value}
          </div>
          {/* 错误 */}
          {
            (_props.error || _props.hasErrorPlaceholder)  &&
            <div class={cn('error-hint min-h-18px m-t-.25em text-12px color-[var(--error-color)]')}>
              <span>{_props.error}</span>
            </div>
          }
        </LabelOrDivWrapper>
      </div>
    )
  },
})
  • 添加 type 类型的 FormItem
    • 只读属性 readonly={true}
    • 数据为日期字符串 value={getModelValue.value}
    • 点击打开日期选择弹出框 onClick={() => {dateVisibleRef.value = true}}

完成 ItemList.tsx 自定义日期范围 ⇧

@/components/form/FormItem.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
import { defineComponent, reactive, ref, watchEffect } 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 { ItemSummary } from '@/modules/items/ItemSummary.tsx'
import { DateTime } from '@/shared/dateTime.ts'
import { FloatButton } from '@/components/FloatButton.tsx'
import { Overlay } from 'vant'
import { FormContainer } from '@/components/form/FormContainer.tsx'
import { FormItem } from '@/components/form/FormItem.tsx'

export const ItemList = defineComponent({
  name: 'ItemList',
  setup(/* _props, _ctx */) {
    const selectedDateRef = ref('本月')
    const currentDate = new DateTime()
    const customDate = reactive<{ start: string; end: string }>({
      start: currentDate.getOriginDate()?.format(),
      end: currentDate.getOriginDate()?.format()
    })
    const dateList = [
      {start: currentDate.firstDayOfMonth(), end: currentDate.lastDayOfMonth()},
      {
        start: currentDate.subtract(1, 'month').firstDayOfMonth(),
        end: currentDate.subtract(1, 'month').lastDayOfMonth()
      },
      {start: currentDate.firstDayOfYear(), end: currentDate.lastDayOfYear()},
    ] as const
    const refOverlayVisible = ref(false)
    watchEffect(() => {
      refOverlayVisible.value = selectedDateRef.value === '自定义日期范围'
    })
    // 表单操作
    const onSubmitCustomDateRange = (e: SubmitEvent) => {
      e.preventDefault()
      refOverlayVisible.value = false
    }
    const onCnacelCustomDateRange = () => {
      refOverlayVisible.value = false
    }
    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">
                  <ItemSummary
                    startDate={dateList[0].start.format()}
                    endDate={dateList[0].end.format()} />
                </ItemTab>
                <ItemTab tabName="上月" tabId="lastMonth">
                  <ItemSummary
                    startDate={dateList[1].start.format()}
                    endDate={dateList[1].end.format()} />
                </ItemTab>
                <ItemTab tabName="今年" tabId="thisYear">
                  <ItemSummary
                    startDate={dateList[2].start.format()}
                    endDate={dateList[2].end.format()} />
                </ItemTab>
                <ItemTab tabName="自定义日期范围" tabId="customDateRange">
                  <ItemSummary
                    startDate={customDate.start}
                    endDate={customDate.end} />
                </ItemTab>
              </SwitchTabs>

              {/* 自定义日期范围弹出层 */}
              <Overlay
                show={refOverlayVisible.value}
                onClick={() => (refOverlayVisible.value = false)}
                class="flex items-center justify-center b-3 b-solid b-red bg-[var(--overlay-mask-bg)]" >
                <div
                  onClick={(e: MouseEvent) => e.stopPropagation()}
                  class="bg-[var(--overlay-bg)] w-80vw">
                  <header class="bg-[var(--main)] text-[var(--dialog-header-text)] py-12px px-1em">
                    请选择时间
                  </header>
                  <main>
                    <FormContainer onSubmit={onSubmitCustomDateRange}>
                      <FormItem type="date" label="开始时间" v-model={customDate.start} />
                      <FormItem type="date" label="结束时间" v-model={customDate.end} />
                      <footer class="flex justify-between mt-1em children:(w-[calc(50%-.25em)])">
                        <FormItem type="button" onClick={onCnacelCustomDateRange} buttonConfig={{level: 'default', text: '取 消' }}/>
                        <FormItem type="submit-button" buttonConfig={{level: 'primary', text: '确 认' }}/>
                      </footer>
                    </FormContainer>
                  </main>
                </div>
              </Overlay>

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

后续自定义时间限制;最早时间,日期跨度最大前后3年


·未完待续·

参考文章

相关文章

  • 无

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