Refactor time utility functions for improved type safety and error handling

- Removed deprecated formatDate function.
- Enhanced formatWithZone to support TypeScript types for parameters.
- Updated getTimeZoneOffset to use TypeScript types.
- Improved formatWithDate for better error messages and type safety.
This commit is contained in:
雨霖铃
2026-01-28 22:39:44 +08:00
parent 710bffd2f8
commit 92026ee7fa
6 changed files with 813 additions and 843 deletions
+12 -12
View File
@@ -6,6 +6,7 @@ const CopyButton = ({
className = 'action-btn',
successMessage = '复制成功!',
errorMessage = '复制失败,请手动复制。',
copyingMessage = '复制中...',
}) => {
const [status, setStatus] = useState('idle'); // 'idle' | 'copying' | 'success' | 'error'
@@ -53,25 +54,24 @@ const CopyButton = ({
// 根据状态计算当前显示的文本
const currentText =
status === 'success' ? successMessage : status === 'error' ? errorMessage : buttonText;
status === 'success'
? successMessage
: status === 'error'
? errorMessage
: status === 'copying'
? copyingMessage
: buttonText;
// 动态样式:只在非默认状态下覆盖颜色,平时让 className 控制
const getStyle = () => {
const baseStyle = {
transition: 'all 0.3s ease',
cursor: 'pointer', // 确保有手型光标
// 这里去掉了 padding/border/radius 的硬编码,建议在 CSS 类中定义
// 除非你想强制覆盖
};
if (status === 'success') {
return { ...baseStyle, backgroundColor: '#4CAF50', color: 'white' };
return { backgroundColor: '#4CAF50', color: 'white' };
}
if (status === 'error') {
return { ...baseStyle, backgroundColor: '#f44336', color: 'white' };
return { backgroundColor: '#f44336', color: 'white' };
}
return baseStyle;
return {};
};
return (
@@ -79,7 +79,7 @@ const CopyButton = ({
onClick={handleClick}
className={`${className} ${status} copy-button`}
style={getStyle()}
disabled={status === 'copying'}
disabled={status === 'success' || status === 'copying'}
>
{currentText}
</button>
+45 -43
View File
@@ -1,5 +1,5 @@
import {formatWithDate, formatWithZone, TimezoneOptions} from "../utils/timeUtils";
import {useState, useCallback} from "react";
import { formatWithDate, formatWithZone } from '../utils/timeUtils';
import { useState, useCallback } from 'react';
/**
* 日期时间转时间戳组件
@@ -44,26 +44,26 @@ const TIME_ZONE_LIST = [
// 时间戳单位选项
const TIMESTAMP_UNITS = [
{value: 'milliseconds', label: '毫秒(ms)'},
{value: 'seconds', label: '秒(s)'},
{ value: 'milliseconds', label: '毫秒(ms)' },
{ value: 'seconds', label: '秒(s)' },
];
export function DatetimeToTimestamp() {
/** @type {[string, function]} 输入的日期时间字符串 */
const [dateValue, setDateValue] = useState(() => formatWithZone(Date.now()));
const [dateValue, setDateValue] = useState(() => formatWithZone(Date.now(), 'Asia/Shanghai'));
/** @type {[string, function]} 选择的时区 */
const [selectedZone, setSelectedZone] = useState('Asia/Shanghai');
/** @type {[string, function]} 转换结果 */
const [result, setResult] = useState('');
/** @type {[string, function]} 时间戳单位 ('milliseconds' | 'seconds') */
const [unit, setUnit] = useState('milliseconds');
/** @type {[string, function]} 错误信息 */
const [error, setError] = useState('');
/**
* 转换日期时间为时间戳
* @type {function(): void}
@@ -72,24 +72,22 @@ export function DatetimeToTimestamp() {
try {
setError('');
const timestamp = formatWithDate(dateValue, selectedZone);
if (isNaN(timestamp)) {
setError('无效的日期时间格式');
setResult('');
return;
}
const finalResult = unit === 'milliseconds'
? timestamp
: Math.floor(timestamp / 1000);
const finalResult = unit === 'milliseconds' ? timestamp : Math.floor(timestamp / 1000);
setResult(finalResult.toString());
} catch (err) {
setError('转换失败,请检查输入格式');
setResult('');
}
}, [dateValue, selectedZone, unit]);
/**
* 处理日期时间输入变化
* @type {function(React.ChangeEvent<HTMLInputElement>): void}
@@ -98,7 +96,7 @@ export function DatetimeToTimestamp() {
setDateValue(e.target.value);
setError(''); // 清除错误信息
}, []);
/**
* 处理时区选择变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void}
@@ -106,31 +104,33 @@ export function DatetimeToTimestamp() {
const handleZoneChange = useCallback((e) => {
setSelectedZone(e.target.value);
}, []);
/**
* 处理时间戳单位变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void}
*/
const handleUnitChange = useCallback((e) => {
const newUnit = e.target.value;
setUnit(newUnit);
// 如果已有结果,重新计算
if (result) {
const currentResult = parseInt(result, 10);
if (!isNaN(currentResult)) {
const newResult = newUnit === 'milliseconds'
? currentResult * 1000
: Math.floor(currentResult / 1000);
setResult(newResult.toString());
const handleUnitChange = useCallback(
(e) => {
const newUnit = e.target.value;
setUnit(newUnit);
// 如果已有结果,重新计算
if (result) {
const currentResult = parseInt(result, 10);
if (!isNaN(currentResult)) {
const newResult =
newUnit === 'milliseconds' ? currentResult * 1000 : Math.floor(currentResult / 1000);
setResult(newResult.toString());
}
}
}
}, [result]);
},
[result],
);
return (
<div className="datetime-converter">
<h2 className="converter-title"></h2>
<div className="converter-form">
<div className="input-group">
<input
@@ -148,16 +148,20 @@ export function DatetimeToTimestamp() {
onChange={handleZoneChange}
aria-label="选择时区"
>
<TimezoneOptions zones={TIME_ZONE_LIST}/>
{TIME_ZONE_LIST.map((zone) => (
<option key={zone} value={zone}>
{zone}
</option>
))}
</select>
</div>
{error && (
<div className="error-message" role="alert">
{error}
</div>
)}
<div className="action-group">
<button
className="converter-btn action-btn"
@@ -167,7 +171,7 @@ export function DatetimeToTimestamp() {
</button>
</div>
<div className="result-group">
<input
type="text"
@@ -183,16 +187,14 @@ export function DatetimeToTimestamp() {
onChange={handleUnitChange}
aria-label="选择时间戳单位"
>
{TIMESTAMP_UNITS.map(({value, label}) => (
{TIMESTAMP_UNITS.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
</div>
</div>
);
}
}
+24 -24
View File
@@ -1,5 +1,5 @@
import {useEffect, useState, useRef, useCallback} from "react";
import CopyButton from "./CopyButton";
import { useEffect, useState, useRef, useCallback } from 'react';
import CopyButton from './CopyButton';
/**
* 时间戳显示和执行组件
@@ -22,16 +22,16 @@ import CopyButton from "./CopyButton";
export function TimestampExecution() {
/** @type {[number, function]} 当前时间戳(毫秒)和更新函数 */
const [currentTimestamp, setCurrentTimestamp] = useState(() => Math.floor(Date.now()));
/** @type {[boolean, function]} 是否显示毫秒(true=毫秒,false=秒) */
const [showMilliseconds, setShowMilliseconds] = useState(true);
/** @type {[boolean, function]} 时间戳是否正在自动更新 */
const [isRunningTimestamp, setIsRunningTimestamp] = useState(true);
/** @type {React.RefObject<NodeJS.Timeout | null>} 定时器引用,用于清理 */
const timerRef = useRef(null);
/**
* 计算显示的时间戳值
* @type {number}
@@ -39,13 +39,13 @@ export function TimestampExecution() {
const displayTimestamp = showMilliseconds
? currentTimestamp
: Math.floor(currentTimestamp / 1000);
/**
* 计算单位文本
* @type {string}
*/
const unitText = showMilliseconds ? '毫秒' : '秒';
/**
* 定时更新时间戳的副作用
* 根据 isRunningTimestamp 和 showMilliseconds 控制定时器的启停和间隔
@@ -56,7 +56,7 @@ export function TimestampExecution() {
clearInterval(timerRef.current);
timerRef.current = null;
}
// 如果需要运行,创建新的定时器
if (isRunningTimestamp) {
const interval = showMilliseconds ? 100 : 1000;
@@ -64,7 +64,7 @@ export function TimestampExecution() {
setCurrentTimestamp(Math.floor(Date.now()));
}, interval);
}
// 清理函数
return () => {
if (timerRef.current) {
@@ -73,68 +73,68 @@ export function TimestampExecution() {
}
};
}, [isRunningTimestamp, showMilliseconds]);
/**
* 切换时间戳显示单位(毫秒/秒)
* @type {function(): void}
*/
const toggleUnit = useCallback(() => {
setShowMilliseconds(prev => !prev);
setShowMilliseconds((prev) => !prev);
}, []);
/**
* 切换时间戳自动更新状态(启动/停止)
* @type {function(): void}
*/
const toggleTimestamp = useCallback(() => {
setIsRunningTimestamp(prev => !prev);
setIsRunningTimestamp((prev) => !prev);
}, []);
/**
* 切换单位按钮的辅助文本
* @type {string}
*/
const unitButtonLabel = showMilliseconds ? '切换为秒显示' : '切换为毫秒显示';
/**
* 启动/停止按钮的辅助文本
* @type {string}
*/
const toggleButtonLabel = isRunningTimestamp ? '停止时间戳自动更新' : '开始时间戳自动更新';
/**
* 启动/停止按钮的显示文本
* @type {string}
*/
const toggleButtonText = isRunningTimestamp ? '停止' : '开始';
return (
<div className="timestamp-container">
<div className="timestamp-display">
<span className="timestamp-value">{displayTimestamp}</span>
<span className="timestamp-unit">{unitText}</span>
</div>
<div className="timestamp-controls">
<button
type="button"
className="timestamp-btn action-btn"
className="action-btn"
onClick={toggleUnit}
aria-label={unitButtonLabel}
title={unitButtonLabel}
>
</button>
<CopyButton
text={String(currentTimestamp)}
buttonText="复制时间戳"
aria-label="复制当前时间戳到剪贴板"
/>
<button
type="button"
className={`timestamp-btn ${isRunningTimestamp ? 'stop-btn' : 'action-btn'}`}
className={`${isRunningTimestamp ? 'stop-btn' : 'action-btn'}`}
onClick={toggleTimestamp}
aria-label={toggleButtonLabel}
title={toggleButtonLabel}
@@ -144,4 +144,4 @@ export function TimestampExecution() {
</div>
</div>
);
}
}
+28 -24
View File
@@ -1,5 +1,5 @@
import {useState, useCallback} from "react";
import {formatWithZone, TimezoneOptions} from "../utils/timeUtils";
import { useState, useCallback } from 'react';
import { formatWithZone } from '../utils/timeUtils';
/**
* 时间戳转日期时间组件
@@ -43,26 +43,26 @@ const TIME_ZONE_LIST = [
// 时间戳单位选项
const TIMESTAMP_UNITS = [
{value: 'milliseconds', label: '毫秒(ms)'},
{value: 'seconds', label: '秒(s)'},
{ value: 'milliseconds', label: '毫秒(ms)' },
{ value: 'seconds', label: '秒(s)' },
];
export function TimestampToDatetime() {
/** @type {[string, function]} 输入的时间戳值 */
const [timestampValue, setTimestampValue] = useState(Date.now());
/** @type {[string, function]} 转换结果 */
const [timestampResult, setTimestampResult] = useState('');
/** @type {[string, function]} 时间戳单位 ('milliseconds' | 'seconds') */
const [unit, setUnit] = useState('milliseconds');
/** @type {[string, function]} 选择的时区 */
const [selectedZone, setSelectedZone] = useState('Asia/Shanghai');
/** @type {[string, function]} 错误信息 */
const [error, setError] = useState('');
/**
* 转换时间戳为日期时间
* @type {function(): void}
@@ -70,20 +70,20 @@ export function TimestampToDatetime() {
const handleConvertTimestampToDate = useCallback(() => {
try {
setError('');
if (!timestampValue) {
setError('请输入时间戳');
setTimestampResult('');
return;
}
const numericValue = Number(timestampValue);
if (isNaN(numericValue)) {
setError('无效的时间戳格式');
setTimestampResult('');
return;
}
const result = formatWithZone(numericValue, selectedZone, unit);
setTimestampResult(result);
} catch (err) {
@@ -91,7 +91,7 @@ export function TimestampToDatetime() {
setTimestampResult('');
}
}, [timestampValue, selectedZone, unit]);
/**
* 处理时间戳输入变化
* @type {function(React.ChangeEvent<HTMLInputElement>): void}
@@ -100,7 +100,7 @@ export function TimestampToDatetime() {
setTimestampValue(e.target.value);
setError(''); // 清除错误信息
}, []);
/**
* 处理时区选择变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void}
@@ -108,7 +108,7 @@ export function TimestampToDatetime() {
const handleZoneChange = useCallback((e) => {
setSelectedZone(e.target.value);
}, []);
/**
* 处理时间戳单位变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void}
@@ -116,11 +116,11 @@ export function TimestampToDatetime() {
const handleUnitChange = useCallback((e) => {
setUnit(e.target.value);
}, []);
return (
<div className="datetime-converter">
<h2 className="converter-title"></h2>
<div className="converter-form">
<div className="input-group">
<input
@@ -138,20 +138,20 @@ export function TimestampToDatetime() {
onChange={handleUnitChange}
aria-label="选择时间戳单位"
>
{TIMESTAMP_UNITS.map(({value, label}) => (
{TIMESTAMP_UNITS.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
{error && (
<div className="error-message" role="alert">
{error}
</div>
)}
<div className="action-group">
<button
className="converter-btn action-btn"
@@ -161,7 +161,7 @@ export function TimestampToDatetime() {
</button>
</div>
<div className="result-group">
<input
type="text"
@@ -177,10 +177,14 @@ export function TimestampToDatetime() {
onChange={handleZoneChange}
aria-label="选择时区"
>
<TimezoneOptions zones={TIME_ZONE_LIST}/>
{TIME_ZONE_LIST.map((zone) => (
<option key={zone} value={zone}>
{zone}
</option>
))}
</select>
</div>
</div>
</div>
)
}
);
}
+686 -710
View File
File diff suppressed because it is too large Load Diff
+18 -30
View File
@@ -1,20 +1,8 @@
export function formatDate(value, timezone = 'Asia/Shanghai') {
return (new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
})).format(value);
}
export const TimezoneOptions = ({zones}) => {
return (zones.map((zone) => (<option key={zone} value={zone}>{zone}</option>)));
}
export const formatWithZone = (timestamp, zone = 'Asia/Shanghai', unit = 'milliseconds') => {
export const formatWithZone = (
timestamp: number | string,
zone = 'Asia/Shanghai',
unit = 'milliseconds',
) => {
try {
const ms = unit === 'milliseconds' ? Number(timestamp) : Number(timestamp) * 1000;
return new Intl.DateTimeFormat('zh-CN', {
@@ -29,41 +17,41 @@ export const formatWithZone = (timestamp, zone = 'Asia/Shanghai', unit = 'millis
} catch (e) {
return '格式错误';
}
}
};
export const getTimeZoneOffset = (timeZone) => {
export const getTimeZoneOffset = (timeZone: string) => {
const now = new Date();
const utc = new Date(now.toLocaleString('en-US', {timeZone: 'UTC'}));
const target = new Date(now.toLocaleString('en-US', {timeZone: timeZone}));
const utc = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }));
const target = new Date(now.toLocaleString('en-US', { timeZone: timeZone }));
return target.getTime() - utc.getTime();
}
};
export const formatWithDate = (
date,
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
date: string,
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone,
) => {
try {
// 创建日期对象
const dateObj = new Date(date);
// 检查日期是否有效
if (isNaN(dateObj.getTime())) {
return '无效的日期';
}
// 如果提供了时区,则需要特殊处理
if (timeZone) {
// 获取给定时区相对于UTC的时间差(毫秒)
const utc = dateObj.getTime() + dateObj.getTimezoneOffset() * 60000;
// 计算目标时区相对于UTC的偏移量
const targetOffset = getTimeZoneOffset(timeZone);
return utc + targetOffset;
} else {
return dateObj.getTime();
}
} catch (error) {
return '日期转换错误: ' + error.message;
return '日期转换错误: ' + error;
}
}
};