This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
import type { ExtractPropTypes, PropType } from 'vue'
import { CANCEL_EVENT, CHANGE_EVENT, CONFIRM_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
import { makeNumberProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
import type {
DateLike,
DatePickerBaseEvent,
DatePickerChangeEvent,
DatePickerFilter,
DatePickerFormatter,
DatePickerType,
} from './type'
export const datepickerProps = {
/**
* @description 初始值
*/
modelValue: {
type: [Number, String, Object] as PropType<DateLike>,
},
/**
* @description 时间类型,可选值 `date`(年月日) `time`(时分秒) `year-month`(年月) `month-day`(月日) `datehour`(年月日时) `hour-minute`
*/
type: makeStringProp<DatePickerType>('date'),
/**
* @description 是否显示顶部导航
*/
showToolbar: truthProp,
/**
* @description 设置标题
*/
title: makeStringProp(''),
/**
* @description 确定按钮文案
*/
okText: makeStringProp(''),
/**
* @description 取消按钮文案
*/
cancelText: makeStringProp(''),
/**
* @description 每列是否展示中文
*/
isShowChinese: Boolean,
/**
* @description 分钟步进值
*/
minuteStep: makeNumberProp(1),
/**
* @description 开始日期
*/
minDate: {
type: [Number, String, Object] as PropType<DateLike>,
default: () => new Date(new Date().getFullYear() - 10, 0, 1),
},
/**
* @description 结束日期
*/
maxDate: {
type: [Number, String, Object] as PropType<DateLike>,
default: () => new Date(new Date().getFullYear() + 10, 11, 31),
},
/**
* @description 选项格式化函数
*/
formatter: Function as PropType<DatePickerFormatter>,
/**
* @description 选项过滤函数
*/
filter: Function as PropType<DatePickerFilter>,
/**
* @description 是否开启3D效果
*/
threeDimensional: Boolean,
/**
* @description 惯性滚动时长
*/
swipeDuration: makeNumericProp(1000),
/**
* @description 可见的选项个数
*/
visibleOptionNum: makeNumericProp(7),
/**
* @description 选项高度
*/
optionHeight: makeNumericProp(36),
}
export type DatePickerProps = ExtractPropTypes<typeof datepickerProps>
export const datepickerEmits = {
[UPDATE_MODEL_EVENT]: (val: Date) => val instanceof Object,
[CHANGE_EVENT]: (evt: DatePickerChangeEvent) => evt instanceof Object,
[CONFIRM_EVENT]: (evt: DatePickerBaseEvent) => evt instanceof Object,
[CANCEL_EVENT]: (evt: DatePickerBaseEvent) => evt instanceof Object,
}
export type DatePickerEmits = typeof datepickerEmits

View File

@@ -0,0 +1,405 @@
<script setup lang="ts">
import { computed, defineComponent, nextTick, onBeforeMount, reactive, watch } from 'vue'
import { CANCEL_EVENT, CONFIRM_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { isDate, isEqualValue, padZero } from '../_utils'
import type { PickerBaseEvent, PickerChangeEvent } from '../picker'
import NutPicker from '../picker/picker.vue'
import type { PickerOption } from '../pickercolumn'
import { datepickerEmits, datepickerProps } from './datepicker'
import type { DateLike, DatePickerBaseEvent, DatePickerColumnType, DatePickerRangeItem } from './type'
const props = defineProps(datepickerProps)
const emit = defineEmits(datepickerEmits)
const ZH_CN_LOCALES: {
[props: string]: string
} = {
day: '日',
year: '年',
month: '月',
hour: '时',
minute: '分',
seconds: '秒',
}
interface State {
currentDate: Date
selectedValue: string[]
}
const state: State = reactive({
currentDate: new Date(),
selectedValue: [],
})
function normalizeDate(value?: DateLike) {
if (value == null)
return new Date()
if (isDate(value))
return value
return new Date(value)
}
const innerMinDate = computed(() => {
return normalizeDate(props.minDate)
})
const innerMaxDate = computed(() => {
return normalizeDate(props.maxDate)
})
function formatValue(value: Date) {
return new Date(Math.min(Math.max(value.getTime(), innerMinDate.value.getTime()), innerMaxDate.value.getTime()))
}
function getMonthEndDay(year: number, month: number): number {
return 32 - new Date(year, month - 1, 32).getDate()
}
function getBoundary(type: 'min' | 'max', value: Date) {
const boundary = type === 'min' ? innerMinDate.value : innerMaxDate.value
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
}
let seconds = minute
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()
if (value.getMinutes() === minute)
seconds = boundary.getSeconds()
}
}
}
}
return {
[`${type}Year`]: year,
[`${type}Month`]: month,
[`${type}Date`]: date,
[`${type}Hour`]: hour,
[`${type}Minute`]: minute,
[`${type}Seconds`]: seconds,
}
}
const ranges = computed<DatePickerRangeItem[]>(() => {
const { minYear, minDate, minMonth, minHour, minMinute, minSeconds } = getBoundary('min', state.currentDate)
const { maxYear, maxDate, maxMonth, maxHour, maxMinute, maxSeconds } = getBoundary('max', state.currentDate)
return generateList([
{
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],
},
{
type: 'seconds',
range: [minSeconds, maxSeconds],
},
])
})
const columns = computed(() => {
return ranges.value.map((item, columnIndex) => {
return generateValue(item.range[0], item.range[1], getDateIndex(item.type), item.type, columnIndex)
})
})
function handleChange({
columnIndex,
selectedValue,
selectedOptions,
}: PickerChangeEvent) {
const formatDate: (number | string)[] = [...selectedValue]
if (props.type === 'month-day' && formatDate.length < 3)
formatDate.unshift(new Date(state.currentDate || innerMinDate.value || innerMaxDate.value).getFullYear())
if (props.type === 'year-month' && formatDate.length < 3)
formatDate.push(new Date(state.currentDate || innerMinDate.value || innerMaxDate.value).getDate())
const year = Number(formatDate[0])
const month = Number(formatDate[1]) - 1
const day = Math.min(Number(formatDate[2]), getMonthEndDay(Number(formatDate[0]), Number(formatDate[1])))
let date: Date | null = null
if (props.type === 'date' || props.type === 'month-day' || props.type === 'year-month') {
date = new Date(year, month, day)
}
else if (props.type === 'datetime') {
date = new Date(year, month, day, Number(formatDate[3]), Number(formatDate[4]))
}
else if (props.type === 'datehour') {
date = new Date(year, month, day, Number(formatDate[3]))
}
else if (props.type === 'hour-minute' || props.type === 'time') {
date = new Date(state.currentDate)
const year = date.getFullYear()
const month = date.getMonth()
const day = date.getDate()
date = new Date(year, month, day, Number(formatDate[0]), Number(formatDate[1]), Number(formatDate[2] || 0))
}
state.currentDate = formatValue(date!)
emit('change', { date: date!, columnIndex, selectedValue, selectedOptions })
}
function formatterOption(type: DatePickerColumnType, value: string | number) {
const { formatter, isShowChinese } = props
const text = padZero(value, 2)
let option: PickerOption
if (formatter)
option = formatter(type, { text, value: text })
else
option = { text: `${text}${isShowChinese ? ZH_CN_LOCALES[type] : ''}`, value: text }
return option
}
/**
* @param min 最小值
* @param max 最大值
* @param value 当前显示的值
* @param type 类型
* @param columnIndex
*/
function generateValue(min: number, max: number, value: number | string, type: DatePickerColumnType, columnIndex: number) {
const options: PickerOption[] = []
let index = 0
while (min <= max) {
options.push(formatterOption(type, min))
if (type === 'minute')
min += props.minuteStep
else
min += 1
if (min <= Number(value))
index += 1
}
state.selectedValue[columnIndex] = options[index]?.value as string
return props.filter ? props.filter(type, options) : options
}
function getDateIndex(type: DatePickerColumnType) {
if (type === 'year')
return state.currentDate.getFullYear()
if (type === 'month')
return state.currentDate.getMonth() + 1
if (type === 'day')
return state.currentDate.getDate()
if (type === 'hour')
return state.currentDate.getHours()
if (type === 'minute')
return state.currentDate.getMinutes()
if (type === 'seconds')
return state.currentDate.getSeconds()
return 0
}
function convertEvent({ selectedValue, selectedOptions }: PickerBaseEvent): DatePickerBaseEvent {
let date: Date | null = null
switch (props.type) {
case 'date':
case 'datehour':
case 'datetime':
case 'year-month': {
const [
year = 0,
month = 0,
day = 0,
hour = 0,
minute = 0,
seconds = 0,
] = selectedValue
date = new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(seconds))
break
}
case 'time':
case 'hour-minute': {
const [
hour = 0,
minute = 0,
seconds = 0,
] = selectedValue
date = new Date(0, 0, 0, Number(hour), Number(minute), Number(seconds))
break
}
case 'month-day': {
const [
month = 0,
day = 0,
] = selectedValue
date = new Date(0, Number(month) - 1, Number(day))
break
}
}
if (date == null)
date = new Date()
return {
date,
selectedValue,
selectedOptions,
}
}
function handleCancel(event: PickerBaseEvent) {
emit(CANCEL_EVENT, convertEvent(event))
}
function handleConfirm(event: PickerBaseEvent) {
emit(CONFIRM_EVENT, convertEvent(event))
}
function generateList<T>(list: T[]) {
switch (props.type) {
case 'date':
return list.slice(0, 3)
case 'datetime':
return list.slice(0, 5)
case 'time':
return list.slice(3, 6)
case 'year-month':
return list.slice(0, 2)
case 'month-day':
return list.slice(1, 3)
case 'datehour':
return list.slice(0, 4)
case 'hour-minute':
return list.slice(3, 5)
}
return list
}
function getSelectedValue(time: Date) {
return generateList([
time.getFullYear(),
time.getMonth() + 1,
time.getDate(),
time.getHours(),
time.getMinutes(),
time.getSeconds(),
].map(it => String(it)))
}
onBeforeMount(() => {
state.currentDate = formatValue(normalizeDate(props.modelValue))
})
watch(
() => props.modelValue,
(value) => {
const newValue = formatValue(normalizeDate(value))
if (!isEqualValue(newValue, state.currentDate)) {
state.currentDate = newValue
state.selectedValue = getSelectedValue(newValue)
}
},
)
watch(
() => state.currentDate,
(value) => {
if (!isEqualValue(value, normalizeDate(props.modelValue))) {
emit(UPDATE_MODEL_EVENT, value)
nextTick(() => {
state.selectedValue = getSelectedValue(value)
})
}
},
)
</script>
<script lang="ts">
const componentName = `${PREFIX}-date-picker`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutPicker
v-model="state.selectedValue"
:show-toolbar="props.showToolbar"
:title="props.title"
:ok-text="props.okText"
:cancel-text="props.cancelText"
:columns="columns"
:three-dimensional="props.threeDimensional"
:swipe-duration="props.swipeDuration"
:visible-option-num="props.visibleOptionNum"
:option-height="props.optionHeight"
@change="handleChange"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<template #top>
<slot name="top" />
</template>
<slot />
</NutPicker>
</template>
<style lang="scss">
@import "./index";
</style>

View File

@@ -0,0 +1 @@
@import "../picker/index";

View File

@@ -0,0 +1,2 @@
export * from './datepicker'
export * from './type'

View File

@@ -0,0 +1,27 @@
import type { PickerBaseEvent, PickerChangeEvent } from '../picker'
import type { PickerOption } from '../pickercolumn'
export type DateLike = number | string | Date
export const datepickerType = ['date', 'time', 'year-month', 'month-day', 'datehour', 'hour-minute', 'datetime'] as const
export type DatePickerType = (typeof datepickerType)[number]
export const datepickerColumnType = ['year', 'month', 'day', 'hour', 'minute', 'seconds'] as const
export type DatePickerColumnType = (typeof datepickerColumnType)[number]
export interface DatePickerBaseEvent extends PickerBaseEvent {
date: Date
}
export interface DatePickerChangeEvent extends DatePickerBaseEvent, PickerChangeEvent {
}
export type DatePickerFormatter = (type: DatePickerColumnType, option: PickerOption) => PickerOption
export type DatePickerFilter = (type: DatePickerColumnType, options: PickerOption[]) => PickerOption[]
export interface DatePickerRangeItem {
type: DatePickerColumnType
range: [number, number]
}