关于我是如何二次开发了 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,让你的代码更健壮,智能提示也会更加完美!

相关推荐
Cachel wood14 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
一个处女座的程序猿O(∩_∩)O3 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
燃先生._.9 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
2401_8576009512 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_8576009512 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL12 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
轻口味13 小时前
Vue.js 核心概念:模板、指令、数据绑定
vue.js
2402_8575834913 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js
java_heartLake14 小时前
Vue3之性能优化
javascript·vue.js·性能优化
ddd君3177415 小时前
组件的声明、创建、渲染
vue.js