1
0
shijia.me 5 сар өмнө
parent
commit
5a1d3ffd8b
35 өөрчлөгдсөн 1430 нэмэгдсэн , 1496 устгасан
  1. 1 1
      .storybook/base/base.js
  2. 4 3
      package.json
  3. 20 38
      packages/semi-foundation/datePicker/_utils/getDefaultPickerDate.ts
  4. 2 1
      packages/semi-foundation/datePicker/_utils/getMonthTable.ts
  5. 6 3
      packages/semi-foundation/datePicker/_utils/getYearAndMonth.ts
  6. 1 2
      packages/semi-foundation/datePicker/_utils/index.ts
  7. 0 6
      packages/semi-foundation/datePicker/_utils/isTimestamp.ts
  8. 1 1
      packages/semi-foundation/datePicker/_utils/isUnixTimestamp.ts
  9. 0 3
      packages/semi-foundation/datePicker/_utils/isValidDate.ts
  10. 112 225
      packages/semi-foundation/datePicker/foundation.ts
  11. 6 4
      packages/semi-foundation/datePicker/inputFoundation.ts
  12. 11 9
      packages/semi-foundation/datePicker/monthFoundation.ts
  13. 58 92
      packages/semi-foundation/datePicker/monthsGridFoundation.ts
  14. 5 3
      packages/semi-foundation/datePicker/yearAndMonthFoundation.ts
  15. 4 3
      packages/semi-foundation/package.json
  16. 56 9
      packages/semi-foundation/timePicker/ComboxFoundation.ts
  17. 166 93
      packages/semi-foundation/timePicker/foundation.ts
  18. 0 8
      packages/semi-foundation/timePicker/utils/index.ts
  19. 85 81
      packages/semi-foundation/utils/date-fns-extra.ts
  20. 12 0
      packages/semi-foundation/utils/date.ts
  21. 14 0
      packages/semi-ui/configProvider/_story/FixTimeZoneDST/index.tsx
  22. 2 1
      packages/semi-ui/configProvider/_story/configProvider.stories.jsx
  23. 3 3
      packages/semi-ui/datePicker/_story/DisabledDate/index.jsx
  24. 5 2
      packages/semi-ui/datePicker/dateInput.tsx
  25. 9 5
      packages/semi-ui/datePicker/datePicker.tsx
  26. 60 24
      packages/semi-ui/datePicker/monthsGrid.tsx
  27. 11 9
      packages/semi-ui/datePicker/yearAndMonth.tsx
  28. 4 3
      packages/semi-ui/package.json
  29. 12 23
      packages/semi-ui/timePicker/Combobox.tsx
  30. 5 5
      packages/semi-ui/timePicker/TimeInput.tsx
  31. 26 67
      packages/semi-ui/timePicker/TimePicker.tsx
  32. 35 0
      packages/semi-ui/timePicker/_story/TimeZone/index.tsx
  33. 14 0
      packages/semi-ui/timePicker/_story/WithoutTimeZone/index.tsx
  34. 5 1
      packages/semi-ui/timePicker/_story/timepicker.stories.jsx
  35. 675 768
      yarn.lock

+ 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"
-}
+}

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

@@ -1,54 +1,36 @@
 import { addMonths, Locale as dateFnsLocale } from 'date-fns';
-import isValidDate from './isValidDate';
-import { compatibleParse } from './parser';
-import isTimestamp from './isTimestamp';
+
+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);

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

@@ -4,9 +4,8 @@ import isBetween from './isBetween';
 import isWithinInterval from './isWithinInterval';
 import isSameDay from './isSameDay';
 
-import isTimestamp from './isTimestamp';
 import isUnixTimestamp from './isUnixTimestamp';
-import isValidDate from './isValidDate';
+import { isValidDate, isTimestamp } from '../../utils/date';
 import getDefaultFormatToken from './getDefaultFormatToken';
 import getYears from './getYears';
 import getMonthsInYear from './getMonthsInYear';

+ 0 - 6
packages/semi-foundation/datePicker/_utils/isTimestamp.ts

@@ -1,6 +0,0 @@
-import isValidDate from './isValidDate';
-import isNumber from '../../utils/isNumber';
-
-export default function isTimestamp(ts: any) {
-    return isNumber(ts) && isValidDate(new Date(ts));
-}

+ 1 - 1
packages/semi-foundation/datePicker/_utils/isUnixTimestamp.ts

@@ -1,5 +1,5 @@
 import isNumber from '../../utils/isNumber';
-import isValidDate from './isValidDate';
+import { isValidDate } from '../../utils/date';
 
 export default function isUnixTimestamp(ts: any) {
     return isNumber(ts) && ts.toString().length === 10 && isValidDate(new Date(ts * 1000));

+ 0 - 3
packages/semi-foundation/datePicker/_utils/isValidDate.ts

@@ -1,3 +0,0 @@
-export default function isValidDate(date: any) {
-    return date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date as any);
-}

+ 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"
     }
-}
+}

+ 56 - 9
packages/semi-foundation/timePicker/ComboxFoundation.ts

@@ -1,7 +1,11 @@
+import { TZDate } from '@date-fns/tz';
+
 import { strings } from './constants';
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import { isValidDate } from '../datePicker/_utils/index';
 import isNullOrUndefined from '../utils/isNullOrUndefined';
+import { TZDateUtil } from '../utils/date-fns-extra';
+import { TimePickerFoundationProps } from './foundation';
 
 const HOUR = 1000 * 60 * 60;
 const DAY = 24 * HOUR;
@@ -33,9 +37,43 @@ function generateOptions(length: number, disabledOptions: number[], hideDisabled
     return arr;
 }
 
-class ComboboxFoundation extends BaseFoundation<DefaultAdapter> {
+export interface ComboboxFoundationProps extends Pick<
+TimePickerFoundationProps,
+| 'format'
+| 'disabledHours'
+| 'disabledMinutes'
+| 'disabledSeconds'
+| 'hideDisabledOptions'
+| 'use12Hours'
+| 'timeZone'
+| 'hourStep'
+| 'minuteStep'
+| 'secondStep'
+| 'position'
+| 'type'
+> {
+    defaultOpenValue?: TimePickerFoundationProps['value'];
+    showHour?: boolean;
+    showMinute?: boolean;
+    showSecond?: boolean;
+    onChange?: (value: { isAM: boolean; value: string; timeStampValue: number }) => void;
+    onCurrentSelectPanelChange?: (range: string) => void;
+    isAM?: boolean;
+    timeStampValue?: TZDate
+}
+
+export interface ComboboxFoundationState {
+    showHour: boolean;
+    showMinute: boolean;
+    showSecond: boolean;
+    hourOptions: number[];
+    minuteOptions: number[];
+    secondOptions: number[]
+}
+
+class ComboboxFoundation extends BaseFoundation<DefaultAdapter<ComboboxFoundationProps, ComboboxFoundationState>> {
 
-    constructor(adapter: DefaultAdapter) {
+    constructor(adapter: DefaultAdapter<ComboboxFoundationProps, ComboboxFoundationState>) {
         super({ ...adapter });
     }
 
@@ -52,7 +90,7 @@ class ComboboxFoundation extends BaseFoundation<DefaultAdapter> {
             hideDisabledOptions,
             minuteStep,
             secondStep,
-        } = this.getProps();
+        } = this._adapter.getProps();
 
         const format = this.getValidFormat();
 
@@ -130,23 +168,32 @@ class ComboboxFoundation extends BaseFoundation<DefaultAdapter> {
      * from 13-bit timestamp  -> get display date
      * by combobox use
      */
-    getDisplayDateFromTimeStamp(timeStamp: Date | string) {
-        let date;
+    getDisplayDateFromTimeStamp(timeStamp: TZDate): TZDate {
+        const timeZone = this._getTimeZone();
+        let date: TZDate;
         if (timeStamp) {
-            date = new Date(timeStamp);
+            date = new TZDate(timeStamp, timeZone);
         }
         if (!timeStamp || !isValidDate(date)) {
             return this.createDateDefault();
         }
         return date;
     }
+
+    _getTimeZone(_timeZone?: string | number) {
+        const { timeZone } = this._adapter.getProps();
+        const currentTimeZone = _timeZone ?? timeZone;
+        const normalizedTimeZone = TZDateUtil.normalizeTimeZone(currentTimeZone);
+        return normalizedTimeZone;
+    }
+
     /**
      * create a date at 00:00:00
      */
-
     createDateDefault() {
-        const now = new Date();
-        return new Date(now.getFullYear(), now.getMonth(), now.getDate());
+        const timeZone = this._getTimeZone();
+        const now = TZDateUtil.createTZDate(timeZone);
+        return new TZDate(now.getFullYear(), now.getMonth(), now.getDate(), timeZone);
     }
 }
 

+ 166 - 93
packages/semi-foundation/timePicker/foundation.ts

@@ -1,36 +1,85 @@
 import { strings } from './constants';
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import {
-    formatToString,
-    parseToDate,
     hourIsDisabled,
     minuteIsDisabled,
     secondIsDisabled,
     transformToArray,
-    isTimeFormatLike
+    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, getHours, Locale } from 'date-fns';
 import isNullOrUndefined from '../utils/isNullOrUndefined';
+import { TZDate } from '@date-fns/tz';
+import { Position } from '../tooltip/foundation';
+import { TZDateUtil } from '../utils/date-fns-extra';
+import { isValidDate } from '../utils/date';
+
+export type BaseValueType = string | number | Date | undefined;
+export type Type = 'time' | 'timeRange';
+
+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 type Position =
-    | 'top'
-    | 'topLeft'
-    | 'topRight'
-    | 'left'
-    | 'leftTop'
-    | 'leftBottom'
-    | 'right'
-    | 'rightTop'
-    | 'rightBottom'
-    | 'bottom'
-    | 'bottomLeft'
-    | 'bottomRight'
-    | 'leftTopOver'
-    | 'rightTopOver';
-
-export interface TimePickerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+export interface TimePickerAdapter extends DefaultAdapter<TimePickerFoundationProps, TimePickerFoundationState> {
     togglePanel: (show: boolean) => void;
     registerClickOutSide: () => void;
     setInputValue: (inputValue: string, cb?: () => void) => void;
@@ -45,9 +94,8 @@ 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> {
-
-    constructor(adapter: TimePickerAdapter<P, S>) {
+class TimePickerFoundation extends BaseFoundation<TimePickerAdapter> {
+    constructor(adapter: TimePickerAdapter) {
         super({ ...adapter });
     }
 
@@ -73,7 +121,10 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
     isDisabledHMS({ hours, minutes, seconds }: { hours: number; minutes: number; seconds: number }) {
         const { disabledHours, disabledMinutes, disabledSeconds } = this.getProps();
         const hDis = !isNullOrUndefined(hours) && hourIsDisabled(disabledHours, hours);
-        const mDis = !isNullOrUndefined(hours) && !isNullOrUndefined(minutes) && minuteIsDisabled(disabledMinutes, hours, minutes);
+        const mDis =
+            !isNullOrUndefined(hours) &&
+            !isNullOrUndefined(minutes) &&
+            minuteIsDisabled(disabledMinutes, hours, minutes);
         const sDis =
             !isNullOrUndefined(hours) &&
             !isNullOrUndefined(minutes) &&
@@ -101,38 +152,30 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
      * User input value => save timestamp
      */
     initDataFromDefaultValue() {
-        const defaultValue = this.getProp('defaultValue');
-        let value = this.getProp('value');
-        const timeZone = this.getProp('timeZone');
-        const formatToken = this.getValidFormat();
-        const { rangeSeparator, dateFnsLocale } = this.getProps();
-
-        value = value || defaultValue;
+        const { value: propValue, defaultValue, timeZone, rangeSeparator, dateFnsLocale } = this._adapter.getProps();
+        let value: BaseValueType | BaseValueType[] = propValue || defaultValue;
 
         if (!Array.isArray(value)) {
             value = value ? [value] : [];
         }
 
-        const parsedValues: Date[] = [];
         let invalid = false;
-        (value as any[]).forEach(v => {
-            const pv = parseToDate(v, formatToken, dateFnsLocale);
-            if (!isNaN(pv.getTime())) {
-                parsedValues.push(this.isValidTimeZone(timeZone) ? utcToZonedTime(pv, timeZone) : pv);
-            }
-        });
+        const formatToken = this.getValidFormat();
+        const parsedValues: TZDate[] = value
+            .map(v => this._parseSingle({ date: v, formatToken, locale: dateFnsLocale, timeZone }))
+            .filter(Boolean);
 
         const isAM = [true, false];
-        parsedValues.map((item, idx)=>{
-            isAM[idx]= getHours(item) < 12;
+        parsedValues.forEach((item, idx) => {
+            isAM[idx] = getHours(item) < 12;
         });
 
+        let stateValue: TZDate[] = [];
         if (parsedValues.length === value.length) {
-            value = parsedValues;
+            stateValue = parsedValues;
         } else {
-            value = [];
-
-            if (value.length) {
+            stateValue = [];
+            if (stateValue.length) {
                 invalid = true;
             }
         }
@@ -140,15 +183,42 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
         let inputValue = '';
 
         if (!invalid) {
-            inputValue = (value as any[]).map(v => formatToString(v, formatToken, dateFnsLocale)).join(rangeSeparator);
+            inputValue = stateValue
+                .map(v => this._formatSingle({ date: v, formatToken, locale: dateFnsLocale }))
+                .join(rangeSeparator);
         }
 
         this.setState({
             isAM,
-            value,
+            value: stateValue,
             inputValue,
             invalid,
-        } as any);
+        });
+    }
+
+    _parseSingle(options: {
+        date: BaseValueType | TZDate;
+        timeZone: string | number | undefined;
+        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 });
+    }
+
+    _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 });
+    }
+
+    _expose(date: TZDate) {
+        return TZDateUtil.expose(date);
     }
 
     getValidFormat(validFormat?: string) {
@@ -163,18 +233,22 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
     }
 
     handlePanelChange(result: { isAM: boolean; value: string; timeStampValue: number }, index = 0) {
-        // console.log(result, index);
+        const { dateFnsLocale, timeZone } = this._adapter.getProps();
         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) {
             const panelIsAM = Boolean(result.isAM);
-            const date = parseToDate(result.timeStampValue, formatToken, dateFnsLocale);
+            const date = this._parseSingle({
+                date: result.timeStampValue,
+                timeZone,
+                formatToken,
+                locale: dateFnsLocale,
+            });
             value[index] = date;
             isAM[index] = panelIsAM;
             const inputValue = this.formatValue(value);
@@ -199,20 +273,12 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
     }
 
     refreshProps(props: any = {}) {
-        const { value, timeZone, __prevTimeZone } = props;
-        
-        let dates = this.parseValue(value);
+        const { value, timeZone } = props;
 
-        let invalid = dates.some(d => isNaN(Number(d)));
+        let dates = this.parseValue({ value, timeZone });
+
+        let invalid = dates.some(d => !isValidDate(d));
         if (!invalid) {
-            if (this.isValidTimeZone(timeZone)) {
-                dates = dates.map(date =>
-                    utcToZonedTime(
-                        this.isValidTimeZone(__prevTimeZone) ? zonedTimeToUtc(date, __prevTimeZone) : date,
-                        timeZone
-                    )
-                );
-            }
             invalid = dates.some(d =>
                 this.isDisabledHMS({ hours: d.getHours(), minutes: d.getMinutes(), seconds: d.getSeconds() })
             );
@@ -287,7 +353,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 +372,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)) {
@@ -317,11 +383,11 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
 
     validateStr(inputValue = '') {
         const dates = this.parseInput(inputValue);
-    
+
         return this.validateDates(dates);
     }
 
-    validateDates(dates: Array<Date> = []) {
+    validateDates(dates: Array<TZDate> = []) {
         let invalid = dates.some(d => isNaN(Number(d)));
 
         if (!invalid) {
@@ -358,7 +424,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');
@@ -374,7 +440,7 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
                 if (isUndefined(date)) {
                     str = '';
                 } else {
-                    str = formatToString(date, validFormat, dateFnsLocale);
+                    str = this._formatSingle({ date, formatToken: validFormat, locale: dateFnsLocale });
                 }
                 return str;
             });
@@ -384,63 +450,69 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
     }
 
     parseInput(str: string) {
+        const { rangeSeparator, dateFnsLocale, timeZone } = this._adapter.getProps();
         const validFormat = this.getValidFormat();
-        const rangeSeparator = this.getProp('rangeSeparator');
-        const dateFnsLocale = this.getProp('dateFnsLocale');
 
         if (str && typeof str === 'string') {
-            return split(str, rangeSeparator).map(v => parseToDate(v, validFormat, dateFnsLocale));
+            return split(str, rangeSeparator).map(v =>
+                this._parseSingle({ date: v, formatToken: validFormat, locale: dateFnsLocale, timeZone })
+            );
         }
 
         return [];
     }
 
-    parseValue(value: string | Date | Array<string | Date> = []) {
+    parseValue(options: { value: BaseValueType | BaseValueType[]; timeZone: string | number }): TZDate[] {
+        const { value, timeZone } = options;
         const formatToken = this.getValidFormat();
-        const dateFnsLocale = this.getProp('dateFnsLocale');
+        const { dateFnsLocale } = this._adapter.getProps();
 
+        let parsedValue: TZDate[] = [];
         let _value = value;
         if (!Array.isArray(_value)) {
             _value = _value ? [_value] : [];
         }
 
         if (Array.isArray(_value)) {
-            return _value.map(v => parseToDate(v, formatToken, dateFnsLocale));
+            parsedValue = _value.map(v => this._parseSingle({ date: v, formatToken, locale: dateFnsLocale, timeZone }));
         }
 
-        return [];
+        return parsedValue;
     }
 
-    _notifyChange(value: Date[], inputValue: string) {
+    _notifyChange(value: TZDate[], inputValue: string) {
+        const { rangeSeparator } = this._adapter.getProps();
+        const formatToken = this.getValidFormat();
         let str: string | string[] = inputValue;
-        let _value: Date | Date[] = value;
-        const timeZone = this.getProp('timeZone');
+        let _value: TZDate | TZDate[] = value;
         if (this._adapter.isRangePicker()) {
-            const rangeSeparator = this.getProp('rangeSeparator');
             str = split(inputValue, rangeSeparator);
         } else {
             _value = Array.isArray(_value) ? _value[0] : _value;
         }
 
-        if (this.isValidTimeZone(timeZone) && _value) {
-            const formatToken = this.getValidFormat();
+        let exposeDate: Date | Date[];
+        let exposeStr: string | string[] = str;
+
+        if (_value) {
             if (Array.isArray(_value)) {
-                _value = _value.map(v => zonedTimeToUtc(v, timeZone));
-                str = _value.map(v => format(v, formatToken));
+                exposeDate = _value.map(v => this._expose(v));
+                exposeStr = _value.map(v => this._formatSingle({ date: v, formatToken }));
             } else {
-                _value = zonedTimeToUtc(_value, timeZone);
-                str = format(_value, formatToken);
+                exposeDate = this._expose(_value);
+                exposeStr = this._formatSingle({ date: _value, formatToken });
             }
         }
+
         const onChangeWithDateFirst = this.getProp('onChangeWithDateFirst');
         if (onChangeWithDateFirst) {
-            this._adapter.notifyChange(_value, str);
+            this._adapter.notifyChange(exposeDate, exposeStr);
         } else {
-            this._adapter.notifyChange(str, _value);
+            this._adapter.notifyChange(exposeStr, exposeDate);
         }
     }
 
-    _hasChanged(dates: Date[] = [], oldDates: Date[] = []) {
+    _hasChanged(dates: TZDate[] = [], oldDates: TZDate[] = []) {
         const formatToken = this.getValidFormat();
         const dateFnsLocale = this.getProp('dateFnsLocale');
 
@@ -452,7 +524,8 @@ class TimePickerFoundation<P = Record<string, any>, S = Record<string, any>> ext
                 if (
                     isValid(date) &&
                     isValid(oldDate) &&
-                    formatToString(date, formatToken, dateFnsLocale) === formatToString(oldDate, formatToken, dateFnsLocale)
+                    this._formatSingle({ date, formatToken, locale: dateFnsLocale }) ===
+                        this._formatSingle({ date: oldDate, formatToken, locale: dateFnsLocale })
                 ) {
                     return false;
                 }

+ 0 - 8
packages/semi-foundation/timePicker/utils/index.ts

@@ -41,14 +41,6 @@ export const parseToDate = (input: string | Date | number, formatToken = strings
  */
 export const parseToTimestamp = (input: string | Date | number, formatToken = strings.DEFAULT_FORMAT, dateFnsLocale = defaultLocale) => Number(parseToDate(input, formatToken, dateFnsLocale));
 
-/**
- *
- * @param {Date|number} dateOrTimestamp
- * @param {string} formatToken
- * @returns {string}
- */
-export const formatToString = (dateOrTimestamp: Date | number, formatToken = strings.DEFAULT_FORMAT, dateFnsLocale = defaultLocale) => format(dateOrTimestamp, formatToken, { locale: dateFnsLocale });
-
 export const hourIsDisabled = (disabledHours: () => boolean, hour: number) => {
     if (typeof disabledHours === 'function') {
         const disabledOptions = disabledHours();

+ 85 - 81
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 { isTimestamp, isValidDate } from './date';
 
 /**
  * Need to be IANA logo without daylight saving time
@@ -133,80 +128,89 @@ 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 {
+    /**
+     * 在指定时区当前时刻创建一个 TZDate
+     */
+    static createTZDate(timeZone: string | number) {
+        const normalizedTimeZone = TZDateUtil.normalizeTimeZone(timeZone);
+        return TZDate.tz(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);
+            }
+            // 兜底的 parse 方式
+            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 (isValidDate(date)) {
+            utcDate = date as Date;
+        } else if (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);
-
-/**
- * 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;
-
-export { format, parse, utcToZonedTime, zonedTimeToUtc, getCurrentTimeZone };
+    /**
+     * 将 ConfigProvider 的 timeZone 转为 IANA 时区
+     */
+    static normalizeTimeZone(timeZone?: string | number): string {
+        if (typeof timeZone !== 'undefined') {
+            return toIANA(timeZone);
+        } else {
+            return Intl.DateTimeFormat().resolvedOptions().timeZone;
+        }
+    }
+}

+ 12 - 0
packages/semi-foundation/utils/date.ts

@@ -0,0 +1,12 @@
+import { isNumber } from 'lodash';
+
+/**
+ * 是否是 Date 或 TZDate
+ */
+export function isValidDate(date: any) {
+    return date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date as any);
+}
+
+export function isTimestamp(ts: any) {
+    return isNumber(ts) && 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"
     }
-}
+}

+ 12 - 23
packages/semi-ui/timePicker/Combobox.tsx

@@ -2,34 +2,23 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { format as dateFnsFormat } from 'date-fns';
 import { noop } from 'lodash';
+import { TZDate } from '@date-fns/tz';
 
 import BaseComponent, { BaseProps } from '../_base/baseComponent';
 import { strings } from '@douyinfe/semi-foundation/timePicker/constants';
 import ScrollList from '../scrollList/index';
 import ScrollItem from '../scrollList/scrollItem';
-import ComboboxFoundation, { formatOption } from '@douyinfe/semi-foundation/timePicker/ComboxFoundation';
+import ComboboxFoundation, {
+    ComboboxFoundationProps,
+    formatOption,
+} from '@douyinfe/semi-foundation/timePicker/ComboxFoundation';
 import LocaleConsumer from '../locale/localeConsumer';
 import { TimePickerProps } from './TimePicker';
 import { Locale } from '../locale/interface';
 
-
-export type ComboboxProps = Pick<TimePickerProps, 'format' | 'prefixCls' | 'disabledHours' |
-'disabledMinutes' |
-'disabledSeconds' |
-'hideDisabledOptions' |
-'use12Hours' |
-'scrollItemProps' |
-'panelFooter' |
-'panelHeader'> & BaseProps & {
-    defaultOpenValue?: TimePickerProps['value'];
-    showHour?: boolean;
-    showMinute?: boolean;
-    showSecond?: boolean;
-    onChange?: (value: { isAM: boolean; value: string; timeStampValue: number }) => void;
-    onCurrentSelectPanelChange?: (range: string) => void;
-    isAM?: boolean;
-    timeStampValue?: any
-};
+export interface ComboboxProps
+    extends ComboboxFoundationProps, BaseProps,
+    Pick<TimePickerProps, 'prefixCls' | 'scrollItemProps' | 'panelFooter' | 'panelHeader'> { }
 
 export interface ComboboxState {
     showHour: boolean;
@@ -62,7 +51,7 @@ class Combobox extends BaseComponent<ComboboxProps, ComboboxState> {
         onCurrentSelectPanelChange: PropTypes.func,
         use12Hours: PropTypes.bool,
         isAM: PropTypes.bool,
-        timeStampValue: PropTypes.any,
+        timeStampValue: PropTypes.object,
         scrollItemProps: PropTypes.object,
     };
 
@@ -175,8 +164,7 @@ class Combobox extends BaseComponent<ComboboxProps, ComboboxState> {
         }
         const disabledOptions = disabledHours();
 
-        let hourOptionsAdj,
-            hourAdj;
+        let hourOptionsAdj, hourAdj;
         if (use12Hours) {
             hourOptionsAdj = [12].concat(hourOptions.filter(h => h < 12 && h > 0));
             hourAdj = hour % 12 || 12;
@@ -300,7 +288,8 @@ class Combobox extends BaseComponent<ComboboxProps, ComboboxState> {
         );
     }
 
-    getDisplayDateFromTimeStamp = (timeStampValue: Date | string) => this.foundation.getDisplayDateFromTimeStamp(timeStampValue);
+    getDisplayDateFromTimeStamp = (timeStampValue: TZDate) =>
+        this.foundation.getDisplayDateFromTimeStamp(timeStampValue);
 
     render() {
         const { timeStampValue, panelHeader, panelFooter } = this.props;

+ 5 - 5
packages/semi-ui/timePicker/TimeInput.tsx

@@ -12,7 +12,6 @@ import { TimePickerProps } from './TimePicker';
 
 
 export type TimeInputProps = Pick<TimePickerProps,
-'value' |
 'format' |
 'prefixCls' |
 'placeholder' |
@@ -20,7 +19,7 @@ export type TimeInputProps = Pick<TimePickerProps,
 'inputReadOnly' |
 'disabled' |
 'type' |
-'timeZone' | 
+'timeZone' |
 'defaultOpen' |
 'disabledHours' |
 'disabledMinutes' |
@@ -33,7 +32,7 @@ export type TimeInputProps = Pick<TimePickerProps,
 'localeCode' |
 'insetLabel' |
 'validateStatus' |
-'borderless'|
+'borderless' |
 'preventScroll'> & BaseProps & {
     onChange?: (value: string) => void;
     onEsc?: () => void;
@@ -41,7 +40,8 @@ export type TimeInputProps = Pick<TimePickerProps,
     defaultOpenValue?: boolean;
     currentSelectPanel?: string;
     timeStampValue?: any;
-    invalid?: boolean
+    invalid?: boolean;
+    value?: string
 };
 
 class TimeInput extends BaseComponent<TimeInputProps, any> {
@@ -186,7 +186,7 @@ class TimeInput extends BaseComponent<TimeInputProps, any> {
                 hideSuffix
                 className={inputCls}
                 ref={this.setRef as any}
-                value={value as any}
+                value={value}
                 placeholder={placeholder || locale.placeholder[type]}
                 readonly={Boolean(inputReadOnly)}
                 onChange={this.handleChange}

+ 26 - 67
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,87 +33,36 @@ export interface Panel {
     panelFooter?: React.ReactNode | React.ReactNode[]
 }
 
-export type BaseValueType = string | number | Date | undefined;
-
-export type Type = 'time' | 'timeRange';
-
-export type TimePickerProps = {
+export interface TimePickerProps extends TimePickerFoundationProps {
     '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
 }
 
+export interface TimePickerState extends TimePickerFoundationState { }
+
 export default class TimePicker extends BaseComponent<TimePickerProps, TimePickerState> {
     static contextType = ConfigContext;
     static propTypes = {
@@ -217,21 +170,20 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
 
     clickOutSideHandler: (e: MouseEvent) => void;
 
-
     constructor(props: TimePickerProps) {
         super(props);
         const { format = strings.DEFAULT_FORMAT } = props;
 
         this.state = {
             open: props.open || props.defaultOpen || false,
-            value: [], // Date[]
+            value: [], // TZDate[]
             inputValue: '', // time string
             currentSelectPanel: 0,
             isAM: [true, false],
             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);
@@ -240,7 +192,7 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
         this.useCustomTrigger = typeof this.props.triggerRender === 'function';
     }
 
-    get adapter(): TimePickerAdapter<TimePickerProps, TimePickerState> {
+    get adapter(): TimePickerAdapter {
         return {
             ...super.adapter,
             togglePanel: show => {
@@ -254,9 +206,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 +264,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 +286,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;

+ 35 - 0
packages/semi-ui/timePicker/_story/TimeZone/index.tsx

@@ -0,0 +1,35 @@
+import React from 'react';
+import { Space, TimePicker } from '@douyinfe/semi-ui';
+
+export default function Demo() {
+    const defaultTimestamp = 1745532000000;
+    return (
+        <Space align="start" vertical>
+            <div>默认时间:{new Date(defaultTimestamp).toISOString()}</div>
+            <TimePicker
+                prefix="0"
+                timeZone={0}
+                defaultValue={defaultTimestamp}
+                onChange={(date, dateString) => console.log('DatePicker changed: ', date, dateString)}
+            />
+            <TimePicker
+                prefix="Asia/Shanghai"
+                timeZone="Asia/Shanghai"
+                defaultValue={defaultTimestamp}
+                onChange={(date, dateString) => console.log('DatePicker changed: ', date, dateString)}
+            />
+            <TimePicker
+                prefix="8"
+                timeZone={8}
+                defaultValue={defaultTimestamp}
+                onChange={(date, dateString) => console.log('DatePicker changed: ', date, dateString)}
+            />
+            <TimePicker
+                prefix="Africa/Cairo"
+                timeZone="Africa/Cairo"
+                defaultValue={defaultTimestamp}
+                onChange={(date, dateString) => console.log('DatePicker changed: ', date, dateString)}
+            />
+        </Space>
+    );
+}

+ 14 - 0
packages/semi-ui/timePicker/_story/WithoutTimeZone/index.tsx

@@ -0,0 +1,14 @@
+import React from 'react';
+import { TimePicker } from '@douyinfe/semi-ui';
+
+// Test 1. 无时区时,应使用计算机时区
+// Test 2. 无时区时,选择时间应使用计算机时间
+export default function Demo() {
+    const defaultTimestamp = 1745532000000;
+    return (
+        <TimePicker
+            defaultValue={defaultTimestamp}
+            onChange={(date, dateString) => console.log('DatePicker changed: ', date, dateString)}
+        />
+    );
+}

+ 5 - 1
packages/semi-ui/timePicker/_story/timepicker.stories.jsx

@@ -7,6 +7,8 @@ import { get } from 'lodash';
 import Callbacks from './Callbacks';
 import CustomTrigger from './CustomTrigger';
 import DisabledTime from './DisabledTime';
+import TimeZone from './TimeZone';
+import WithoutTimeZone from './WithoutTimeZone';
 
 let TimePicker;
 
@@ -20,7 +22,9 @@ export default {
 export {
   Callbacks,
   CustomTrigger,
-  DisabledTime
+  DisabledTime,
+  TimeZone,
+  WithoutTimeZone
 }
 
 // auto add scrollItemProps.cycled = false, prevent waiting indefinitely in snapshot testing

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 675 - 768
yarn.lock


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно