日期范围快捷选择拓展
大家好,最近在项目中,我遇到了一些关于日期选择的小挑战,决定跟大家分享一下我的解决方案,希望对你们有所帮助。
一、背景故事
我们都知道,在前端开发中,日期选择器是个常用的组件。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.json
和 attributes.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.json
和 attributes.json
太麻烦,可以尝试写个脚本自动生成,或者考虑使用 TypeScript,让你的代码更健壮,智能提示也会更加完美!