From 4f494e5abccfe4e1fcde5b59745188f384a27652 Mon Sep 17 00:00:00 2001 From: mwplay <tqj.zyy@gmail.com> Date: Fri, 13 Dec 2024 15:53:31 +0800 Subject: [PATCH 1/2] add timezone change button --- .../BrowserCell/BrowserCell.react.js | 11 ++- src/components/BrowserRow/BrowserRow.react.js | 14 ++++ .../DateTimeEditor/DateTimeEditor.react.js | 71 +++++++------------ .../TimeZoneToggle/TimeZoneToggle.react.js | 20 ++++++ .../TimeZoneToggle/TimeZoneToggle.scss | 49 +++++++++++++ .../Data/Browser/BrowserTable.react.js | 4 ++ .../Data/Browser/BrowserToolbar.react.js | 11 ++- .../Data/Browser/DataBrowser.react.js | 52 ++++++++++++-- src/lib/DateUtils.js | 38 +++++++--- 9 files changed, 207 insertions(+), 63 deletions(-) create mode 100644 src/components/TimeZoneToggle/TimeZoneToggle.react.js create mode 100644 src/components/TimeZoneToggle/TimeZoneToggle.scss diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js index 74614feb69..072bc3c98b 100644 --- a/src/components/BrowserCell/BrowserCell.react.js +++ b/src/components/BrowserCell/BrowserCell.react.js @@ -7,7 +7,7 @@ */ import * as Filters from 'lib/Filters'; import { List, Map } from 'immutable'; -import { dateStringUTC } from 'lib/DateUtils'; +import { yearMonthDayTimeFormatter } from 'lib/DateUtils'; import getFileName from 'lib/getFileName'; import Parse from 'parse'; import Pill from 'components/Pill/Pill.react'; @@ -152,7 +152,11 @@ export default class BrowserCell extends Component { } else if (typeof value === 'string') { this.props.value = new Date(this.props.value); } - this.copyableValue = content = dateStringUTC(this.props.value); + if (this.props.useLocalTime) { + this.copyableValue = content = yearMonthDayTimeFormatter(this.props.value, false); + } else { + this.copyableValue = content = this.props.value.toISOString(); + } } else if (this.props.type === 'Boolean') { this.copyableValue = content = this.props.value ? 'True' : 'False'; } else if (this.props.type === 'Object' || this.props.type === 'Bytes') { @@ -222,6 +226,9 @@ export default class BrowserCell extends Component { ?.then(() => this.renderCellContent()) ?.catch(err => console.log(err)); } + if (this.props.useLocalTime !== prevProps.useLocalTime && this.props.type === 'Date') { + this.renderCellContent(); + } if (this.props.current) { if (prevProps.selectedCells === this.props.selectedCells) { const node = this.cellRef.current; diff --git a/src/components/BrowserRow/BrowserRow.react.js b/src/components/BrowserRow/BrowserRow.react.js index 0e6f40a694..7a0fc776d3 100644 --- a/src/components/BrowserRow/BrowserRow.react.js +++ b/src/components/BrowserRow/BrowserRow.react.js @@ -1,6 +1,7 @@ import Parse from 'parse'; import encode from 'parse/lib/browser/encode'; import React, { Component } from 'react'; +import { formatDateTime } from 'lib/DateUtils'; import BrowserCell from 'components/BrowserCell/BrowserCell.react'; import styles from 'dashboard/Data/Browser/Browser.scss'; @@ -19,6 +20,18 @@ export default class BrowserRow extends Component { return isRefDifferent ? JSON.stringify(obj) !== JSON.stringify(nextObj) : isRefDifferent; } + renderField(name, type, value, targetClass) { + if (!value) { + return ''; + } + + switch(type) { + case 'Date': + return formatDateTime(value, this.props.useLocalTime); + // ... 其他 case + } + } + render() { const { className, @@ -159,6 +172,7 @@ export default class BrowserRow extends Component { setShowAggregatedData={this.props.setShowAggregatedData} setErrorAggregatedData={this.props.setErrorAggregatedData} firstSelectedCell={this.props.firstSelectedCell} + useLocalTime={this.props.useLocalTime} /> ); })} diff --git a/src/components/DateTimeEditor/DateTimeEditor.react.js b/src/components/DateTimeEditor/DateTimeEditor.react.js index 3799d845e0..dac706af9e 100644 --- a/src/components/DateTimeEditor/DateTimeEditor.react.js +++ b/src/components/DateTimeEditor/DateTimeEditor.react.js @@ -9,42 +9,23 @@ import DateTimePicker from 'components/DateTimePicker/DateTimePicker.react'; import hasAncestor from 'lib/hasAncestor'; import React from 'react'; import styles from 'components/DateTimeEditor/DateTimeEditor.scss'; +import { yearMonthDayTimeFormatter } from 'lib/DateUtils'; export default class DateTimeEditor extends React.Component { constructor(props) { - super(); - + super(props); this.state = { - open: false, - position: null, value: props.value, text: props.value.toISOString(), + open: false }; - - this.checkExternalClick = this.checkExternalClick.bind(this); - this.handleKey = this.handleKey.bind(this); - this.inputRef = React.createRef(); this.editorRef = React.createRef(); - } - - componentWillReceiveProps(props) { - this.setState({ value: props.value, text: props.value.toISOString() }); + this.inputRef = React.createRef(); } componentDidMount() { - document.body.addEventListener('click', this.checkExternalClick); - this.inputRef.current.addEventListener('keypress', this.handleKey); - } - - componentWillUnmount() { - document.body.removeEventListener('click', this.checkExternalClick); - this.inputRef.current.removeEventListener('keypress', this.handleKey); - } - - checkExternalClick(e) { - if (!hasAncestor(e.target, this.editorRef.current)) { - this.props.onCommit(this.state.value); - } + this.inputRef.current.focus(); + this.inputRef.current.select(); } handleKey(e) { @@ -70,25 +51,21 @@ export default class DateTimeEditor extends React.Component { if (isNaN(date.getTime())) { this.setState({ value: this.props.value, - text: this.props.value.toISOString(), + text: this.props.value.toISOString() }); } else { - if (this.state.text.endsWith('Z')) { - this.setState({ value: date }); - } else { - const utc = new Date( - Date.UTC( - date.getFullYear(), - date.getMonth(), - date.getDate(), - date.getHours(), - date.getMinutes(), - date.getSeconds(), - date.getMilliseconds() - ) - ); - this.setState({ value: utc }); - } + const utc = new Date( + Date.UTC( + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds() + ) + ); + this.setState({ value: utc }); } } @@ -100,10 +77,11 @@ export default class DateTimeEditor extends React.Component { <DateTimePicker value={this.state.value} width={240} - onChange={value => this.setState({ value: value, text: value.toISOString() })} - close={() => - this.setState({ open: false }, () => this.props.onCommit(this.state.value)) - } + local={false} + onChange={value => this.setState({ value, text: value.toISOString() })} + close={() => { + this.setState({ open: false }, () => this.props.onCommit(this.state.value)); + }} /> </div> ); @@ -120,6 +98,7 @@ export default class DateTimeEditor extends React.Component { onClick={this.toggle.bind(this)} onChange={this.inputDate.bind(this)} onBlur={this.commitDate.bind(this)} + onKeyDown={this.handleKey.bind(this)} /> {popover} </div> diff --git a/src/components/TimeZoneToggle/TimeZoneToggle.react.js b/src/components/TimeZoneToggle/TimeZoneToggle.react.js new file mode 100644 index 0000000000..3f01234053 --- /dev/null +++ b/src/components/TimeZoneToggle/TimeZoneToggle.react.js @@ -0,0 +1,20 @@ +import React from 'react'; +import styles from './TimeZoneToggle.scss'; + +const TimeZoneToggle = ({ value, onChange }) => { + const switchStyle = { + backgroundColor: value ? 'rgb(0, 219, 124)' : 'rgb(204, 204, 204)', + transition: 'background-color 0.15s ease-out' + }; + + return ( + <div className={styles.container} onClick={() => onChange(!value)}> + <div className={`${styles.switch} ${value ? styles.right : styles.left}`} style={switchStyle}> + <span className={styles.option}>{value ? 'Local' : 'UTC'}</span> + <span className={styles.slider} /> + </div> + </div> + ); +}; + +export default TimeZoneToggle; \ No newline at end of file diff --git a/src/components/TimeZoneToggle/TimeZoneToggle.scss b/src/components/TimeZoneToggle/TimeZoneToggle.scss new file mode 100644 index 0000000000..05aa6e31c0 --- /dev/null +++ b/src/components/TimeZoneToggle/TimeZoneToggle.scss @@ -0,0 +1,49 @@ +.container { + display: inline-block; + cursor: pointer; + height: 30px; + line-height: 30px; +} + +.switch { + position: relative; + display: flex; + align-items: center; + width: 50px; + height: 18px; + border-radius: 12px; + margin: 6px 8px 0 8px; +} + +.option { + position: absolute; + width: 30px; + text-align: center; + color: white; + font-size: 10px; + z-index: 1; + line-height: 18px; +} + +.left .option { + left: 18px; +} + +.right .option { + left: 2px; +} + +.slider { + position: absolute; + width: 16px; + height: 16px; + background: white; + border-radius: 50%; + left: 2px; + transition: transform 0.15s ease-out; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.right .slider { + transform: translateX(30px); +} \ No newline at end of file diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js index 59dc53d5da..f8699d96ea 100644 --- a/src/dashboard/Data/Browser/BrowserTable.react.js +++ b/src/dashboard/Data/Browser/BrowserTable.react.js @@ -16,6 +16,7 @@ import React from 'react'; import styles from 'dashboard/Data/Browser/Browser.scss'; import Button from 'components/Button/Button.react'; import { CurrentApp } from 'context/currentApp'; +import { formatDateTime } from 'lib/DateUtils'; const MAX_ROWS = 200; // Number of rows to render at any time const ROWS_OFFSET = 160; @@ -218,6 +219,7 @@ export default class BrowserTable extends React.Component { setShowAggregatedData={this.props.setShowAggregatedData} setErrorAggregatedData={this.props.setErrorAggregatedData} firstSelectedCell={this.props.firstSelectedCell} + useLocalTime={this.props.useLocalTime} /> <Button value="Clone" @@ -298,6 +300,7 @@ export default class BrowserTable extends React.Component { setShowAggregatedData={this.props.setShowAggregatedData} setErrorAggregatedData={this.props.setErrorAggregatedData} firstSelectedCell={this.props.firstSelectedCell} + useLocalTime={this.props.useLocalTime} /> <Button value="Add" @@ -387,6 +390,7 @@ export default class BrowserTable extends React.Component { setShowAggregatedData={this.props.setShowAggregatedData} setErrorAggregatedData={this.props.setErrorAggregatedData} firstSelectedCell={this.props.firstSelectedCell} + useLocalTime={this.props.useLocalTime} /> ); } diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js index b8e44cc038..b32afea249 100644 --- a/src/dashboard/Data/Browser/BrowserToolbar.react.js +++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js @@ -19,6 +19,7 @@ import ColumnsConfiguration from 'components/ColumnsConfiguration/ColumnsConfigu import SecureFieldsDialog from 'dashboard/Data/Browser/SecureFieldsDialog.react'; import LoginDialog from 'dashboard/Data/Browser/LoginDialog.react'; import Toggle from 'components/Toggle/Toggle.react'; +import TimeZoneToggle from 'components/TimeZoneToggle/TimeZoneToggle.react'; const BrowserToolbar = ({ className, @@ -79,7 +80,10 @@ const BrowserToolbar = ({ togglePanel, isPanelVisible, - classwiseCloudFunctions + classwiseCloudFunctions, + + useLocalTime, + onToggleTimeZone }) => { const selectionLength = Object.keys(selection).length; const isPendingEditCloneRows = editCloneRows && editCloneRows.length > 0; @@ -439,6 +443,11 @@ const BrowserToolbar = ({ <MenuItem text={'Cancel all pending rows'} onClick={onCancelPendingEditRows} /> </BrowserMenu> )} + <div className={styles.toolbarSeparator} /> + <div style={{ display: 'inline-flex', alignItems: 'center', marginRight: '10px' }}> +<span style={{ marginRight: '8px', fontSize: '12px', color: '#ffffff' }}>TimeZone</span> +<TimeZoneToggle value={useLocalTime} onChange={onToggleTimeZone} /> +</div> </Toolbar> ); }; diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js index 90a5dd8347..0ede40bc5e 100644 --- a/src/dashboard/Data/Browser/DataBrowser.react.js +++ b/src/dashboard/Data/Browser/DataBrowser.react.js @@ -5,6 +5,7 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ +import { List } from 'immutable'; import ContextMenu from 'components/ContextMenu/ContextMenu.react'; import copy from 'copy-to-clipboard'; import BrowserTable from 'dashboard/Data/Browser/BrowserTable.react'; @@ -32,15 +33,37 @@ export default class DataBrowser extends React.Component { props.className, columnPreferences[props.className] ); + + const simplifiedSchema = props.schema ? + this.getSimplifiedSchema(props.schema, props.className) : {}; + const allClassesSchema = (props.schema && props.classes) ? + this.getAllClassesSchema(props.schema, props.classes) : {}; + this.state = { order: order, current: null, editing: false, copyableValue: undefined, selectedObjectId: undefined, - simplifiedSchema: this.getSimplifiedSchema(props.schema, props.className), - allClassesSchema: this.getAllClassesSchema(props.schema, props.classes), - isPanelVisible: false, + simplifiedSchema, + allClassesSchema, + selection: {}, + data: null, + lastMax: -1, + totalCount: 0, + lastError: null, + relation: null, + isUnique: false, + uniqueField: null, + filters: new List(), + ordering: ColumnPreferences.getColumnSort( + props.className, + props.columns, + props.app.applicationId, + order + ), + editCloneRows: [], + isMultiselect: false, selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 }, firstSelectedCell: null, selectedData: [], @@ -48,7 +71,11 @@ export default class DataBrowser extends React.Component { panelWidth: 300, isResizing: false, maxWidth: window.innerWidth - 300, + columnSizeControlled: false, showAggregatedData: true, + errorAggregatedData: null, + isPanelVisible: false, + useLocalTime: localStorage.getItem('parse_dashboard_useLocalTime') === 'true', }; this.handleResizeDiv = this.handleResizeDiv.bind(this); @@ -202,9 +229,14 @@ export default class DataBrowser extends React.Component { } } - getAllClassesSchema(schema) { + getAllClassesSchema(schema, classes) { + if (!schema || !schema.data || !schema.data.get('classes') || !classes) { + return {}; + } + const allClasses = Object.keys(schema.data.get('classes').toObject()); const schemaSimplifiedData = {}; + allClasses.forEach(className => { const classSchema = schema.data.get('classes').get(className); if (classSchema) { @@ -216,7 +248,6 @@ export default class DataBrowser extends React.Component { }; }); } - return schemaSimplifiedData; }); return schemaSimplifiedData; } @@ -545,6 +576,14 @@ export default class DataBrowser extends React.Component { } } + toggleTimeZone = () => { + this.setState(prevState => { + const newValue = !prevState.useLocalTime; + localStorage.setItem('parse_dashboard_useLocalTime', newValue); + return { useLocalTime: newValue }; + }); + } + render() { const { className, @@ -585,6 +624,7 @@ export default class DataBrowser extends React.Component { isResizing={this.state.isResizing} setShowAggregatedData={this.setShowAggregatedData} firstSelectedCell={this.state.firstSelectedCell} + useLocalTime={this.state.useLocalTime} {...other} /> {this.state.isPanelVisible && ( @@ -642,6 +682,8 @@ export default class DataBrowser extends React.Component { allClassesSchema={this.state.allClassesSchema} togglePanel={this.togglePanelVisibility} isPanelVisible={this.state.isPanelVisible} + useLocalTime={this.state.useLocalTime} + onToggleTimeZone={this.toggleTimeZone} {...other} /> diff --git a/src/lib/DateUtils.js b/src/lib/DateUtils.js index 57163eafe0..ca279b3681 100644 --- a/src/lib/DateUtils.js +++ b/src/lib/DateUtils.js @@ -141,19 +141,20 @@ export function yearMonthDayFormatter(date) { }); } -export function yearMonthDayTimeFormatter(date, timeZone) { +export function yearMonthDayTimeFormatter(date, useUTC) { const options = { year: 'numeric', - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', hour12: false, + timeZone: useUTC ? 'UTC' : undefined }; - if (timeZone) { - options.timeZoneName = 'short'; - } - return date.toLocaleDateString('en-US', options); + return date.toLocaleString('en-US', options) + .replace(',', '') // 移除日期和时间之间的逗号 + .replace(/(\d+)\/(\d+)\/(\d+)/, '$3-$1-$2'); // 转换为 yyyy-MM-dd 格式 } export function getDateMethod(local, methodName) { @@ -171,3 +172,22 @@ export function pad(number) { } return r; } + +export function formatDateTime(date, useLocalTime = false) { + if (!date) return ''; + + const d = new Date(date); + + // 根据useLocalTime决定使用本地时间还是UTC时间 + const year = useLocalTime ? d.getFullYear() : d.getUTCFullYear(); + const month = (useLocalTime ? d.getMonth() : d.getUTCMonth()) + 1; + const day = useLocalTime ? d.getDate() : d.getUTCDate(); + const hours = useLocalTime ? d.getHours() : d.getUTCHours(); + const minutes = useLocalTime ? d.getMinutes() : d.getUTCMinutes(); + const seconds = useLocalTime ? d.getSeconds() : d.getUTCSeconds(); + + // 补零函数 + const pad = (num) => String(num).padStart(2, '0'); + + return `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; +} From c7bece882f3957bfa7ff7e9a9cac2f2afc48be9f Mon Sep 17 00:00:00 2001 From: mwplay <tqj.zyy@gmail.com> Date: Fri, 13 Dec 2024 16:12:31 +0800 Subject: [PATCH 2/2] refactor: Update comments for clarity and add tests for date formatting functions --- src/components/BrowserRow/BrowserRow.react.js | 2 +- .../TimeZoneToggle/TimeZoneToggle.test.js | 37 ++++++++++++++++ .../Data/Browser/DataBrowser.test.js | 39 +++++++++++++++++ src/lib/DateUtils.js | 8 ++-- src/lib/tests/DateUtils.test.js | 42 +++++++++++++++++++ 5 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 src/components/TimeZoneToggle/TimeZoneToggle.test.js create mode 100644 src/dashboard/Data/Browser/DataBrowser.test.js diff --git a/src/components/BrowserRow/BrowserRow.react.js b/src/components/BrowserRow/BrowserRow.react.js index 7a0fc776d3..6ce1554bdc 100644 --- a/src/components/BrowserRow/BrowserRow.react.js +++ b/src/components/BrowserRow/BrowserRow.react.js @@ -28,7 +28,7 @@ export default class BrowserRow extends Component { switch(type) { case 'Date': return formatDateTime(value, this.props.useLocalTime); - // ... 其他 case + // ... other cases } } diff --git a/src/components/TimeZoneToggle/TimeZoneToggle.test.js b/src/components/TimeZoneToggle/TimeZoneToggle.test.js new file mode 100644 index 0000000000..d4c28433fb --- /dev/null +++ b/src/components/TimeZoneToggle/TimeZoneToggle.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import TimeZoneToggle from '../TimeZoneToggle.react'; + +describe('TimeZoneToggle', () => { + it('should render UTC text when value is false', () => { + const wrapper = mount(<TimeZoneToggle value={false} onChange={() => {}} />); + expect(wrapper.find('.option').text()).toBe('UTC'); + expect(wrapper.find('.switch').hasClass('left')).toBe(true); + }); + + it('should render Local text when value is true', () => { + const wrapper = mount(<TimeZoneToggle value={true} onChange={() => {}} />); + expect(wrapper.find('.option').text()).toBe('Local'); + expect(wrapper.find('.switch').hasClass('right')).toBe(true); + }); + + it('should call onChange with opposite value when clicked', () => { + const onChange = jest.fn(); + const wrapper = mount(<TimeZoneToggle value={false} onChange={onChange} />); + + wrapper.find('.container').simulate('click'); + expect(onChange).toHaveBeenCalledWith(true); + + wrapper.setProps({ value: true }); + wrapper.find('.container').simulate('click'); + expect(onChange).toHaveBeenCalledWith(false); + }); + + it('should have correct background color based on value', () => { + const wrapper = mount(<TimeZoneToggle value={false} onChange={() => {}} />); + expect(wrapper.find('.switch').prop('style').backgroundColor).toBe('rgb(204, 204, 204)'); + + wrapper.setProps({ value: true }); + expect(wrapper.find('.switch').prop('style').backgroundColor).toBe('rgb(0, 219, 124)'); + }); +}); \ No newline at end of file diff --git a/src/dashboard/Data/Browser/DataBrowser.test.js b/src/dashboard/Data/Browser/DataBrowser.test.js new file mode 100644 index 0000000000..fa99473ad3 --- /dev/null +++ b/src/dashboard/Data/Browser/DataBrowser.test.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import DataBrowser from '../DataBrowser.react'; + +describe('DataBrowser TimeZone Feature', () => { + let wrapper; + const mockLocalStorage = { + getItem: jest.fn(), + setItem: jest.fn(), + }; + + beforeEach(() => { + global.localStorage = mockLocalStorage; + }); + + it('should initialize with stored timezone preference', () => { + mockLocalStorage.getItem.mockReturnValue('true'); + wrapper = mount(<DataBrowser {...defaultProps} />); + expect(wrapper.state().useLocalTime).toBe(true); + }); + + it('should toggle timezone and save preference', () => { + wrapper = mount(<DataBrowser {...defaultProps} />); + + wrapper.instance().toggleTimeZone(); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith( + 'parse_dashboard_useLocalTime', + 'true' + ); + expect(wrapper.state().useLocalTime).toBe(true); + + wrapper.instance().toggleTimeZone(); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith( + 'parse_dashboard_useLocalTime', + 'false' + ); + expect(wrapper.state().useLocalTime).toBe(false); + }); +}); \ No newline at end of file diff --git a/src/lib/DateUtils.js b/src/lib/DateUtils.js index ca279b3681..871c7f02d0 100644 --- a/src/lib/DateUtils.js +++ b/src/lib/DateUtils.js @@ -153,8 +153,8 @@ export function yearMonthDayTimeFormatter(date, useUTC) { timeZone: useUTC ? 'UTC' : undefined }; return date.toLocaleString('en-US', options) - .replace(',', '') // 移除日期和时间之间的逗号 - .replace(/(\d+)\/(\d+)\/(\d+)/, '$3-$1-$2'); // 转换为 yyyy-MM-dd 格式 + .replace(',', '') // Remove comma between date and time + .replace(/(\d+)\/(\d+)\/(\d+)/, '$3-$1-$2'); // Convert to yyyy-MM-dd format } export function getDateMethod(local, methodName) { @@ -178,7 +178,7 @@ export function formatDateTime(date, useLocalTime = false) { const d = new Date(date); - // 根据useLocalTime决定使用本地时间还是UTC时间 + // Determine whether to use local time or UTC time based on useLocalTime flag const year = useLocalTime ? d.getFullYear() : d.getUTCFullYear(); const month = (useLocalTime ? d.getMonth() : d.getUTCMonth()) + 1; const day = useLocalTime ? d.getDate() : d.getUTCDate(); @@ -186,7 +186,7 @@ export function formatDateTime(date, useLocalTime = false) { const minutes = useLocalTime ? d.getMinutes() : d.getUTCMinutes(); const seconds = useLocalTime ? d.getSeconds() : d.getUTCSeconds(); - // 补零函数 + // Pad numbers with leading zeros const pad = (num) => String(num).padStart(2, '0'); return `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; diff --git a/src/lib/tests/DateUtils.test.js b/src/lib/tests/DateUtils.test.js index 1b667ffeea..78ba198f75 100644 --- a/src/lib/tests/DateUtils.test.js +++ b/src/lib/tests/DateUtils.test.js @@ -65,3 +65,45 @@ describe('daysInMonth', () => { expect(DateUtils.daysInMonth(new Date(2015, 8))).toBe(30); }); }); + +describe('DateUtils', () => { + describe('formatDateTime', () => { + const testDate = new Date('2024-03-21T08:00:00.000Z'); + + it('should format date in UTC time when useLocalTime is false', () => { + const result = DateUtils.formatDateTime(testDate, false); + expect(result).toBe('2024-03-21 08:00:00'); + }); + + it('should format date in local time when useLocalTime is true', () => { + // 假设本地时区是 UTC+8 + const result = DateUtils.formatDateTime(testDate, true); + expect(result).toBe('2024-03-21 16:00:00'); + }); + + it('should handle null date', () => { + const result = DateUtils.formatDateTime(null, false); + expect(result).toBe(''); + }); + + it('should handle invalid date', () => { + const result = DateUtils.formatDateTime('invalid date', false); + expect(result).toBe(''); + }); + }); + + describe('yearMonthDayTimeFormatter', () => { + const testDate = new Date('2024-03-21T08:00:00.000Z'); + + it('should format date in UTC format', () => { + const result = DateUtils.yearMonthDayTimeFormatter(testDate, true); + expect(result).toBe('2024-03-21 08:00:00'); + }); + + it('should format date in local format', () => { + const result = DateUtils.yearMonthDayTimeFormatter(testDate, false); + // 假设本地时区是 UTC+8 + expect(result).toBe('2024-03-21 16:00:00'); + }); + }); +});