2025-12-24 19:42:28 +08:00

529 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<uni-popup ref="popup" background-color="#fff" class="popup" @change="popupChange">
<view class="center flex-col wh-full">
<view class="flex-row justify-between p-15">
<view @click="close">
<uni-icons type="closeempty" size="20"></uni-icons>
</view>
<view>{{ title }}</view>
<view></view>
</view>
<view class="picker-view">
<picker-view class="wh-full pl-20 " :value="innerIndex" indicator-style='height: 50px;'
@change="changeHandler">
<picker-view-column v-for="(item,index) in columns" :key="index">
<view class="flex-col-center" v-for="(val,i) in item" :key="i"> {{ val }}</view>
</picker-view-column>
</picker-view>
</view>
<view class="confirm-button" @click="confirm">确定</view>
</view>
</uni-popup>
</template>
<script>
import dayjs from 'dayjs'
function times(n, iteratee) {
let index = -1
const result = Array(n < 0 ? 0 : n)
while (++index < n) {
result[index] = iteratee(index)
}
return result
}
function padZero(value) {
return `00${value}`.slice(-2)
}
function range(min = 0, max = 0, value = 0) {
return Math.max(min, Math.min(max, Number(value)))
}
function number(value) {
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
}
function timeTest(value) {
if (!value) return false
// 判断是否数值或者字符串数值(意味着为时间戳)转为数值否则new Date无法识别字符串时间戳
if (number(value)) value = +value
return !/Invalid|NaN/.test(new Date(value).toString())
}
export default {
name: 'datetime-picker',
props: {
value: {
type: [String, Number],
default: Number(new Date())
},
// 顶部标题
title: {
type: String,
default: ''
},
// 展示格式mode=date为日期选择mode=time为时间选择mode=year-month为年月选择mode=datetime为日期时间选择
mode: {
type: String,
default: 'datetime'
},
// 可选的最大时间
maxDate: {
type: Number,
// 最大默认值为后10年
default: new Date(new Date().getFullYear() + 10, 0, 1).getTime()
},
// 可选的最小时间
minDate: {
type: Number,
// 最小默认值为前10年
default: new Date(new Date().getFullYear() - 10, 0, 1).getTime()
},
// 可选的最小小时仅mode=time有效
minHour: {
type: Number,
default: 0
},
// 可选的最大小时仅mode=time有效
maxHour: {
type: Number,
default: 23
},
// 可选的最小分钟仅mode=time有效
minMinute: {
type: Number,
default: 0
},
// 可选的最大分钟仅mode=time有效
maxMinute: {
type: Number,
default: 59
},
// 选项过滤函数
filter: {
type: [Function, null],
default: null
},
// 选项格式化函数
formatter: {
type: [Function, null],
default: null
},
// 各列的默认索引
defaultIndex: {
type: Array,
default: () => []
},
// 支持通过 props 传递弹窗状态变化回调
onPopupChange: {
type: Function,
default: null,
}
},
data() {
return {
columns: [],
innerDefaultIndex: [],
innerFormatter: (type, value) => value,
// 上一次选择的列索引
lastIndex: [],
// 索引值 对应picker-view的value
innerIndex: [],
// 各列的值
innerColumns: [],
// 上一次的变化列索引
columnIndex: 0,
}
},
watch: {
// 监听默认索引的变化,重新设置对应的值
innerDefaultIndex: {
immediate: true,
handler(n) {
this.setIndexs(n, true)
}
},
// 监听columns参数的变化
columns: {
immediate: true,
deep: true,
handler(n) {
this.setColumns(n)
}
},
},
mounted() {
this.init()
},
// #ifdef VUE3
emits: ['close', 'canel', 'confirm', 'change'],
// #endif
methods: {
popupChange(e) {
// 如果传递了回调函数,调用它
if (this.onPopupChange) {
try {
this.onPopupChange(e)
} catch (err) {
console.error('[DatetimePicker] 调用 onPopupChange 失败:', err)
}
}
},
init() {
this.innerValue = this.correctValue(this.value)
this.updateColumnValue(this.innerValue)
},
open() {
this.$refs.popup.open('bottom')
},
// 关闭选择器
close() {
this.$refs.popup.close()
this.$emit('close')
},
// 点击工具栏的确定按钮
confirm() {
this.$emit('confirm', {
value: this.innerValue,
mode: this.mode
})
this.close()
},
//用正则截取输出值,当出现多组数字时,抛出错误
intercept(e, type) {
let judge = e.match(/\d+/g)
//判断是否掺杂数字
if (judge.length > 1) {
console.error("请勿在过滤或格式化函数时添加数字")
return 0
} else if (type && judge[0].length == 4) {//判断是否是年份
return judge[0]
} else if (judge[0].length > 2) {
console.error("请勿在过滤或格式化函数时添加数字")
return 0
} else {
return judge[0]
}
},
isArray(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
}
return Object.prototype.toString.call(value) === '[object Array]'
},
deepClone(obj) {
// 对常见的“非”值,直接返回原来值
if ([null, undefined, NaN, false].includes(obj)) return obj
if (typeof obj !== 'object' && typeof obj !== 'function') {
// 原始类型直接返回
return obj
}
const o = this.isArray(obj) ? [] : {}
for (const i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = typeof obj[i] === 'object' ? this.deepClone(obj[i]) : obj[i]
}
}
return o
},
setIndexs(index, setLastIndex) {
this.innerIndex = this.deepClone(index)
if (setLastIndex) {
this.setLastIndex(index)
}
},
setLastIndex(index) {
// 当能进入此方法意味着当前设置的各列默认索引即为“上一次”的选中值需要记录是因为changeHandler中
// 需要拿前后的变化值进行对比,得出当前发生改变的是哪一列
this.lastIndex = this.deepClone(index)
},
setColumns(columns) {
this.innerColumns = this.deepClone(columns)
// 如果在设置各列数据时没有被设置默认的各列索引defaultIndex那么用0去填充它数组长度为列的数量
if (this.innerIndex.length === 0) {
this.innerIndex = new Array(columns.length).fill(0)
}
},
changeHandler(e) {
const {value} = e.detail
let index = 0,
columnIndex = 0
// 通过对比前后两次的列索引,得出当前变化的是哪一列
for (let i = 0; i < value.length; i++) {
let item = value[i]
if (item !== (this.lastIndex[i] || 0)) { // 把undefined转为合法假值0
// 设置columnIndex为当前变化列的索引
columnIndex = i
// index则为变化列中的变化项的索引
index = item
break // 终止循环,即使少一次循环,也是性能的提升
}
}
this.columnIndex = columnIndex
const values = this.innerColumns
// 将当前的各项变化索引,设置为"上一次"的索引变化值
this.setLastIndex(value)
this.setIndexs(value)
this.zchange({indexs: value, values})
},
// 列发生变化时触发
zchange(e) {
const {indexs, values} = e
let selectValue = ''
if (this.mode === 'time') {
// 根据value各列索引从各列数组中取出当前时间的选中值
selectValue = `${this.intercept(values[0][indexs[0]])}:${this.intercept(values[1][indexs[1]])}`
} else {
// 将选择的值转为数值,比如'03'转为数值的3'2019'转为数值的2019
const year = parseInt(this.intercept(values[0][indexs[0]], 'year'))
const month = parseInt(this.intercept(values[1][indexs[1]]))
let date = parseInt(values[2] ? this.intercept(values[2][indexs[2]]) : 1)
let hour = 0, minute = 0
// 此月份的最大天数
const maxDate = dayjs(`${year}-${month}`).daysInMonth()
// year-month模式下date不会出现在列中设置为1为了符合后边需要减1的需求
if (this.mode === 'year-month') {
date = 1
}
// 不允许超过maxDate值
date = Math.min(maxDate, date)
if (this.mode === 'datetime') {
hour = parseInt(this.intercept(values[3][indexs[3]]))
minute = parseInt(this.intercept(values[4][indexs[4]]))
}
// 转为时间模式
selectValue = Number(new Date(year, month - 1, date, hour, minute))
}
// 取出准确的合法值,防止超越边界的情况
selectValue = this.correctValue(selectValue)
this.innerValue = selectValue
this.updateColumnValue(selectValue)
// 发出change时间value为当前选中的时间戳
this.$emit('change', {
value: selectValue,
// #ifndef MP-WEIXIN
// 微信小程序不能传递this实例会因为循环引用而报错
picker: this.$refs.picker,
// #endif
mode: this.mode
})
},
// 更新各列的值进行补0、格式化等操作
updateColumnValue(value) {
this.innerValue = value
this.updateColumns()
this.updateIndexs(value)
},
// 更新索引
updateIndexs(value) {
let values = []
const formatter = this.formatter || this.innerFormatter
if (this.mode === 'time') {
// 将time模式的时间用:分隔成数组
const timeArr = value.split(':')
// 使用formatter格式化方法进行管道处理
values = [formatter('hour', timeArr[0]), formatter('minute', timeArr[1])]
} else {
values = [
formatter('year', `${dayjs(value).year()}`),
// 月份补0
formatter('month', padZero(dayjs(value).month() + 1))
]
if (this.mode === 'date') {
// date模式需要添加天列
values.push(formatter('day', padZero(dayjs(value).date())))
}
if (this.mode === 'datetime') {
// 数组的push方法可以写入多个参数
values.push(formatter('day', padZero(dayjs(value).date())), formatter('hour', padZero(dayjs(value).hour())), formatter('minute', padZero(dayjs(value).minute())))
}
}
// 根据当前各列的所有值,从各列默认值中找到默认值在各列中的索引
const indexs = this.columns.map((column, index) => {
// 通过取大值,可以保证不会出现找不到索引的-1情况
return Math.max(0, column.findIndex(item => parseInt(item) === parseInt(values[index])))
})
this.innerDefaultIndex = indexs
},
// 更新各列的值
updateColumns() {
const formatter = this.formatter || this.innerFormatter
// 获取各列的值并且map后对各列的具体值进行补0操作
this.columns = this.getOriginColumns().map((column) => column.values.map((value) => formatter(column.type, value)))
},
getOriginColumns() {
// 生成各列的值
const results = this.getRanges().map(({type, range}) => {
let values = times(range[1] - range[0] + 1, (index) => {
let value = range[0] + index
value = type === 'year' ? `${value}` : padZero(value)
if (type === 'month') {
value = `${value}`
}
if (type === 'day') {
value = `${value}`
}
if (type === 'hour') {
value = `${value}`
}
if (type === "minute") {
value = `${value}`
}
return value
})
// 进行过滤
if (this.filter) {
values = this.filter(type, values)
}
return {type, values}
})
return results
},
// 得出合法的时间
correctValue(value) {
const isDateMode = this.mode !== 'time'
if (isDateMode && !timeTest(value)) {
// 如果是日期类型,但是又没有设置合法的当前时间的话,使用最小时间为当前时间
value = this.minDate
} else if (!isDateMode && !value) {
// 如果是时间类型,而又没有默认值的话,就用最小时间
value = `${`00${this.minHour}`.slice(-2)}:${`00${this.minMinute}`.slice(-2)}`
}
// 时间类型
if (!isDateMode) {
if (String(value).indexOf(':') === -1) return console.error('时间错误请传递如12:24的格式')
let [hour, minute] = value.split(':')
// 对时间补零,同时控制在最小值和最大值之间
hour = padZero(range(this.minHour, this.maxHour, Number(hour)))
minute = padZero(range(this.minMinute, this.maxMinute, Number(minute)))
return `${hour}:${minute}`
} else {
// 如果是日期格式,控制在最小日期和最大日期之间
value = dayjs(value).isBefore(dayjs(this.minDate)) ? this.minDate : value
value = dayjs(value).isAfter(dayjs(this.maxDate)) ? this.maxDate : value
return value
}
},
// 获取每列的最大和最小值
getRanges() {
if (this.mode === 'time') {
return [
{
type: 'hour',
range: [this.minHour, this.maxHour],
},
{
type: 'minute',
range: [this.minMinute, this.maxMinute],
},
];
}
const {maxYear, maxDate, maxMonth, maxHour, maxMinute,} = this.getBoundary('max', this.innerValue);
const {minYear, minDate, minMonth, minHour, minMinute,} = this.getBoundary('min', this.innerValue);
const result = [
{
type: 'year',
range: [minYear, maxYear],
},
{
type: 'month',
range: [minMonth, maxMonth],
},
{
type: 'day',
range: [minDate, maxDate],
},
{
type: 'hour',
range: [minHour, maxHour],
},
{
type: 'minute',
range: [minMinute, maxMinute],
},
];
if (this.mode === 'date')
result.splice(3, 2);
if (this.mode === 'year-month')
result.splice(2, 3);
return result;
},
// 根据minDate、maxDate、minHour、maxHour等边界值判断各列的开始和结束边界值
getBoundary(type, innerValue) {
const value = new Date(innerValue)
const boundary = new Date(this[`${type}Date`])
const year = dayjs(boundary).year()
let month = 1
let date = 1
let hour = 0
let minute = 0
if (type === 'max') {
month = 12
// 月份的天数
date = dayjs(value).daysInMonth()
hour = 23
minute = 59
}
// 获取边界值,逻辑是:当年达到了边界值(最大或最小年),就检查月允许的最大和最小值,以此类推
if (dayjs(value).year() === year) {
month = dayjs(boundary).month() + 1
if (dayjs(value).month() + 1 === month) {
date = dayjs(boundary).date()
if (dayjs(value).date() === date) {
hour = dayjs(boundary).hour()
if (dayjs(value).hour() === hour) {
minute = dayjs(boundary).minute()
}
}
}
}
return {
[`${type}Year`]: year,
[`${type}Month`]: month,
[`${type}Date`]: date,
[`${type}Hour`]: hour,
[`${type}Minute`]: minute
}
},
},
}
</script>
<style lang="scss" scoped>
.popup {
z-index: 9999;
}
.center {
padding-bottom: var(--window-bottom);
}
.picker-view {
height: 400rpx;
}
.confirm-button {
width: 90%;
margin: 10px auto;
padding: 12px 0;
background-color: #007bff;
color: #fff;
text-align: center;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s;
&:active {
background-color: #0056b3;
}
}
</style>