关于我是如何二次开发了 antd-vue 的a-range-picker组件,同时还添加了 vscod智能提示这件事

日期范围快捷选择拓展

大家好,最近在项目中,我遇到了一些关于日期选择的小挑战,决定跟大家分享一下我的解决方案,希望对你们有所帮助。

一、背景故事

我们都知道,在前端开发中,日期选择器是个常用的组件。Ant Design Vue(版本 1.7.2)的 a-range-picker 组件用起来也挺顺手的。但是,有时候它的快捷选择范围不能完全满足我们的业务需求,比如想要添加"最近三个月"或者"未来七天"这样的选项。

而且,在非 TypeScript 的 Vue2 项目中,VSCode 对自定义组件的属性提示并不友好,写起代码来总感觉少了点啥。于是,我决定 roll up my sleeves(撸起袖子)来解决这两个问题!

二、拓展日期范围选择器

1. 自定义日期范围选择组件

首先,我们需要封装一个自己的日期范围选择组件,对 a-range-picker 进行二次开发,添加我们需要的快捷选择。

下面是完整的组件代码,不要被吓到,我会逐步解释的!

vue 复制代码
<template>
  <div>
    <!-- 使用自定义的快捷选择日期范围 -->
    <a-range-picker
      v-bind="$attrs"
      v-model="dateArr"
      :format="format"
      :show-time="disableTime ? false : showTimeOptions"
      :ranges="presetRanges"
      style="width: 100%"
      @click="onDatePickerClick"
    />
  </div>
</template>

<script>
import moment from 'moment'

export default {
  name: 'DateRangePicker',
  props: {
    // 开始日期
    start: {
      type: String,
      default: ''
    },
    // 结束日期
    end: {
      type: String,
      default: ''
    },
    // 是否禁用时间选择
    disableTime: {
      type: Boolean,
      default: false
    },
    // 日期格式
    format: {
      type: String,
      default: 'YYYY-MM-DD HH:mm:ss'
    },
    // 允许的快捷日期范围
    allowedRanges: {
      type: Array,
      default: () => [
        'today',
        'yesterday',
        'last7Days',
        'last30Days',
        'last90Days',
        'thisMonth',
        'lastMonth',
        'last3Months',
        'last6Months',
        'tomorrow',
        'next3Days',
        'next7Days',
        'next1Month',
        'next3Months',
        'next6Months'
      ]
    },
    // 日期方向
    dateDirection: {
      type: String,
      default: 'past',
      validator: value => ['past', 'next', 'recent'].includes(value)
    },
    // 是否限制为最近六个月
    limitToSixMonths: {
      type: Boolean,
      default: true
    }
  },
  computed: {
    // 日期数组,用于双向绑定
    dateArr: {
      get() {
        return [
          this.start ? moment(this.start, this.format) : null,
          this.end ? moment(this.end, this.format) : null
        ]
      },
      set(newValue) {
        const [startDate, endDate] = newValue
        this.$emit('update:start', startDate ? startDate.format(this.format) : '')
        this.$emit('update:end', endDate ? endDate.format(this.format) : '')
      }
    },
    // 时间选项配置
    showTimeOptions() {
      return {
        defaultValue: [
          moment('00:00:00', 'HH:mm:ss'),
          moment('23:59:59', 'HH:mm:ss')
        ],
        format: 'HH:mm:ss'
      }
    },
    // 快捷选择范围
    presetRanges() {
      const today = moment()
      const ranges = {
        // 过去的日期范围
        today: ['今天', [today.clone().startOf('day'), today.clone().endOf('day')]],
        yesterday: [
          '昨天',
          [
            today.clone().subtract(1, 'days').startOf('day'),
            today.clone().subtract(1, 'days').endOf('day')
          ]
        ],
        last7Days: [
          '最近7天',
          [
            today.clone().subtract(7, 'days').startOf('day'),
            today.clone().endOf('day')
          ]
        ],
        last30Days: [
          '最近30天',
          [
            today.clone().subtract(30, 'days').startOf('day'),
            today.clone().endOf('day')
          ]
        ],
        last90Days: [
          '最近90天',
          [
            today.clone().subtract(90, 'days').startOf('day'),
            today.clone().endOf('day')
          ]
        ],
        thisMonth: ['本月', [today.clone().startOf('month'), today.clone().endOf('month')]],
        lastMonth: [
          '上个月',
          [
            today.clone().subtract(1, 'months').startOf('month'),
            today.clone().subtract(1, 'months').endOf('month')
          ]
        ],
        last3Months: [
          '最近3个月',
          [
            today.clone().subtract(3, 'months').startOf('month'),
            today.clone().endOf('month')
          ]
        ],
        last6Months: [
          '最近6个月',
          [
            today.clone().subtract(6, 'months').startOf('month'),
            today.clone().endOf('month')
          ]
        ],
        // 未来的日期范围
        tomorrow: [
          '明天',
          [
            today.clone().add(1, 'days').startOf('day'),
            today.clone().add(1, 'days').endOf('day')
          ]
        ],
        next3Days: [
          '未来三天',
          [
            today.clone().add(1, 'days').startOf('day'),
            today.clone().add(3, 'days').endOf('day')
          ]
        ],
        next7Days: [
          '未来七天',
          [
            today.clone().add(1, 'days').startOf('day'),
            today.clone().add(7, 'days').endOf('day')
          ]
        ],
        next1Month: [
          '未来一个月',
          [
            today.clone().add(1, 'days').startOf('day'),
            today.clone().add(1, 'months').endOf('month')
          ]
        ],
        next3Months: [
          '未来三个月',
          [
            today.clone().add(1, 'days').startOf('day'),
            today.clone().add(3, 'months').endOf('month')
          ]
        ],
        next6Months: [
          '未来六个月',
          [
            today.clone().add(1, 'days').startOf('day'),
            today.clone().add(6, 'months').endOf('month')
          ]
        ]
      }

      // 过滤出需要的快捷范围
      const filteredRanges = {}
      this.allowedRanges.forEach(rangeKey => {
        const isPastRange =
          rangeKey.startsWith('last') ||
          ['today', 'yesterday', 'thisMonth', 'lastMonth'].includes(rangeKey)
        const isNextRange =
          rangeKey.startsWith('next') || ['tomorrow'].includes(rangeKey)

        if (
          (this.dateDirection === 'past' && isPastRange) ||
          (this.dateDirection === 'next' && isNextRange) ||
          this.dateDirection === 'recent'
        ) {
          if (ranges[rangeKey]) {
            let [label, range] = ranges[rangeKey]
            let [startDate, endDate] = range

            // 限制日期范围
            if (this.limitToSixMonths) {
              const sixMonthsAgo = moment()
                .subtract(6, 'months')
                .startOf('day')
              const sixMonthsLater = moment()
                .add(6, 'months')
                .endOf('day')
              startDate = moment.max(startDate, sixMonthsAgo)
              endDate = moment.min(endDate, sixMonthsLater)
            }

            // 确保开始日期不晚于结束日期
            if (startDate.isSameOrBefore(endDate)) {
              filteredRanges[label] = [startDate, endDate]
            }
          }
        }
      })

      return filteredRanges
    }
  },
  mounted() {
    // 设置默认日期范围
    this.setDefaultDateRange()
  },
  methods: {
    // 禁用日期函数
    disabledDate(current) {
      if (!this.limitToSixMonths) {
        return false
      }

      const today = moment().startOf('day')
      if (this.dateDirection === 'past') {
        const sixMonthsAgo = moment()
          .subtract(6, 'months')
          .startOf('day')
        return (
          current &&
          (current.isAfter(today, 'day') || current.isBefore(sixMonthsAgo, 'day'))
        )
      } else if (this.dateDirection === 'next') {
        return current && current.isBefore(today, 'day')
      } else if (this.dateDirection === 'recent') {
        const sixMonthsAgo = moment()
          .subtract(6, 'months')
          .startOf('day')
        const sixMonthsLater = moment()
          .add(6, 'months')
          .endOf('day')
        return (
          current &&
          (current.isBefore(sixMonthsAgo, 'day') || current.isAfter(sixMonthsLater, 'day'))
        )
      }
      return false
    },
    // 点击事件,刷新快捷选择
    onDatePickerClick() {
      this.$forceUpdate()
    },
    // 设置默认日期范围
    setDefaultDateRange() {
      let startDate, endDate
      const today = moment()

      switch (this.defaultRange) {
        case 'yesterday':
          startDate = today.clone().subtract(1, 'days').startOf('day')
          endDate = today.clone().subtract(1, 'days').endOf('day')
          break
        case 'lastMonth':
          startDate = today.clone().subtract(1, 'months').startOf('month')
          endDate = today.clone().subtract(1, 'months').endOf('month')
          break
        case 'last30Days':
          startDate = today.clone().subtract(30, 'days').startOf('day')
          endDate = today.clone().endOf('day')
          break
        case 'last90Days':
          startDate = today.clone().subtract(90, 'days').startOf('day')
          endDate = today.clone().endOf('day')
          break
        case 'last3Months':
          startDate = today.clone().subtract(3, 'months').startOf('month')
          endDate = today.clone().endOf('month')
          break
        default:
          return
      }

      this.$emit('update:start', startDate.format(this.format))
      this.$emit('update:end', endDate.format(this.format))
    }
  }
}
</script>

<style scoped lang="less"></style>

2. 组件解析

这个组件主要是为了让 a-range-picker 支持我们自定义的快捷日期选择。通过 allowedRanges 属性,我们可以控制显示哪些快捷选项。

  • allowedRanges :你可以自由地添加你需要的快捷选项,比如 'last7Days''next1Month' 等。
  • dateDirection:控制日期的方向,是选择过去的日期、未来的日期,还是两者都可以。
  • limitToSixMonths:限制日期范围在最近六个月内,防止用户选择太远的日期。

三、实现 VSCode 中的组件属性智能提示

在非 TypeScript 的 Vue2 项目中,我们也可以让 VSCode 对自定义组件的属性提供智能提示。方法就是配置 Vetur 插件!

1. 创建 tags.jsonattributes.json

在项目根目录下,创建 tags.json

json 复制代码
{
  "DateRangePicker": {
    "description": "日期范围选择组件",
    "attributes": [
      "start",
      "end",
      "disable-time",
      "format",
      "allowed-ranges",
      "date-direction",
      "limit-to-six-months"
    ]
  }
}

然后创建 attributes.json

json 复制代码
{
  "DateRangePicker/start": {
    "type": "string",
    "description": "开始日期,格式为字符串"
  },
  "DateRangePicker/end": {
    "type": "string",
    "description": "结束日期,格式为字符串"
  },
  "DateRangePicker/disable-time": {
    "type": "boolean",
    "description": "是否禁用时间选择"
  },
  "DateRangePicker/format": {
    "type": "string",
    "description": "日期格式,默认为 'YYYY-MM-DD HH:mm:ss'"
  },
  "DateRangePicker/allowed-ranges": {
    "type": "array",
    "description": "允许用户选择的快捷预设日期范围"
  },
  "DateRangePicker/date-direction": {
    "type": "string",
    "description": "日期范围的方向:'past'、'next' 或 'recent'"
  },
  "DateRangePicker/limit-to-six-months": {
    "type": "boolean",
    "description": "是否限制日期范围为最近六个月"
  }
}

2. 配置 Vetur

package.json 中添加:

json 复制代码
{
  // ...其他配置
  "vetur": {
    "tags": "./tags.json",
    "attributes": "./attributes.json"
  }
}

3. 重启 VSCode

完成配置后,重启 VSCode,或者执行 Developer: Reload Window,让 Vetur 读取新的配置。

4. 验证效果

现在,当你在模板中使用 <DateRangePicker> 时,VSCode 会自动提示可用的属性,甚至在你悬停在属性上时,还会显示我们在 attributes.json 中写的描述,是不是很方便?

四、总结

通过以上步骤,我们成功地拓展了日期范围选择器的快捷选择功能,让它更加符合我们的业务需求。同时,在非 TypeScript 的项目中,我们也能享受到 VSCode 组件属性智能提示带来的便利。

希望我的分享能对你有所帮助,如果你有更好的方法或者建议,欢迎一起讨论!


小彩蛋 :如果你觉得每次手动更新 tags.jsonattributes.json 太麻烦,可以尝试写个脚本自动生成,或者考虑使用 TypeScript,让你的代码更健壮,智能提示也会更加完美!

相关推荐
wycode21 分钟前
Vue2实践(3)之用component做一个动态表单(二)
前端·javascript·vue.js
wycode1 小时前
Vue2实践(2)之用component做一个动态表单(一)
前端·javascript·vue.js
第七种黄昏1 小时前
Vue3 中的 ref、模板引用和 defineExpose 详解
前端·javascript·vue.js
pepedd8642 小时前
还在开发vue2老项目吗?本文带你梳理vue版本区别
前端·vue.js·trae
前端缘梦3 小时前
深入理解 Vue 中的虚拟 DOM:原理与实战价值
前端·vue.js·面试
HWL56793 小时前
pnpm(Performant npm)的安装
前端·vue.js·npm·node.js
柯南95273 小时前
Vue 3 reactive.ts 源码理解
vue.js
柯南95274 小时前
Vue 3 Ref 源码解析
vue.js
小高0074 小时前
面试官:npm run build 到底干了什么?从 package.json 到 dist 的 7 步拆解
前端·javascript·vue.js
JayceM5 小时前
Vue中v-show与v-if的区别
前端·javascript·vue.js