- {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 69cb823dd9..648543a5e4 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,11 +263,15 @@ function RangerPicker() {
'nextIcon',
'superPrevIcon',
'superNextIcon',
+ 'autoFill',
+ 'isWholeDay',
] as any,
setup(props, { attrs, expose }) {
const needConfirmButton = computed(
() => (props.picker === 'date' && !!props.showTime) || props.picker === 'time',
);
+
+ const isDoubleClickRef = ref(false);
const presets = computed(() => props.presets);
const ranges = computed(() => props.ranges);
const presetList = usePresets(presets, ranges);
@@ -311,12 +320,91 @@ function RangerPicker() {
const [mergedValue, setInnerValue] = useMergedState>(null, {
value: toRef(props, 'value'),
defaultValue: props.defaultValue,
- postState: values =>
- props.picker === 'time' && !props.order
+ postState: values => {
+ // 处理包含preset的value格式 [date, date, preset]
+ if (
+ values &&
+ Array.isArray(values) &&
+ (values as any).length === 3 &&
+ (values as any)[2]
+ ) {
+ const preset = (values as any)[2];
+ // 如果preset有value属性,使用preset的value作为日期范围
+ if (preset.value && Array.isArray(preset.value) && preset.value.length === 2) {
+ const presetValues = preset.value;
+
+ // 检查preset.value是否是函数,如果是则执行函数获取当前值
+ let startValue =
+ typeof presetValues[0] === 'function' ? presetValues[0]() : presetValues[0];
+ let endValue =
+ typeof presetValues[1] === 'function' ? presetValues[1]() : presetValues[1];
+
+ // 判断props.isWholeDay是否为true,如果为false,则使用当前时间的时分秒
+ if (!props.isWholeDay) {
+ const now = props.generateConfig.getNow();
+ const currentHour = props.generateConfig.getHour(now);
+ const currentMinute = props.generateConfig.getMinute(now);
+ const currentSecond = props.generateConfig.getSecond(now);
+
+ startValue = props.generateConfig.setHour(
+ props.generateConfig.setMinute(
+ props.generateConfig.setSecond(startValue, currentSecond),
+ currentMinute,
+ ),
+ currentHour,
+ );
+ endValue = props.generateConfig.setHour(
+ props.generateConfig.setMinute(
+ props.generateConfig.setSecond(endValue, currentSecond),
+ currentMinute,
+ ),
+ currentHour,
+ );
+ }
+
+ if (startValue && endValue) {
+ // 设置当前preset
+ setCurrentPreset(preset);
+ // 返回preset计算出的日期范围
+ return props.picker === 'time' && !props.order
+ ? [startValue, endValue]
+ : reorderValues([startValue, endValue], props.generateConfig);
+ }
+ }
+ }
+
+ // 处理普通格式的value
+ return props.picker === 'time' && !props.order
? values
- : reorderValues(values, props.generateConfig),
+ : 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({
@@ -489,7 +577,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);
@@ -539,8 +631,88 @@ 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];
+ }
+
+ // 如果通过preset触发,且preset有value属性,重新计算日期范围
+ if (
+ fromPreset &&
+ currentPreset.value &&
+ Array.isArray(currentPreset.value) &&
+ currentPreset.value.length === 2
+ ) {
+ const presetValues = currentPreset.value;
+ // 检查preset.value是否是函数,如果是则执行函数获取当前值
+ const presetStartValue =
+ typeof presetValues[0] === 'function' ? presetValues[0]() : presetValues[0];
+ const presetEndValue =
+ typeof presetValues[1] === 'function' ? presetValues[1]() : presetValues[1];
+
+ if (presetStartValue && presetEndValue) {
+ // 如果启用了isWholeDay,应用时间设置
+ if (props.isWholeDay && showTime) {
+ const startWithTime = generateConfig.setHour(
+ generateConfig.setMinute(generateConfig.setSecond(presetStartValue, 0), 0),
+ 0,
+ );
+ const endWithTime = generateConfig.setHour(
+ generateConfig.setMinute(generateConfig.setSecond(presetEndValue, 59), 59),
+ 23,
+ );
+ values = [startWithTime, endWithTime];
+ } else if (showTime) {
+ // 如果未启用isWholeDay但启用了showTime,使用当前时间的时分秒
+ const now = generateConfig.getNow();
+ const currentHour = generateConfig.getHour(now);
+ const currentMinute = generateConfig.getMinute(now);
+ const currentSecond = generateConfig.getSecond(now);
+
+ const startWithCurrentTime = generateConfig.setHour(
+ generateConfig.setMinute(
+ generateConfig.setSecond(presetStartValue, currentSecond),
+ currentMinute,
+ ),
+ currentHour,
+ );
+ const endWithCurrentTime = generateConfig.setHour(
+ generateConfig.setMinute(
+ generateConfig.setSecond(presetEndValue, currentSecond),
+ currentMinute,
+ ),
+ currentHour,
+ );
+ values = [startWithCurrentTime, endWithCurrentTime];
+ } else {
+ // 如果没有启用showTime,保持preset的原始时间
+ values = [presetStartValue, presetEndValue];
+ }
+ }
+ }
+
setSelectedValue(values);
+ // 如果不是通过 preset 触发的,清除 currentPreset
+ if (!fromPreset) {
+ setCurrentPreset(null);
+ }
+
const startStr =
values && values[0]
? formatValue(values[0], { generateConfig, locale, format: formatList.value[0] })
@@ -575,7 +747,12 @@ function RangerPicker() {
(!isEqual(generateConfig, getValue(mergedValue.value, 0), startValue) ||
!isEqual(generateConfig, getValue(mergedValue.value, 1), endValue))
) {
- onChange(values, [startStr, endStr]);
+ // 如果通过preset触发,传递preset信息
+ const presetToPass = fromPreset ? currentPreset.value : currentPreset.value;
+ onChange(
+ [startValue, endValue, presetToPass],
+ [startStr, endStr, presetToPass?.key || null],
+ );
}
}
@@ -718,7 +895,7 @@ function RangerPicker() {
) {
return false;
}
- triggerChange(selectedValue.value, index);
+ triggerChange(selectedValue.value, index, false);
resetText();
},
onCancel: () => {
@@ -822,6 +999,11 @@ function RangerPicker() {
setSelectedValue(mergedValue.value);
});
+ // 当 mergedValue 变化时,检查是否匹配某个 preset
+ watch(mergedValue, newValue => {
+ checkAndSetPreset(newValue);
+ });
+
// ============================ Warning ============================
if (process.env.NODE_ENV !== 'production') {
watchEffect(() => {
@@ -887,6 +1069,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 }) =>
@@ -969,11 +1182,46 @@ 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;
+
+ // Reset double click state
+ isDoubleClickRef.value = false;
- if (type === 'submit' || (type !== 'key' && !needConfirmButton.value)) {
+ // 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 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;
+
+ if (!bothValuesComplete) {
+ const nextIndex = ((currentIndex + 1) % 2) as 0 | 1;
+ setMergedActivePickerIndex(nextIndex);
+ }
+ }
+
// clear hover value style
if (mergedActivePickerIndex.value === 0) {
onStartLeave();
@@ -993,6 +1241,7 @@ function RangerPicker() {
hideRanges: computed(() => true),
onSelect: onContextSelect,
open: mergedOpen,
+ isDoubleClickRef,
});
return () => {
@@ -1051,7 +1300,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);
}
@@ -1106,8 +1355,66 @@ function RangerPicker() {
{
- triggerChange(nextValue, null);
+ currentPreset={currentPreset.value}
+ onClick={(nextValue, preset) => {
+ setCurrentPreset(preset);
+ // 如果preset有value属性,使用preset的value作为日期范围
+ let valuesToUse = nextValue;
+ if (preset.value && Array.isArray(preset.value) && preset.value.length === 2) {
+ const presetValues = preset.value;
+ // 检查preset.value是否是函数,如果是则执行函数获取当前值
+ const presetStartValue =
+ typeof presetValues[0] === 'function' ? presetValues[0]() : presetValues[0];
+ const presetEndValue =
+ typeof presetValues[1] === 'function' ? presetValues[1]() : presetValues[1];
+
+ if (presetStartValue && presetEndValue) {
+ // 如果启用了isWholeDay,应用时间设置
+ if (props.isWholeDay && props.showTime) {
+ const startWithTime = props.generateConfig.setHour(
+ props.generateConfig.setMinute(
+ props.generateConfig.setSecond(presetStartValue, 0),
+ 0,
+ ),
+ 0,
+ );
+ const endWithTime = props.generateConfig.setHour(
+ props.generateConfig.setMinute(
+ props.generateConfig.setSecond(presetEndValue, 59),
+ 59,
+ ),
+ 23,
+ );
+ valuesToUse = [startWithTime, endWithTime];
+ } else if (props.showTime) {
+ // 如果未启用isWholeDay但启用了showTime,使用当前时间的时分秒
+ const now = props.generateConfig.getNow();
+ const currentHour = props.generateConfig.getHour(now);
+ const currentMinute = props.generateConfig.getMinute(now);
+ const currentSecond = props.generateConfig.getSecond(now);
+
+ const startWithCurrentTime = props.generateConfig.setHour(
+ props.generateConfig.setMinute(
+ props.generateConfig.setSecond(presetStartValue, currentSecond),
+ currentMinute,
+ ),
+ currentHour,
+ );
+ const endWithCurrentTime = props.generateConfig.setHour(
+ props.generateConfig.setMinute(
+ props.generateConfig.setSecond(presetEndValue, currentSecond),
+ currentMinute,
+ ),
+ currentHour,
+ );
+ valuesToUse = [startWithCurrentTime, endWithCurrentTime];
+ } else {
+ // 如果没有启用showTime,保持preset的原始时间
+ valuesToUse = [presetStartValue, presetEndValue];
+ }
+ }
+ }
+ triggerChange(valuesToUse, null, true);
triggerOpen(false, mergedActivePickerIndex.value);
}}
onHover={hoverValue => {
@@ -1184,7 +1491,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 5b95de32d3..933e3ab29f 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 58b7106da3..3f1255cdad 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;
diff --git a/components/vc-picker/panels/PanelBody.tsx b/components/vc-picker/panels/PanelBody.tsx
index b05be4a720..5dc3d5d6ad 100644
--- a/components/vc-picker/panels/PanelBody.tsx
+++ b/components/vc-picker/panels/PanelBody.tsx
@@ -49,7 +49,7 @@ function PanelBody(_props: PanelBodyProps) {
titleCell,
headerCells,
} = useMergeProps(_props);
- const { onDateMouseenter, onDateMouseleave, mode } = useInjectPanel();
+ const { onDateMouseenter, onDateMouseleave, mode, isDoubleClickRef } = useInjectPanel();
const cellPrefixCls = `${prefixCls}-cell`;
@@ -99,6 +99,14 @@ function PanelBody(_props: PanelBodyProps) {
onSelect(currentDate);
}
}}
+ onDblclick={e => {
+ e.stopPropagation();
+ if (!disabled && isDoubleClickRef) {
+ // 设置双击状态
+ isDoubleClickRef.value = true;
+ onSelect(currentDate);
+ }
+ }}
onMouseenter={() => {
if (!disabled && onDateMouseenter) {
onDateMouseenter(currentDate);