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', className = 'action-btn',
successMessage = '复制成功!', successMessage = '复制成功!',
errorMessage = '复制失败,请手动复制。', errorMessage = '复制失败,请手动复制。',
copyingMessage = '复制中...',
}) => { }) => {
const [status, setStatus] = useState('idle'); // 'idle' | 'copying' | 'success' | 'error' const [status, setStatus] = useState('idle'); // 'idle' | 'copying' | 'success' | 'error'
@@ -53,25 +54,24 @@ const CopyButton = ({
// 根据状态计算当前显示的文本 // 根据状态计算当前显示的文本
const currentText = const currentText =
status === 'success' ? successMessage : status === 'error' ? errorMessage : buttonText; status === 'success'
? successMessage
: status === 'error'
? errorMessage
: status === 'copying'
? copyingMessage
: buttonText;
// 动态样式:只在非默认状态下覆盖颜色,平时让 className 控制 // 动态样式:只在非默认状态下覆盖颜色,平时让 className 控制
const getStyle = () => { const getStyle = () => {
const baseStyle = {
transition: 'all 0.3s ease',
cursor: 'pointer', // 确保有手型光标
// 这里去掉了 padding/border/radius 的硬编码,建议在 CSS 类中定义
// 除非你想强制覆盖
};
if (status === 'success') { if (status === 'success') {
return { ...baseStyle, backgroundColor: '#4CAF50', color: 'white' }; return { backgroundColor: '#4CAF50', color: 'white' };
} }
if (status === 'error') { if (status === 'error') {
return { ...baseStyle, backgroundColor: '#f44336', color: 'white' }; return { backgroundColor: '#f44336', color: 'white' };
} }
return baseStyle; return {};
}; };
return ( return (
@@ -79,7 +79,7 @@ const CopyButton = ({
onClick={handleClick} onClick={handleClick}
className={`${className} ${status} copy-button`} className={`${className} ${status} copy-button`}
style={getStyle()} style={getStyle()}
disabled={status === 'copying'} disabled={status === 'success' || status === 'copying'}
> >
{currentText} {currentText}
</button> </button>
+16 -14
View File
@@ -1,5 +1,5 @@
import {formatWithDate, formatWithZone, TimezoneOptions} from "../utils/timeUtils"; import { formatWithDate, formatWithZone } from '../utils/timeUtils';
import {useState, useCallback} from "react"; import { useState, useCallback } from 'react';
/** /**
* 日期时间转时间戳组件 * 日期时间转时间戳组件
@@ -50,7 +50,7 @@ const TIMESTAMP_UNITS = [
export function DatetimeToTimestamp() { export function DatetimeToTimestamp() {
/** @type {[string, function]} 输入的日期时间字符串 */ /** @type {[string, function]} 输入的日期时间字符串 */
const [dateValue, setDateValue] = useState(() => formatWithZone(Date.now())); const [dateValue, setDateValue] = useState(() => formatWithZone(Date.now(), 'Asia/Shanghai'));
/** @type {[string, function]} 选择的时区 */ /** @type {[string, function]} 选择的时区 */
const [selectedZone, setSelectedZone] = useState('Asia/Shanghai'); const [selectedZone, setSelectedZone] = useState('Asia/Shanghai');
@@ -79,9 +79,7 @@ export function DatetimeToTimestamp() {
return; return;
} }
const finalResult = unit === 'milliseconds' const finalResult = unit === 'milliseconds' ? timestamp : Math.floor(timestamp / 1000);
? timestamp
: Math.floor(timestamp / 1000);
setResult(finalResult.toString()); setResult(finalResult.toString());
} catch (err) { } catch (err) {
@@ -111,7 +109,8 @@ export function DatetimeToTimestamp() {
* 处理时间戳单位变化 * 处理时间戳单位变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void} * @type {function(React.ChangeEvent<HTMLSelectElement>): void}
*/ */
const handleUnitChange = useCallback((e) => { const handleUnitChange = useCallback(
(e) => {
const newUnit = e.target.value; const newUnit = e.target.value;
setUnit(newUnit); setUnit(newUnit);
@@ -119,13 +118,14 @@ export function DatetimeToTimestamp() {
if (result) { if (result) {
const currentResult = parseInt(result, 10); const currentResult = parseInt(result, 10);
if (!isNaN(currentResult)) { if (!isNaN(currentResult)) {
const newResult = newUnit === 'milliseconds' const newResult =
? currentResult * 1000 newUnit === 'milliseconds' ? currentResult * 1000 : Math.floor(currentResult / 1000);
: Math.floor(currentResult / 1000);
setResult(newResult.toString()); setResult(newResult.toString());
} }
} }
}, [result]); },
[result],
);
return ( return (
<div className="datetime-converter"> <div className="datetime-converter">
@@ -148,7 +148,11 @@ export function DatetimeToTimestamp() {
onChange={handleZoneChange} onChange={handleZoneChange}
aria-label="选择时区" aria-label="选择时区"
> >
<TimezoneOptions zones={TIME_ZONE_LIST}/> {TIME_ZONE_LIST.map((zone) => (
<option key={zone} value={zone}>
{zone}
</option>
))}
</select> </select>
</div> </div>
@@ -190,9 +194,7 @@ export function DatetimeToTimestamp() {
))} ))}
</select> </select>
</div> </div>
</div> </div>
</div> </div>
); );
} }
+6 -6
View File
@@ -1,5 +1,5 @@
import {useEffect, useState, useRef, useCallback} from "react"; import { useEffect, useState, useRef, useCallback } from 'react';
import CopyButton from "./CopyButton"; import CopyButton from './CopyButton';
/** /**
* 时间戳显示和执行组件 * 时间戳显示和执行组件
@@ -79,7 +79,7 @@ export function TimestampExecution() {
* @type {function(): void} * @type {function(): void}
*/ */
const toggleUnit = useCallback(() => { const toggleUnit = useCallback(() => {
setShowMilliseconds(prev => !prev); setShowMilliseconds((prev) => !prev);
}, []); }, []);
/** /**
@@ -87,7 +87,7 @@ export function TimestampExecution() {
* @type {function(): void} * @type {function(): void}
*/ */
const toggleTimestamp = useCallback(() => { const toggleTimestamp = useCallback(() => {
setIsRunningTimestamp(prev => !prev); setIsRunningTimestamp((prev) => !prev);
}, []); }, []);
/** /**
@@ -118,7 +118,7 @@ export function TimestampExecution() {
<div className="timestamp-controls"> <div className="timestamp-controls">
<button <button
type="button" type="button"
className="timestamp-btn action-btn" className="action-btn"
onClick={toggleUnit} onClick={toggleUnit}
aria-label={unitButtonLabel} aria-label={unitButtonLabel}
title={unitButtonLabel} title={unitButtonLabel}
@@ -134,7 +134,7 @@ export function TimestampExecution() {
<button <button
type="button" type="button"
className={`timestamp-btn ${isRunningTimestamp ? 'stop-btn' : 'action-btn'}`} className={`${isRunningTimestamp ? 'stop-btn' : 'action-btn'}`}
onClick={toggleTimestamp} onClick={toggleTimestamp}
aria-label={toggleButtonLabel} aria-label={toggleButtonLabel}
title={toggleButtonLabel} title={toggleButtonLabel}
+8 -4
View File
@@ -1,5 +1,5 @@
import {useState, useCallback} from "react"; import { useState, useCallback } from 'react';
import {formatWithZone, TimezoneOptions} from "../utils/timeUtils"; import { formatWithZone } from '../utils/timeUtils';
/** /**
* 时间戳转日期时间组件 * 时间戳转日期时间组件
@@ -177,10 +177,14 @@ export function TimestampToDatetime() {
onChange={handleZoneChange} onChange={handleZoneChange}
aria-label="选择时区" aria-label="选择时区"
> >
<TimezoneOptions zones={TIME_ZONE_LIST}/> {TIME_ZONE_LIST.map((zone) => (
<option key={zone} value={zone}>
{zone}
</option>
))}
</select> </select>
</div> </div>
</div> </div>
</div> </div>
) );
} }
+26 -50
View File
@@ -1,5 +1,6 @@
/* 隐藏页面滚动条 */ /* 隐藏页面滚动条 */
html, body { html,
body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
@@ -437,6 +438,30 @@ body,
-ms-overflow-style: none; -ms-overflow-style: none;
} }
button {
background: #4caf50;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
transition:
transform 0.08s ease,
box-shadow 0.2s ease,
background-color 0.25s ease;
}
button:hover {
background: #3d8b40;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
button:active {
background: #4caf50;
transform: scale(0.96) translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 超小屏幕适配 - 修复边框遮挡问题 */ /* 超小屏幕适配 - 修复边框遮挡问题 */
@media (max-width: 399px) { @media (max-width: 399px) {
.nav { .nav {
@@ -578,23 +603,6 @@ body,
cursor: pointer; cursor: pointer;
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
box-shadow: 0 2px #00000004; box-shadow: 0 2px #00000004;
transition: .8s;
opacity: 4;
}
.action-btn:hover {
background: #3d8b40;
}
.action-btn:active {
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.2);
opacity: 0;
}
.action-btn:active::after {
box-shadow: none;
opacity: 0.4;
transition: 0s;
} }
.stop-btn { .stop-btn {
@@ -680,22 +688,6 @@ select {
flex-wrap: wrap; flex-wrap: wrap;
} }
.timestamp-btn {
padding: 10px 20px;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
border: none;
cursor: pointer;
font-size: 0.95rem;
min-width: 120px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
/* 确保 CopyButton 也使用相同的样式 */ /* 确保 CopyButton 也使用相同的样式 */
.timestamp-controls .action-btn, .timestamp-controls .action-btn,
.timestamp-controls .stop-btn { .timestamp-controls .stop-btn {
@@ -705,19 +697,6 @@ select {
border-radius: 8px; border-radius: 8px;
font-weight: 500; font-weight: 500;
font-size: 0.95rem; font-size: 0.95rem;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.timestamp-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.timestamp-btn:active {
transform: translateY(0);
} }
.timestamp-status { .timestamp-status {
@@ -780,7 +759,6 @@ select {
gap: 8px; gap: 8px;
} }
.timestamp-btn,
.timestamp-controls .action-btn, .timestamp-controls .action-btn,
.timestamp-controls .stop-btn { .timestamp-controls .stop-btn {
width: auto; width: auto;
@@ -805,7 +783,6 @@ select {
gap: 4px; gap: 4px;
} }
.timestamp-btn,
.timestamp-controls .action-btn, .timestamp-controls .action-btn,
.timestamp-controls .stop-btn { .timestamp-controls .stop-btn {
padding: 6px 4px; padding: 6px 4px;
@@ -834,7 +811,6 @@ select {
gap: 3px; gap: 3px;
} }
.timestamp-btn,
.timestamp-controls .action-btn, .timestamp-controls .action-btn,
.timestamp-controls .stop-btn { .timestamp-controls .stop-btn {
padding: 5px 3px; padding: 5px 3px;
+12 -24
View File
@@ -1,20 +1,8 @@
export function formatDate(value, timezone = 'Asia/Shanghai') { export const formatWithZone = (
return (new Intl.DateTimeFormat('zh-CN', { timestamp: number | string,
year: 'numeric', zone = 'Asia/Shanghai',
month: 'numeric', unit = 'milliseconds',
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') => {
try { try {
const ms = unit === 'milliseconds' ? Number(timestamp) : Number(timestamp) * 1000; const ms = unit === 'milliseconds' ? Number(timestamp) : Number(timestamp) * 1000;
return new Intl.DateTimeFormat('zh-CN', { return new Intl.DateTimeFormat('zh-CN', {
@@ -29,18 +17,18 @@ export const formatWithZone = (timestamp, zone = 'Asia/Shanghai', unit = 'millis
} catch (e) { } catch (e) {
return '格式错误'; return '格式错误';
} }
} };
export const getTimeZoneOffset = (timeZone) => { export const getTimeZoneOffset = (timeZone: string) => {
const now = new Date(); const now = new Date();
const utc = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' })); const utc = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }));
const target = new Date(now.toLocaleString('en-US', { timeZone: timeZone })); const target = new Date(now.toLocaleString('en-US', { timeZone: timeZone }));
return target.getTime() - utc.getTime(); return target.getTime() - utc.getTime();
} };
export const formatWithDate = ( export const formatWithDate = (
date, date: string,
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone,
) => { ) => {
try { try {
// 创建日期对象 // 创建日期对象
@@ -64,6 +52,6 @@ export const formatWithDate = (
return dateObj.getTime(); return dateObj.getTime();
} }
} catch (error) { } catch (error) {
return '日期转换错误: ' + error.message; return '日期转换错误: ' + error;
}
} }
};