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

View File

@ -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' },
]);
</script>

View File

@ -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<DateType, ExtraProps = {}>(
pickerRef.value?.blur();
},
});
const maybeToStrings = (dates: DateType[]) => {
const maybeToStrings = (dates: RangeValue<DateType>) => {
return props.valueFormat ? generateConfig.toString(dates, props.valueFormat) : dates;
};
const onChange = (dates: RangeValue<DateType>, dateStrings: [string, string]) => {
const values = maybeToStrings(dates);
emit('update:value', values);
emit('change', values, dateStrings);
const onChange: RangePickerOnChange<DateType> = (values, formatStrings) => {
const [startValue, endValue, currentPreset] = values;
const [startStr, endStr] = formatStrings;
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();
};
const onOpenChange = (open: boolean) => {
@ -109,7 +114,7 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
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<DateType>['onCalendarChange'] = (

View File

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

View File

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

View File

@ -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<any>[],
default: () => [],
},
currentPreset: {
type: Object as () => PresetDate<any> | null,
default: null,
},
onClick: Function,
onHover: Function,
},
@ -19,21 +24,24 @@ export default defineComponent({
return (
<div class={`${props.prefixCls}-presets`}>
<ul>
{props.presets.map(({ label, value }, index) => (
{props.presets.map(preset => (
<li
key={index}
key={preset.key}
class={{
[`${props.prefixCls}-preset-active`]: props.currentPreset?.key === preset.key,
}}
onClick={e => {
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}
</li>
))}
</ul>

View File

@ -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<DateType> = {
separator?: VueNode;
allowEmpty?: [boolean, boolean];
mode?: [PanelMode, PanelMode];
onChange?: (values: RangeValue<DateType>, formatString: [string, string]) => void;
onChange?: RangePickerOnChange<DateType>;
onCalendarChange?: (
values: RangeValue<DateType>,
formatString: [string, string],
@ -133,6 +134,10 @@ export type RangePickerSharedProps<DateType> = {
nextIcon?: VueNode;
superPrevIcon?: VueNode;
superNextIcon?: VueNode;
/** 双击日期时自动设置为开始和结束日期 */
autoFill?: boolean;
/** 在 showTime 模式下,是否设置为整天(开始时间 00:00:00结束时间 23:59:59 */
isWholeDay?: boolean;
};
type OmitPickerProps<Props> = Omit<
@ -258,6 +263,8 @@ function RangerPicker<DateType>() {
'nextIcon',
'superPrevIcon',
'superNextIcon',
'autoFill',
'isWholeDay',
] as any,
setup(props, { attrs, expose }) {
const needConfirmButton = computed(
@ -319,6 +326,31 @@ function RangerPicker<DateType>() {
: 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 ===========================
// Config view panel
const [startViewDate, endViewDate, setViewDate] = useRangeViewDates({
@ -491,7 +523,11 @@ function RangerPicker<DateType>() {
}, 0);
}
function triggerChange(newValue: RangeValue<DateType>, sourceIndex: 0 | 1) {
function triggerChange(
newValue: RangeValue<DateType>,
sourceIndex: 0 | 1,
fromPreset = false,
) {
let values = newValue;
let startValue = getValue(values, 0);
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);
// 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<DateType>() {
(!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<DateType>() {
) {
return false;
}
triggerChange(selectedValue.value, index);
triggerChange(selectedValue.value, index, false);
resetText();
},
onCancel: () => {
@ -824,6 +888,11 @@ function RangerPicker<DateType>() {
setSelectedValue(mergedValue.value);
});
// mergedValue preset
watch(mergedValue, newValue => {
checkAndSetPreset(newValue);
});
// ============================ Warning ============================
if (process.env.NODE_ENV !== 'production') {
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;
if (dateRender) {
panelDateRender = ({ current: date, today }) =>
@ -971,7 +1071,7 @@ function RangerPicker<DateType>() {
}
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<DateType>() {
// 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<DateType>() {
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<DateType>() {
<PresetPanel
prefixCls={prefixCls}
presets={presetList.value}
onClick={nextValue => {
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<DateType>() {
values = updateValues(values, null, 1);
}
triggerChange(values, null);
triggerChange(values, null, false);
triggerOpen(false, mergedActivePickerIndex.value);
}}
class={`${prefixCls}-clear`}

View File

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

View File

@ -112,4 +112,11 @@ export type CustomFormat<DateType> = (value: DateType) => string;
export interface PresetDate<T> {
label: VueNode;
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;