You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

563 lines
14 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view>
<wd-picker-view
:custom-class="customClass"
ref="datePickerview"
v-model="pickerValue"
:columns="columns"
:columns-height="columnsHeight"
:columnChange="columnChange"
:loading="loading"
:loading-color="loadingColor"
@change="onChange"
@pickstart="onPickStart"
@pickend="onPickEnd"
></wd-picker-view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-datetime-picker-view',
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared'
}
</script>
<script lang="ts" setup>
import { getCurrentInstance, onBeforeMount, ref, watch } from 'vue'
import { debounce, getType, isDef, padZero, range } from '../common/util'
import { type DateTimeType, getPickerValue } from './type'
// 本地时间戳
/** @description 判断时间戳是否合法 */
const isValidDate = (date) => isDef(date) && !Number.isNaN(date)
/**
* @description 生成n个元素并使用iterator接口进行填充
* @param n
* @param iteratee
* @return {any[]}
*/
const times = (n, iteratee) => {
let index = -1
const length = n < 0 ? 0 : n
const result = Array(length)
while (++index < n) {
result[index] = iteratee(index)
}
return result
}
/**
* @description 获取某年某月有多少天
* @param {Number} year
* @param {Number} month
* @return {Number} day
*/
const getMonthEndDay = (year, month) => {
return 32 - new Date(year, month - 1, 32).getDate()
}
interface Props {
customClass?: string
// 选中项,当 type 为 time 时,类型为字符串,否则为 Date
modelValue: string | number | Date
// PickerView的Props 开始
// 加载中
loading?: boolean
loadingColor?: string
// 选项总高度
columnsHeight?: number
// 选项对象中value对应的 key
valueKey?: string
// 选项对象中,展示的文本对应的 key
labelKey?: string
// PickerView的Props 结束
// 时间选择器的类型
type?: DateTimeType
// 自定义过滤选项的函数,返回列的选项数组
// eslint-disable-next-line @typescript-eslint/ban-types
filter?: Function
// 自定义弹出层选项文案的格式化函数,返回一个字符串
// eslint-disable-next-line @typescript-eslint/ban-types
formatter?: Function
// 自定义列筛选条件
// eslint-disable-next-line @typescript-eslint/ban-types
columnFormatter?: Function
// 最小日期 20(x-10)年1月1日
minDate?: number
// 最大日期 20(x+10)年1月1日
maxDate?: number
// 最小小时
minHour?: number
// 最大小时
maxHour?: number
// 最小分钟
minMinute?: number
// 最大分钟
maxMinute?: number
startSymbol?: boolean
}
const props = withDefaults(defineProps<Props>(), {
customClass: '',
// PickerView的Props 开始
// 加载中
loading: false,
loadingColor: '#4D80F0',
// 选项总高度
columnsHeight: 217,
// 选项对象中value对应的 key
valueKey: 'value',
// 选项对象中,展示的文本对应的 key
labelKey: 'label',
type: 'datetime',
minDate: new Date(new Date().getFullYear() - 10, 0, 1).getTime(),
maxDate: new Date(new Date().getFullYear() + 10, 11, 31).getTime(),
minHour: 0,
maxHour: 23,
minMinute: 0,
maxMinute: 59,
startSymbol: false
})
// pickerview
const datePickerview = ref()
// 内部保持时间戳的
const innerValue = ref<null | number>(null)
// 传递给pickerView的columns的数据
const columns = ref<Array<string | number>>([])
// 传递给pickerView的value的数据
const pickerValue = ref<string | number | boolean | Array<string | number | boolean>>([])
// 自定义组件是否已经调用created hook
const created = ref<boolean>(false)
const { proxy } = getCurrentInstance() as any
/**
* @description updateValue 防抖函数的占位符
*/
const updateValue = debounce(() => {
// 只有等created hook初始化数据之后observer才能执行此操作
if (!created.value) return
const val = correctValue(props.modelValue)
const isEqual = val === innerValue.value
if (!isEqual) {
updateColumnValue(val)
} else {
columns.value = updateColumns()
}
}, 50)
watch(
() => props.modelValue,
(val, oldVal) => {
if (val === oldVal) return
// 外部传入值更改时 更新picker数据
const value = correctValue(val)
updateColumnValue(value)
},
{ deep: true, immediate: true }
)
watch(
() => props.type,
(target) => {
const type = ['date', 'year-month', 'time', 'datetime']
if (type.indexOf(target) === -1) {
console.error(`type must be one of ${type}`)
}
// 每次type更新时都需要刷新整个列表
updateValue()
},
{ deep: true, immediate: true }
)
watch(
() => props.filter,
(fn) => {
if (fn && getType(fn) !== 'function') {
console.error('The type of filter must be Function')
}
updateValue()
},
{ deep: true, immediate: true }
)
watch(
() => props.formatter,
(fn) => {
if (fn && getType(fn) !== 'function') {
console.error('The type of formatter must be Function')
}
updateValue()
},
{ deep: true, immediate: true }
)
watch(
() => props.columnFormatter,
(fn) => {
if (fn && getType(fn) !== 'function') {
console.error('The type of columnFormatter must be Function')
}
updateValue()
},
{ deep: true, immediate: true }
)
watch(
[
() => props.minDate,
() => props.maxDate,
() => props.minHour,
() => props.maxHour,
() => props.minMinute,
() => props.minMinute,
() => props.maxMinute
],
() => {
updateValue()
},
{
deep: true,
immediate: true
}
)
onBeforeMount(() => {
// 初始化完毕打开observer触发render的开关
created.value = true
const innerValue = correctValue(props.modelValue)
updateColumnValue(innerValue)
})
// onMounted(() => {
// // 手动进行一次render
// const innerValue = correctValue(props.modelValue)
// updateColumnValue(innerValue)
// })
const emit = defineEmits(['change', 'pickstart', 'pickend', 'update:modelValue'])
/** pickerView触发change事件同步修改pickerValue */
function onChange({ value }) {
// 更新pickerView的value
pickerValue.value = value
// pickerValue => innerValue
const result = updateInnerValue()
emit('update:modelValue', result)
// 这个地方的value返回的是picker数组实际上在此处我们应该返回 change 的是 value date类型的值
emit('change', {
value: result,
picker: proxy.$.exposed
})
}
/**
* @description 使用formatter格式化getOriginColumns的结果
* @return {Array<Array<Number>>} 用于传入picker的columns
*/
function updateColumns() {
const { formatter, columnFormatter } = props
if (columnFormatter) {
return columnFormatter(proxy.$.exposed)
} else {
return getOriginColumns().map((column) => {
return column.values.map((value) => {
return {
label: formatter ? formatter(column.type, padZero(value)) : padZero(value),
value
}
})
})
}
}
/**
* 设置数据列
* @param columnList 数据列
*/
function setColumns(columnList) {
columns.value = columnList
}
/**
* @description 根据getRanges得到的范围计算所有的列的数据
* @return {{values: any[], type: String}[]} 年
*/
function getOriginColumns() {
const { filter } = props
return getRanges().map(({ type, range }) => {
let values = times(range[1] - range[0] + 1, (index) => {
return range[0] + index
})
if (filter) {
values = filter(type, values)
}
return {
type,
values
}
})
}
/**
* @description 根据时间戳生成年月日时分的边界范围
* @return {Array<{type:String,range:Array<Number>}>}
*/
function getRanges() {
if (props.type === 'time') {
return [
{
type: 'hour',
range: [props.minHour, props.maxHour]
},
{
type: 'minute',
range: [props.minMinute, props.maxMinute]
}
]
}
const { maxYear, maxDate, maxMonth, maxHour, maxMinute } = getBoundary('max', innerValue.value)
const { minYear, minDate, minMonth, minHour, minMinute } = getBoundary('min', innerValue.value)
const result = [
{
type: 'year',
range: [minYear, maxYear]
},
{
type: 'month',
range: [minMonth, maxMonth]
},
{
type: 'date',
range: [minDate, maxDate]
},
{
type: 'hour',
range: [minHour, maxHour]
},
{
type: 'minute',
range: [minMinute, maxMinute]
}
]
if (props.type === 'date') result.splice(3, 2)
if (props.type === 'year-month') result.splice(2, 3)
return result
}
/**
* @description 修正时间入参,判定是否为规范时间类型
* @param {String | Number} value
* @return {String | Number} innerValue
*/
function correctValue(value) {
const isDateType = props.type !== 'time'
if (isDateType && !isValidDate(value)) {
// 是Date类型但入参不可用使用最小时间戳代替
value = props.minDate
} else if (!isDateType && !value) {
// 非Date类型无入参使用最小小时代替
value = `${padZero(props.minHour)}:00`
}
// 当type为time时
if (!isDateType) {
// 非Date类型直接走此逻辑
let [hour, minute] = value.split(':')
hour = padZero(range(hour, props.minHour, props.maxHour))
minute = padZero(range(minute, props.minMinute, props.maxMinute))
return `${hour}:${minute}`
}
// date type
value = Math.min(Math.max(value, props.minDate), props.maxDate)
return value
}
/**
* @description 根据时间戳,计算所有选项的范围
* @param {'min'|'max'} type 类型
* @param {Number} innerValue 时间戳
*/
function getBoundary(type, innerValue) {
const value = new Date(innerValue)
const boundary = new Date(props[`${type}Date`])
const year = boundary.getFullYear()
let month = 1
let date = 1
let hour = 0
let minute = 0
if (type === 'max') {
month = 12
date = getMonthEndDay(value.getFullYear(), value.getMonth() + 1)
hour = 23
minute = 59
}
if (value.getFullYear() === year) {
month = boundary.getMonth() + 1
if (value.getMonth() + 1 === month) {
date = boundary.getDate()
if (value.getDate() === date) {
hour = boundary.getHours()
if (value.getHours() === hour) {
minute = boundary.getMinutes()
}
}
}
}
return {
[`${type}Year`]: year,
[`${type}Month`]: month,
[`${type}Date`]: date,
[`${type}Hour`]: hour,
[`${type}Minute`]: minute
}
}
/**
* @description 根据传入的value以及type初始化innerValue期间会使用format格式化数据
* @param value
* @return {Array}
*/
function updateColumnValue(value) {
const values = getPickerValue(value, props.type)
// 更新pickerView的value,columns
if (props.modelValue !== value) {
emit('update:modelValue', value)
emit('change', {
value,
picker: proxy.$.exposed
})
}
innerValue.value = value
columns.value = updateColumns()
pickerValue.value = values
}
/**
* @description 根据当前的选中项 处理innerValue
* @return {date} innerValue
*/
function updateInnerValue() {
const { type } = props
let values: Array<number> = []
let innerValue = ''
values = datePickerview.value.getValues()
if (type === 'time') {
innerValue = `${padZero(values[0])}:${padZero(values[1])}`
return innerValue
}
// 处理年份 索引位0
const year = values[0] && parseInt(String(values[0]))
// 处理月 索引位1
const month = values[1] && parseInt(String(values[1]))
const maxDate = getMonthEndDay(year, month)
// 处理 date 日期 索引位2
let date: string | number = 1
if (type !== 'year-month') {
date = (values[2] && parseInt(String(values[2]))) > maxDate ? maxDate : values[2] && parseInt(String(values[2]))
}
// 处理 时分 索引位34
let hour = 0
let minute = 0
if (type === 'datetime') {
hour = values[3] && parseInt(String(values[3]))
minute = values[4] && parseInt(String(values[4]))
}
const value = new Date(year, month - 1, date, hour, minute)
innerValue = correctValue(value)
return innerValue
}
/**
* @description 选中项改变,多级联动
*/
function columnChange(picker) {
// time 和 year-mouth 无需联动
if (props.type === 'time' || props.type === 'year-month') {
return
}
/** 重新计算年月日时分秒,修正时间。 */
const values = picker.getValues()
const year = values[0]
const month = values[1]
const maxDate = getMonthEndDay(year, month)
let date = values[2]
date = date > maxDate ? maxDate : date
let hour = 0
let minute = 0
if (props.type === 'datetime') {
hour = values[3]
minute = values[4]
}
const value = new Date(year, month - 1, date, hour, minute)
/** 根据计算选中项的时间戳,重新计算所有的选项列表 */
// 更新选中时间戳
innerValue.value = correctValue(value)
// 根据innerValue获取最新的时间表重新生成对应的数据源
const newColumns = updateColumns().slice(0, 3)
// 深拷贝联动之前的选中项
const selectedIndex = picker.selectedIndex.value.slice(0)
/**
* 选中年会修改对应的年份的月数,和月份对应的日期。
* 选中月,会修改月份对应的日数
*/
newColumns.forEach((columns, index) => {
const nextColumnIndex = index + 1
const nextColumnData = newColumns[nextColumnIndex]
// `日`不控制任何其它列
if (index === 2) return
picker.setColumnData(
nextColumnIndex,
nextColumnData,
selectedIndex[nextColumnIndex] <= nextColumnData.length - 1 ? selectedIndex[nextColumnIndex] : 0
)
})
}
function onPickStart() {
emit('pickstart')
}
function onPickEnd() {
emit('pickend')
}
function getSelects() {
return datePickerview.value && datePickerview.value.getSelects ? datePickerview.value.getSelects() : undefined
}
defineExpose({
updateColumns,
setColumns,
getSelects,
correctValue,
getPickerValue,
getOriginColumns,
formatter: props.formatter,
startSymbol: props.startSymbol
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>