mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
feat(components): [calendar] add controller-type and formatter props (#23045)
* feat(components): [calendar] support controller-type prop * refactor: move handleDateChange to useCalendar * feat: update * style: update * style: update * feat: support formatter prop * feat: update * docs: update version number * chore: use ExtractPublicPropTypes * test: update * chore: improve parameter type * feat: update version number * feat: update * feat: update * Apply suggestions from code review Co-authored-by: rzzf <cszhjh@gmail.com> * docs: update example --------- Co-authored-by: rzzf <cszhjh@gmail.com> Co-authored-by: warmthsea <2586244885@qq.com>
This commit is contained in:
@@ -15,6 +15,14 @@ calendar/basic
|
||||
|
||||
:::
|
||||
|
||||
## Controller Type ^(2.13.1)
|
||||
|
||||
:::demo You can set the type of the controller for Calendar header. When setting `select`, you can use `formatter` to customize `label`.
|
||||
|
||||
calendar/controller-type
|
||||
|
||||
:::
|
||||
|
||||
## Custom Content
|
||||
|
||||
:::demo Customize what is displayed in the calendar cell by setting `scoped-slot` named `date-cell`. In `scoped-slot` you can get the date (the date of the current cell), data (including the type, isSelected, day attribute). For details, please refer to the API documentation below.
|
||||
@@ -49,10 +57,12 @@ Note, date time locale (month name, first day of the week ...) are also configur
|
||||
|
||||
### Attributes
|
||||
|
||||
| Name | Description | Type | Default |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------- |
|
||||
| model-value / v-model | binding value | ^[Date] | — |
|
||||
| range | time range, including start time and end time. Start time must be start day of week, end time must be end day of week, the time span cannot exceed two months. | ^[array]`[Date, Date]` | — |
|
||||
| Name | Description | Type | Default |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------- |
|
||||
| model-value / v-model | binding value | ^[Date] | — |
|
||||
| range | time range, including start time and end time. Start time must be start day of week, end time must be end day of week, the time span cannot exceed two months. | ^[array]`[Date, Date]` | — |
|
||||
| controller-type ^(2.13.1) | type of the controller for Calendar header | ^[enum]`'button' \| 'select'` | button |
|
||||
| formatter ^(2.13.1) | format label when `controller-type` is 'select' | ^[Function]`(value: number, type: 'year' \| 'month') => string \| number` | — |
|
||||
|
||||
### Slots
|
||||
|
||||
|
||||
15
docs/examples/calendar/controller-type.vue
Normal file
15
docs/examples/calendar/controller-type.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<el-radio-group v-model="controllerType">
|
||||
<el-radio-button label="select" value="select" />
|
||||
<el-radio-button label="button" value="button" />
|
||||
</el-radio-group>
|
||||
|
||||
<el-calendar v-model="value" :controller-type="controllerType" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const controllerType = ref<'select' | 'button'>('select')
|
||||
const value = ref(new Date())
|
||||
</script>
|
||||
@@ -247,4 +247,84 @@ describe('Calendar.vue', () => {
|
||||
expect(wrapper.find('.el-calendar__header').text()).toEqual(AXIOM)
|
||||
expect(wrapper.find('.current.is-today').text()).toEqual(AXIOM)
|
||||
})
|
||||
|
||||
it('should work when controller-type is select', async () => {
|
||||
const wrapper = mount(
|
||||
defineComponent({
|
||||
data: () => ({ value: new Date('2025-12-09') }),
|
||||
render() {
|
||||
return <Calendar v-model={this.value} controller-type="select" />
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
|
||||
const btn = wrapper.find('.el-button')
|
||||
const yearSelect = selects[0]
|
||||
const yearOptions = yearSelect.findAllComponents({ name: 'ElOption' })
|
||||
const monthSelect = selects[1]
|
||||
const monthOptions = monthSelect.findAllComponents({ name: 'ElOption' })
|
||||
const yearVm = yearSelect.vm as any
|
||||
const monthVm = monthSelect.vm as any
|
||||
const firstRow = wrapper.element.querySelector('.el-calendar-table__row')
|
||||
|
||||
expect(yearVm.modelValue).toBe(2025)
|
||||
expect(monthVm.modelValue).toBe(12)
|
||||
expect(firstRow?.firstElementChild?.innerHTML).toContain('30')
|
||||
expect(firstRow?.lastElementChild?.innerHTML).toContain('6')
|
||||
|
||||
await yearOptions[9].trigger('click')
|
||||
expect(yearVm.modelValue).toBe(2024)
|
||||
expect(monthVm.modelValue).toBe(12)
|
||||
expect(firstRow?.firstElementChild?.innerHTML).toContain('1')
|
||||
expect(firstRow?.lastElementChild?.innerHTML).toContain('7')
|
||||
|
||||
await monthOptions[10].trigger('click')
|
||||
expect(yearVm.modelValue).toBe(2024)
|
||||
expect(monthVm.modelValue).toBe(11)
|
||||
expect(firstRow?.firstElementChild?.innerHTML).toContain('27')
|
||||
expect(firstRow?.lastElementChild?.innerHTML).toContain('2')
|
||||
|
||||
await btn.trigger('click')
|
||||
expect(yearVm.modelValue).toBe(2025)
|
||||
expect(monthVm.modelValue).toBe(12)
|
||||
expect(firstRow?.firstElementChild?.innerHTML).toContain('30')
|
||||
expect(firstRow?.lastElementChild?.innerHTML).toContain('6')
|
||||
})
|
||||
|
||||
it('should work with formatter prop', async () => {
|
||||
const formatter = (value: number, type: 'year' | 'month') => {
|
||||
if (type === 'year') {
|
||||
return `${value}年`
|
||||
} else {
|
||||
return `${value}月`
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = mount(
|
||||
defineComponent({
|
||||
data: () => ({ value: new Date('2025-12-09') }),
|
||||
render() {
|
||||
return (
|
||||
<Calendar
|
||||
v-model={this.value}
|
||||
controller-type="select"
|
||||
formatter={formatter}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
|
||||
const yearSelect = selects[0]
|
||||
const yearOptions = yearSelect.findAllComponents({ name: 'ElOption' })
|
||||
const monthSelect = selects[1]
|
||||
const monthOptions = monthSelect.findAllComponents({ name: 'ElOption' })
|
||||
|
||||
expect(yearOptions[0].text()).toBe('2015年')
|
||||
expect(monthOptions[0].text()).toBe('1月')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,6 +33,22 @@ export const calendarProps = buildProps({
|
||||
type: definePropType<[Date, Date]>(Array),
|
||||
validator: isValidRange,
|
||||
},
|
||||
/**
|
||||
* @description type of the controller for the Calendar header
|
||||
*/
|
||||
controllerType: {
|
||||
type: String,
|
||||
values: ['button', 'select'],
|
||||
default: 'button',
|
||||
},
|
||||
/**
|
||||
* @description format label when `controller-type` is 'select'
|
||||
*/
|
||||
formatter: {
|
||||
type: definePropType<
|
||||
(value: number, type: 'year' | 'month') => string | number
|
||||
>(Function),
|
||||
},
|
||||
} as const)
|
||||
export type CalendarProps = ExtractPropTypes<typeof calendarProps>
|
||||
export type CalendarPropsPublic = ExtractPublicPropTypes<typeof calendarProps>
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<div :class="ns.e('header')">
|
||||
<slot name="header" :date="i18nDate">
|
||||
<div :class="ns.e('title')">{{ i18nDate }}</div>
|
||||
<div v-if="validatedRange.length === 0" :class="ns.e('button-group')">
|
||||
<div
|
||||
v-if="validatedRange.length === 0 && controllerType === 'button'"
|
||||
:class="ns.e('button-group')"
|
||||
>
|
||||
<el-button-group>
|
||||
<el-button size="small" @click="selectDate('prev-month')">
|
||||
{{ t('el.datepicker.prevMonth') }}
|
||||
@@ -16,6 +19,16 @@
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="validatedRange.length === 0 && controllerType === 'select'"
|
||||
:class="ns.e('select-controller')"
|
||||
>
|
||||
<select-controller
|
||||
:date="date"
|
||||
:formatter="formatter"
|
||||
@date-change="handleDateChange"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="validatedRange.length === 0" :class="ns.e('body')">
|
||||
@@ -50,6 +63,7 @@ import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import DateTable from './date-table.vue'
|
||||
import { useCalendar } from './use-calendar'
|
||||
import { calendarEmits, calendarProps } from './calendar'
|
||||
import SelectController from './select-controller.vue'
|
||||
|
||||
const ns = useNamespace('calendar')
|
||||
|
||||
@@ -68,6 +82,7 @@ const {
|
||||
realSelectedDay,
|
||||
selectDate,
|
||||
validatedRange,
|
||||
handleDateChange,
|
||||
} = useCalendar(props, emit, COMPONENT_NAME)
|
||||
|
||||
const { t } = useLocale()
|
||||
|
||||
32
packages/components/calendar/src/select-controller.ts
Normal file
32
packages/components/calendar/src/select-controller.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
buildProps,
|
||||
definePropType,
|
||||
isObject,
|
||||
isString,
|
||||
} from '@element-plus/utils'
|
||||
|
||||
import type { ExtractPropTypes, ExtractPublicPropTypes } from 'vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
export const selectControllerProps = buildProps({
|
||||
date: {
|
||||
type: definePropType<Dayjs>(Object),
|
||||
required: true,
|
||||
},
|
||||
formatter: {
|
||||
type: definePropType<
|
||||
(value: number, type: 'year' | 'month') => string | number
|
||||
>(Function),
|
||||
},
|
||||
} as const)
|
||||
export type SelectControllerProps = ExtractPropTypes<
|
||||
typeof selectControllerProps
|
||||
>
|
||||
export type SelectControllerPropsPublic = ExtractPublicPropTypes<
|
||||
typeof selectControllerProps
|
||||
>
|
||||
|
||||
export const selectControllerEmits = {
|
||||
'date-change': (date: Dayjs | 'today') => isObject(date) || isString(date),
|
||||
}
|
||||
export type SelectControllerEmits = typeof selectControllerEmits
|
||||
90
packages/components/calendar/src/select-controller.vue
Normal file
90
packages/components/calendar/src/select-controller.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<el-select
|
||||
:model-value="yearValue"
|
||||
size="small"
|
||||
:class="nsSelect.e('year')"
|
||||
:validate-event="false"
|
||||
:options="yearOptions"
|
||||
@change="handleYearChange"
|
||||
/>
|
||||
<el-select
|
||||
:model-value="monthValue"
|
||||
size="small"
|
||||
:class="nsSelect.e('month')"
|
||||
:validate-event="false"
|
||||
:options="monthOptions"
|
||||
@change="handleMonthChange"
|
||||
/>
|
||||
<el-button size="small" @click="selectToday">
|
||||
{{ t('el.datepicker.today') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import ElSelect from '@element-plus/components/select'
|
||||
import { ElButton } from '@element-plus/components/button'
|
||||
import { isFunction } from '@element-plus/utils'
|
||||
import {
|
||||
selectControllerEmits,
|
||||
selectControllerProps,
|
||||
} from './select-controller'
|
||||
|
||||
defineOptions({
|
||||
name: 'SelectController',
|
||||
})
|
||||
|
||||
const props = defineProps(selectControllerProps)
|
||||
const emit = defineEmits(selectControllerEmits)
|
||||
|
||||
const nsSelect = useNamespace('calendar-select')
|
||||
const { t, lang } = useLocale()
|
||||
|
||||
const monthOptions = Array.from({ length: 12 }, (_, index) => {
|
||||
const actualMonth = index + 1
|
||||
const label = isFunction(props.formatter)
|
||||
? props.formatter(actualMonth, 'month')
|
||||
: actualMonth
|
||||
return {
|
||||
value: actualMonth,
|
||||
label,
|
||||
}
|
||||
})
|
||||
|
||||
const yearValue = computed(() => props.date.year())
|
||||
const monthValue = computed(() => props.date.month() + 1)
|
||||
// Get an array of 20 years
|
||||
const yearOptions = computed(() => {
|
||||
const years = []
|
||||
for (let i = -10; i < 10; i++) {
|
||||
const year = yearValue.value + i
|
||||
if (year > 0) {
|
||||
const label = isFunction(props.formatter)
|
||||
? props.formatter(year, 'year')
|
||||
: year
|
||||
years.push({ value: year, label })
|
||||
}
|
||||
}
|
||||
return years
|
||||
})
|
||||
|
||||
const handleYearChange = (year: number) => {
|
||||
emit(
|
||||
'date-change',
|
||||
dayjs(new Date(year, monthValue.value - 1, 1)).locale(lang.value)
|
||||
)
|
||||
}
|
||||
|
||||
const handleMonthChange = (month: number) => {
|
||||
emit(
|
||||
'date-change',
|
||||
dayjs(new Date(yearValue.value, month - 1, 1)).locale(lang.value)
|
||||
)
|
||||
}
|
||||
|
||||
const selectToday = () => {
|
||||
emit('date-change', 'today')
|
||||
}
|
||||
</script>
|
||||
@@ -178,6 +178,14 @@ export const useCalendar = (
|
||||
}
|
||||
}
|
||||
|
||||
const handleDateChange = (date: Dayjs | 'today') => {
|
||||
if (date === 'today') {
|
||||
selectDate('today')
|
||||
} else {
|
||||
pickDay(date)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
calculateValidatedDateRange,
|
||||
date,
|
||||
@@ -185,5 +193,6 @@ export const useCalendar = (
|
||||
pickDay,
|
||||
selectDate,
|
||||
validatedRange,
|
||||
handleDateChange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ import '@element-plus/components/base/style/css'
|
||||
import '@element-plus/theme-chalk/el-calendar.css'
|
||||
import '@element-plus/components/button/style/css'
|
||||
import '@element-plus/components/button-group/style/css'
|
||||
import '@element-plus/components/select/style/css'
|
||||
|
||||
@@ -2,3 +2,4 @@ import '@element-plus/components/base/style'
|
||||
import '@element-plus/theme-chalk/src/calendar.scss'
|
||||
import '@element-plus/components/button/style'
|
||||
import '@element-plus/components/button-group/style'
|
||||
import '@element-plus/components/select/style'
|
||||
|
||||
@@ -22,6 +22,20 @@
|
||||
@include e(body) {
|
||||
padding: 12px 20px 35px;
|
||||
}
|
||||
|
||||
@include e(select-controller) {
|
||||
.#{$namespace}-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.#{$namespace}-calendar-select__year {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.#{$namespace}-calendar-select__month {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include b(calendar-table) {
|
||||
|
||||
Reference in New Issue
Block a user