Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/components/date-picker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ export default defineComponent({
e.stopPropagation();
popupVisible.value = false;
onChange?.([], { dayjsValue: dayjs(), trigger: 'clear' });
props.onClear?.({ e });
}

// 头部快速切换
Expand Down Expand Up @@ -406,6 +407,8 @@ export default defineComponent({
presetsPlacement: props.presetsPlacement,
popupVisible: popupVisible.value,
needConfirm: props.needConfirm,
disableTime: props.disableTime,
range: props.range,
onCellClick,
onCellMouseEnter,
onCellMouseLeave,
Expand Down
61 changes: 61 additions & 0 deletions packages/components/date-picker/_example/range-status.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<template>
<t-space direction="vertical" :size="24">
<div>
<div>week 模式:range = ['2025-03-15','2025-12-01'],默认值 2025-02-20(越界,应为错误态)</div>
<t-date-picker
v-model="weekVal"
mode="week"
allow-input
:first-day-of-week="firstDayOfWeek"
:range="periodRange"
clearable
placeholder="选择一周"
/>
</div>

<div>
<div>month 模式:range = ['2025-03-15','2025-12-01'],默认值 2025-02(越界,应为错误态)</div>
<t-date-picker
v-model="monthVal"
mode="month"
allow-input
:range="periodRange"
clearable
placeholder="选择月份"
/>
</div>

<div>
<div>quarter 模式:range = ['2025-03-15','2025-12-01'],默认值 2024-Q4(越界,应为错误态)</div>
<t-date-picker
v-model="quarterVal"
mode="quarter"
allow-input
:range="periodRange"
clearable
placeholder="选择季度"
/>
</div>

<div>
<div>year 模式:range = ['2025-03-15','2025-12-01'],默认值 2024(越界,应为错误态)</div>
<t-date-picker v-model="yearVal" mode="year" allow-input :range="periodRange" clearable placeholder="选择年份" />
</div>
</t-space>
</template>

<script setup>
import { ref } from 'vue';

// 统一的范围:按月/季/年粒度比较
const periodRange = ['2025-03-15', '2025-12-01'];

// 周起始(与 dayjs 同步使用时可一并设置)
const firstDayOfWeek = 1;

// 设定越界默认值以演示错误态
const weekVal = ref(new Date(2025, 1, 20)); // 2025-02-20,周不与 range 相交 → 错误
const monthVal = ref(new Date(2025, 1, 1)); // 2025-02 → 错误
const quarterVal = ref(new Date(2024, 10, 1)); // 2024-11 → Q4 2024 → 错误
const yearVal = ref(new Date(2024, 0, 1)); // 2024 → 错误
</script>
91 changes: 91 additions & 0 deletions packages/components/date-picker/_example/range.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<t-space direction="vertical">
<!-- 受控面板 + 函数范围:仅允许未来 90 天 -->
<t-date-picker
v-model="valueFn"
v-model:panelActiveDate="panelActiveDate"
mode="date"
format="YYYY-MM-DD"
:range="rangeFn"
:need-confirm="false"
placeholder="选择未来90天内的日期"
@panel-active-date="handlePanelActiveDate"
@change="handleChange"
@month-change="handleMonthChange"
@year-change="handleYearChange"
/>

<!-- 数组范围 + 默认面板日期:仅允许 2026 年 -->
<t-date-picker
v-model="valueArr"
mode="date"
format="YYYY-MM-DD"
:range="rangeArr"
:default-panel-active-date="defaultPanelActiveDate"
placeholder="仅可选 2026 年内的日期,同时禁用周六"
:disable-date="disableDate"
@change="handleChange"
@month-change="handleMonthChange"
@year-change="handleYearChange"
/>

年选择:2019 到 2024:
<t-date-picker mode="year" clearable :range="['2019', '2024']" />

月份选择:2022-10 到 2025-01:
<t-date-picker mode="month" clearable :range="['2022-10', '2025-01']" />

季度选择:2022-10 到 2025-01:
<t-date-picker mode="quarter" clearable :range="['2022-10', '2025-01']" />

周选择:2000-10 到 2025-01:
<t-date-picker mode="week" clearable :range="['2000-10', '2025-01']" />
</t-space>
</template>

<script setup>
import { ref } from 'vue';
import dayjs from 'dayjs';

const disableDate = (date) => dayjs(date).day() === 6;

// 示例1:函数范围 + 受控面板
const valueFn = ref('');
const panelActiveDate = ref({
year: dayjs().year() - 5,
month: 10, // 0-11
});

// 仅允许今天到未来 90 天(返回 true 表示可选)
const rangeFn = (d) => {
const now = dayjs().startOf('day');
const target = dayjs(d).startOf('day');
const diff = target.diff(now, 'day');
return diff >= 0 && diff <= 90;
};

function handlePanelActiveDate(val, ctx) {
// ctx.trigger: 'year-select' | 'month-select' | 'today' | 'year-arrow-next' | 'year-arrow-previous' | ...
if (String(ctx?.trigger).includes('year')) {
panelActiveDate.value.year = typeof val === 'number' ? val : dayjs(val).year();
}
if (String(ctx?.trigger).includes('month') || ctx?.trigger === 'today') {
panelActiveDate.value.month = typeof val === 'number' ? val : dayjs(val).month();
}
}

// 示例2:数组范围 + 默认面板日期
const valueArr = ref('');
const rangeArr = ['2026-01-01', '2026-11-20'];
const defaultPanelActiveDate = { year: 2026, month: 5 }; // 0-11,5 表示 6 月

function handleChange(value, context) {
console.log('onChange:', value, context);
}
function handleMonthChange(context) {
console.log('onMonthChange:', context);
}
function handleYearChange(context) {
console.log('onYearChange:', context);
}
</script>
112 changes: 85 additions & 27 deletions packages/components/date-picker/components/base/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { defineComponent, PropType, ref, computed, watch } from 'vue';
import { defineComponent, PropType, ref, computed, watch, toRefs } from 'vue';
import { PaginationMini, JumperTrigger } from '../../../pagination';
import TSelect from '../../../select';
import { useConfig, usePrefixClass } from '@tdesign/shared-hooks';
import { useSelectRange } from '../../hooks';

import type { TdDatePickerProps } from '../../type';

Expand All @@ -12,6 +13,7 @@ export default defineComponent({
type: String as PropType<TdDatePickerProps['mode']>,
default: 'date',
},
range: [Array, Function] as PropType<TdDatePickerProps['range']>,
year: Number,
month: Number,
internalYear: Array as PropType<Array<number>>,
Expand All @@ -21,10 +23,24 @@ export default defineComponent({
onJumperClick: Function as PropType<(context: { e: MouseEvent; trigger: JumperTrigger }) => {}>,
},
setup(props) {
const { year, month } = toRefs(props);
const { classPrefix } = useConfig('classPrefix');
const COMPONENT_NAME = usePrefixClass('date-picker__header');
const { globalConfig } = useConfig('datePicker');

const {
monthHasAnyAllowed,
yearHasAnyAllowed,
decadeHasAnyAllowed,
paginationDisabled,
canLoadMoreTop,
canLoadMoreBottom,
} = useSelectRange({
range: props.range,
mode: props.mode,
year: year,
month: month,
});
const yearOptions = ref(initOptions(props.year));
const showMonthPicker = computed(() => props.mode === 'date' || props.mode === 'week');
// 年份选择展示区间
Expand All @@ -44,9 +60,31 @@ export default defineComponent({
);
});

const monthOptions = computed(() =>
globalConfig.value.months.map((item: string, index: number) => ({ label: item, value: index })),
);
const monthOptions = computed(() => {
// 仅展示可选月份(不显示越界的,而不是置灰禁用)
return globalConfig.value.months.map((item: string, index: number) => ({
label: item,
value: index,
disabled: !monthHasAnyAllowed(props.year, index),
}));
});

// 顶部/底部是否展示“加载更多”内容(...)
const showPanelTop = computed(() => {
const options = yearOptions.value;
if (!options.length) return false;
const first = options[0].value;
return canLoadMoreTop(first);
});

const showPanelBottom = computed(() => {
const options = yearOptions.value;
if (!options.length) return false;
const last = options[options.length - 1].value;
return canLoadMoreBottom(last);
});

// 分页禁用逻辑由 useSelectRange 提供

function initOptions(year: number) {
const options = [];
Expand All @@ -56,14 +94,19 @@ export default defineComponent({
const maxYear = year - extraYear + 100;

for (let i = minYear; i <= maxYear; i += 10) {
options.push({ label: `${i} - ${i + 9}`, value: i + 9 });
const end = i + 9;
// 仅加入可选的年代
if (decadeHasAnyAllowed(end)) {
options.push({ label: `${i} - ${end}`, value: end, disabled: false });
}
}
} else {
options.push({ label: `${year}`, value: year });
// 中心年份(仅在可选范围内时加入)
if (yearHasAnyAllowed(year)) options.push({ label: `${year}`, value: year, disabled: false });

for (let i = 1; i <= 10; i++) {
options.push({ label: `${year + i}`, value: year + i });
options.unshift({ label: `${year - i}`, value: year - i });
if (yearHasAnyAllowed(year + i)) options.push({ label: `${year + i}`, value: year + i, disabled: false });
if (yearHasAnyAllowed(year - i)) options.unshift({ label: `${year - i}`, value: year - i, disabled: false });
}
}

Expand All @@ -76,20 +119,22 @@ export default defineComponent({
const extraYear = year % 10;
if (type === 'add') {
for (let i = year - extraYear + 10; i <= year - extraYear + 50; i += 10) {
options.push({ label: `${i} - ${i + 9}`, value: i });
const end = i + 9;
// 仅加入可选的年代
if (decadeHasAnyAllowed(end)) options.push({ label: `${i} - ${end}`, value: end, disabled: false });
}
} else {
for (let i = year - extraYear - 1; i > year - extraYear - 50; i -= 10) {
options.unshift({ label: `${i - 9} - ${i}`, value: i });
if (decadeHasAnyAllowed(i)) options.unshift({ label: `${i - 9} - ${i}`, value: i, disabled: false });
}
}
} else if (type === 'add') {
for (let i = year + 1; i <= year + 10; i++) {
options.push({ label: `${i}`, value: i });
if (yearHasAnyAllowed(i)) options.push({ label: `${i}`, value: i, disabled: false });
}
} else {
for (let i = year - 1; i > year - 10; i--) {
options.unshift({ label: `${i}`, value: i });
if (yearHasAnyAllowed(i)) options.unshift({ label: `${i}`, value: i, disabled: false });
}
}

Expand Down Expand Up @@ -130,9 +175,9 @@ export default defineComponent({
// 滚动顶部底部自动加载
function handleScroll({ e }: any) {
if (e.target.scrollTop === 0) {
handlePanelTopClick(e);
if (showPanelTop.value) handlePanelTopClick(e);
} else if (e.target.scrollTop === e.target.scrollHeight - e.target.clientHeight) {
handlePanelBottomClick(e);
if (showPanelBottom.value) handlePanelBottomClick(e);
}
}

Expand Down Expand Up @@ -167,7 +212,9 @@ export default defineComponent({
class={`${COMPONENT_NAME.value}-controller-month`}
value={props.month}
options={monthOptions.value}
onChange={(val: number) => props.onMonthChange?.(val)}
onChange={(val: number) => {
props.onMonthChange?.(val);
}}
popupProps={{
attach: (triggerElement: HTMLElement) => triggerElement.parentNode,
overlayClassName: `${COMPONENT_NAME.value}-controller-month-popup`,
Expand All @@ -178,26 +225,37 @@ export default defineComponent({
class={`${COMPONENT_NAME.value}-controller-year`}
value={props.mode === 'year' ? nearestYear.value : props.year}
options={yearOptions.value}
onChange={(val: number) => props.onYearChange?.(val)}
onChange={(val: number) => {
props.onYearChange?.(val);
}}
popupProps={{
onScroll: handleScroll,
attach: (triggerElement: HTMLElement) => triggerElement.parentNode,
overlayClassName: `${COMPONENT_NAME.value}-controller-year-popup`,
}}
panelTopContent={() => (
<div class={`${classPrefix.value}-select-option`} onClick={handlePanelTopClick}>
...
</div>
)}
panelBottomContent={() => (
<div class={`${classPrefix.value}-select-option`} onClick={handlePanelBottomClick}>
...
</div>
)}
panelTopContent={() =>
showPanelTop.value && (
<div class={`${classPrefix.value}-select-option`} onClick={handlePanelTopClick}>
...
</div>
)
}
panelBottomContent={() =>
showPanelBottom.value && (
<div class={`${classPrefix.value}-select-option`} onClick={handlePanelBottomClick}>
...
</div>
)
}
/>
</div>

<PaginationMini tips={labelMap.value[props.mode]} size="small" onChange={props.onJumperClick} />
<PaginationMini
tips={labelMap.value[props.mode]}
size="small"
onChange={props.onJumperClick}
disabled={paginationDisabled.value}
/>
</div>
);
},
Expand Down
Loading
Loading