feat: 增加双击自动填充数据,以及自动设置是否全天的bug

pull/8366/head
yuanyi 2025-09-17 16:12:17 +04:00
parent 53c4256f10
commit 5843bf5fd9
10 changed files with 318 additions and 36 deletions

View File

@ -0,0 +1,123 @@
<docs>
---
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
</docs>
<template>
<a-space direction="vertical" :size="12">
<div>
<h4>Auto Fill 功能</h4>
<p>双击日期会自动设置为开始和结束日期</p>
<a-range-picker :auto-fill="true" @change="onAutoFillChange" />
</div>
<div>
<h4>Whole Day 功能</h4>
<p> showTime 模式下自动设置整天时间</p>
<a-range-picker
show-time
:is-whole-day="true"
format="YYYY/MM/DD HH:mm:ss"
@change="onWholeDayChange"
/>
</div>
<div>
<h4>组合使用</h4>
<p>同时启用 autoFill isWholeDay</p>
<a-range-picker
show-time
:auto-fill="true"
:is-whole-day="true"
format="YYYY/MM/DD HH:mm:ss"
@change="onCombinedChange"
/>
</div>
</a-space>
</template>
<script lang="ts" setup>
import dayjs, { Dayjs } from 'dayjs';
type RangeValue = [Dayjs, Dayjs];
const onAutoFillChange = (
values: RangeValue,
dateStrings: [string, string],
currentPreset?: any,
) => {
if (values) {
console.log(
'Auto Fill - From: ',
values[0].format('YYYY-MM-DD'),
', to: ',
values[1].format('YYYY-MM-DD'),
);
console.log('Auto Fill - From: ', dateStrings[0], ', to: ', dateStrings[1]);
if (currentPreset) {
console.log('Auto Fill - Selected preset: ', currentPreset.label);
}
} else {
console.log('Auto Fill - Clear');
}
};
const onWholeDayChange = (
values: RangeValue,
dateStrings: [string, string],
currentPreset?: any,
) => {
if (values) {
console.log(
'Whole Day - From: ',
values[0].format('YYYY-MM-DD HH:mm:ss'),
', to: ',
values[1].format('YYYY-MM-DD HH:mm:ss'),
);
console.log('Whole Day - From: ', dateStrings[0], ', to: ', dateStrings[1]);
if (currentPreset) {
console.log('Whole Day - Selected preset: ', currentPreset.label);
}
} else {
console.log('Whole Day - Clear');
}
};
const onCombinedChange = (
values: RangeValue,
dateStrings: [string, string],
currentPreset?: any,
) => {
if (values) {
console.log(
'Combined - From: ',
values[0].format('YYYY-MM-DD HH:mm:ss'),
', to: ',
values[1].format('YYYY-MM-DD HH:mm:ss'),
);
console.log('Combined - From: ', dateStrings[0], ', to: ', dateStrings[1]);
if (currentPreset) {
console.log('Combined - Selected preset: ', currentPreset.label);
}
} else {
console.log('Combined - Clear');
}
};
</script>

View File

@ -17,6 +17,7 @@
<Suffix /> <Suffix />
<statusVue /> <statusVue />
<placementVue /> <placementVue />
<AutoFillWholeDay />
</demo-sort> </demo-sort>
</template> </template>
<script> <script>
@ -36,6 +37,7 @@ import Suffix from './suffix.vue';
import Bordered from './bordered.vue'; import Bordered from './bordered.vue';
import RangePicker from './range-picker.vue'; import RangePicker from './range-picker.vue';
import placementVue from './placement.vue'; import placementVue from './placement.vue';
import AutoFillWholeDay from './auto-fill-whole-day.vue';
import statusVue from './status.vue'; import statusVue from './status.vue';
import CN from '../index.zh-CN.md'; import CN from '../index.zh-CN.md';
import US from '../index.en-US.md'; import US from '../index.en-US.md';
@ -62,6 +64,7 @@ export default defineComponent({
SelectInRnage, SelectInRnage,
Bordered, Bordered,
RangePicker, RangePicker,
AutoFillWholeDay,
}, },
}); });
</script> </script>

View File

@ -40,25 +40,31 @@ const onChange = (date: Dayjs) => {
console.log('Clear'); console.log('Clear');
} }
}; };
const onRangeChange = (dates: RangeValue, dateStrings: string[]) => { const onRangeChange = (values: RangeValue, dateStrings: [string, string], currentPreset?: any) => {
if (dates) { if (values) {
console.log('From: ', dates[0], ', to: ', dates[1]); console.log('From: ', values[0], ', to: ', values[1]);
console.log('From: ', dateStrings[0], ', to: ', dateStrings[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 { } else {
console.log('Clear'); console.log('Clear');
} }
}; };
const presets = ref([ const presets = ref([
{ label: 'Yesterday', value: dayjs().add(-1, 'd') }, { label: 'Yesterday', value: dayjs().add(-1, 'd'), key: 'yesterday' },
{ label: 'Last Week', value: dayjs().add(-7, 'd') }, { label: 'Last Week', value: dayjs().add(-7, 'd'), key: 'lastweek' },
{ label: 'Last Month', value: dayjs().add(-1, 'month') }, { label: 'Last Month', value: dayjs().add(-1, 'month'), key: 'lastmonth' },
]); ]);
const rangePresets = ref([ const rangePresets = ref([
{ label: 'Last 7 Days', value: [dayjs().add(-7, 'd'), dayjs()] }, { label: 'Last 7 Days', value: [dayjs().add(-7, 'd'), dayjs()], key: 'last7days' },
{ label: 'Last 14 Days', value: [dayjs().add(-14, 'd'), dayjs()] }, { label: 'Last 14 Days', value: [dayjs().add(-14, 'd'), dayjs()], key: 'last14days' },
{ label: 'Last 30 Days', value: [dayjs().add(-30, 'd'), dayjs()] }, { label: 'Last 30 Days', value: [dayjs().add(-30, 'd'), dayjs()], key: 'last30days' },
{ label: 'Last 90 Days', value: [dayjs().add(-90, 'd'), dayjs()] }, { label: 'Last 90 Days', value: [dayjs().add(-90, 'd'), dayjs()], key: 'last90days' },
]); ]);
</script> </script>

View File

@ -13,7 +13,7 @@ import useConfigInject from '../../config-provider/hooks/useConfigInject';
import classNames from '../../_util/classNames'; import classNames from '../../_util/classNames';
import type { CommonProps, RangePickerProps } from './props'; import type { CommonProps, RangePickerProps } from './props';
import { 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 type { RangePickerSharedProps } from '../../vc-picker/RangePicker';
import { FormItemInputContext, useInjectFormItemContext } from '../../form/FormItemContext'; import { FormItemInputContext, useInjectFormItemContext } from '../../form/FormItemContext';
import omit from '../../_util/omit'; import omit from '../../_util/omit';
@ -84,13 +84,18 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
pickerRef.value?.blur(); pickerRef.value?.blur();
}, },
}); });
const maybeToStrings = (dates: DateType[]) => { const maybeToStrings = (dates: RangeValue<DateType>) => {
return props.valueFormat ? generateConfig.toString(dates, props.valueFormat) : dates; return props.valueFormat ? generateConfig.toString(dates, props.valueFormat) : dates;
}; };
const onChange = (dates: RangeValue<DateType>, dateStrings: [string, string]) => { const onChange: RangePickerOnChange<DateType> = (values, formatStrings) => {
const values = maybeToStrings(dates); const [startValue, endValue, currentPreset] = values;
emit('update:value', values); const [startStr, endStr] = formatStrings;
emit('change', values, dateStrings); const dates: RangeValue<DateType> = [startValue, endValue];
const dateStrings: [string, string] = [startStr, endStr];
const processedValues = maybeToStrings(dates);
emit('update:value', processedValues);
emit('change', processedValues, dateStrings, currentPreset);
formItemContext.onFieldChange(); formItemContext.onFieldChange();
}; };
const onOpenChange = (open: boolean) => { const onOpenChange = (open: boolean) => {
@ -109,7 +114,7 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
emit('panelChange', values, modes); emit('panelChange', values, modes);
}; };
const onOk = (dates: DateType[]) => { const onOk = (dates: DateType[]) => {
const value = maybeToStrings(dates); const value = props.valueFormat ? generateConfig.toString(dates, props.valueFormat) : dates;
emit('ok', value); emit('ok', value);
}; };
const onCalendarChange: RangePickerSharedProps<DateType>['onCalendarChange'] = ( const onCalendarChange: RangePickerSharedProps<DateType>['onCalendarChange'] = (

View File

@ -269,8 +269,13 @@ export interface RangePickerProps<DateType> {
onChange?: ( onChange?: (
value: RangeValue<DateType> | RangeValue<string> | null, value: RangeValue<DateType> | RangeValue<string> | null,
dateString: [string, string], dateString: [string, string],
currentPreset?: any,
) => void; ) => void;
'onUpdate:value'?: (value: RangeValue<DateType> | RangeValue<string> | null) => void; 'onUpdate:value'?: (value: RangeValue<DateType> | RangeValue<string> | null) => void;
/** 双击日期时自动设置为开始和结束日期 */
autoFill?: boolean;
/** 在 showTime 模式下,是否设置为整天(开始时间 00:00:00结束时间 23:59:59 */
isWholeDay?: boolean;
onCalendarChange?: ( onCalendarChange?: (
values: RangeValue<DateType> | RangeValue<string>, values: RangeValue<DateType> | RangeValue<string>,
formatString: [string, string], formatString: [string, string],

View File

@ -961,6 +961,7 @@ const genPickerStyle: GenerateStyle<PickerToken> = token => {
controlItemBgHover, controlItemBgHover,
presetsWidth, presetsWidth,
presetsMaxWidth, presetsMaxWidth,
fontWeightStrong,
} = token; } = token;
return [ return [
@ -1326,6 +1327,12 @@ const genPickerStyle: GenerateStyle<PickerToken> = token => {
'&:hover': { '&:hover': {
background: controlItemBgHover, background: controlItemBgHover,
}, },
[`&${componentCls}-preset-active`]: {
background: controlItemBgActive,
color: colorPrimary,
fontWeight: fontWeightStrong,
},
}, },
}, },
}, },

View File

@ -1,13 +1,18 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { PresetDate } from './interface';
export default defineComponent({ export default defineComponent({
name: 'PresetPanel', name: 'PresetPanel',
props: { props: {
prefixCls: String, prefixCls: String,
presets: { presets: {
type: Array, type: Array as () => PresetDate<any>[],
default: () => [], default: () => [],
}, },
currentPreset: {
type: Object as () => PresetDate<any> | null,
default: null,
},
onClick: Function, onClick: Function,
onHover: Function, onHover: Function,
}, },
@ -19,21 +24,24 @@ export default defineComponent({
return ( return (
<div class={`${props.prefixCls}-presets`}> <div class={`${props.prefixCls}-presets`}>
<ul> <ul>
{props.presets.map(({ label, value }, index) => ( {props.presets.map(preset => (
<li <li
key={index} key={preset.key}
class={{
[`${props.prefixCls}-preset-active`]: props.currentPreset?.key === preset.key,
}}
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
props.onClick(value); props.onClick(preset.value, preset);
}} }}
onMouseenter={() => { onMouseenter={() => {
props.onHover?.(value); props.onHover?.(preset.value);
}} }}
onMouseleave={() => { onMouseleave={() => {
props.onHover?.(null); props.onHover?.(null);
}} }}
> >
{label} {preset.label}
</li> </li>
))} ))}
</ul> </ul>

View File

@ -5,6 +5,7 @@ import type {
RangeValue, RangeValue,
EventValue, EventValue,
PresetDate, PresetDate,
RangePickerOnChange,
} from './interface'; } from './interface';
import type { PickerBaseProps, PickerDateProps, PickerTimeProps } from './Picker'; import type { PickerBaseProps, PickerDateProps, PickerTimeProps } from './Picker';
import type { SharedTimeProps } from './panels/TimePanel'; import type { SharedTimeProps } from './panels/TimePanel';
@ -108,7 +109,7 @@ export type RangePickerSharedProps<DateType> = {
separator?: VueNode; separator?: VueNode;
allowEmpty?: [boolean, boolean]; allowEmpty?: [boolean, boolean];
mode?: [PanelMode, PanelMode]; mode?: [PanelMode, PanelMode];
onChange?: (values: RangeValue<DateType>, formatString: [string, string]) => void; onChange?: RangePickerOnChange<DateType>;
onCalendarChange?: ( onCalendarChange?: (
values: RangeValue<DateType>, values: RangeValue<DateType>,
formatString: [string, string], formatString: [string, string],
@ -133,6 +134,10 @@ export type RangePickerSharedProps<DateType> = {
nextIcon?: VueNode; nextIcon?: VueNode;
superPrevIcon?: VueNode; superPrevIcon?: VueNode;
superNextIcon?: VueNode; superNextIcon?: VueNode;
/** 双击日期时自动设置为开始和结束日期 */
autoFill?: boolean;
/** 在 showTime 模式下,是否设置为整天(开始时间 00:00:00结束时间 23:59:59 */
isWholeDay?: boolean;
}; };
type OmitPickerProps<Props> = Omit< type OmitPickerProps<Props> = Omit<
@ -258,6 +263,8 @@ function RangerPicker<DateType>() {
'nextIcon', 'nextIcon',
'superPrevIcon', 'superPrevIcon',
'superNextIcon', 'superNextIcon',
'autoFill',
'isWholeDay',
] as any, ] as any,
setup(props, { attrs, expose }) { setup(props, { attrs, expose }) {
const needConfirmButton = computed( const needConfirmButton = computed(
@ -319,6 +326,31 @@ function RangerPicker<DateType>() {
: reorderValues(values, props.generateConfig), : reorderValues(values, props.generateConfig),
}); });
// ========================= Current Preset =========================
const [currentPreset, setCurrentPreset] = useState<PresetDate<RangeValue<DateType>> | null>(
null,
);
// preset
const checkAndSetPreset = (values: RangeValue<DateType>) => {
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 =========================== // =========================== View Date ===========================
// Config view panel // Config view panel
const [startViewDate, endViewDate, setViewDate] = useRangeViewDates({ const [startViewDate, endViewDate, setViewDate] = useRangeViewDates({
@ -491,7 +523,11 @@ function RangerPicker<DateType>() {
}, 0); }, 0);
} }
function triggerChange(newValue: RangeValue<DateType>, sourceIndex: 0 | 1) { function triggerChange(
newValue: RangeValue<DateType>,
sourceIndex: 0 | 1,
fromPreset = false,
) {
let values = newValue; let values = newValue;
let startValue = getValue(values, 0); let startValue = getValue(values, 0);
let endValue = getValue(values, 1); let endValue = getValue(values, 1);
@ -541,8 +577,33 @@ function RangerPicker<DateType>() {
} }
} }
// 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); setSelectedValue(values);
// preset currentPreset
if (!fromPreset) {
setCurrentPreset(null);
}
const startStr = const startStr =
values && values[0] values && values[0]
? formatValue(values[0], { generateConfig, locale, format: formatList.value[0] }) ? formatValue(values[0], { generateConfig, locale, format: formatList.value[0] })
@ -577,7 +638,10 @@ function RangerPicker<DateType>() {
(!isEqual(generateConfig, getValue(mergedValue.value, 0), startValue) || (!isEqual(generateConfig, getValue(mergedValue.value, 0), startValue) ||
!isEqual(generateConfig, getValue(mergedValue.value, 1), endValue)) !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<DateType>() {
) { ) {
return false; return false;
} }
triggerChange(selectedValue.value, index); triggerChange(selectedValue.value, index, false);
resetText(); resetText();
}, },
onCancel: () => { onCancel: () => {
@ -824,6 +888,11 @@ function RangerPicker<DateType>() {
setSelectedValue(mergedValue.value); setSelectedValue(mergedValue.value);
}); });
// mergedValue preset
watch(mergedValue, newValue => {
checkAndSetPreset(newValue);
});
// ============================ Warning ============================ // ============================ Warning ============================
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
watchEffect(() => { watchEffect(() => {
@ -889,6 +958,37 @@ function RangerPicker<DateType>() {
}; };
} }
// 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<DateType> | null = null; let panelDateRender: DateRender<DateType> | null = null;
if (dateRender) { if (dateRender) {
panelDateRender = ({ current: date, today }) => panelDateRender = ({ current: date, today }) =>
@ -971,7 +1071,7 @@ function RangerPicker<DateType>() {
} }
const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { 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 currentIndex = mergedActivePickerIndex.value;
const isDoubleClick = isDoubleClickRef.value; const isDoubleClick = isDoubleClickRef.value;
const shouldSwitch = type === 'mouse' && needConfirmButton.value && isDoubleClick; const shouldSwitch = type === 'mouse' && needConfirmButton.value && isDoubleClick;
@ -979,13 +1079,28 @@ function RangerPicker<DateType>() {
// Reset double click state // Reset double click state
isDoubleClickRef.value = false; 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) { if (type === 'submit' || (type !== 'key' && !needConfirmButton.value) || shouldSwitch) {
// triggerChange will also update selected values // triggerChange will also update selected values
triggerChange(values, mergedActivePickerIndex.value); triggerChange(values, mergedActivePickerIndex.value, false);
// If double click, switch to next input // If autoFill is enabled and we have both values, close the panel
// But check if both inputs are complete, if so don't switch to avoid animation before popup closes if (
if (shouldSwitch) { 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 startValue = getValue(values, 0);
const endValue = getValue(values, 1); const endValue = getValue(values, 1);
const bothValuesComplete = startValue && endValue; const bothValuesComplete = startValue && endValue;
@ -1074,7 +1189,7 @@ function RangerPicker<DateType>() {
onOk: () => { onOk: () => {
if (getValue(selectedValue.value, mergedActivePickerIndex.value)) { if (getValue(selectedValue.value, mergedActivePickerIndex.value)) {
// triggerChangeOld(selectedValue.value); // triggerChangeOld(selectedValue.value);
triggerChange(selectedValue.value, mergedActivePickerIndex.value); triggerChange(selectedValue.value, mergedActivePickerIndex.value, false);
if (onOk) { if (onOk) {
onOk(selectedValue.value); onOk(selectedValue.value);
} }
@ -1129,8 +1244,10 @@ function RangerPicker<DateType>() {
<PresetPanel <PresetPanel
prefixCls={prefixCls} prefixCls={prefixCls}
presets={presetList.value} presets={presetList.value}
onClick={nextValue => { currentPreset={currentPreset.value}
triggerChange(nextValue, null); onClick={(nextValue, preset) => {
setCurrentPreset(preset);
triggerChange(nextValue, null, true);
triggerOpen(false, mergedActivePickerIndex.value); triggerOpen(false, mergedActivePickerIndex.value);
}} }}
onHover={hoverValue => { onHover={hoverValue => {
@ -1207,7 +1324,7 @@ function RangerPicker<DateType>() {
values = updateValues(values, null, 1); values = updateValues(values, null, 1);
} }
triggerChange(values, null); triggerChange(values, null, false);
triggerOpen(false, mergedActivePickerIndex.value); triggerOpen(false, mergedActivePickerIndex.value);
}} }}
class={`${prefixCls}-clear`} class={`${prefixCls}-clear`}

View File

@ -22,6 +22,7 @@ export default function usePresets<T>(
return { return {
label, label,
value: newValues, value: newValues,
key: label, // 添加 key 属性
}; };
}); });
} }

View File

@ -112,4 +112,11 @@ export type CustomFormat<DateType> = (value: DateType) => string;
export interface PresetDate<T> { export interface PresetDate<T> {
label: VueNode; label: VueNode;
value: T; value: T;
key: string; // 重要需要用key来高亮选中状态
} }
// 扩展的 onChange 回调类型values 和 formatString 都包含第三个 preset 元素
export type RangePickerOnChange<DateType> = (
values: [DateType | null, DateType | null, PresetDate<RangeValue<DateType>> | null],
formatString: [string, string, string | null],
) => void;