Browse Source

feat: timeZone DST wip

shijia.me 5 months ago
parent
commit
1fd47c67d3

+ 1 - 1
.storybook/base/base.js

@@ -112,7 +112,7 @@ module.exports = {
             '@douyinfe/semi-animation-styled': resolve('packages/semi-animation-styled'),
             '@douyinfe/semi-json-viewer-core': resolve('packages/semi-json-viewer-core/src'),
         };
-        config.devtool = 'source-map';
+        config.devtool = 'eval-source-map';
         // config.output.publicPath = "/storybook/"
 
         return config;

+ 4 - 3
package.json

@@ -67,7 +67,7 @@
         "cross-env": "^5.2.1",
         "css": "^2.2.4",
         "cypress-real-events": "^1.8.1",
-        "date-fns": "^2.23.0",
+        "date-fns": "^4.1.0",
         "debug": "^4.3.2",
         "execa": "5",
         "fast-xml-parser": "^4.2.7",
@@ -231,7 +231,8 @@
         "@types/react": "^18.0.5",
         "@types/react-dom": "^18.0.1",
         "babel-plugin-lodash/@babel/types": "~7.20.0",
-        "cheerio": "1.0.0-rc.12"
+        "cheerio": "1.0.0-rc.12",
+        "@douyinfe/semi-site-banner/**/@douyinfe/semi-ui": "*"
     },
     "lint-staged": {
         "src/**/*.{js,jsx,ts,tsx}": [
@@ -246,4 +247,4 @@
     },
     "license": "MIT",
     "packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
-}
+}

+ 21 - 38
packages/semi-foundation/datePicker/_utils/getDefaultPickerDate.ts

@@ -1,54 +1,37 @@
 import { addMonths, Locale as dateFnsLocale } from 'date-fns';
-import isValidDate from './isValidDate';
-import { compatibleParse } from './parser';
-import isTimestamp from './isTimestamp';
+import { TZDate } from '@date-fns/tz';
+
+import { TZDateUtil } from 'utils/date-fns-extra';
+
+type BaseValueType = string | number | Date;
+
+interface GetDefaultPickerValueDateOptions {
+    defaultPickerValue?: BaseValueType | BaseValueType[];
+    format: string;
+    dateFnsLocale: dateFnsLocale;
+    timeZone?: string
+}
 
 /**
  * get left panel picker date and right panel picker date
  */
 export default function getDefaultPickerDate(options: GetDefaultPickerValueDateOptions) {
-    const { defaultPickerValue, format, dateFnsLocale } = options;
+    const { defaultPickerValue, format, dateFnsLocale, timeZone } = options;
     let nowDate = Array.isArray(defaultPickerValue) ? defaultPickerValue[0] : defaultPickerValue;
     let nextDate = Array.isArray(defaultPickerValue) ? defaultPickerValue[1] : undefined;
 
-    switch (true) {
-        case isValidDate(nowDate):
-            break;
-        case isTimestamp(nowDate):
-            nowDate = new Date(nowDate);
-            break;
-        case typeof nowDate === 'string':
-            nowDate = compatibleParse(nowDate as string, format, undefined, dateFnsLocale);
-            break;
-        default:
-            nowDate = new Date();
-            break;
+    let nowTZDate = TZDateUtil.createTZDate(timeZone);
+    if (nowDate) {
+        nowTZDate = TZDateUtil.parse({ date: nowDate, formatToken: format, locale: dateFnsLocale, timeZone });
     }
 
-    switch (true) {
-        case isValidDate(nextDate):
-            break;
-        case isTimestamp(nextDate):
-            nextDate = new Date(nextDate);
-            break;
-        case typeof nextDate === 'string':
-            nextDate = compatibleParse(nextDate as string, format, undefined, dateFnsLocale);
-            break;
-        default:
-            nextDate = addMonths(nowDate as Date, 1);
-            break;
+    let nextTZDate = addMonths(nowTZDate, 1);
+    if (nextDate) {
+        nextTZDate = TZDateUtil.parse({ date: nextDate, formatToken: format, locale: dateFnsLocale, timeZone });
     }
 
     return {
-        nowDate: nowDate as Date,
-        nextDate: nextDate as Date,
+        nowDate: nowTZDate,
+        nextDate: nextTZDate,
     };
-}
-
-type BaseValueType = string | number | Date;
-
-interface GetDefaultPickerValueDateOptions {
-    defaultPickerValue?: BaseValueType | BaseValueType[];
-    format: string;
-    dateFnsLocale: dateFnsLocale
 }

+ 2 - 1
packages/semi-foundation/datePicker/_utils/getMonthTable.ts

@@ -2,6 +2,7 @@
  *
  * @param {string} month
  */
+import { TZDate } from '@date-fns/tz';
 import {
     startOfMonth,
     lastDayOfMonth,
@@ -68,7 +69,7 @@ function getWeeks(date: Date, weekStartsOn: WeekStartNumber = 0) {
     return weeks;
 }
 
-const getMonthTable = (month: Date, weekStartsOn: WeekStartNumber) => {
+const getMonthTable = (month: TZDate, weekStartsOn: WeekStartNumber) => {
     const weeks = getWeeks(month, weekStartsOn);
     const monthText = format(month, 'yyyy-MM');
     return { monthText, weeks, month };

+ 6 - 3
packages/semi-foundation/datePicker/_utils/getYearAndMonth.ts

@@ -1,6 +1,9 @@
-export default function getYearAndMonth(year: { left: number; right: number }, month: { left: number; right: number }) {
-    const nowYear = new Date().getFullYear();
-    const nowMonth = new Date().getMonth();
+import { TZDateUtil } from "../../utils/date-fns-extra";
+
+export default function getYearAndMonth(year: { left: number; right: number }, month: { left: number; right: number }, timeZone: string | number) {
+    const nowDate = TZDateUtil.createTZDate(timeZone);
+    const nowYear = nowDate.getFullYear();
+    const nowMonth = nowDate.getMonth();
 
     const rightMonth = month.right || (nowMonth + 2);
     const rightYear = year.right || (rightMonth <= 12 ? nowYear : nowYear + 1);

+ 112 - 225
packages/semi-foundation/datePicker/foundation.ts

@@ -1,11 +1,10 @@
-import { format, isValid, isSameSecond, isEqual as isDateEqual, isDate } from 'date-fns';
-import { get, isObject, isString, isEqual, isFunction } from 'lodash';
+import { isValid, isSameSecond, isEqual as isDateEqual, isDate, Locale } from 'date-fns';
+import { get, isObject, isEqual, isFunction } from 'lodash';
+import { TZDate } from '@date-fns/tz';
 
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
-import { isValidDate, isTimestamp } from './_utils/index';
 import isNullOrUndefined from '../utils/isNullOrUndefined';
-import { utcToZonedTime, zonedTimeToUtc } from '../utils/date-fns-extra';
-import { compatibleParse } from './_utils/parser';
+import { TZDateUtil } from '../utils/date-fns-extra';
 import { getDefaultFormatTokenByType } from './_utils/getDefaultFormatToken';
 import { strings } from './constants';
 import { strings as inputStrings } from '../input/constants';
@@ -13,11 +12,10 @@ import { strings as inputStrings } from '../input/constants';
 import getInsetInputFormatToken from './_utils/getInsetInputFormatToken';
 import getInsetInputValueFromInsetInputStr from './_utils/getInsetInputValueFromInsetInputStr';
 
-import type { ArrayElement, Motion } from '../utils/type';
+import type { ArrayElement } from '../utils/type';
 import type { Type, DateInputFoundationProps, InsetInputValue } from './inputFoundation';
 import type { MonthsGridFoundationProps } from './monthsGridFoundation';
 import type { WeekStartNumber } from './_utils/getMonthTable';
-import isValidTimeZone from './_utils/isValidTimeZone';
 import warning from '../utils/warning';
 
 export type ValidateStatus = ArrayElement<typeof strings.STATUS>;
@@ -65,6 +63,8 @@ export type TriggerRenderProps = {
     componentProps?: DatePickerFoundationProps
 };
 
+// 所有暴露给用户的日期,使用 Date 类型
+// 所有内部的日期,使用 TZDate 类型
 export type DateOffsetType = (selectedDate?: Date) => Date;
 export type DensityType = 'default' | 'compact';
 export type DisabledDateType = (date?: Date, options?: DisabledDateOptions) => boolean;
@@ -177,9 +177,9 @@ export interface DatePickerFoundationState {
     isRange: boolean;
     /** value of trigger input */
     inputValue: string;
-    value: Date[];
+    value: TZDate[];
     // Save last selected date, maybe include null
-    cachedSelectedValue: (Date | null)[];
+    cachedSelectedValue: (TZDate | null)[];
     prevTimeZone: string | number;
     rangeInputFocus: RangeType;
     autofocus: boolean;
@@ -202,9 +202,9 @@ export interface DatePickerAdapter extends DefaultAdapter<DatePickerFoundationPr
     notifyConfirm: DatePickerFoundationProps['onConfirm'];
     notifyOpenChange: DatePickerFoundationProps['onOpenChange'];
     notifyPresetsClick: DatePickerFoundationProps['onPresetClick'];
-    updateValue: (value: Date[]) => void;
+    updateValue: (value: TZDate[]) => void;
     updatePrevTimezone: (prevTimeZone: string | number) => void;
-    updateCachedSelectedValue: (cachedSelectedValue: Date[]) => void;
+    updateCachedSelectedValue: (cachedSelectedValue: TZDate[]) => void;
     updateInputValue: (inputValue: string) => void;
     needConfirm: () => boolean;
     typeIsYearOrMonth: () => boolean;
@@ -245,7 +245,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     initFromProps({ value, timeZone, prevTimeZone }: Pick<DatePickerFoundationProps, 'value' | 'timeZone'> & { prevTimeZone?: string | number }) {
         const _value = (Array.isArray(value) ? [...value] : (value || value === 0) && [value]) || [];
 
-        const result = this.parseWithTimezone(_value, timeZone, prevTimeZone);
+        const result = this._parseValue({ value: _value, timeZone });
         this._adapter.updatePrevTimezone(prevTimeZone);
         // reset input value when value update
         this.clearInputValue();
@@ -263,35 +263,29 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      * 
      * If the user passes an empty value, you need to set the range input focus to rangeStart, so that the user can continue to select from the beginning after clearing
      */
-    initRangeInputFocus(result: Date[]) {
+    initRangeInputFocus(result: TZDate[]) {
         const { triggerRender } = this.getProps();
         if (this._isRangeType() && isFunction(triggerRender) && result.length === 0) {
             this._adapter.setRangeInputFocus('rangeStart');
         }
     }
 
-    /**
-     * value 可能是 UTC value 也可能是 zoned value
-     * 
-     * UTC value -> 受控传入的 value
-     * 
-     * zoned value -> statue.value,保存的是当前计算机时区下选择的日期
-     * 
-     * 如果是时区变化,则需要将旧 zoned value 转为新时区下的 zoned value
-     * 
-     * 如果是 value 变化,则不需要传入之前的时区,将 UTC value 转为 zoned value 即可
-     * 
-     */
-    parseWithTimezone(value: ValueType, timeZone: string | number, prevTimeZone: string | number) {
-        const result: Date[] = [];
+    _parseSingle(options: { date: BaseValueType | TZDate; timeZone: string | number; locale?: Locale; formatToken?: string }): TZDate | null {
+        const { dateFnsLocale, format } = this._adapter.getProps();
+        const { date, timeZone } = options;
+        const currentLocale = options.locale ?? dateFnsLocale;
+        const currentFormatToken = options.formatToken ?? format;
+        return TZDateUtil.parse({ date, timeZone, locale: currentLocale, formatToken: currentFormatToken });
+    }
+
+    _parseValue(options: { value: ValueType | TZDate | TZDate[]; timeZone: string | number }): TZDate[] {
+        const { value, timeZone } = options;
+        const result: TZDate[] = [];
         if (Array.isArray(value) && value.length) {
             for (const v of value) {
-                let parsedV = (v || v === 0) && this._parseValue(v);
+                let parsedV = (v || v === 0) && this._parseSingle({ date: v, timeZone });
                 if (parsedV) {
-                    if (isValidTimeZone(prevTimeZone)) {
-                        parsedV = zonedTimeToUtc(parsedV, prevTimeZone);
-                    }
-                    result.push(isValidTimeZone(timeZone) ? utcToZonedTime(parsedV, timeZone) : parsedV);
+                    result.push(parsedV);
                 } else {
                     warning(true, `[Semi DatePicker] value cannot be parsed, value: ${String(v)}`);
                 }
@@ -305,35 +299,6 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         return Boolean(this.getProp('multiple'));
     }
 
-    /**
-     *
-     *  Verify and parse the following three format inputs
-     *
-        1. Date object
-        2. ISO 9601-compliant string
-        3. ts timestamp
-
-        Unified here to format the incoming value and output it as a Date object
-     *
-     */
-    _parseValue(value: BaseValueType): Date {
-        const dateFnsLocale = this._adapter.getProp('dateFnsLocale');
-        let dateObj: Date;
-        if (!value && value !== 0) {
-            return new Date();
-        }
-        if (isValidDate(value)) {
-            dateObj = value as Date;
-        } else if (isString(value)) {
-            dateObj = compatibleParse(value as string, this.getProp('format'), undefined, dateFnsLocale);
-        } else if (isTimestamp(value)) {
-            dateObj = new Date(value);
-        } else {
-            throw new TypeError('defaultValue should be valid Date object/timestamp or string');
-        }
-        return dateObj;
-    }
-
     destroy() {
         // Ensure that event listeners will be uninstalled and users may not trigger closePanel
         this._adapter.togglePanel(false);
@@ -372,7 +337,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     /**
      * call it when change state value or input value
      */
-    resetCachedSelectedValue(willUpdateDates?: Date[]) {
+    resetCachedSelectedValue(willUpdateDates?: TZDate[]) {
         const { value, cachedSelectedValue } = this._adapter.getStates();
         const newCachedSelectedValue = Array.isArray(willUpdateDates) ? willUpdateDates : value;
         if (!isEqual(newCachedSelectedValue, cachedSelectedValue)) {
@@ -446,30 +411,6 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         this._adapter.notifyBlur(e);
     }
 
-    /**
-     * cachedSelectedValue can be `(Date|null)[]` or `null`
-     */
-    isCachedSelectedValueValid(dates: Date[]) {
-        const cachedSelectedValue = dates || this._adapter.getState('cachedSelectedValue');
-        const { type } = this._adapter.getProps();
-        let isValid = true;
-        switch (true) {
-            case type === 'dateRange':
-            case type === 'dateTimeRange':
-                if (!this._isRangeValueComplete(cachedSelectedValue)) {
-                    isValid = false;
-                }
-                break;
-            default:
-                const value = cachedSelectedValue?.filter(item => item);
-                if (!(Array.isArray(value) && value.length)) {
-                    isValid = false;
-                }
-                break;
-        }
-        return isValid;
-    }
-
     /**
      * 将输入框内容置空
      */
@@ -610,7 +551,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      * Since the clear button is not integrated in Input, you need to manually clear value, inputValue, cachedValue
      */
     handleRangeInputClear(e: any) {
-        const value: Date[] = [];
+        const value: TZDate[] = [];
         const inputValue = '';
         if (!this._isControlledComponent('value')) {
             this._updateValueAndInput(value, true, inputValue);
@@ -627,6 +568,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
     // Parses input only after user returns
     handleInputComplete(input: any = '') {
+        const { timeZone } = this._adapter.getProps();
         // console.log(input);
         let parsedResult = input ?
             this._isMultiple() ?
@@ -638,7 +580,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
         // Use the current date as the value when the current input is empty and the last input is also empty
         if (!parsedResult || !parsedResult.length) {
-            const nowDate = new Date();
+            const nowDate = TZDateUtil.createTZDate(timeZone);
             if (this._isRangeType()) {
                 parsedResult = [nowDate, nowDate];
             } else {
@@ -657,29 +599,25 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     /**
      * Parse the input, return the time object if it is valid,
      *  otherwise return "
-     *
-     * @param {string} input
-     * @returns  {Date [] | '}
      */
-    parseInput(input = '', format?: string) {
-        let result: Date[] = [];
+    parseInput(input = '', format?: string): TZDate[] {
+        let result: TZDate[] = [];
         // console.log(input);
-        const { dateFnsLocale, rangeSeparator } = this.getProps();
+        const { dateFnsLocale, rangeSeparator, timeZone } = this._adapter.getProps();
 
         if (input && input.length) {
             const type = this.getProp('type');
             const formatToken = format || this.getProp('format') || getDefaultFormatTokenByType(type);
-            let parsedResult,
-                formatedInput;
-            const nowDate = new Date();
+            let parsedResult: TZDate | null | TZDate[];
+            let formattedInput: string;
             switch (type) {
                 case 'date':
                 case 'dateTime':
                 case 'month':
-                    parsedResult = input ? compatibleParse(input, formatToken, nowDate, dateFnsLocale) : '';
-                    formatedInput = parsedResult && isValid(parsedResult) && this.localeFormat(parsedResult as Date, formatToken);
-                    if (parsedResult && formatedInput === input) {
-                        result = [parsedResult as Date];
+                    parsedResult = input ? this._parseSingle({ date: input, timeZone, formatToken, locale: dateFnsLocale }) : null;
+                    formattedInput = parsedResult && isValid(parsedResult) && this._formatSingle({ date: parsedResult, formatToken });
+                    if (parsedResult && formattedInput === input) {
+                        result = [parsedResult as TZDate];
                     }
                     break;
                 case 'dateRange':
@@ -690,14 +628,14 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
                     parsedResult =
                         values &&
                         values.reduce((arr, cur) => {
-                            const parsedVal = cur && compatibleParse(cur, formatToken, nowDate, dateFnsLocale);
+                            const parsedVal = cur && this._parseSingle({ date: cur, timeZone, formatToken, locale: dateFnsLocale });
                             parsedVal && arr.push(parsedVal);
                             return arr;
                         }, []);
-                    formatedInput =
+                    formattedInput =
                         parsedResult &&
-                        parsedResult.map(v => v && isValid(v) && this.localeFormat(v, formatToken)).join(separator);
-                    if (parsedResult && formatedInput === input) {
+                        parsedResult.map(v => v && isValid(v) && this._formatSingle({ date: v, formatToken })).join(separator);
+                    if (parsedResult && formattedInput === input) {
                         parsedResult.sort((d1, d2) => d1.getTime() - d2.getTime());
                         result = parsedResult;
                     }
@@ -713,38 +651,37 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     /**
      * get date which may include null from input
      */
-    getLooseDateFromInput(input: string): Array<Date | null> {
+    getLooseDateFromInput(input: string): Array<TZDate | null> {
         const value = this._isMultiple() ? this.parseMultipleInputLoose(input) : this.parseInputLoose(input);
         return value;
     }
 
     /**
-     * parse input into `Array<Date|null>`, loose means return value includes `null`
+     * parse input into `Array<TZDate|null>`, loose means return value includes `null`
      * 
      * @example
      * ```javascript
-     * parseInputLoose('2022-03-15 ~ '); // [Date, null]
-     * parseInputLoose(' ~ 2022-03-15 '); // [null, Date]
+     * parseInputLoose('2022-03-15 ~ '); // [TZDate, null]
+     * parseInputLoose(' ~ 2022-03-15 '); // [null, TZDate]
      * parseInputLoose(''); // []
      * parseInputLoose('2022-03- ~ 2022-0'); // [null, null]
      * ```
      */
-    parseInputLoose(input = ''): Array<Date | null> {
-        let result: Array<Date | null> = [];
-        const { dateFnsLocale, rangeSeparator, type, format } = this.getProps();
+    parseInputLoose(input = ''): Array<TZDate | null> {
+        let result: Array<TZDate | null> = [];
+        const { dateFnsLocale, rangeSeparator, type, format, timeZone } = this._adapter.getProps();
 
         if (input && input.length) {
             const formatToken = format || getDefaultFormatTokenByType(type);
-            let parsedResult, formatedInput;
-            const nowDate = new Date();
+            let parsedResult, formattedInput;
             switch (type) {
                 case 'date':
                 case 'dateTime':
                 case 'month':
-                    const _parsedResult = compatibleParse(input, formatToken, nowDate, dateFnsLocale);
-                    if (isValidDate(_parsedResult)) {
-                        formatedInput = this.localeFormat(_parsedResult as Date, formatToken);
-                        if (formatedInput === input) {
+                    const _parsedResult = this._parseSingle({ date: input, timeZone, formatToken, locale: dateFnsLocale });
+                    if (_parsedResult) {
+                        formattedInput = this._formatSingle({ date: _parsedResult, formatToken });
+                        if (formattedInput === input) {
                             parsedResult = _parsedResult;
                         }
                     } else {
@@ -760,11 +697,11 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
                         values &&
                         values.reduce((arr, cur) => {
                             let parsedVal = null;
-                            const _parsedResult = compatibleParse(cur, formatToken, nowDate, dateFnsLocale);
-                            if (isValidDate(_parsedResult)) {
-                                formatedInput = this.localeFormat(_parsedResult as Date, formatToken);
-                                if (formatedInput === cur) {
-                                    parsedVal = _parsedResult;
+                            const _parsedResult = this._parseSingle({ date: input, timeZone, formatToken, locale: dateFnsLocale });
+                            if (_parsedResult) {
+                                formattedInput = this._formatSingle({ date: _parsedResult, formatToken });
+                                if (formattedInput === input) {
+                                    parsedResult = _parsedResult;
                                 }
                             }
                             arr.push(parsedVal);
@@ -784,19 +721,19 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     }
 
     /**
-     * parse multiple into `Array<Date|null>`, loose means return value includes `null`
+     * parse multiple into `Array<TZDate|null>`, loose means return value includes `null`
      * 
      * @example
      * ```javascript
-     * parseMultipleInputLoose('2021-01-01,2021-10-15'); // [Date, Date];
-     * parseMultipleInputLoose('2021-01-01,2021-10-'); // [Date, null];
+     * parseMultipleInputLoose('2021-01-01,2021-10-15'); // [TZDate, TZDate];
+     * parseMultipleInputLoose('2021-01-01,2021-10-'); // [TZDate, null];
      * parseMultipleInputLoose(''); // [];
      * ```
      */
     parseMultipleInputLoose(input = '', separator: string = strings.DEFAULT_SEPARATOR_MULTIPLE, needDedupe = false) {
         const max = this.getProp('max');
         const inputArr = input.split(separator);
-        const result: Date[] = [];
+        const result: TZDate[] = [];
 
         for (const curInput of inputArr) {
             let tmpParsed = curInput && this.parseInputLoose(curInput);
@@ -822,16 +759,11 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     /**
      * Parses the input when multiple is true, if valid,
      *  returns a list of time objects, otherwise returns an array
-     *
-     * @param {string} [input='']
-     * @param {string} [separator=',']
-     * @param {boolean} [needDedupe=false]
-     * @returns {Date[]}
      */
-    parseMultipleInput(input = '', separator: string = strings.DEFAULT_SEPARATOR_MULTIPLE, needDedupe = false) {
+    parseMultipleInput(input = '', separator: string = strings.DEFAULT_SEPARATOR_MULTIPLE, needDedupe = false): TZDate[] {
         const max = this.getProp('max');
         const inputArr = input.split(separator);
-        const result: Date[] = [];
+        const result: TZDate[] = [];
 
         for (const curInput of inputArr) {
             let tmpParsed = curInput && this.parseInput(curInput);
@@ -857,11 +789,8 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
     /**
      * dates[] => string
-     *
-     * @param {Date[]} dates
-     * @returns {string}
      */
-    formatDates(dates: Date[] = [], customFormat?: string) {
+    formatDates(dates: TZDate[] = [], customFormat?: string) {
         let str = '';
         const rangeSeparator = this.getProp('rangeSeparator');
 
@@ -873,7 +802,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
                 case 'date':
                 case 'dateTime':
                 case 'month':
-                    str = this.localeFormat(dates[0], formatToken);
+                    str = this._formatSingle({ date: dates[0], formatToken });
                     break;
 
                 case 'dateRange':
@@ -881,13 +810,16 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
                 case 'monthRange':
                     const startIsTruthy = !isNullOrUndefined(dates[0]);
                     const endIsTruthy = !isNullOrUndefined(dates[1]);
+                    const start = startIsTruthy && this._formatSingle({ date: dates[0], formatToken });
+                    const end = endIsTruthy && this._formatSingle({ date: dates[1], formatToken });
+
                     if (startIsTruthy && endIsTruthy) {
-                        str = `${this.localeFormat(dates[0], formatToken)}${rangeSeparator}${this.localeFormat(dates[1], formatToken)}`;
+                        str = `${start}${rangeSeparator}${end}`;
                     } else {
                         if (startIsTruthy) {
-                            str = `${this.localeFormat(dates[0], formatToken)}${rangeSeparator}`;
+                            str = `${start}${rangeSeparator}`;
                         } else if (endIsTruthy) {
-                            str = `${rangeSeparator}${this.localeFormat(dates[1], formatToken)}`;
+                            str = `${rangeSeparator}${end}`;
                         }
                     }
                     break;
@@ -901,11 +833,8 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
     /**
      * dates[] => string
-     *
-     * @param {Date[]} dates
-     * @returns {string}
      */
-    formatMultipleDates(dates: Date[] = [], separator: string = strings.DEFAULT_SEPARATOR_MULTIPLE, customFormat?: string) {
+    formatMultipleDates(dates: TZDate[] = [], separator: string = strings.DEFAULT_SEPARATOR_MULTIPLE, customFormat?: string) {
         const strs = [];
         if (Array.isArray(dates) && dates.length) {
             const type = this.getProp('type');
@@ -934,12 +863,9 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      * Update date value and the value of the input box
      * 1. Select Update
      * 2. Input Update
-     * @param {Date|''} value
-     * @param {Boolean} forceUpdateValue
-     * @param {String} input
      */
-    _updateValueAndInput(value: Date | Array<Date>, forceUpdateValue?: boolean, input?: string) {
-        let _value: Array<Date>;
+    _updateValueAndInput(value: TZDate | Array<TZDate>, forceUpdateValue?: boolean, input?: string) {
+        let _value: Array<TZDate>;
         if (forceUpdateValue || value) {
             if (!Array.isArray(value)) {
                 _value = value ? [value] : [];
@@ -958,10 +884,8 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
     /**
      * when changing the selected value through the date panel
-     * @param {*} value
-     * @param {*} options
      */
-    handleSelectedChange(value: Date[], options?: { fromPreset?: boolean; needCheckFocusRecord?: boolean }) {
+    handleSelectedChange(value: TZDate[], options?: { fromPreset?: boolean; needCheckFocusRecord?: boolean }) {
         const { type, format, rangeSeparator, insetInput } = this._adapter.getProps();
         const { value: stateValue } = this.getStates();
         const controlled = this._isControlledComponent();
@@ -1018,20 +942,18 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
     /**
      * when changing the year and month through the panel when the type is year or month or monthRange
-     * @param {*} item
      */
     handleYMSelectedChange(item: { currentMonth?: { left: number; right: number }; currentYear?: { left: number; right: number } } = {}) {
-        // console.log(item);
         const { currentMonth, currentYear } = item;
-        const { type } = this.getProps();
+        const { type, timeZone } = this._adapter.getProps();
+        const normalizedTimeZone = TZDateUtil.normalizeTimeZone(timeZone);
 
         if (type === 'month') {
-            const date = new Date(currentYear['left'], currentMonth['left'] - 1);
-
+            const date = new TZDate(currentYear['left'], currentMonth['left'] - 1, normalizedTimeZone);
             this.handleSelectedChange([date]);
         } else {
-            const dateLeft = new Date(currentYear['left'], currentMonth['left'] - 1);
-            const dateRight = new Date(currentYear['right'], currentMonth['right'] - 1);
+            const dateLeft = new TZDate(currentYear['left'], currentMonth['left'] - 1, normalizedTimeZone);
+            const dateRight = new TZDate(currentYear['right'], currentMonth['right'] - 1, normalizedTimeZone);
 
             this.handleSelectedChange([dateLeft, dateRight]);
 
@@ -1063,7 +985,6 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
     handlePresetClick(item: PresetType, e: any) {
         const { type, timeZone } = this.getProps();
-        const prevTimeZone = this.getState('prevTimezone');
         const start = typeof item.start === 'function' ? item.start() : item.start;
         const end = typeof item.end === 'function' ? item.end() : item.end;
 
@@ -1072,12 +993,12 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
             case 'month':
             case 'dateTime':
             case 'date':
-                value = this.parseWithTimezone([start], timeZone, prevTimeZone);
+                value = this._parseValue({ value: [start], timeZone });
                 this.handleSelectedChange(value);
                 break;
             case 'dateTimeRange':
             case 'dateRange':
-                value = this.parseWithTimezone([start, end], timeZone, prevTimeZone);
+                value = this._parseValue({ value: [start, end], timeZone });
                 this.handleSelectedChange(value, { needCheckFocusRecord: false });
                 break;
             default:
@@ -1088,39 +1009,13 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
     /**
      * 根据 type 处理 onChange 返回的参数
-     *
-     *  - 返回的日期需要把用户时间转换为设置的时区时间
-     *      - 用户时间:用户计算机系统时间
-     *      - 时区时间:通过 ConfigProvider 设置的 timeZone
-     *  - 例子:用户设置时区为+9,计算机所在时区为+8区,然后用户选择了22:00
-     *      - DatePicker 内部保存日期 state 为 +8 的 22:00 => a = new Date("2021-05-25 22:00:00")
-     *      - 传出去时,需要把 +8 的 22:00 => +9 的 22:00 => b = zonedTimeToUtc(a, "+09:00");
-     *
-     * According to the type processing onChange returned parameters
-     *
-     *   - the returned date needs to convert the user time to the set time zone time
-     *       - user time: user computer system time
-     *       - time zone time: timeZone set by ConfigProvider
-     *   - example: the user sets the time zone to + 9, the computer's time zone is + 8 zone, and then the user selects 22:00
-     *       - DatePicker internal save date state is + 8 22:00 = > a = new Date ("2021-05-25 22:00:00")
-     *       - when passed out, you need to + 8 22:00 = > + 9 22:00 = > b = zonedTimeToUtc (a, "+ 09:00");
-     *
-     *  e.g.
-     *  let a = new Date ("2021-05-25 22:00:00");
-     *       = > Tue May 25 2021 22:00:00 GMT + 0800 (China Standard Time)
-     *  let b = zonedTimeToUtc (a, "+ 09:00");
-     *       = > Tue May 25 2021 21:00:00 GMT + 0800 (China Standard Time)
-     *
-     * @param {Date|Date[]} value
-     * @return {{ notifyDate: Date|Date[], notifyValue: string|string[]}}
+     * 
+     * 需返回 UTC Date
      */
-    disposeCallbackArgs(value: Date | Date[]) {
-        let _value = Array.isArray(value) ? value : (value && [value]) || [];
-        const timeZone = this.getProp('timeZone');
+    disposeCallbackArgs(value: TZDate | TZDate[]) {
+        const tzValue = Array.isArray(value) ? value : (value && [value]) || [];
+        const exposeValue = tzValue.map(date => TZDateUtil.expose(date));
 
-        if (isValidTimeZone(timeZone)) {
-            _value = _value.map(date => zonedTimeToUtc(date, timeZone));
-        }
         const type = this.getProp('type');
         const formatToken = this.getProp('format') || getDefaultFormatTokenByType(type);
 
@@ -1131,18 +1026,18 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
             case 'dateTime':
             case 'month':
                 if (!this._isMultiple()) {
-                    notifyValue = _value[0] && this.localeFormat(_value[0], formatToken);
-                    [notifyDate] = _value;
+                    notifyValue = tzValue[0] && this._formatSingle({ date: tzValue[0], formatToken });
+                    [notifyDate] = exposeValue;
                 } else {
-                    notifyValue = _value.map(v => v && this.localeFormat(v, formatToken));
-                    notifyDate = [..._value];
+                    notifyValue = tzValue.map(v => v && this._formatSingle({ date: tzValue[0], formatToken }));
+                    notifyDate = [...exposeValue];
                 }
                 break;
             case 'dateRange':
             case 'dateTimeRange':
             case 'monthRange':
-                notifyValue = _value.map(v => v && this.localeFormat(v, formatToken));
-                notifyDate = [..._value];
+                notifyValue = tzValue.map(v => v && this._formatSingle({ date: tzValue[0], formatToken }));
+                notifyDate = [...exposeValue];
                 break;
             default:
                 break;
@@ -1156,9 +1051,8 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
     /**
      * Notice: Check whether the date is the same as the state value before calling
-     * @param {Date[]} value
      */
-    _notifyChange(value: Date[]) {
+    _notifyChange(value: TZDate[]) {
         if (this._isRangeType() && !this._isRangeValueComplete(value)) {
             return;
         }
@@ -1174,7 +1068,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     /**
      * Get the date changed through the date panel or enter
      */
-    _getChangedDates(dates: Date[]) {
+    _getChangedDates(dates: TZDate[]) {
         const type = this._adapter.getProp('type');
         const { cachedSelectedValue: lastDate } = this._adapter.getStates();
         const changedDates = [];
@@ -1208,19 +1102,19 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      * @param value The date that needs to be judged whether to disable
      * @param selectedValue Selected date, when selecting a range, pass this date to the second parameter of `disabledDate`
      */
-    _someDateDisabled(value: Date[], selectedValue: Date[]) {
+    _someDateDisabled(value: TZDate[], selectedValue: TZDate[]) {
         const { rangeInputFocus } = this.getStates();
         const disabledOptions = { rangeStart: '', rangeEnd: '', rangeInputFocus };
 
         // DisabledDate needs to pass the second parameter
         if (this._isRangeType() && Array.isArray(selectedValue)) {
             if (isValid(selectedValue[0])) {
-                const rangeStart = format(selectedValue[0], 'yyyy-MM-dd');
+                const rangeStart = this._formatSingle({ date: selectedValue[0], formatToken: 'yyyy-MM-dd' });
                 disabledOptions.rangeStart = rangeStart;
             }
 
             if (isValid(selectedValue[1])) {
-                const rangeEnd = format(selectedValue[1], 'yyyy-MM-dd');
+                const rangeEnd = this._formatSingle({ date: selectedValue[1], formatToken: 'yyyy-MM-dd' });
                 disabledOptions.rangeEnd = rangeEnd;
             }
         }
@@ -1236,16 +1130,16 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         return isSomeDateDisabled;
     }
 
-
     /**
      * Format locale date
      * locale get from LocaleProvider
-     * @param {Date} date
-     * @param {String} token
      */
-    localeFormat(date: Date, token: string) {
-        const dateFnsLocale = this._adapter.getProp('dateFnsLocale');
-        return format(date, token, { locale: dateFnsLocale });
+    _formatSingle(options: { date: TZDate; formatToken?: string; locale?: Locale }) {
+        const { date } = options;
+        const { dateFnsLocale, format } = this._adapter.getProps();
+        const currentToken = options.formatToken ?? format;
+        const currentLocale = options.locale ?? dateFnsLocale;
+        return TZDateUtil.format({ date, formatToken: currentToken, locale: currentLocale });
     }
 
     _isRangeType = () => {
@@ -1253,7 +1147,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         return /range/i.test(type);
     };
 
-    _isRangeValueComplete = (value: Date[] | Date) => {
+    _isRangeValueComplete = (value: TZDate[] | TZDate) => {
         let result = false;
         if (Array.isArray(value)) {
             result = !value.some(date => isNullOrUndefined(date));
@@ -1266,11 +1160,8 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      * Before passing the date to the user, you need to convert the date to UTC time
      * dispose date from computer date to utc date
      * When given timeZone prop, you should convert computer date to utc date before passing to user
-     * @param {(date: Date) => Boolean} fn
-     * @param {Date|Date[]} date
-     * @returns {Boolean}
      */
-    disposeDateFn(fn: (date: Date, ...rest: any) => boolean, date: Date | Date[], ...rest: any[]) {
+    disposeDateFn(fn: (date: Date, ...rest: any) => boolean, date: TZDate | TZDate[], ...rest: any[]) {
         const { notifyDate } = this.disposeCallbackArgs(date);
 
         const dateIsArray = Array.isArray(date);
@@ -1289,21 +1180,17 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     /**
      * Determine whether the date is disabled
      * Whether the date is disabled
-     * @param {Date} date
-     * @returns {Boolean}
      */
-    disabledDisposeDate(date: Date, ...rest: any[]) {
-        const { disabledDate } = this.getProps();
+    disabledDisposeDate(date: TZDate, ...rest: any[]) {
+        const { disabledDate } = this._adapter.getProps();
         return this.disposeDateFn(disabledDate, date, ...rest);
     }
 
     /**
      * Determine whether the date is disabled
      * Whether the date time is disabled
-     * @param {Date|Date[]} date
-     * @returns {Object}
      */
-    disabledDisposeTime(date: Date | Date[], ...rest: any[]) {
+    disabledDisposeTime(date: TZDate | TZDate[], ...rest: any[]) {
         const { disabledTime } = this.getProps();
         return this.disposeDateFn(disabledTime, date, ...rest);
     }

+ 6 - 4
packages/semi-foundation/datePicker/inputFoundation.ts

@@ -12,6 +12,7 @@ import getDefaultPickerDate from './_utils/getDefaultPickerDate';
 import { compatibleParse } from './_utils/parser';
 import { isValidDate } from './_utils';
 import copy from 'fast-copy';
+import { TZDate } from '@date-fns/tz';
 
 const KEY_CODE_ENTER = 'Enter';
 const KEY_CODE_TAB = 'Tab';
@@ -51,7 +52,7 @@ export interface InsetInputProps {
 
 export interface DateInputFoundationProps extends DateInputElementProps, DateInputEventHandlerProps {
     [x: string]: any;
-    value?: Date[];
+    value?: TZDate[];
     disabled?: boolean;
     type?: Type;
     showClear?: boolean;
@@ -65,7 +66,8 @@ export interface DateInputFoundationProps extends DateInputElementProps, DateInp
     insetInput?: boolean | InsetInputProps;
     insetInputValue?: InsetInputValue;
     density?: typeof strings.DENSITY_SET[number];
-    defaultPickerValue?: ValueType
+    defaultPickerValue?: ValueType;
+    timeZone?: string | number
 }
 
 export interface InsetInputValue {
@@ -202,9 +204,9 @@ export default class InputFoundation extends BaseFoundation<DateInputAdapter> {
 
     _autoFillTimeToInsetInputValue(options: { insetInputValue: InsetInputValue; format: string; valuePath: string}) {
         const { valuePath, insetInputValue, format } = options;
-        const { type, defaultPickerValue, dateFnsLocale } = this._adapter.getProps();
+        const { type, defaultPickerValue, dateFnsLocale, timeZone } = this._adapter.getProps();
         const insetInputValueWithTime = copy(insetInputValue);
-        const { nowDate, nextDate } = getDefaultPickerDate({ defaultPickerValue, format, dateFnsLocale });
+        const { nowDate, nextDate } = getDefaultPickerDate({ defaultPickerValue, format, dateFnsLocale, timeZone });
 
         if (type.includes('Time')) {
             let timeStr = '';

+ 11 - 9
packages/semi-foundation/datePicker/monthFoundation.ts

@@ -4,10 +4,11 @@ import getMonthTable, { WeekStartNumber } from './_utils/getMonthTable';
 import getDayOfWeek from './_utils/getDayOfWeek';
 import { format } from 'date-fns';
 import isNullOrUndefined from '../utils/isNullOrUndefined';
+import { TZDate } from '@date-fns/tz';
 
 export interface MonthFoundationProps {
     forwardRef: any;
-    month: Date;
+    month: TZDate;
     selected: Set<string>;
     rangeStart: string;
     rangeEnd: string;
@@ -16,7 +17,7 @@ export interface MonthFoundationProps {
     onDayClick: (day: MonthDayInfo) => void;
     onDayHover: (day: MonthDayInfo) => void;
     weekStartsOn: WeekStartNumber;
-    disabledDate: (day: Date, options?: { rangeStart: string; rangeEnd: string }) => boolean;
+    disabledDate: (day: TZDate, options?: { rangeStart: string; rangeEnd: string }) => boolean;
     weeksRowNum: number;
     onWeeksRowNumChange: (weeksRowNum: number) => void;
     renderDate: () => void;
@@ -28,7 +29,8 @@ export interface MonthFoundationProps {
     focusRecordsRef: any;
     locale: any;
     localeCode: string;
-    multiple: boolean
+    multiple: boolean;
+    timeZone?: string | number
 }
 
 export type MonthDayInfo = {
@@ -44,7 +46,7 @@ export type MonthDayInfo = {
 export interface MonthInfo {
     weeks: Array<MonthDayInfo[]>;
     monthText: string ;
-    month?: Date
+    month?: TZDate
 }
 
 export interface MonthFoundationState {
@@ -82,18 +84,18 @@ export default class CalendarMonthFoundation extends BaseFoundation<MonthAdapter
     }
 
     _getToday() {
-        const today = new Date();
+        const { timeZone } = this._adapter.getProps();
+        const today = new TZDate(timeZone);
         const todayText = format(today, 'yyyy-MM-dd');
 
         this._adapter.updateToday(todayText);
     }
 
     getMonthTable() {
-        const month: Date = this._adapter.getProp('month');
-        const weeksRowNum = this.getState('weeksRowNum');
+        const { month, weekStartsOn } = this._adapter.getProps();
+        const { weeksRowNum } = this._adapter.getStates();
         if (month) {
             this.updateWeekDays();
-            const weekStartsOn: WeekStartNumber = this._adapter.getProp('weekStartsOn');
             const monthTable = getMonthTable(month, weekStartsOn);
             const { weeks } = monthTable;
             this._adapter.updateMonthTable(monthTable);
@@ -109,7 +111,7 @@ export default class CalendarMonthFoundation extends BaseFoundation<MonthAdapter
     }
 
     updateWeekDays() {
-        const weekStartsOn = this._adapter.getProp('weekStartsOn');
+        const { weekStartsOn } = this._adapter.getProps();
         const weekdays = getDayOfWeek({ weekStartsOn });
         this._adapter.setWeekDays(weekdays);
     }

+ 58 - 92
packages/semi-foundation/datePicker/monthsGridFoundation.ts

@@ -16,7 +16,7 @@ import { isBefore, isValidDate, getDefaultFormatToken, getFullDateOffset } from
 import { formatFullDate, WeekStartNumber } from './_utils/getMonthTable';
 import { compatibleParse } from './_utils/parser';
 import { includes, isSet, isEqual, isFunction } from 'lodash';
-import { zonedTimeToUtc } from '../utils/date-fns-extra';
+import { TZDateUtil } from '../utils/date-fns-extra';
 import { getDefaultFormatTokenByType } from './_utils/getDefaultFormatToken';
 import isNullOrUndefined from '../utils/isNullOrUndefined';
 import { BaseValueType, DateInputFoundationProps, PresetPosition, ValueType } from './foundation';
@@ -24,6 +24,8 @@ import { MonthDayInfo } from './monthFoundation';
 import { ArrayElement } from '../utils/type';
 import isValidTimeZone from './_utils/isValidTimeZone';
 import { YearAndMonthFoundationProps } from './yearAndMonthFoundation';
+import { TZDate } from '@date-fns/tz';
+import getDefaultPickerDate from './_utils/getDefaultPickerDate';
 
 const dateDiffFns = {
     month: differenceInCalendarMonths,
@@ -57,14 +59,14 @@ export type YearMonthChangeType = 'prevMonth' | 'nextMonth' | 'prevYear' | 'next
 export interface MonthsGridFoundationProps extends MonthsGridElementProps, Pick<YearAndMonthFoundationProps, 'startYear' | 'endYear'> {
     type?: Type;
     /** may be null if selection is not complete when type is dateRange or dateTimeRange */
-    defaultValue?: (Date | null)[];
+    defaultValue?: (TZDate | null)[];
     defaultPickerValue?: ValueType;
     multiple?: boolean;
     max?: number;
     splitPanels?: boolean;
     weekStartsOn?: WeekStartNumber;
-    disabledDate?: (date: Date, options?: { rangeStart: string; rangeEnd: string }) => boolean;
-    disabledTime?: (date: Date | Date[], panelType: PanelType) => void;
+    disabledDate?: (date: TZDate, options?: { rangeStart: string; rangeEnd: string }) => boolean;
+    disabledTime?: (date: TZDate | TZDate[], panelType: PanelType) => void;
     disabledTimePicker?: boolean;
     hideDisabledOptions?: boolean;
     onMaxSelect?: (v?: any) => void;
@@ -83,10 +85,10 @@ export interface MonthsGridFoundationProps extends MonthsGridElementProps, Pick<
     timeZone?: string | number;
     syncSwitchMonth?: boolean;
     onChange?: (
-        value: [Date] | [Date, Date],
+        value: [TZDate] | [TZDate, TZDate],
         options?: { closePanel?: boolean; needCheckFocusRecord?: boolean }
     ) => void;
-    onPanelChange?: (date: Date | Date[], dateString: string | string[]) => void;
+    onPanelChange?: (date: TZDate | TZDate[], dateString: string | string[]) => void;
     setRangeInputFocus?: (rangeInputFocus: 'rangeStart' | 'rangeEnd') => void;
     isAnotherPanelHasOpened?: (currentRangeInput: 'rangeStart' | 'rangeEnd') => boolean;
     focusRecordsRef?: any;
@@ -99,11 +101,11 @@ export interface MonthsGridFoundationProps extends MonthsGridElementProps, Pick<
 
 export interface MonthInfo {
     /** The date displayed in the current date panel, update when switching year and month */
-    pickerDate: Date;
+    pickerDate: TZDate;
     /**
      * Default date or selected date (when selected)
      */
-    showDate: Date;
+    showDate: TZDate;
     isTimePickerOpen: boolean;
     isYearPickerOpen: boolean
 }
@@ -145,7 +147,7 @@ export interface MonthsGridAdapter extends DefaultAdapter<MonthsGridFoundationPr
 }
 
 export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapter> {
-    newBiMonthPanelDate: [Date, Date];
+    newBiMonthPanelDate: [TZDate, TZDate];
 
     constructor(adapter: MonthsGridAdapter) {
         super({ ...adapter });
@@ -154,26 +156,27 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
     }
 
     init() {
-        const defaultValue = this.getProp('defaultValue');
+        const { defaultValue } = this._adapter.getProps();
         this.initDefaultPickerValue();
         this.updateSelectedFromProps(defaultValue);
     }
 
-    initDefaultPickerValue() {
-        const defaultPickerValue = compatibleParse(this.getProp('defaultPickerValue'));
 
-        if (defaultPickerValue && isValidDate(defaultPickerValue)) {
-            this._updatePanelDetail(strings.PANEL_TYPE_LEFT, {
-                pickerDate: defaultPickerValue,
-            });
 
-            this._updatePanelDetail(strings.PANEL_TYPE_RIGHT, {
-                pickerDate: addMonths(defaultPickerValue, 1),
-            });
-        }
+    initDefaultPickerValue() {
+        const { format, dateFnsLocale, type, defaultPickerValue: _defaultPickerValue, timeZone } = this._adapter.getProps();
+        const currentFormat = format || getDefaultFormatTokenByType(type);
+        const { nowDate, nextDate } = getDefaultPickerDate({ defaultPickerValue: _defaultPickerValue, format: currentFormat, dateFnsLocale, timeZone });
+
+        this._updatePanelDetail(strings.PANEL_TYPE_LEFT, {
+            pickerDate: nowDate,
+        });
+        this._updatePanelDetail(strings.PANEL_TYPE_RIGHT, {
+            pickerDate: nextDate
+        });
     }
 
-    updateSelectedFromProps(values: (Date | null)[], refreshPicker = true) {
+    updateSelectedFromProps(values: (TZDate | null)[], refreshPicker = true) {
         const type: Type = this.getProp('type');
         const { selected, rangeStart, rangeEnd } = this.getStates();
         if (values && values?.length) {
@@ -228,7 +231,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         }
     }
 
-    _initDatePickerFromValue(values: Date[], refreshPicker = true) {
+    _initDatePickerFromValue(values: TZDate[], refreshPicker = true) {
         const { monthLeft } = this._adapter.getStates();
         const newMonthLeft = { ...monthLeft };
         // REMOVE:
@@ -236,10 +239,10 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         const newSelected = new Set<string>();
         const isMultiple = this._isMultiple();
         if (!isMultiple) {
-            values[0] && newSelected.add(format(values[0] as Date, strings.FORMAT_FULL_DATE));
+            values[0] && newSelected.add(format(values[0], strings.FORMAT_FULL_DATE));
         } else {
             values.forEach(date => {
-                date && newSelected.add(format(date as Date, strings.FORMAT_FULL_DATE));
+                date && newSelected.add(format(date, strings.FORMAT_FULL_DATE));
             });
         }
         if (refreshPicker) {
@@ -256,7 +259,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         this._adapter.updateDaySelected(newSelected);
     }
 
-    _initDateRangePickerFromValue(values: (Date | null)[], withTime = false) {
+    _initDateRangePickerFromValue(values: (TZDate | null)[], withTime = false) {
         // init month panel
         const monthLeft = this.getState('monthLeft') as MonthsGridFoundationState['monthLeft'];
         const monthRight = this.getState('monthRight') as MonthsGridFoundationState['monthRight'];
@@ -270,7 +273,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
             this.handleShowDateAndTime(strings.PANEL_TYPE_LEFT, adjustResult.monthLeft.pickerDate);
             this.handleShowDateAndTime(strings.PANEL_TYPE_RIGHT, adjustResult.monthRight.pickerDate);
         } else {
-            const selectedDate = values.find(item => item) as Date;
+            const selectedDate = values.find(item => item);
             // 如果日期不完整且输入日期不在面板范围内,则更新面板
             if (selectedDate) {                
                 const notLeftPanelDate = Math.abs(differenceInCalendarMonths(selectedDate, monthLeft.pickerDate)) > 0;
@@ -285,8 +288,8 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
 
         // init range
         const formatToken = withTime ? strings.FORMAT_DATE_TIME : strings.FORMAT_FULL_DATE;
-        let rangeStart = values[0] && format(values[0] as Date, formatToken);
-        let rangeEnd = values[1] && format(values[1] as Date, formatToken);
+        let rangeStart = values[0] && format(values[0], formatToken);
+        let rangeEnd = values[1] && format(values[1], formatToken);
 
         if (this._isNeedSwap(rangeStart, rangeEnd)) {
             [rangeStart, rangeEnd] = [rangeEnd, rangeStart];
@@ -296,11 +299,11 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         this._adapter.setHoverDay(rangeEnd);
     }
 
-    _initDateTimePickerFromValue(values: Date[]) {
+    _initDateTimePickerFromValue(values: TZDate[]) {
         this._initDatePickerFromValue(values);
     }
 
-    _initDateTimeRangePickerFormValue(values: (Date | null)[]) {
+    _initDateTimeRangePickerFormValue(values: (TZDate | null)[]) {
         this._initDateRangePickerFromValue(values, true);
     }
 
@@ -315,7 +318,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
      *  - panelType=right, target=new Date('2022-09-01') and left panel is in '2022-09' => call it, left panel minus one month to '2022-08'
      *  - panelType=left, target=new Date('2021-12-01') and right panel is in '2021-12' => call it, right panel add one month to '2021-01'
      */
-    handleSyncChangeMonths(options: { panelType: PanelType; target: Date }) {
+    handleSyncChangeMonths(options: { panelType: PanelType; target: TZDate }) {
         const { panelType, target } = options;
         const { type } = this._adapter.getProps();
         const { monthLeft, monthRight } = this._adapter.getStates();
@@ -357,7 +360,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
     /**
      * Change month by yam panel
      */
-    toMonth(panelType: PanelType, target: Date) {
+    toMonth(panelType: PanelType, target: TZDate) {
         const { type } = this._adapter.getProps();
         const diff = this._getDiff('month', target, panelType);
         this.handleYearOrMonthChange(diff < 0 ? 'prevMonth' : 'nextMonth', panelType, Math.abs(diff), false);
@@ -367,12 +370,12 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         }
     }
 
-    toYear(panelType: PanelType, target: Date) {
+    toYear(panelType: PanelType, target: TZDate) {
         const diff = this._getDiff('year', target, panelType);
         this.handleYearOrMonthChange(diff < 0 ? 'prevYear' : 'nextYear', panelType, Math.abs(diff), false);
     }
 
-    toYearMonth(panelType: PanelType, target: Date) {
+    toYearMonth(panelType: PanelType, target: TZDate) {
         this.toYear(panelType, target);
         this.toMonth(panelType, target);
     }
@@ -425,7 +428,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
     /**
      * Calculate the year and month difference
      */
-    _getDiff(type: 'month' | 'year', target: Date, panelType: PanelType) {
+    _getDiff(type: 'month' | 'year', target: TZDate, panelType: PanelType) {
         const panelDetail = this._getPanelDetail(panelType);
         const diff = dateDiffFns[type] && dateDiffFns[type](target, panelDetail.pickerDate);
         return diff;
@@ -439,49 +442,17 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
     /**
      * Format locale date
      * locale get from LocaleProvider
-     * @param {Date} date
-     * @param {String} token
-     * @returns
      */
-    localeFormat(date: Date, token: string) {
+    localeFormat(date: TZDate, token: string) {
         const dateFnsLocale = this._adapter.getProp('dateFnsLocale');
         return format(date, token, { locale: dateFnsLocale });
     }
 
     /**
      * 根据 type 处理 onChange 返回的参数
-     *
-     *  - 返回的日期需要把用户时间转换为设置的时区时间
-     *      - 用户时间:用户计算机系统时间
-     *      - 时区时间:通过 ConfigProvider 设置的 timeZone
-     *  - 例子:用户设置时区为+9,计算机所在时区为+8区,然后用户选择了22:00
-     *      - DatePicker 内部保存日期 state 为 +8 的 22:00 => a = new Date("2021-05-25 22:00:00")
-     *      - 传出去时,需要把 +8 的 22:00 => +9 的 22:00 => b = zonedTimeToUtc(a, "+09:00");
-     *
-     * The parameters returned by onChange are processed according to type
-     *
-     *  -The returned date needs to convert the user time to the set time zone time
-     *      -User time: user computer system time
-     *      -Time zone: timeZone set by ConfigProvider
-     *  -Example: The user sets the time zone to + 9, and the time zone where the computer is located is + 8, and then the user selects 22:00
-     *      -DatePicker internal save date state is + 8 22:00 = > a = new Date ("2021-05-25 22:00:00")
-     *      -When passing out, you need to put + 8's 22:00 = > + 9's 22:00 = > b = zonedTimeToUtc (a, "+ 09:00");
-     *
-     *  e.g.
-     *  let a = new Date ("2021-05-25 22:00:00");
-     *       = > Tue May 25 2021 22:00:00 GMT + 0800 (China Standard Time)
-     *  let b = zonedTimeToUtc (a, "+ 09:00");
-     *       = > Tue May 25 2021 21:00:00 GMT + 0800 (China Standard Time)
-     *
-     * @param {Date|Date[]} value
      */
-    disposeCallbackArgs(value: Date | Date[]) {
-        let _value = Array.isArray(value) ? value : (value && [value]) || [];
-        const timeZone = this.getProp('timeZone');
-
-        if (isValidTimeZone(timeZone)) {
-            _value = _value.map(date => zonedTimeToUtc(date, timeZone));
-        }
+    disposeCallbackArgs(value: TZDate | TZDate[]) {
+        const _value = Array.isArray(value) ? value : (value && [value]) || [];
         const type = this.getProp('type');
         const formatToken = this.getProp('format') || getDefaultFormatTokenByType(type);
 
@@ -559,7 +530,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
      */
     updateDateAfterChangeYM(
         type: YearMonthChangeType,
-        targetDate: Date
+        targetDate: TZDate
     ) {
         const { multiple, disabledDate, type: dateType } = this.getProps();
         const { selected: selectedSet, rangeStart, rangeEnd, monthLeft } = this.getStates();
@@ -568,7 +539,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         const options = { closePanel: false };
         if (!multiple && !includeRange && selectedSet.size) {
             const selectedStr = Array.from(selectedSet)[0] as string;
-            const selectedDate = new Date(selectedStr);
+            const selectedDate = new TZDate(selectedStr);
             const year = targetDate.getFullYear();
             const month = targetDate.getMonth();
             let fullDate = set(selectedDate, { year, month });
@@ -608,7 +579,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         }
     }
 
-    handleDateSelected(day: { fullDate: string; fullValidDate?: Date }, panelType: PanelType) {
+    handleDateSelected(day: { fullDate: string; fullValidDate?: TZDate }, panelType: PanelType) {
         const { max, type, isControlledComponent, dateFnsLocale } = this.getProps();
         const multiple = this._isMultiple();
         const { selected } = this.getStates();
@@ -642,29 +613,25 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
             this._adapter.updateDaySelected(newSelected);
         }
 
-        this._adapter.notifySelectedChange(newSelectedDates as [Date]);
+        this._adapter.notifySelectedChange(newSelectedDates as [TZDate]);
     }
 
-    handleShowDateAndTime(panelType: PanelType, pickerDate: number | Date, showDate?: Date) {
+    handleShowDateAndTime(panelType: PanelType, pickerDate: number | TZDate, showDate?: TZDate) {
         const _showDate = showDate || pickerDate;
         this._updatePanelDetail(panelType, { showDate: _showDate, pickerDate });
     }
 
     /**
      * link date and time
-     *
-     * @param {Date|string} date
-     * @param {Date|string} time
-     * @returns {Date}
      */
-    _mergeDateAndTime(date: Date | string, time: Date | string) {
+    _mergeDateAndTime(date: TZDate | string, time: TZDate | string) {
         const dateFnsLocale = this._adapter.getProp('dateFnsLocale');
         const dateStr = format(
-            isValidDate(date) ? date as Date : compatibleParse(date as string, strings.FORMAT_FULL_DATE, undefined, dateFnsLocale),
+            isValidDate(date) ? date as TZDate : compatibleParse(date as string, strings.FORMAT_FULL_DATE, undefined, dateFnsLocale),
             strings.FORMAT_FULL_DATE
         );
         const timeStr = format(
-            isValidDate(time) ? time as Date : compatibleParse(time as string, strings.FORMAT_TIME_PICKER, undefined, dateFnsLocale),
+            isValidDate(time) ? time as TZDate : compatibleParse(time as string, strings.FORMAT_TIME_PICKER, undefined, dateFnsLocale),
             strings.FORMAT_TIME_PICKER
         );
         const timeFormat = this.getValidTimeFormat();
@@ -735,7 +702,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
                 compatibleParse(rangeStart, dateFormat, undefined, dateFnsLocale),
                 compatibleParse(rangeEnd, dateFormat, undefined, dateFnsLocale),
             ];
-            let date: [Date, Date] = [startDate, endDate];
+            let date: [TZDate, TZDate] = [startDate, endDate];
 
             // If the type is dateRangeTime, add the value of time
             if (type === 'dateTimeRange') {
@@ -758,7 +725,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         }
     }
 
-    _isNeedSwap(rangeStart: Date | string, rangeEnd: Date | string) {
+    _isNeedSwap(rangeStart: TZDate | string, rangeEnd: TZDate | string) {
         // Check whether the start and end are reasonable and whether they need to be reversed
         return rangeStart && rangeEnd && isBefore(rangeEnd, rangeStart);
     }
@@ -823,12 +790,13 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
 
     handleTimeChange(newTime: { timeStampValue: number }, panelType: PanelType) {
         const { rangeEnd, rangeStart } = this.getStates();
-        const dateFnsLocale = this.getProp('dateFnsLocale');
+        const { dateFnsLocale, timeZone } = this._adapter.getProps();
         const ts = newTime.timeStampValue;
         const type = this.getProp('type');
         const panelDetail = this._getPanelDetail(panelType);
         const { showDate } = panelDetail;
-        const timeDate = new Date(ts);
+        const validTimeZone = TZDateUtil.normalizeTimeZone(timeZone);
+        const timeDate = new TZDate(ts, validTimeZone);
         const dateFormat = this.getValidDateFormat();
 
         const destRange = panelType === strings.PANEL_TYPE_RIGHT ? rangeEnd : rangeStart;
@@ -859,7 +827,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         const milSeconds = timeDate.getMilliseconds();
 
         const dateArgs = [year, monthNo, date, hours, minutes, seconds, milSeconds] as const;
-        const fullValidDate = new Date(...dateArgs);
+        const fullValidDate = new TZDate(...dateArgs, validTimeZone);
 
         if (type === 'dateTimeRange') {
             this.handleShowDateAndTime(panelType, fullValidDate, showDate);
@@ -880,10 +848,8 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
 
     /**
      * Update the time part in the range
-     * @param {string} panelType
-     * @param {Date} timeDate
      */
-    _updateTimeInDateRange(panelType: PanelType, timeDate: Date) {
+    _updateTimeInDateRange(panelType: PanelType, timeDate: TZDate) {
         const { isControlledComponent, dateFnsLocale } = this.getProps();
         let rangeStart = this.getState('rangeStart');
         let rangeEnd = this.getState('rangeEnd');
@@ -923,8 +889,8 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
     _updatePanelDetail(
         panelType: PanelType,
         kvs: {
-            showDate?: number | Date;
-            pickerDate?: number | Date;
+            showDate?: number | TZDate;
+            pickerDate?: number | TZDate;
             isTimePickerOpen?: boolean;
             isYearPickerOpen?: boolean
         }

+ 5 - 3
packages/semi-foundation/datePicker/yearAndMonthFoundation.ts

@@ -5,6 +5,7 @@ import { ArrayElement } from '../utils/type';
 import { strings } from './constants';
 import { PanelType } from './monthsGridFoundation';
 import copy from 'fast-copy';
+import { TZDate } from '@date-fns/tz';
 
 type Type = ArrayElement<typeof strings.TYPE_SET>;
 
@@ -18,7 +19,7 @@ export interface YearAndMonthFoundationProps {
     monthCycled?: boolean;
     yearCycled?: boolean;
     noBackBtn?: boolean;
-    disabledDate?: (date: Date) => boolean;
+    disabledDate?: (date: TZDate) => boolean;
     density?: string;
     presetPosition?: PresetPosition;
     renderQuickControls?: any;
@@ -26,7 +27,8 @@ export interface YearAndMonthFoundationProps {
     type?: Type;
     yearAndMonthOpts?: any;
     startYear?: number;
-    endYear?: number
+    endYear?: number;
+    timeZone?: number | string
 }
 
 export interface YearAndMonthFoundationState {
@@ -118,7 +120,7 @@ export default class YearAndMonthFoundation extends BaseFoundation<YearAndMonthA
      * After selecting a year, if the currentMonth is disabled, automatically select a non-disabled month
      */
     autoSelectMonth(item: YearScrollItem, panelType: PanelType, year: { left: number; right: number }) {
-        const { disabledDate, locale } = this._adapter.getProps();
+        const { disabledDate } = this._adapter.getProps();
         const { months, currentMonth } = this._adapter.getStates();
 
         const oppositeType = panelType === strings.PANEL_TYPE_LEFT ? 'right' : 'left';

+ 4 - 3
packages/semi-foundation/package.json

@@ -12,8 +12,9 @@
         "@mdx-js/mdx": "^3.0.1",
         "async-validator": "^3.5.0",
         "classnames": "^2.2.6",
-        "date-fns": "^2.29.3",
-        "date-fns-tz": "^1.3.8",
+        "date-fns": "^4.1.0",
+        "@date-fns/tz": "^1.2.0",
+        "@date-fns/utc": "^2.1.0",
         "fast-copy": "^3.0.1 ",
         "lodash": "^4.17.21",
         "lottie-web": "^5.12.2",
@@ -47,4 +48,4 @@
         "merge2": "^1.4.1",
         "through2": "^4.0.2"
     }
-}
+}

+ 85 - 21
packages/semi-foundation/timePicker/foundation.ts

@@ -10,9 +10,12 @@ import {
     isTimeFormatLike
 } from './utils';
 import { split, isUndefined } from 'lodash';
-import { isValid, format, getHours } from 'date-fns';
-import { utcToZonedTime, zonedTimeToUtc } from '../utils/date-fns-extra';
+import { isValid, format, getHours, Locale } from 'date-fns';
 import isNullOrUndefined from '../utils/isNullOrUndefined';
+import { TZDate } from '@date-fns/tz';
+
+export type BaseValueType = string | number | Date | undefined;
+export type Type = 'time' | 'timeRange';
 
 export type Position =
     | 'top'
@@ -30,14 +33,75 @@ export type Position =
     | 'leftTopOver'
     | 'rightTopOver';
 
-export interface TimePickerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+export interface TimePickerFoundationProps {
+    open?: boolean;
+    timeZone?: string | number;
+    dateFnsLocale?: Locale;
+    rangeSeparator?: string;
+    autoAdjustOverflow?: boolean;
+    autoFocus?: boolean; // TODO: autoFocus did not take effect
+    borderless?: boolean;
+    className?: string;
+    clearText?: string;
+    clearIcon?: any;
+    defaultOpen?: boolean;
+    defaultValue?: BaseValueType | BaseValueType[];
+    disabled?: boolean;
+    disabledHours?: () => number[];
+    disabledMinutes?: (selectedHour: number) => number[];
+    disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[];
+    focusOnOpen?: boolean;
+    format?: string;
+    getPopupContainer?: () => HTMLElement;
+    hideDisabledOptions?: boolean;
+    hourStep?: number;
+    id?: string;
+    inputReadOnly?: boolean;
+    inputStyle?: Record<string, any>;
+    insetLabelId?: string;
+    localeCode?: string;
+    minuteStep?: number;
+    motion?: boolean;
+    placeholder?: string;
+    popupClassName?: string;
+    position?: Position;
+    prefixCls?: string;
+    preventScroll?: boolean;
+    secondStep?: number;
+    showClear?: boolean;
+    stopPropagation?: boolean;
+    triggerRender?: (props?: any) => any;
+    type?: Type;
+    use12Hours?: boolean;
+    value?: BaseValueType | BaseValueType[];
+    zIndex?: number | string;
+    onBlur?: (e: any) => void;
+    onChange?: TimePickerAdapter['notifyChange'];
+    onChangeWithDateFirst?: boolean;
+    onFocus?: (e: any) => void;
+    onOpenChange?: (open: boolean) => void
+}
+
+export interface TimePickerFoundationState {
+    open: boolean;
+    value: TZDate[];
+    inputValue: string;
+    currentSelectPanel: string | number;
+    isAM: [boolean, boolean];
+    showHour: boolean;
+    showMinute: boolean;
+    showSecond: boolean;
+    invalid: boolean
+}
+
+export interface TimePickerAdapter extends DefaultAdapter<TimePickerFoundationProps, TimePickerFoundationState> {
     togglePanel: (show: boolean) => void;
     registerClickOutSide: () => void;
     setInputValue: (inputValue: string, cb?: () => void) => void;
     unregisterClickOutSide: () => void;
     notifyOpenChange: (open: boolean) => void;
-    notifyChange(value: Date | Date[], input: string | string[]): void;
-    notifyChange(input: string | string[], value: Date | Date[]): void;
+    notifyChange(value: TZDate | TZDate[], input: string | string[]): void;
+    notifyChange(input: string | string[], value: TZDate | TZDate[]): void;
     notifyFocus: (e: any) => void;
     notifyBlur: (e: any) => void;
     isRangePicker: () => boolean
@@ -45,9 +109,9 @@ export interface TimePickerAdapter<P = Record<string, any>, S = Record<string, a
 
 // TODO: split, timePicker different components cannot share a foundation
 
-class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<TimePickerAdapter<P, S>, P, S> {
+class TimePickerFoundation extends BaseFoundation<TimePickerAdapter> {
 
-    constructor(adapter: TimePickerAdapter<P, S>) {
+    constructor(adapter: TimePickerAdapter) {
         super({ ...adapter });
     }
 
@@ -113,7 +177,7 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
             value = value ? [value] : [];
         }
 
-        const parsedValues: Date[] = [];
+        const parsedValues: TZDate[] = [];
         let invalid = false;
         (value as any[]).forEach(v => {
             const pv = parseToDate(v, formatToken, dateFnsLocale);
@@ -123,8 +187,8 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
         });
 
         const isAM = [true, false];
-        parsedValues.map((item, idx)=>{
-            isAM[idx]= getHours(item) < 12;
+        parsedValues.map((item, idx) => {
+            isAM[idx] = getHours(item) < 12;
         });
 
         if (parsedValues.length === value.length) {
@@ -166,10 +230,10 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
         // console.log(result, index);
         const formatToken = this.getValidFormat();
         const dateFnsLocale = this.getProp('dateFnsLocale');
-        const oldValue: Date[] = this.getState('value');
+        const { value: oldValue } = this._adapter.getStates();
         let isAM = this.getState('isAM');
 
-        const value: Date[] = transformToArray(oldValue);
+        const value: TZDate[] = transformToArray(oldValue);
         isAM = transformToArray(isAM);
 
         if (result) {
@@ -287,7 +351,7 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
 
         const dates = this.parseInput(input);
         const invalid = this.validateDates(dates);
-        const states: { invalid: boolean; value?: Date[] } = { invalid };
+        const states: { invalid: boolean; value?: TZDate[] } = { invalid };
         const oldValue = this.getState('value');
         let value = transformToArray(oldValue);
 
@@ -306,7 +370,7 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
     }
 
     /* istanbul ignore next */
-    doValidate(args: string | Array<Date>) {
+    doValidate(args: string | Array<TZDate>) {
         if (typeof args === 'string') {
             return this.validateStr(args);
         } else if (Array.isArray(args)) {
@@ -321,7 +385,7 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
         return this.validateDates(dates);
     }
 
-    validateDates(dates: Array<Date> = []) {
+    validateDates(dates: Array<TZDate> = []) {
         let invalid = dates.some(d => isNaN(Number(d)));
 
         if (!invalid) {
@@ -358,7 +422,7 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
         }
     }
 
-    formatValue(dates: Date[]): string {
+    formatValue(dates: TZDate[]): string {
         const validFormat = this.getValidFormat();
         const rangeSeparator = this.getProp('rangeSeparator');
         const dateFnsLocale = this.getProp('dateFnsLocale');
@@ -395,9 +459,9 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
         return [];
     }
 
-    parseValue(value: string | Date | Array<string | Date> = []) {
+    parseValue(value: string | Date | Array<string | Date> = []): TZDate[] {
         const formatToken = this.getValidFormat();
-        const dateFnsLocale = this.getProp('dateFnsLocale');
+        const { dateFnsLocale, timeZone } = this._adapter.getProps();
 
         let _value = value;
         if (!Array.isArray(_value)) {
@@ -411,9 +475,9 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
         return [];
     }
 
-    _notifyChange(value: Date[], inputValue: string) {
+    _notifyChange(value: TZDate[], inputValue: string) {
         let str: string | string[] = inputValue;
-        let _value: Date | Date[] = value;
+        let _value: TZDate | TZDate[] = value;
         const timeZone = this.getProp('timeZone');
         if (this._adapter.isRangePicker()) {
             const rangeSeparator = this.getProp('rangeSeparator');
@@ -440,7 +504,7 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
         }
     }
 
-    _hasChanged(dates: Date[] = [], oldDates: Date[] = []) {
+    _hasChanged(dates: TZDate[] = [], oldDates: TZDate[] = []) {
         const formatToken = this.getValidFormat();
         const dateFnsLocale = this.getProp('dateFnsLocale');
 

+ 92 - 79
packages/semi-foundation/utils/date-fns-extra.ts

@@ -1,11 +1,6 @@
-import {
-    toDate,
-    format as dateFnsFormat,
-    utcToZonedTime as dateFnsUtcToZonedTime,
-    zonedTimeToUtc as dateFnsZonedTimeToUtc,
-    OptionsWithTZ
-} from 'date-fns-tz';
-import { parse as dateFnsParse } from 'date-fns';
+import { parse as dateFnsParse, Locale, format as dateFnsFormat, isValid, parseISO } from 'date-fns';
+import { tz, TZDate } from '@date-fns/tz';
+import { isNumber } from 'lodash';
 
 /**
  * Need to be IANA logo without daylight saving time
@@ -133,80 +128,98 @@ export function isValidTimezoneIANAString(timeZoneString: string) {
     }
 }
 
-/**
- *
- * @param {string | number | Date} date
- * @param {string} formatToken
- * @param {object} [options]
- * @param {string} [options.timeZone]
- * @returns {Date}
- */
-/* istanbul ignore next */
-const parse = (date: string | number | Date, formatToken: string, options?: any) => {
-    if (typeof date === 'string') {
-        date = dateFnsParse(date, formatToken, new Date(), options);
+export class TZDateUtil {
+    /**
+     * 在指定时区,以 Date.now() 创建一个 TZDate
+     */
+    static createTZDate(timeZone: string | number) {
+        const normalizedTimeZone = TZDateUtil.normalizeTimeZone(timeZone);
+        return new TZDate(Date.now(), normalizedTimeZone);
     }
-    if (options && options.timeZone != null && options.timeZone !== '') {
-        const timeZone = toIANA(options.timeZone);
-        options = { ...options, timeZone };
+    /**
+     * 将日期字符串转为 Date 对象
+     */
+    private static compatibleParse(options: {
+        date: string;
+        timeZone: string;
+        formatToken?: string;
+        baseDate?: Date;
+        locale?: Locale
+    }): Date | null {
+        let { date, formatToken, baseDate, locale, timeZone } = options;
+        let result = null;
+        if (date) {
+            if (formatToken) {
+                baseDate = baseDate || new Date();
+                result = dateFnsParse(date, formatToken, baseDate, { locale, in: tz(timeZone) });
+            }
+            if (!isValid(result)) {
+                result = parseISO(date);
+            }
+            if (!isValid(result)) {
+                result = new Date(Date.parse(date));
+            }
+            const yearInvalid = isValid(result) && String(result.getFullYear()).length > 4;
+            if (!isValid(result) || yearInvalid) {
+                result = null;
+            }
+        }
+        return result;
     }
-
-    return toDate(date, options);
-};
-
-/* istanbul ignore next */
-const format = (date: number | Date, formatToken: string, options?: any) => {
-    if (options && options.timeZone != null && options.timeZone !== '') {
-        const timeZone = toIANA(options.timeZone);
-        options = { ...options, timeZone };
-
-        date = dateFnsUtcToZonedTime(date, timeZone, options);
+    /**
+     * 将日期转为 TZDate
+     * 
+     * - timeZone:不设置默认为本地时区
+     */
+    static parse(options: { date: string | number | Date | TZDate; formatToken?: string; timeZone?: string | number; locale?: Locale }): TZDate | null {
+        const { date, timeZone } = options;
+        const normalizedTimeZone = TZDateUtil.normalizeTimeZone(timeZone);
+
+        let utcDate: Date | null = null;
+        if (TZDateUtil.isValidDate(date)) {
+            utcDate = date as Date;
+        } else if (date instanceof TZDate) {
+            utcDate = new Date(date);
+        } else if (TZDateUtil.isTimestamp(date)) {
+            utcDate = new Date(date as number);
+        } else if (typeof date === 'string') {
+            utcDate = TZDateUtil.compatibleParse({ ...options, timeZone: normalizedTimeZone, date });
+        }
+        if (!utcDate) {
+            return null;
+        }
+        return new TZDate(utcDate, normalizedTimeZone);
+    }
+    /**
+     * 将 TZDate 按照 format 格式化
+     */
+    static format(options: { date: TZDate; formatToken: string; locale?: Locale }) {
+        const { date, formatToken, locale } = options;
+        return dateFnsFormat(date, formatToken, { locale });
+    }
+    /**
+     * 将 TZDate 转为 UTC Date
+     */
+    static expose(date: TZDate): Date {
+        return new Date(date);
     }
 
-    return dateFnsFormat(date, formatToken, options);
-};
-
-/**
- * Returns a Date which will format as the local time of any time zone from a specific UTC time
- * 
- * @example
- * ```javascript
- * import { utcToZonedTime } from 'date-fns-tz'
- * const { isoDate, timeZone } = fetchInitialValues() // 2014-06-25T10:00:00.000Z, America/New_York
- * const date = utcToZonedTime(isoDate, timeZone) // In June 10am UTC is 6am in New York (-04:00)
- * renderDatePicker(date) // 2014-06-25 06:00:00 (in the system time zone)
- * renderTimeZoneSelect(timeZone) // America/New_York
- * ```
- * 
- * @see https://github.com/marnusw/date-fns-tz#utctozonedtime
- */
-const utcToZonedTime = (date: string | number | Date, timeZone: string | number, options?: OptionsWithTZ) => dateFnsUtcToZonedTime(date, toIANA(timeZone), options);
-
-/**
- * Given a date and any time zone, returns a Date with the equivalent UTC time
- * 
- * @example
- * ```
- * import { zonedTimeToUtc } from 'date-fns-tz'
- * const date = getDatePickerValue() // e.g. 2014-06-25 10:00:00 (picked in any time zone)
- * const timeZone = getTimeZoneValue() // e.g. America/Los_Angeles
- * const utcDate = zonedTimeToUtc(date, timeZone) // In June 10am in Los Angeles is 5pm UTC
- * postToServer(utcDate.toISOString(), timeZone) // post 2014-06-25T17:00:00.000Z, America/Los_Angeles
- * ```
- * 
- * @see https://github.com/marnusw/date-fns-tz#zonedtimetoutc
- */
-const zonedTimeToUtc = (date: string | number | Date, timeZone: string | number, options?: OptionsWithTZ) => dateFnsZonedTimeToUtc(date, toIANA(timeZone), options);
+    /**
+     * 将 ConfigProvider 的 timeZone 转为 IANA 时区
+     */
+    static normalizeTimeZone(timeZone?: string | number): string {
+        if (typeof timeZone !== undefined) {
+            return toIANA(timeZone);
+        } else {
+            return Intl.DateTimeFormat().resolvedOptions().timeZone;
+        }
+    }
 
-/**
- * return current system hour offset based on utc:
- *
- * ```
- * 8 => "GMT+08:00"
- * -9.5 => "GMT-09:30"
- * -8 => "GMT-08:00"
- * ```
- */
-const getCurrentTimeZone = () => new Date().getTimezoneOffset() / 60;
+    static isValidDate(date: any) {
+        return date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date as any);
+    }
 
-export { format, parse, utcToZonedTime, zonedTimeToUtc, getCurrentTimeZone };
+    static isTimestamp(ts: any) {
+        return isNumber(ts) && TZDateUtil.isValidDate(new Date(ts));
+    }
+}

+ 14 - 0
packages/semi-ui/configProvider/_story/FixTimeZoneDST/index.tsx

@@ -0,0 +1,14 @@
+import React from 'react';
+import { ConfigProvider, DatePicker, TimePicker } from '@douyinfe/semi-ui';
+
+export default function FixTimeZoneDST() {
+    const defaultValue = 1745532000000;
+    return (
+        <>
+            <ConfigProvider timeZone={0}>
+                <DatePicker defaultValue={defaultValue} type='dateTime' />
+                {/* <TimePicker defaultValue={defaultValue} /> */}
+            </ConfigProvider>
+        </>
+    );
+}

+ 2 - 1
packages/semi-ui/configProvider/_story/configProvider.stories.jsx

@@ -7,6 +7,7 @@ import RTLForm from './RTLDirection/RTLForm';
 import ConfigContext from '../context';
 import { Button, ConfigProvider, Select, Tooltip, } from '../../index';
 import semiGlobal from "../../_utils/semi-global";
+import FixTimeZoneDST from './FixTimeZoneDST';
 
 export default {
     title: 'ConfigProvider',
@@ -15,7 +16,7 @@ export default {
     },
 };
 
-export { ChangeTimeZone, GetContainer };
+export { ChangeTimeZone, GetContainer, FixTimeZoneDST };
 
 export const RTLTableDemo = () => (
     <RTLWrapper>

+ 3 - 3
packages/semi-ui/datePicker/_story/DisabledDate/index.jsx

@@ -2,7 +2,7 @@ import React, { useState, useMemo } from 'react';
 import { DatePicker, ConfigProvider, Select } from '@douyinfe/semi-ui';
 import * as _ from 'lodash';
 import * as dateFns from 'date-fns';
-import { utcToZonedTime } from 'date-fns-tz';
+// import { utcToZonedTime } from 'date-fns-tz';
 
 const { Option } = Select;
 
@@ -165,7 +165,7 @@ function Demo() {
                             console.log('selected', date);
                         }}
                     />
-                    <DatePicker
+                    {/* <DatePicker
                         type="dateTime"
                         disabledTime={str => {
                             const date = new Date(str);
@@ -178,7 +178,7 @@ function Demo() {
                         onChange={date => {
                             console.log('selected', date);
                         }}
-                    />
+                    /> */}
                 </div>
             </ConfigProvider>
         </div>

+ 5 - 2
packages/semi-ui/datePicker/dateInput.tsx

@@ -18,6 +18,7 @@ import { noop } from '@douyinfe/semi-foundation/utils/function';
 import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
 import { IconCalendar, IconCalendarClock, IconClear } from '@douyinfe/semi-icons';
 import { BaseValueType, ValueType } from '@douyinfe/semi-foundation/datePicker/foundation';
+import { TZDate } from '@date-fns/tz';
 
 import BaseComponent, { BaseProps } from '../_base/baseComponent';
 import Input from '../input/index';
@@ -33,11 +34,12 @@ export interface DateInputProps extends DateInputFoundationProps, BaseProps {
     onFocus?: (e: React.MouseEvent<HTMLInputElement>, rangeType?: RangeType) => void;
     onClear?: (e: React.MouseEvent<HTMLDivElement>) => void;
     onInsetInputChange?: (options: InsetInputChangeProps) => void;
-    value?: Date[];
+    value?: TZDate[];
     inputRef?: React.RefObject<HTMLInputElement>;
     rangeInputStartRef?: React.RefObject<HTMLInputElement>;
     rangeInputEndRef?: React.RefObject<HTMLInputElement>;
-    showClearIgnoreDisabled?: boolean
+    showClearIgnoreDisabled?: boolean;
+    timeZone?: string | number
 }
 
 // eslint-disable-next-line @typescript-eslint/ban-types
@@ -75,6 +77,7 @@ export default class DateInput extends BaseComponent<DateInputProps, {}> {
             PropTypes.object,
             PropTypes.array,
         ]),
+        timeZone: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     };
 
     static defaultProps = {

+ 9 - 5
packages/semi-ui/datePicker/datePicker.tsx

@@ -25,6 +25,8 @@ import Footer from './footer';
 import Trigger from '../trigger';
 import YearAndMonth, { YearAndMonthProps } from './yearAndMonth';
 import '@douyinfe/semi-foundation/datePicker/datePicker.scss';
+import { TZDate } from '@date-fns/tz';
+
 import { Locale } from '../locale/interface';
 import { TimePickerProps } from '../timePicker/TimePicker';
 import { ScrollItemProps } from '../scrollList/scrollItem';
@@ -566,7 +568,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
     }
 
     renderDateInput() {
-        const { insetInput, dateFnsLocale, density, type, format, rangeSeparator, defaultPickerValue } = this.props;
+        const { insetInput, dateFnsLocale, density, type, format, rangeSeparator, defaultPickerValue, timeZone } = this.props;
         const { insetInputValue, value } = this.state;
 
         const props = {
@@ -575,14 +577,15 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             insetInputValue,
             rangeSeparator,
             type,
-            value: value as Date[],
+            value: value as TZDate[],
             handleInsetDateFocus: this.handleInsetDateFocus,
             handleInsetTimeFocus: this.handleInsetTimeFocus,
             onInsetInputChange: this.handleInsetInputChange,
             rangeInputStartRef: this.rangeInputStartRef,
             rangeInputEndRef: this.rangeInputEndRef,
             density,
-            defaultPickerValue
+            defaultPickerValue,
+            timeZone
         };
 
         return insetInput ? <DateInput {...props} insetInput={insetInput} /> : null;
@@ -672,7 +675,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             clearIcon,
             disabled: inputDisabled,
             inputValue,
-            value: value as Date[],
+            value: value as TZDate[],
             defaultPickerValue,
             onChange: this.handleInputChange,
             onEnterPress: this.handleInputComplete,
@@ -797,7 +800,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
     };
 
     renderYearMonthPanel = (locale: Locale['DatePicker'], localeCode: string) => {
-        const { density, presetPosition, yearAndMonthOpts, type, startYear, endYear } = this.props;
+        const { density, presetPosition, yearAndMonthOpts, type, startYear, endYear, timeZone } = this.props;
 
         const date = this.state.value[0];
         const year = { left: 0, right: 0 };
@@ -834,6 +837,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
                 yearAndMonthOpts={yearAndMonthOpts}
                 startYear={startYear}
                 endYear={endYear}
+                timeZone={timeZone}
             />
         );
     };

+ 60 - 24
packages/semi-ui/datePicker/monthsGrid.tsx

@@ -4,7 +4,15 @@ import classnames from 'classnames';
 import PropTypes from 'prop-types';
 import { format as formatFn, addMonths, isSameDay } from 'date-fns';
 
-import MonthsGridFoundation, { MonthInfo, MonthsGridAdapter, MonthsGridDateAdapter, MonthsGridFoundationProps, MonthsGridFoundationState, MonthsGridRangeAdapter, PanelType } from '@douyinfe/semi-foundation/datePicker/monthsGridFoundation';
+import MonthsGridFoundation, {
+    MonthInfo,
+    MonthsGridAdapter,
+    MonthsGridDateAdapter,
+    MonthsGridFoundationProps,
+    MonthsGridFoundationState,
+    MonthsGridRangeAdapter,
+    PanelType,
+} from '@douyinfe/semi-foundation/datePicker/monthsGridFoundation';
 import { strings, numbers, cssClasses } from '@douyinfe/semi-foundation/datePicker/constants';
 import { compatibleParse } from '@douyinfe/semi-foundation/datePicker/_utils/parser';
 import { noop, stubFalse } from 'lodash';
@@ -17,6 +25,7 @@ import { IconClock, IconCalendar } from '@douyinfe/semi-icons';
 import { getDefaultFormatTokenByType } from '@douyinfe/semi-foundation/datePicker/_utils/getDefaultFormatToken';
 import getDefaultPickerDate from '@douyinfe/semi-foundation/datePicker/_utils/getDefaultPickerDate';
 import { ScrollItemProps } from '../scrollList/scrollItem';
+import { TZDate } from '@date-fns/tz';
 
 const prefixCls = cssClasses.PREFIX;
 
@@ -75,7 +84,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
         triggerRender: PropTypes.func,
         presetPosition: PropTypes.oneOf(strings.PRESET_POSITION_SET),
         renderQuickControls: PropTypes.node,
-        renderDateInput: PropTypes.node
+        renderDateInput: PropTypes.node,
     };
 
     static defaultProps = {
@@ -92,7 +101,11 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
     constructor(props: MonthsGridProps) {
         super(props);
         const validFormat = props.format || getDefaultFormatTokenByType(props.type);
-        const { nowDate, nextDate } = getDefaultPickerDate({ defaultPickerValue: props.defaultPickerValue, format: validFormat, dateFnsLocale: props.dateFnsLocale });
+        const { nowDate, nextDate } = getDefaultPickerDate({
+            defaultPickerValue: props.defaultPickerValue,
+            format: validFormat,
+            dateFnsLocale: props.dateFnsLocale,
+        });
 
         const dateState = {
             // Direct use of full date string storage, mainly considering the month rendering comparison to save a conversion
@@ -157,7 +170,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
             notifyMaxLimit: v => this.props.onMaxSelect(v),
             notifyPanelChange: (date, dateString) => this.props.onPanelChange(date, dateString),
             setRangeInputFocus: rangeInputFocus => this.props.setRangeInputFocus(rangeInputFocus),
-            isAnotherPanelHasOpened: currentRangeInput => this.props.isAnotherPanelHasOpened(currentRangeInput)
+            isAnotherPanelHasOpened: currentRangeInput => this.props.isAnotherPanelHasOpened(currentRangeInput),
         };
     }
 
@@ -176,7 +189,6 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
             this.foundation.initDefaultPickerValue();
         }
 
-
         const isRange = this.foundation.isRangeType();
         if (isRange) {
             /**
@@ -242,7 +254,6 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
         let leftHeight = (leftRect && leftRect.height) || 0;
         let rightHeight = (rightRect && rightRect.height) || 0;
 
-
         if (switchLeft) {
             leftHeight += switchLeft.getBoundingClientRect().height;
         }
@@ -253,7 +264,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
         return Math.max(leftHeight, rightHeight);
     };
 
-    renderPanel(month: Date, panelType: PanelType) {
+    renderPanel(month: TZDate, panelType: PanelType) {
         let monthCls = classnames(`${prefixCls}-month-grid-${panelType}`);
         const { monthLeft, monthRight, currentPanelHeight } = this.state;
         const { insetInput } = this.props;
@@ -285,7 +296,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
                  * 如果缓存的currentPanelHeight为0,则需要计算滚动列表的高度
                  * 如果有缓存的值则使用currentPanelHeight(若此高度<实际值,则会影响ScrollList中渲染列表的循环次数)
                  * 详见 packages/semi-foundation/scrollList/itemFoundation.js initWheelList函数
-                 * 
+                 *
                  * When left and right are tpk at the same time, the panel will have a minHeight
                  * If the cached currentPanelHeight is 0, you need to calculate the height of the scrolling list
                  * If there is a cached value, use currentPanelHeight (if this height is less than the actual value, it will affect the number of cycles in the ScrollList to render the list)
@@ -310,7 +321,11 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
                 {yearAndMonthLayer}
                 {timePickerLayer}
                 {/* {isYearPickerOpen || isTimePickerOpen ? null : panelContent} */}
-                {this.foundation.isRangeType() ? panelContent : isYearPickerOpen || isTimePickerOpen ? null : panelContent}
+                {this.foundation.isRangeType()
+                    ? panelContent
+                    : isYearPickerOpen || isTimePickerOpen
+                        ? null
+                        : panelContent}
                 {this.renderSwitch(panelType)}
             </div>
         );
@@ -328,9 +343,23 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
         this.foundation.showYearPicker(panelType);
     }
 
-    renderMonth(month: Date, panelType: PanelType) {
+    renderMonth(month: TZDate, panelType: PanelType) {
         const { selected, rangeStart, rangeEnd, hoverDay, maxWeekNum, offsetRangeStart, offsetRangeEnd } = this.state;
-        const { weekStartsOn, disabledDate, locale, localeCode, renderDate, renderFullDate, startDateOffset, endDateOffset, density, rangeInputFocus, syncSwitchMonth, multiple } = this.props;
+        const {
+            weekStartsOn,
+            disabledDate,
+            locale,
+            localeCode,
+            renderDate,
+            renderFullDate,
+            startDateOffset,
+            endDateOffset,
+            density,
+            rangeInputFocus,
+            syncSwitchMonth,
+            multiple,
+            timeZone,
+        } = this.props;
         let monthText = '';
         // i18n monthText
         if (month) {
@@ -397,6 +426,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
                     endDateOffset={endDateOffset}
                     focusRecordsRef={this.props.focusRecordsRef}
                     multiple={multiple}
+                    timeZone={timeZone}
                 />
             </div>
         );
@@ -437,7 +467,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
 
     getYAMOpenType = () => {
         return this.foundation.getYAMOpenType();
-    }
+    };
 
     renderTimePicker(panelType: PanelType, panelDetail: MonthInfo) {
         const { type, locale, format, hideDisabledOptions, timePickerOpts, dateFnsLocale } = this.props;
@@ -460,8 +490,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
 
         const { rangeStart, rangeEnd } = this.state;
         const dateFormat = this.foundation.getValidDateFormat();
-        let startDate,
-            endDate;
+        let startDate, endDate;
         if (
             type === 'dateTimeRange' &&
             rangeStart &&
@@ -486,7 +515,9 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
                     panelHeader={placeholder}
                     format={format || strings.FORMAT_TIME_PICKER}
                     timeStampValue={pickerDate}
-                    onChange={(newTime: { timeStampValue: number }) => this.foundation.handleTimeChange(newTime, panelType)}
+                    onChange={(newTime: { timeStampValue: number }) =>
+                        this.foundation.handleTimeChange(newTime, panelType)
+                    }
                     {...restProps}
                 />
             </div>
@@ -508,7 +539,10 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
                 currentYear={{ left: y, right: 0 }}
                 currentMonth={{ left: m, right: 0 }}
                 onSelect={item =>
-                    this.foundation.toYearMonth(panelType, new Date(item.currentYear.left, item.currentMonth.left - 1))
+                    this.foundation.toYearMonth(
+                        panelType,
+                        new TZDate(item.currentYear.left, item.currentMonth.left - 1)
+                    )
                 }
                 onBackToMain={() => {
                     this.foundation.showDatePanel(panelType);
@@ -534,8 +568,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
         }
 
         // switch year/month & time
-        let panelDetail,
-            dateText;
+        let panelDetail, dateText;
 
         // i18n
         const { FORMAT_SWITCH_DATE } = locale.localeFormatToken;
@@ -546,10 +579,14 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
 
         if (panelType === strings.PANEL_TYPE_LEFT) {
             panelDetail = monthLeft;
-            dateText = rangeStart ? formatFn(compatibleParse(rangeStart, dateFormat, undefined, dateFnsLocale), FORMAT_SWITCH_DATE) : '';
+            dateText = rangeStart
+                ? formatFn(compatibleParse(rangeStart, dateFormat, undefined, dateFnsLocale), FORMAT_SWITCH_DATE)
+                : '';
         } else {
             panelDetail = monthRight;
-            dateText = rangeEnd ? formatFn(compatibleParse(rangeEnd, dateFormat, undefined, dateFnsLocale), FORMAT_SWITCH_DATE) : '';
+            dateText = rangeEnd
+                ? formatFn(compatibleParse(rangeEnd, dateFormat, undefined, dateFnsLocale), FORMAT_SWITCH_DATE)
+                : '';
         }
 
         const { isTimePickerOpen, showDate } = panelDetail;
@@ -595,7 +632,6 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
         );
     }
 
-
     render() {
         const { monthLeft, monthRight } = this.state;
         const { type, insetInput, presetPosition, renderQuickControls, renderDateInput } = this.props;
@@ -619,7 +655,7 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
 
         return (
             <div style={{ display: 'flex' }}>
-                {presetPosition === "left" && renderQuickControls}
+                {presetPosition === 'left' && renderQuickControls}
                 <div>
                     {renderDateInput}
                     <div
@@ -627,14 +663,14 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
                         x-type={type}
                         x-panel-yearandmonth-open-type={yearOpenType}
                         // FIXME:
-                        x-insetinput={insetInput ? "true" : "false"}
+                        x-insetinput={insetInput ? 'true' : 'false'}
                         x-preset-position={renderQuickControls === null ? 'null' : presetPosition}
                         ref={current => this.cacheRefCurrent('monthGrid', current)}
                     >
                         {content}
                     </div>
                 </div>
-                {presetPosition === "right" && renderQuickControls}
+                {presetPosition === 'right' && renderQuickControls}
             </div>
         );
     }

+ 11 - 9
packages/semi-ui/datePicker/yearAndMonth.tsx

@@ -15,6 +15,7 @@ import { setYear, setMonth, set } from 'date-fns';
 import { Locale } from '../locale/interface';
 import { strings } from '@douyinfe/semi-foundation/datePicker/constants';
 import { PanelType } from '@douyinfe/semi-foundation/datePicker/monthsGridFoundation';
+import { TZDateUtil } from '@douyinfe/semi-foundation/utils/date-fns-extra';
 
 
 const prefixCls = `${BASE_CLASS_PREFIX}-datepicker`;
@@ -43,6 +44,7 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
         type: PropTypes.oneOf(strings.TYPE_SET),
         startYear: PropTypes.number,
         endYear: PropTypes.number,
+        timeZone: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     };
 
     static defaultProps = {
@@ -59,11 +61,9 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
 
     constructor(props: YearAndMonthProps) {
         super(props);
-        const now = new Date();
+        let { currentYear, currentMonth, timeZone } = props;
 
-        let { currentYear, currentMonth } = props;
-
-        const { year, month } = getYearAndMonth(currentYear, currentMonth);
+        const { year, month } = getYearAndMonth(currentYear, currentMonth, timeZone);
 
         this.state = {
             years: getYears(props.startYear, props.endYear).map(year => ({
@@ -114,7 +114,7 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
 
     static getDerivedStateFromProps(props: YearAndMonthProps, state: YearAndMonthState) {
         const willUpdateStates: Partial<YearAndMonthState> = {};
-        const { year, month } = getYearAndMonth(props.currentYear, props.currentMonth);
+        const { year, month } = getYearAndMonth(props.currentYear, props.currentMonth, props.timeZone);
 
         if (!isEqual(props.currentYear, state.currentYear)) {
             willUpdateStates.currentYear = year;
@@ -129,8 +129,9 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
 
     renderColYear(panelType: PanelType) {
         const { years, currentYear, currentMonth, months } = this.state;
-        const { disabledDate, localeCode, yearCycled, yearAndMonthOpts } = this.props;
-        const currentDate = setMonth(Date.now(), currentMonth[panelType] - 1);
+        const { disabledDate, localeCode, yearCycled, yearAndMonthOpts, timeZone } = this.props;
+        const now = TZDateUtil.createTZDate(timeZone);
+        const currentDate = setMonth(now, currentMonth[panelType] - 1);
         const left = strings.PANEL_TYPE_LEFT;
         const right = strings.PANEL_TYPE_RIGHT;
 
@@ -194,9 +195,10 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
 
     renderColMonth(panelType: PanelType) {
         const { months, currentMonth, currentYear } = this.state;
-        const { locale, localeCode, monthCycled, disabledDate, yearAndMonthOpts } = this.props;
+        const { locale, localeCode, monthCycled, disabledDate, yearAndMonthOpts, timeZone } = this.props;
         let transform = (val: string) => val;
-        const currentDate = setYear(Date.now(), currentYear[panelType]);
+        const now = TZDateUtil.createTZDate(timeZone);
+        const currentDate = setYear(now, currentYear[panelType]);
         const left = strings.PANEL_TYPE_LEFT;
         const right = strings.PANEL_TYPE_RIGHT;
 

+ 4 - 3
packages/semi-ui/package.json

@@ -29,8 +29,9 @@
         "async-validator": "^3.5.0",
         "classnames": "^2.2.6",
         "copy-text-to-clipboard": "^2.1.1",
-        "date-fns": "^2.29.3",
-        "date-fns-tz": "^1.3.8",
+        "date-fns": "^4.1.0",
+        "@date-fns/tz": "^1.2.0",
+        "@date-fns/utc": "^2.1.0",
         "fast-copy": "^3.0.1 ",
         "jsonc-parser": "^3.3.1",
         "lodash": "^4.17.21",
@@ -119,4 +120,4 @@
         "webpack": "^5.77.0",
         "webpackbar": "^5.0.0-3"
     }
-}
+}

+ 24 - 68
packages/semi-ui/timePicker/TimePicker.tsx

@@ -8,7 +8,11 @@ import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
 import { strings, cssClasses } from '@douyinfe/semi-foundation/timePicker/constants';
 import Popover, { PopoverProps } from '../popover';
 import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
-import TimePickerFoundation, { TimePickerAdapter } from '@douyinfe/semi-foundation/timePicker/foundation';
+import TimePickerFoundation, {
+    TimePickerAdapter,
+    TimePickerFoundationProps,
+    TimePickerFoundationState,
+} from '@douyinfe/semi-foundation/timePicker/foundation';
 import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
 import Combobox from './Combobox';
 import TimeInput from './TimeInput';
@@ -29,86 +33,32 @@ export interface Panel {
     panelFooter?: React.ReactNode | React.ReactNode[]
 }
 
-export type BaseValueType = string | number | Date | undefined;
-
-export type Type = 'time' | 'timeRange';
-
 export type TimePickerProps = {
     'aria-describedby'?: React.AriaAttributes['aria-describedby'];
     'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
     'aria-invalid'?: React.AriaAttributes['aria-invalid'];
     'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
     'aria-required'?: React.AriaAttributes['aria-required'];
-    autoAdjustOverflow?: boolean;
-    autoFocus?: boolean; // TODO: autoFocus did not take effect
-    borderless?: boolean;
-    className?: string;
-    clearText?: string;
     clearIcon?: React.ReactNode;
-    dateFnsLocale?: Locale['dateFnsLocale'];
-    defaultOpen?: boolean;
-    defaultValue?: BaseValueType | BaseValueType[];
-    disabled?: boolean;
-    disabledHours?: () => number[];
-    disabledMinutes?: (selectedHour: number) => number[];
-    disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[];
     dropdownMargin?: PopoverProps['margin'];
-    focusOnOpen?: boolean;
-    format?: string;
-    getPopupContainer?: () => HTMLElement;
-    hideDisabledOptions?: boolean;
-    hourStep?: number;
-    id?: string;
-    inputReadOnly?: boolean;
     inputStyle?: React.CSSProperties;
     insetLabel?: React.ReactNode;
-    insetLabelId?: string;
     locale?: Locale['TimePicker'];
-    localeCode?: string;
-    minuteStep?: number;
-    motion?: boolean;
-    open?: boolean;
-    panelFooter?: React.ReactNode | React.ReactNode[];
-    panelHeader?: React.ReactNode | React.ReactNode[];
-    panels?: Panel[]; // FIXME:
-    placeholder?: string;
-    popupClassName?: string;
     popupStyle?: React.CSSProperties;
     position?: Position;
-    prefixCls?: string;
-    preventScroll?: boolean;
-    rangeSeparator?: string;
     scrollItemProps?: ScrollItemProps<any>;
-    secondStep?: number;
-    showClear?: boolean;
     size?: InputSize;
-    stopPropagation?: boolean;
     style?: React.CSSProperties;
-    timeZone?: string | number;
     triggerRender?: (props?: any) => React.ReactNode;
-    type?: Type;
-    use12Hours?: boolean;
     validateStatus?: ValidateStatus;
-    value?: BaseValueType | BaseValueType[];
-    zIndex?: number | string;
     onBlur?: React.FocusEventHandler<HTMLInputElement>;
     onChange?: TimePickerAdapter['notifyChange'];
     onChangeWithDateFirst?: boolean;
     onFocus?: React.FocusEventHandler<HTMLInputElement>;
     onOpenChange?: (open: boolean) => void
-};
-
-export interface TimePickerState {
-    open: boolean;
-    value: Date[];
-    inputValue: string;
-    currentSelectPanel: string | number;
-    isAM: [boolean, boolean];
-    showHour: boolean;
-    showMinute: boolean;
-    showSecond: boolean;
-    invalid: boolean
-}
+} & TimePickerFoundationProps;
+
+export interface TimePickerState extends TimePickerFoundationState { }
 
 export default class TimePicker extends BaseComponent<TimePickerProps, TimePickerState> {
     static contextType = ConfigContext;
@@ -217,7 +167,6 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
 
     clickOutSideHandler: (e: MouseEvent) => void;
 
-
     constructor(props: TimePickerProps) {
         super(props);
         const { format = strings.DEFAULT_FORMAT } = props;
@@ -231,7 +180,7 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
             showHour: Boolean(format.match(/HH|hh|H|h/g)),
             showMinute: Boolean(format.match(/mm/g)),
             showSecond: Boolean(format.match(/ss/g)),
-            invalid: undefined
+            invalid: undefined,
         };
 
         this.foundation = new TimePickerFoundation(this.adapter);
@@ -254,9 +203,10 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
                     const panel = this.savePanelRef && this.savePanelRef.current;
                     const trigger = this.timePickerRef && this.timePickerRef.current;
                     const target = e.target as Element;
-                    const path = e.composedPath && e.composedPath() || [target];
+                    const path = (e.composedPath && e.composedPath()) || [target];
 
-                    if (!(panel && panel.contains(target)) &&
+                    if (
+                        !(panel && panel.contains(target)) &&
                         !(trigger && trigger.contains(target)) &&
                         !(path.includes(trigger) || path.includes(panel))
                     ) {
@@ -311,10 +261,8 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
         this.setState({ currentSelectPanel });
     };
 
-    handlePanelChange = (
-        value: { isAM: boolean; value: string; timeStampValue: number },
-        index: number
-    ) => this.foundation.handlePanelChange(value, index);
+    handlePanelChange = (value: { isAM: boolean; value: string; timeStampValue: number }, index: number) =>
+        this.foundation.handlePanelChange(value, index);
 
     handleInput = (value: string) => this.foundation.handleInputChange(value);
 
@@ -335,9 +283,17 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
             panelProps.panelHeader = get(
                 panels,
                 index,
-                isNullOrUndefined(panelHeader) ? get(defaultHeaderMap, index, null) : Array.isArray(panelHeader) ? panelHeader[index] : panelHeader
+                isNullOrUndefined(panelHeader)
+                    ? get(defaultHeaderMap, index, null)
+                    : Array.isArray(panelHeader)
+                        ? panelHeader[index]
+                        : panelHeader
             );
-            panelProps.panelFooter = get(panels, index, Array.isArray(panelFooter) ? panelFooter[index] : panelFooter) as React.ReactNode;
+            panelProps.panelFooter = get(
+                panels,
+                index,
+                Array.isArray(panelFooter) ? panelFooter[index] : panelFooter
+            ) as React.ReactNode;
         }
 
         return panelProps;

File diff suppressed because it is too large
+ 675 - 768
yarn.lock


Some files were not shown because too many files changed in this diff