<template> <view class="uni-datetime-picker"> <view @click="initTimePicker"> <slot> <view class="uni-datetime-picker-timebox-pointer" :class="{'uni-datetime-picker-disabled': disabled, 'uni-datetime-picker-timebox': border}"> <text class="uni-datetime-picker-text">{{time}}</text> <view v-if="!time" class="uni-datetime-picker-time"> <text class="uni-datetime-picker-text">{{selectTimeText}}</text> </view> </view> </slot> </view> <view v-if="visible" id="mask" class="uni-datetime-picker-mask" @click="tiggerTimePicker"></view> <view v-if="visible" class="uni-datetime-picker-popup" :class="[dateShow && timeShow ? '' : 'fix-nvue-height']" :style="fixNvueBug"> <view class="uni-title"> <text class="uni-datetime-picker-text">{{selectTimeText}}</text> </view> <view v-if="dateShow" class="uni-datetime-picker__container-box"> <picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="ymd" @change="bindDateChange"> <picker-view-column> <view class="uni-datetime-picker-item" v-for="(item,index) in years" :key="index"> <text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text> </view> </picker-view-column> <picker-view-column> <view class="uni-datetime-picker-item" v-for="(item,index) in months" :key="index"> <text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text> </view> </picker-view-column> <picker-view-column> <view class="uni-datetime-picker-item" v-for="(item,index) in days" :key="index"> <text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text> </view> </picker-view-column> </picker-view> <!-- 兼容 nvue 不支持伪类 --> <text class="uni-datetime-picker-sign sign-left">-</text> <text class="uni-datetime-picker-sign sign-right">-</text> </view> <view v-if="timeShow" class="uni-datetime-picker__container-box"> <picker-view class="uni-datetime-picker-view" :class="[hideSecond ? 'time-hide-second' : '']" :indicator-style="indicatorStyle" :value="hms" @change="bindTimeChange"> <picker-view-column> <view class="uni-datetime-picker-item" v-for="(item,index) in hours" :key="index"> <text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text> </view> </picker-view-column> <picker-view-column> <view class="uni-datetime-picker-item" v-for="(item,index) in minutes" :key="index"> <text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text> </view> </picker-view-column> <picker-view-column v-if="!hideSecond"> <view class="uni-datetime-picker-item" v-for="(item,index) in seconds" :key="index"> <text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text> </view> </picker-view-column> </picker-view> <!-- 兼容 nvue 不支持伪类 --> <text class="uni-datetime-picker-sign" :class="[hideSecond ? 'sign-center' : 'sign-left']">:</text> <text v-if="!hideSecond" class="uni-datetime-picker-sign sign-right">:</text> </view> <view class="uni-datetime-picker-btn"> <view @click="clearTime"> <text class="uni-datetime-picker-btn-text">{{clearText}}</text> </view> <view class="uni-datetime-picker-btn-group"> <view class="uni-datetime-picker-cancel" @click="tiggerTimePicker"> <text class="uni-datetime-picker-btn-text">{{cancelText}}</text> </view> <view @click="setTime"> <text class="uni-datetime-picker-btn-text">{{okText}}</text> </view> </view> </view> </view> </view> </template> <script> import { initVueI18n } from '@dcloudio/uni-i18n' import i18nMessages from './i18n/index.js' const { t } = initVueI18n(i18nMessages) import { fixIosDateFormat } from './util' /** * DatetimePicker 时间选择器 * @description 可以同时选择日期和时间的选择器 * @tutorial https://ext.dcloud.net.cn/plugin?id=xxx * @property {String} type = [datetime | date | time] 显示模式 * @property {Boolean} multiple = [true|false] 是否多选 * @property {String|Number} value 默认值 * @property {String|Number} start 起始日期或时间 * @property {String|Number} end 起始日期或时间 * @property {String} return-type = [timestamp | string] * @event {Function} change 选中发生变化触发 */ export default { name: 'UniDatetimePicker', data() { return { indicatorStyle: `height: 50px;`, visible: false, fixNvueBug: {}, dateShow: true, timeShow: true, title: '日期和时间', // 输入框当前时间 time: '', // 当前的年月日时分秒 year: 1920, month: 0, day: 0, hour: 0, minute: 0, second: 0, // 起始时间 startYear: 1920, startMonth: 1, startDay: 1, startHour: 0, startMinute: 0, startSecond: 0, // 结束时间 endYear: 2120, endMonth: 12, endDay: 31, endHour: 23, endMinute: 59, endSecond: 59, } }, props: { type: { type: String, default: 'datetime' }, value: { type: [String, Number], default: '' }, modelValue: { type: [String, Number], default: '' }, start: { type: [Number, String], default: '' }, end: { type: [Number, String], default: '' }, returnType: { type: String, default: 'string' }, disabled: { type: [Boolean, String], default: false }, border: { type: [Boolean, String], default: true }, hideSecond: { type: [Boolean, String], default: false } }, watch: { // #ifndef VUE3 value: { handler(newVal) { if (newVal) { this.parseValue(fixIosDateFormat(newVal)) this.initTime(false) } else { this.time = '' this.parseValue(Date.now()) } }, immediate: true }, // #endif // #ifdef VUE3 modelValue: { handler(newVal) { if (newVal) { this.parseValue(fixIosDateFormat(newVal)) this.initTime(false) } else { this.time = '' this.parseValue(Date.now()) } }, immediate: true }, // #endif type: { handler(newValue) { if (newValue === 'date') { this.dateShow = true this.timeShow = false this.title = '日期' } else if (newValue === 'time') { this.dateShow = false this.timeShow = true this.title = '时间' } else { this.dateShow = true this.timeShow = true this.title = '日期和时间' } }, immediate: true }, start: { handler(newVal) { this.parseDatetimeRange(fixIosDateFormat(newVal), 'start') }, immediate: true }, end: { handler(newVal) { this.parseDatetimeRange(fixIosDateFormat(newVal), 'end') }, immediate: true }, // 月、日、时、分、秒可选范围变化后,检查当前值是否在范围内,不在则当前值重置为可选范围第一项 months(newVal) { this.checkValue('month', this.month, newVal) }, days(newVal) { this.checkValue('day', this.day, newVal) }, hours(newVal) { this.checkValue('hour', this.hour, newVal) }, minutes(newVal) { this.checkValue('minute', this.minute, newVal) }, seconds(newVal) { this.checkValue('second', this.second, newVal) } }, computed: { // 当前年、月、日、时、分、秒选择范围 years() { return this.getCurrentRange('year') }, months() { return this.getCurrentRange('month') }, days() { return this.getCurrentRange('day') }, hours() { return this.getCurrentRange('hour') }, minutes() { return this.getCurrentRange('minute') }, seconds() { return this.getCurrentRange('second') }, // picker 当前值数组 ymd() { return [this.year - this.minYear, this.month - this.minMonth, this.day - this.minDay] }, hms() { return [this.hour - this.minHour, this.minute - this.minMinute, this.second - this.minSecond] }, // 当前 date 是 start currentDateIsStart() { return this.year === this.startYear && this.month === this.startMonth && this.day === this.startDay }, // 当前 date 是 end currentDateIsEnd() { return this.year === this.endYear && this.month === this.endMonth && this.day === this.endDay }, // 当前年、月、日、时、分、秒的最小值和最大值 minYear() { return this.startYear }, maxYear() { return this.endYear }, minMonth() { if (this.year === this.startYear) { return this.startMonth } else { return 1 } }, maxMonth() { if (this.year === this.endYear) { return this.endMonth } else { return 12 } }, minDay() { if (this.year === this.startYear && this.month === this.startMonth) { return this.startDay } else { return 1 } }, maxDay() { if (this.year === this.endYear && this.month === this.endMonth) { return this.endDay } else { return this.daysInMonth(this.year, this.month) } }, minHour() { if (this.type === 'datetime') { if (this.currentDateIsStart) { return this.startHour } else { return 0 } } if (this.type === 'time') { return this.startHour } }, maxHour() { if (this.type === 'datetime') { if (this.currentDateIsEnd) { return this.endHour } else { return 23 } } if (this.type === 'time') { return this.endHour } }, minMinute() { if (this.type === 'datetime') { if (this.currentDateIsStart && this.hour === this.startHour) { return this.startMinute } else { return 0 } } if (this.type === 'time') { if (this.hour === this.startHour) { return this.startMinute } else { return 0 } } }, maxMinute() { if (this.type === 'datetime') { if (this.currentDateIsEnd && this.hour === this.endHour) { return this.endMinute } else { return 59 } } if (this.type === 'time') { if (this.hour === this.endHour) { return this.endMinute } else { return 59 } } }, minSecond() { if (this.type === 'datetime') { if (this.currentDateIsStart && this.hour === this.startHour && this.minute === this.startMinute) { return this.startSecond } else { return 0 } } if (this.type === 'time') { if (this.hour === this.startHour && this.minute === this.startMinute) { return this.startSecond } else { return 0 } } }, maxSecond() { if (this.type === 'datetime') { if (this.currentDateIsEnd && this.hour === this.endHour && this.minute === this.endMinute) { return this.endSecond } else { return 59 } } if (this.type === 'time') { if (this.hour === this.endHour && this.minute === this.endMinute) { return this.endSecond } else { return 59 } } }, /** * for i18n */ selectTimeText() { return t("uni-datetime-picker.selectTime") }, okText() { return t("uni-datetime-picker.ok") }, clearText() { return t("uni-datetime-picker.clear") }, cancelText() { return t("uni-datetime-picker.cancel") } }, mounted() { // #ifdef APP-NVUE const res = uni.getSystemInfoSync(); this.fixNvueBug = { top: res.windowHeight / 2, left: res.windowWidth / 2 } // #endif }, methods: { /** * @param {Object} item * 小于 10 在前面加个 0 */ lessThanTen(item) { return item < 10 ? '0' + item : item }, /** * 解析时分秒字符串,例如:00:00:00 * @param {String} timeString */ parseTimeType(timeString) { if (timeString) { let timeArr = timeString.split(':') this.hour = Number(timeArr[0]) this.minute = Number(timeArr[1]) this.second = Number(timeArr[2]) } }, /** * 解析选择器初始值,类型可以是字符串、时间戳,例如:2000-10-02、'08:30:00'、 1610695109000 * @param {String | Number} datetime */ initPickerValue(datetime) { let defaultValue = null if (datetime) { defaultValue = this.compareValueWithStartAndEnd(datetime, this.start, this.end) } else { defaultValue = Date.now() defaultValue = this.compareValueWithStartAndEnd(defaultValue, this.start, this.end) } this.parseValue(defaultValue) }, /** * 初始值规则: * - 用户设置初始值 value * - 设置了起始时间 start、终止时间 end,并 start < value < end,初始值为 value, 否则初始值为 start * - 只设置了起始时间 start,并 start < value,初始值为 value,否则初始值为 start * - 只设置了终止时间 end,并 value < end,初始值为 value,否则初始值为 end * - 无起始终止时间,则初始值为 value * - 无初始值 value,则初始值为当前本地时间 Date.now() * @param {Object} value * @param {Object} dateBase */ compareValueWithStartAndEnd(value, start, end) { let winner = null value = this.superTimeStamp(value) start = this.superTimeStamp(start) end = this.superTimeStamp(end) if (start && end) { if (value < start) { winner = new Date(start) } else if (value > end) { winner = new Date(end) } else { winner = new Date(value) } } else if (start && !end) { winner = start <= value ? new Date(value) : new Date(start) } else if (!start && end) { winner = value <= end ? new Date(value) : new Date(end) } else { winner = new Date(value) } return winner }, /** * 转换为可比较的时间戳,接受日期、时分秒、时间戳 * @param {Object} value */ superTimeStamp(value) { let dateBase = '' if (this.type === 'time' && value && typeof value === 'string') { const now = new Date() const year = now.getFullYear() const month = now.getMonth() + 1 const day = now.getDate() dateBase = year + '/' + month + '/' + day + ' ' } if (Number(value)) { value = parseInt(value) dateBase = 0 } return this.createTimeStamp(dateBase + value) }, /** * 解析默认值 value,字符串、时间戳 * @param {Object} defaultTime */ parseValue(value) { if (!value) { return } if (this.type === 'time' && typeof value === "string") { this.parseTimeType(value) } else { let defaultDate = null defaultDate = new Date(value) if (this.type !== 'time') { this.year = defaultDate.getFullYear() this.month = defaultDate.getMonth() + 1 this.day = defaultDate.getDate() } if (this.type !== 'date') { this.hour = defaultDate.getHours() this.minute = defaultDate.getMinutes() this.second = defaultDate.getSeconds() } } if (this.hideSecond) { this.second = 0 } }, /** * 解析可选择时间范围 start、end,年月日字符串、时间戳 * @param {Object} defaultTime */ parseDatetimeRange(point, pointType) { // 时间为空,则重置为初始值 if (!point) { if (pointType === 'start') { this.startYear = 1920 this.startMonth = 1 this.startDay = 1 this.startHour = 0 this.startMinute = 0 this.startSecond = 0 } if (pointType === 'end') { this.endYear = 2120 this.endMonth = 12 this.endDay = 31 this.endHour = 23 this.endMinute = 59 this.endSecond = 59 } return } if (this.type === 'time') { const pointArr = point.split(':') this[pointType + 'Hour'] = Number(pointArr[0]) this[pointType + 'Minute'] = Number(pointArr[1]) this[pointType + 'Second'] = Number(pointArr[2]) } else { if (!point) { pointType === 'start' ? this.startYear = this.year - 60 : this.endYear = this.year + 60 return } if (Number(point)) { point = parseInt(point) } // datetime 的 end 没有时分秒, 则不限制 const hasTime = /[0-9]:[0-9]/ if (this.type === 'datetime' && pointType === 'end' && typeof point === 'string' && !hasTime.test( point)) { point = point + ' 23:59:59' } const pointDate = new Date(point) this[pointType + 'Year'] = pointDate.getFullYear() this[pointType + 'Month'] = pointDate.getMonth() + 1 this[pointType + 'Day'] = pointDate.getDate() if (this.type === 'datetime') { this[pointType + 'Hour'] = pointDate.getHours() this[pointType + 'Minute'] = pointDate.getMinutes() this[pointType + 'Second'] = pointDate.getSeconds() } } }, // 获取 年、月、日、时、分、秒 当前可选范围 getCurrentRange(value) { const range = [] for (let i = this['min' + this.capitalize(value)]; i <= this['max' + this.capitalize(value)]; i++) { range.push(i) } return range }, // 字符串首字母大写 capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1) }, // 检查当前值是否在范围内,不在则当前值重置为可选范围第一项 checkValue(name, value, values) { if (values.indexOf(value) === -1) { this[name] = values[0] } }, // 每个月的实际天数 daysInMonth(year, month) { // Use 1 for January, 2 for February, etc. return new Date(year, month, 0).getDate(); }, //兼容 iOS、safari 日期格式 fixIosDateFormat(value) { if (typeof value === 'string') { value = value.replace(/-/g, '/') } return value }, /** * 生成时间戳 * @param {Object} time */ createTimeStamp(time) { if (!time) return if (typeof time === "number") { return time } else { time = time.replace(/-/g, '/') if (this.type === 'date') { time = time + ' ' + '00:00:00' } return Date.parse(time) } }, /** * 生成日期或时间的字符串 */ createDomSting() { const yymmdd = this.year + '-' + this.lessThanTen(this.month) + '-' + this.lessThanTen(this.day) let hhmmss = this.lessThanTen(this.hour) + ':' + this.lessThanTen(this.minute) if (!this.hideSecond) { hhmmss = hhmmss + ':' + this.lessThanTen(this.second) } if (this.type === 'date') { return yymmdd } else if (this.type === 'time') { return hhmmss } else { return yymmdd + ' ' + hhmmss } }, /** * 初始化返回值,并抛出 change 事件 */ initTime(emit = true) { this.time = this.createDomSting() if (!emit) return if (this.returnType === 'timestamp' && this.type !== 'time') { this.$emit('change', this.createTimeStamp(this.time)) this.$emit('input', this.createTimeStamp(this.time)) this.$emit('update:modelValue', this.createTimeStamp(this.time)) } else { this.$emit('change', this.time) this.$emit('input', this.time) this.$emit('update:modelValue', this.time) } }, /** * 用户选择日期或时间更新 data * @param {Object} e */ bindDateChange(e) { const val = e.detail.value this.year = this.years[val[0]] this.month = this.months[val[1]] this.day = this.days[val[2]] }, bindTimeChange(e) { const val = e.detail.value this.hour = this.hours[val[0]] this.minute = this.minutes[val[1]] this.second = this.seconds[val[2]] }, /** * 初始化弹出层 */ initTimePicker() { if (this.disabled) return const value = fixIosDateFormat(this.time) this.initPickerValue(value) this.visible = !this.visible }, /** * 触发或关闭弹框 */ tiggerTimePicker(e) { this.visible = !this.visible }, /** * 用户点击“清空”按钮,清空当前值 */ clearTime() { this.time = '' this.$emit('change', this.time) this.$emit('input', this.time) this.$emit('update:modelValue', this.time) this.tiggerTimePicker() }, /** * 用户点击“确定”按钮 */ setTime() { this.initTime() this.tiggerTimePicker() } } } </script> <style lang="scss"> $uni-primary: #007aff !default; .uni-datetime-picker { /* #ifndef APP-NVUE */ /* width: 100%; */ /* #endif */ } .uni-datetime-picker-view { height: 130px; width: 270px; /* #ifndef APP-NVUE */ cursor: pointer; /* #endif */ } .uni-datetime-picker-item { height: 50px; line-height: 50px; text-align: center; font-size: 14px; } .uni-datetime-picker-btn { margin-top: 60px; /* #ifndef APP-NVUE */ display: flex; cursor: pointer; /* #endif */ flex-direction: row; justify-content: space-between; } .uni-datetime-picker-btn-text { font-size: 14px; color: $uni-primary; } .uni-datetime-picker-btn-group { /* #ifndef APP-NVUE */ display: flex; /* #endif */ flex-direction: row; } .uni-datetime-picker-cancel { margin-right: 30px; } .uni-datetime-picker-mask { position: fixed; bottom: 0px; top: 0px; left: 0px; right: 0px; background-color: rgba(0, 0, 0, 0.4); transition-duration: 0.3s; z-index: 998; } .uni-datetime-picker-popup { border-radius: 8px; padding: 30px; width: 270px; /* #ifdef APP-NVUE */ height: 500px; /* #endif */ /* #ifdef APP-NVUE */ width: 330px; /* #endif */ background-color: #fff; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); transition-duration: 0.3s; z-index: 999; } .fix-nvue-height { /* #ifdef APP-NVUE */ height: 330px; /* #endif */ } .uni-datetime-picker-time { color: grey; } .uni-datetime-picker-column { height: 50px; } .uni-datetime-picker-timebox { border: 1px solid #E5E5E5; border-radius: 5px; padding: 7px 10px; /* #ifndef APP-NVUE */ box-sizing: border-box; cursor: pointer; /* #endif */ } .uni-datetime-picker-timebox-pointer { /* #ifndef APP-NVUE */ cursor: pointer; /* #endif */ } .uni-datetime-picker-disabled { opacity: 0.4; /* #ifdef H5 */ cursor: not-allowed !important; /* #endif */ } .uni-datetime-picker-text { font-size: 14px; line-height: 50px } .uni-datetime-picker-sign { position: absolute; top: 53px; /* 减掉 10px 的元素高度,兼容nvue */ color: #999; /* #ifdef APP-NVUE */ font-size: 16px; /* #endif */ } .sign-left { left: 86px; } .sign-right { right: 86px; } .sign-center { left: 135px; } .uni-datetime-picker__container-box { position: relative; display: flex; align-items: center; justify-content: center; margin-top: 40px; } .time-hide-second { width: 180px; } </style>