diff --git a/components/date-picker/demo/auto-fill-whole-day.vue b/components/date-picker/demo/auto-fill-whole-day.vue new file mode 100644 index 000000000..4c380a869 --- /dev/null +++ b/components/date-picker/demo/auto-fill-whole-day.vue @@ -0,0 +1,123 @@ + +--- +order: 7 +title: + zh-CN: 自动填充和整天模式 + en-US: Auto Fill and Whole Day Mode +--- + +## zh-CN + +RangePicker 支持两个新功能: +1. `autoFill`:双击日期时自动设置为开始和结束日期 +2. `isWholeDay`:在 showTime 模式下,自动设置开始时间为 00:00:00,结束时间为 23:59:59 + +## en-US + +RangePicker supports two new features: +1. `autoFill`: Double-click a date to automatically set it as both start and end date +2. `isWholeDay`: In showTime mode, automatically set start time to 00:00:00 and end time to 23:59:59 + + + + + + diff --git a/components/date-picker/demo/index.vue b/components/date-picker/demo/index.vue index 92808d6ba..a7632d5ee 100644 --- a/components/date-picker/demo/index.vue +++ b/components/date-picker/demo/index.vue @@ -17,6 +17,7 @@ + diff --git a/components/date-picker/demo/presetted-ranges.vue b/components/date-picker/demo/presetted-ranges.vue index 1e27a39c3..8fd512fe3 100644 --- a/components/date-picker/demo/presetted-ranges.vue +++ b/components/date-picker/demo/presetted-ranges.vue @@ -40,25 +40,31 @@ const onChange = (date: Dayjs) => { console.log('Clear'); } }; -const onRangeChange = (dates: RangeValue, dateStrings: string[]) => { - if (dates) { - console.log('From: ', dates[0], ', to: ', dates[1]); +const onRangeChange = (values: RangeValue, dateStrings: [string, string], currentPreset?: any) => { + if (values) { + console.log('From: ', values[0], ', to: ', values[1]); console.log('From: ', dateStrings[0], ', to: ', dateStrings[1]); + if (currentPreset) { + console.log('Selected preset key: ', currentPreset.key); + console.log('Selected preset label: ', currentPreset.label); + } else { + console.log('Manual selection (no preset)'); + } } else { console.log('Clear'); } }; const presets = ref([ - { label: 'Yesterday', value: dayjs().add(-1, 'd') }, - { label: 'Last Week', value: dayjs().add(-7, 'd') }, - { label: 'Last Month', value: dayjs().add(-1, 'month') }, + { label: 'Yesterday', value: dayjs().add(-1, 'd'), key: 'yesterday' }, + { label: 'Last Week', value: dayjs().add(-7, 'd'), key: 'lastweek' }, + { label: 'Last Month', value: dayjs().add(-1, 'month'), key: 'lastmonth' }, ]); const rangePresets = ref([ - { label: 'Last 7 Days', value: [dayjs().add(-7, 'd'), dayjs()] }, - { label: 'Last 14 Days', value: [dayjs().add(-14, 'd'), dayjs()] }, - { label: 'Last 30 Days', value: [dayjs().add(-30, 'd'), dayjs()] }, - { label: 'Last 90 Days', value: [dayjs().add(-90, 'd'), dayjs()] }, + { label: 'Last 7 Days', value: [dayjs().add(-7, 'd'), dayjs()], key: 'last7days' }, + { label: 'Last 14 Days', value: [dayjs().add(-14, 'd'), dayjs()], key: 'last14days' }, + { label: 'Last 30 Days', value: [dayjs().add(-30, 'd'), dayjs()], key: 'last30days' }, + { label: 'Last 90 Days', value: [dayjs().add(-90, 'd'), dayjs()], key: 'last90days' }, ]); diff --git a/components/date-picker/generatePicker/generateRangePicker.tsx b/components/date-picker/generatePicker/generateRangePicker.tsx index a321706c3..b25d2cbd6 100644 --- a/components/date-picker/generatePicker/generateRangePicker.tsx +++ b/components/date-picker/generatePicker/generateRangePicker.tsx @@ -13,7 +13,7 @@ import useConfigInject from '../../config-provider/hooks/useConfigInject'; import classNames from '../../_util/classNames'; import type { CommonProps, RangePickerProps } from './props'; import { commonProps, rangePickerProps } from './props'; -import type { PanelMode, RangeValue } from '../../vc-picker/interface'; +import type { PanelMode, RangeValue, RangePickerOnChange } from '../../vc-picker/interface'; import type { RangePickerSharedProps } from '../../vc-picker/RangePicker'; import { FormItemInputContext, useInjectFormItemContext } from '../../form/FormItemContext'; import omit from '../../_util/omit'; @@ -84,13 +84,18 @@ export default function generateRangePicker( pickerRef.value?.blur(); }, }); - const maybeToStrings = (dates: DateType[]) => { + const maybeToStrings = (dates: RangeValue) => { return props.valueFormat ? generateConfig.toString(dates, props.valueFormat) : dates; }; - const onChange = (dates: RangeValue, dateStrings: [string, string]) => { - const values = maybeToStrings(dates); - emit('update:value', values); - emit('change', values, dateStrings); + const onChange: RangePickerOnChange = (values, formatStrings) => { + const [startValue, endValue, currentPreset] = values; + const [startStr, endStr] = formatStrings; + const dates: RangeValue = [startValue, endValue]; + const dateStrings: [string, string] = [startStr, endStr]; + + const processedValues = maybeToStrings(dates); + emit('update:value', processedValues); + emit('change', processedValues, dateStrings, currentPreset); formItemContext.onFieldChange(); }; const onOpenChange = (open: boolean) => { @@ -109,7 +114,7 @@ export default function generateRangePicker( emit('panelChange', values, modes); }; const onOk = (dates: DateType[]) => { - const value = maybeToStrings(dates); + const value = props.valueFormat ? generateConfig.toString(dates, props.valueFormat) : dates; emit('ok', value); }; const onCalendarChange: RangePickerSharedProps['onCalendarChange'] = ( diff --git a/components/date-picker/generatePicker/props.ts b/components/date-picker/generatePicker/props.ts index 1af653de5..0086abf19 100644 --- a/components/date-picker/generatePicker/props.ts +++ b/components/date-picker/generatePicker/props.ts @@ -269,8 +269,13 @@ export interface RangePickerProps { onChange?: ( value: RangeValue | RangeValue | null, dateString: [string, string], + currentPreset?: any, ) => void; 'onUpdate:value'?: (value: RangeValue | RangeValue | null) => void; + /** 双击日期时自动设置为开始和结束日期 */ + autoFill?: boolean; + /** 在 showTime 模式下,是否设置为整天(开始时间 00:00:00,结束时间 23:59:59) */ + isWholeDay?: boolean; onCalendarChange?: ( values: RangeValue | RangeValue, formatString: [string, string], diff --git a/components/date-picker/style/index.ts b/components/date-picker/style/index.ts index 4e92852fd..e6ec1d394 100644 --- a/components/date-picker/style/index.ts +++ b/components/date-picker/style/index.ts @@ -961,6 +961,7 @@ const genPickerStyle: GenerateStyle = token => { controlItemBgHover, presetsWidth, presetsMaxWidth, + fontWeightStrong, } = token; return [ @@ -1326,6 +1327,12 @@ const genPickerStyle: GenerateStyle = token => { '&:hover': { background: controlItemBgHover, }, + + [`&${componentCls}-preset-active`]: { + background: controlItemBgActive, + color: colorPrimary, + fontWeight: fontWeightStrong, + }, }, }, }, diff --git a/components/vc-picker/PresetPanel.tsx b/components/vc-picker/PresetPanel.tsx index a5d98d02a..55a1e0e23 100644 --- a/components/vc-picker/PresetPanel.tsx +++ b/components/vc-picker/PresetPanel.tsx @@ -1,13 +1,18 @@ import { defineComponent } from 'vue'; +import type { PresetDate } from './interface'; export default defineComponent({ name: 'PresetPanel', props: { prefixCls: String, presets: { - type: Array, + type: Array as () => PresetDate[], default: () => [], }, + currentPreset: { + type: Object as () => PresetDate | null, + default: null, + }, onClick: Function, onHover: Function, }, @@ -19,21 +24,24 @@ export default defineComponent({ return (
    - {props.presets.map(({ label, value }, index) => ( + {props.presets.map(preset => (
  • { e.stopPropagation(); - props.onClick(value); + props.onClick(preset.value, preset); }} onMouseenter={() => { - props.onHover?.(value); + props.onHover?.(preset.value); }} onMouseleave={() => { props.onHover?.(null); }} > - {label} + {preset.label}
  • ))}
diff --git a/components/vc-picker/RangePicker.tsx b/components/vc-picker/RangePicker.tsx index 84a65d4f2..145693fd0 100644 --- a/components/vc-picker/RangePicker.tsx +++ b/components/vc-picker/RangePicker.tsx @@ -5,6 +5,7 @@ import type { RangeValue, EventValue, PresetDate, + RangePickerOnChange, } from './interface'; import type { PickerBaseProps, PickerDateProps, PickerTimeProps } from './Picker'; import type { SharedTimeProps } from './panels/TimePanel'; @@ -108,7 +109,7 @@ export type RangePickerSharedProps = { separator?: VueNode; allowEmpty?: [boolean, boolean]; mode?: [PanelMode, PanelMode]; - onChange?: (values: RangeValue, formatString: [string, string]) => void; + onChange?: RangePickerOnChange; onCalendarChange?: ( values: RangeValue, formatString: [string, string], @@ -133,6 +134,10 @@ export type RangePickerSharedProps = { nextIcon?: VueNode; superPrevIcon?: VueNode; superNextIcon?: VueNode; + /** 双击日期时自动设置为开始和结束日期 */ + autoFill?: boolean; + /** 在 showTime 模式下,是否设置为整天(开始时间 00:00:00,结束时间 23:59:59) */ + isWholeDay?: boolean; }; type OmitPickerProps = Omit< @@ -258,6 +263,8 @@ function RangerPicker() { 'nextIcon', 'superPrevIcon', 'superNextIcon', + 'autoFill', + 'isWholeDay', ] as any, setup(props, { attrs, expose }) { const needConfirmButton = computed( @@ -319,6 +326,31 @@ function RangerPicker() { : reorderValues(values, props.generateConfig), }); + // ========================= Current Preset ========================= + const [currentPreset, setCurrentPreset] = useState> | null>( + null, + ); + + // 检查当前值是否匹配某个 preset + const checkAndSetPreset = (values: RangeValue) => { + if (!values || !values[0] || !values[1]) { + setCurrentPreset(null); + return; + } + + const matchedPreset = presetList.value.find(preset => { + if (!preset.value || !preset.value[0] || !preset.value[1]) { + return false; + } + return ( + isEqual(props.generateConfig, values[0], preset.value[0]) && + isEqual(props.generateConfig, values[1], preset.value[1]) + ); + }); + + setCurrentPreset(matchedPreset || null); + }; + // =========================== View Date =========================== // Config view panel const [startViewDate, endViewDate, setViewDate] = useRangeViewDates({ @@ -491,7 +523,11 @@ function RangerPicker() { }, 0); } - function triggerChange(newValue: RangeValue, sourceIndex: 0 | 1) { + function triggerChange( + newValue: RangeValue, + sourceIndex: 0 | 1, + fromPreset = false, + ) { let values = newValue; let startValue = getValue(values, 0); let endValue = getValue(values, 1); @@ -541,8 +577,33 @@ function RangerPicker() { } } + // Handle isWholeDay: set time to 00:00:00 for start and 23:59:59 for end when showTime is true + if (props.isWholeDay && showTime && values && values[0] && values[1]) { + const startDate = values[0]; + const endDate = values[1]; + + // Set start time to 00:00:00 + const startWithTime = generateConfig.setHour( + generateConfig.setMinute(generateConfig.setSecond(startDate, 0), 0), + 0, + ); + + // Set end time to 23:59:59 + const endWithTime = generateConfig.setHour( + generateConfig.setMinute(generateConfig.setSecond(endDate, 59), 59), + 23, + ); + + values = [startWithTime, endWithTime]; + } + setSelectedValue(values); + // 如果不是通过 preset 触发的,清除 currentPreset + if (!fromPreset) { + setCurrentPreset(null); + } + const startStr = values && values[0] ? formatValue(values[0], { generateConfig, locale, format: formatList.value[0] }) @@ -577,7 +638,10 @@ function RangerPicker() { (!isEqual(generateConfig, getValue(mergedValue.value, 0), startValue) || !isEqual(generateConfig, getValue(mergedValue.value, 1), endValue)) ) { - onChange(values, [startStr, endStr]); + onChange( + [startValue, endValue, currentPreset.value], + [startStr, endStr, currentPreset.value?.key || null], + ); } } @@ -720,7 +784,7 @@ function RangerPicker() { ) { return false; } - triggerChange(selectedValue.value, index); + triggerChange(selectedValue.value, index, false); resetText(); }, onCancel: () => { @@ -824,6 +888,11 @@ function RangerPicker() { setSelectedValue(mergedValue.value); }); + // 当 mergedValue 变化时,检查是否匹配某个 preset + watch(mergedValue, newValue => { + checkAndSetPreset(newValue); + }); + // ============================ Warning ============================ if (process.env.NODE_ENV !== 'production') { watchEffect(() => { @@ -889,6 +958,37 @@ function RangerPicker() { }; } + // Handle isWholeDay: set default time values for start and end + if (props.isWholeDay && showTime) { + const now = generateConfig.getNow(); + let defaultTime: DateType; + + if (mergedActivePickerIndex.value === 0) { + // Start time: 00:00:00 + defaultTime = generateConfig.setHour( + generateConfig.setMinute(generateConfig.setSecond(now, 0), 0), + 0, + ); + } else { + // End time: 23:59:59 + defaultTime = generateConfig.setHour( + generateConfig.setMinute(generateConfig.setSecond(now, 59), 59), + 23, + ); + } + + if (typeof showTime === 'object') { + panelShowTime = { + ...showTime, + defaultValue: defaultTime, + }; + } else { + panelShowTime = { + defaultValue: defaultTime, + }; + } + } + let panelDateRender: DateRender | null = null; if (dateRender) { panelDateRender = ({ current: date, today }) => @@ -971,7 +1071,7 @@ function RangerPicker() { } const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { - const values = updateValues(selectedValue.value, date, mergedActivePickerIndex.value); + let values = updateValues(selectedValue.value, date, mergedActivePickerIndex.value); const currentIndex = mergedActivePickerIndex.value; const isDoubleClick = isDoubleClickRef.value; const shouldSwitch = type === 'mouse' && needConfirmButton.value && isDoubleClick; @@ -979,13 +1079,28 @@ function RangerPicker() { // Reset double click state isDoubleClickRef.value = false; + // Handle autoFill: when double-clicking and autoFill is enabled, set the same date for both start and end + if (props.autoFill && isDoubleClick && type === 'mouse') { + values = [date, date]; + } + if (type === 'submit' || (type !== 'key' && !needConfirmButton.value) || shouldSwitch) { // triggerChange will also update selected values - triggerChange(values, mergedActivePickerIndex.value); + triggerChange(values, mergedActivePickerIndex.value, false); - // If double click, switch to next input - // But check if both inputs are complete, if so don't switch to avoid animation before popup closes - if (shouldSwitch) { + // If autoFill is enabled and we have both values, close the panel + if ( + props.autoFill && + isDoubleClick && + type === 'mouse' && + values && + values[0] && + values[1] + ) { + triggerOpen(false, mergedActivePickerIndex.value); + } else if (shouldSwitch) { + // If double click, switch to next input + // But check if both inputs are complete, if so don't switch to avoid animation before popup closes const startValue = getValue(values, 0); const endValue = getValue(values, 1); const bothValuesComplete = startValue && endValue; @@ -1074,7 +1189,7 @@ function RangerPicker() { onOk: () => { if (getValue(selectedValue.value, mergedActivePickerIndex.value)) { // triggerChangeOld(selectedValue.value); - triggerChange(selectedValue.value, mergedActivePickerIndex.value); + triggerChange(selectedValue.value, mergedActivePickerIndex.value, false); if (onOk) { onOk(selectedValue.value); } @@ -1129,8 +1244,10 @@ function RangerPicker() { { - triggerChange(nextValue, null); + currentPreset={currentPreset.value} + onClick={(nextValue, preset) => { + setCurrentPreset(preset); + triggerChange(nextValue, null, true); triggerOpen(false, mergedActivePickerIndex.value); }} onHover={hoverValue => { @@ -1207,7 +1324,7 @@ function RangerPicker() { values = updateValues(values, null, 1); } - triggerChange(values, null); + triggerChange(values, null, false); triggerOpen(false, mergedActivePickerIndex.value); }} class={`${prefixCls}-clear`} diff --git a/components/vc-picker/hooks/usePresets.ts b/components/vc-picker/hooks/usePresets.ts index 5b95de32d..933e3ab29 100644 --- a/components/vc-picker/hooks/usePresets.ts +++ b/components/vc-picker/hooks/usePresets.ts @@ -22,6 +22,7 @@ export default function usePresets( return { label, value: newValues, + key: label, // 添加 key 属性 }; }); } diff --git a/components/vc-picker/interface.ts b/components/vc-picker/interface.ts index 58b7106da..3f1255cda 100644 --- a/components/vc-picker/interface.ts +++ b/components/vc-picker/interface.ts @@ -112,4 +112,11 @@ export type CustomFormat = (value: DateType) => string; export interface PresetDate { label: VueNode; value: T; + key: string; // 重要,需要用key来高亮选中状态 } + +// 扩展的 onChange 回调类型,values 和 formatString 都包含第三个 preset 元素 +export type RangePickerOnChange = ( + values: [DateType | null, DateType | null, PresetDate> | null], + formatString: [string, string, string | null], +) => void;