From 11e941d5fe2c397fd0239fdff89766293179eb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Wed, 7 Jan 2026 21:42:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(components):=20=E6=B7=BB=E5=8A=A0=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=88=B3=E5=92=8C=E6=97=A5=E6=9C=9F=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了时间戳实时显示组件,支持毫秒/秒单位切换和自动更新 - 添加了日期时间转时间戳转换器,支持多种时区选择和单位转换 - 实现了时间戳转日期时间转换器,支持毫秒/秒单位和时区转换 - 为App.css添加了响应式样式和时间戳组件相关样式 - 为时间戳和转换组件添加了错误处理和输入验证功能 - 实现在index.css中重置标题样式以保持一致性 --- src/App.css | 362 +++++++++++++++++++++++++- src/components/DatetimeToTimestamp.js | 231 ++++++++++++---- src/components/TimestampExecution.js | 168 +++++++++--- src/components/TimestampToDatetime.js | 171 ++++++++++-- src/index.css | 5 + 5 files changed, 828 insertions(+), 109 deletions(-) diff --git a/src/App.css b/src/App.css index c41f43c..a3a7514 100644 --- a/src/App.css +++ b/src/App.css @@ -3,8 +3,10 @@ margin: 0 auto; padding: 20px; font-family: Arial, sans-serif; - width: 350px; + width: 100%; + min-width: 320px; height: 100vh; + box-sizing: border-box; } .nav { @@ -132,3 +134,361 @@ select { box-sizing: border-box; font-size: 14px; } + +/* 时间戳组件样式 */ +.timestamp-container { + padding: 20px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + margin: 20px 0; +} + +.timestamp-title { + color: #2c3e50; + font-size: 1.5rem; + margin-bottom: 20px; + text-align: center; + font-weight: 600; +} + +.timestamp-display { + display: flex; + align-items: center; /* 垂直居中 */ + justify-content: center; /* 横向居中 */ + gap: 10px; + margin-bottom: 25px; + padding: 15px; + background: white; + border-radius: 6px; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); + min-height: 80px; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; +} + +.timestamp-value { + font-size: 2.5rem; + font-weight: 700; + color: #3498db; + font-family: 'Courier New', monospace; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); + white-space: nowrap; + flex-shrink: 0; +} + +.timestamp-unit { + font-size: 1.2rem; + color: #7f8c8d; + font-weight: 500; +} + +.timestamp-controls { + display: flex; + gap: 12px; + justify-content: center; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.timestamp-btn { + padding: 10px 20px; + border-radius: 6px; + 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 也使用相同的样式 */ +.timestamp-controls .action-btn, +.timestamp-controls .stop-btn { + min-width: 120px; + height: 44px; + padding: 10px 20px; + border-radius: 6px; + font-weight: 500; + 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 { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.8); + border-radius: 6px; + font-size: 0.9rem; + border: 1px solid #e0e0e0; +} + +.status-indicator { + display: flex; + align-items: center; + gap: 6px; + font-weight: 500; +} + +.status-indicator.running { + color: #27ae60; +} + +.status-indicator.stopped { + color: #e74c3c; +} + +.unit-indicator { + color: #7f8c8d; + font-weight: 500; +} + +/* 响应式设计 */ +@media (max-width: 480px) { + .timestamp-container { + padding: 15px; + } + + .timestamp-display { + padding: 12px; + min-height: 70px; + align-items: center; /* 垂直居中 */ + justify-content: center; /* 横向居中 */ + } + + .timestamp-value { + font-size: 1.8rem; + line-height: 1.1; + } + + .timestamp-unit { + font-size: 1rem; + flex-shrink: 0; + } + + .timestamp-controls { + flex-direction: column; + align-items: center; + } + + .timestamp-btn, + .timestamp-controls .action-btn, + .timestamp-controls .stop-btn { + width: 100%; + max-width: 200px; + min-width: auto; + height: 44px; + } + + .timestamp-status { + flex-direction: column; + gap: 8px; + text-align: center; + } +} + +/* 超小屏幕适配 */ +@media (max-width: 360px) { + .timestamp-value { + font-size: 1.5rem; + } + + .timestamp-display { + padding: 10px; + align-items: center; /* 垂直居中 */ + justify-content: center; /* 横向居中 */ + } +} + +/* 日期时间转换器样式 */ +.datetime-converter { + padding: 20px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + margin: 20px 0; +} + +.converter-title { + color: #2c3e50; + font-size: 1.5rem; + margin-bottom: 20px; + text-align: center; + font-weight: 600; +} + +.converter-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.input-group, +.result-group { + display: flex; + gap: 12px; + align-items: center; +} + +.datetime-input, +.result-input { + flex: 1; + padding: 10px 14px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 0.95rem; + font-family: 'Courier New', monospace; + background: white; + transition: border-color 0.2s ease; + width: auto !important; + margin-bottom: 0 !important; +} + +.datetime-input:focus, +.result-input:focus { + outline: none; + border-color: #3498db; + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + +.timezone-select, +.unit-select { + padding: 10px 14px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 0.95rem; + background: white; + min-width: 180px; + cursor: pointer; + transition: border-color 0.2s ease; + width: auto !important; + margin-bottom: 0 !important; +} + +.timezone-select:focus, +.unit-select:focus { + outline: none; + border-color: #3498db; + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + +.action-group { + display: flex; + justify-content: center; +} + +.converter-btn { + padding: 10px 24px; + border-radius: 6px; + font-weight: 500; + font-size: 1rem; + min-width: 120px; +} + +.error-message { + padding: 10px 14px; + background: #ffeaea; + border: 1px solid #ffcccc; + border-radius: 6px; + color: #d32f2f; + font-size: 0.9rem; + text-align: center; + animation: fadeIn 0.3s ease; +} + +.result-info { + padding: 12px 16px; + background: rgba(255, 255, 255, 0.9); + border-radius: 6px; + border: 1px solid #e0e0e0; + font-size: 0.95rem; + text-align: center; +} + +.result-label { + font-weight: 600; + color: #2c3e50; +} + +.result-value { + font-family: 'Courier New', monospace; + font-weight: 700; + color: #3498db; + margin: 0 4px; +} + +.result-unit { + color: #7f8c8d; + font-weight: 500; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .input-group, + .result-group { + flex-direction: column; + align-items: stretch; + } + + .timezone-select, + .unit-select { + min-width: auto; + width: 100% !important; + } + + .datetime-converter { + padding: 16px; + } +} + +@media (max-width: 480px) { + .converter-title { + font-size: 1.3rem; + } + + .datetime-input, + .result-input, + .timezone-select, + .unit-select { + padding: 8px 12px; + font-size: 0.9rem; + } + + .converter-btn { + padding: 10px 20px; + font-size: 0.95rem; + } +} diff --git a/src/components/DatetimeToTimestamp.js b/src/components/DatetimeToTimestamp.js index b85d5f6..d56ffb4 100644 --- a/src/components/DatetimeToTimestamp.js +++ b/src/components/DatetimeToTimestamp.js @@ -1,63 +1,198 @@ import {formatWithDate, formatWithZone, TimezoneOptions} from "../utils/timeUtils"; -import {useMemo, useState} from "react"; +import {useState, useCallback} from "react"; + +/** + * 日期时间转时间戳组件 + * + * 功能特性: + * 1. 将日期时间字符串转换为时间戳 + * 2. 支持多种时区选择 + * 3. 支持毫秒和秒单位切换 + * 4. 提供输入验证和错误提示 + * 5. 实时单位转换 + * + * @component + * @example + * ```jsx + * + * ``` + * + * @returns {JSX.Element} 日期时间转时间戳组件 + */ + +// 常用时区列表 +const TIME_ZONE_LIST = [ + 'America/New_York', + 'America/Chicago', + 'America/Denver', + 'America/Los_Angeles', + 'America/Anchorage', + 'America/Honolulu', + 'Europe/London', + 'Europe/Paris', + 'Europe/Berlin', + 'Europe/Moscow', + 'Asia/Tokyo', + 'Asia/Shanghai', + 'Asia/Hong_Kong', + 'Asia/Singapore', + 'Asia/Dubai', + 'Asia/Kolkata', + 'Australia/Sydney', + 'Pacific/Auckland', +]; + +// 时间戳单位选项 +const TIMESTAMP_UNITS = [ + {value: 'milliseconds', label: '毫秒(ms)'}, + {value: 'seconds', label: '秒(s)'}, +]; export function DatetimeToTimestamp() { + /** @type {[string, function]} 输入的日期时间字符串 */ const [dateValue, setDateValue] = useState(() => formatWithZone(Date.now())); + + /** @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'); - const timeZoneList = useMemo(() => ['America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'America/Anchorage', 'America/Honolulu', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Moscow', 'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Singapore', 'Asia/Dubai', 'Asia/Kolkata', 'Australia/Sydney', 'Pacific/Auckland',], []); - const handleConvertDatetimeToTimestamp = () => { - const timestamp = unit === 'milliseconds' ? formatWithDate(dateValue, selectedZone) : Math.floor(formatWithDate(dateValue, selectedZone) / 1000); - setResult(timestamp); - }; + /** @type {[string, function]} 错误信息 */ + const [error, setError] = useState(''); - return (
-

日期时间转时间戳

-
-
- setDateValue(e.target.value)}/> - -
+ /** + * 转换日期时间为时间戳 + * @type {function(): void} + */ + const handleConvertDatetimeToTimestamp = useCallback(() => { + try { + setError(''); + const timestamp = formatWithDate(dateValue, selectedZone); -
- -
+ if (isNaN(timestamp)) { + setError('无效的日期时间格式'); + setResult(''); + return; + } + + const finalResult = unit === 'milliseconds' + ? timestamp + : Math.floor(timestamp / 1000); + + setResult(finalResult.toString()); + } catch (err) { + setError('转换失败,请检查输入格式'); + setResult(''); + } + }, [dateValue, selectedZone, unit]); + + /** + * 处理日期时间输入变化 + * @type {function(React.ChangeEvent): void} + */ + const handleDateChange = useCallback((e) => { + setDateValue(e.target.value); + setError(''); // 清除错误信息 + }, []); + + /** + * 处理时区选择变化 + * @type {function(React.ChangeEvent): void} + */ + const handleZoneChange = useCallback((e) => { + setSelectedZone(e.target.value); + }, []); + + /** + * 处理时间戳单位变化 + * @type {function(React.ChangeEvent): 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()); + } + } + }, [result]); + + return ( +
+

日期时间转时间戳

+ +
+
+ + +
+ + {error && ( +
+ ⚠️ {error} +
+ )} + +
+ +
+ +
+ + +
-
- -
-
); + ); } \ No newline at end of file diff --git a/src/components/TimestampExecution.js b/src/components/TimestampExecution.js index fba72fc..3b8e26e 100644 --- a/src/components/TimestampExecution.js +++ b/src/components/TimestampExecution.js @@ -1,45 +1,147 @@ -import {useEffect, useState} from "react"; +import {useEffect, useState, useRef, useCallback} from "react"; import CopyButton from "./CopyButton"; +/** + * 时间戳显示和执行组件 + * + * 功能特性: + * 1. 实时显示当前时间戳(毫秒/秒) + * 2. 支持毫秒和秒单位切换 + * 3. 支持启动/停止时间戳自动更新 + * 4. 提供复制时间戳功能 + * 5. 响应式设计和良好的可访问性 + * + * @component + * @example + * ```jsx + * + * ``` + * + * @returns {JSX.Element} 时间戳组件 + */ export function TimestampExecution() { - const [currentTimestamp, setCurrentTimestamp] = useState(Math.floor(Date.now())); + /** @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.MutableRefObject} 定时器引用,用于清理 */ + const timerRef = useRef(null); + + /** + * 计算显示的时间戳值 + * @type {number} + */ + const displayTimestamp = showMilliseconds + ? currentTimestamp + : Math.floor(currentTimestamp / 1000); + + /** + * 计算单位文本 + * @type {string} + */ + const unitText = showMilliseconds ? '毫秒' : '秒'; + + /** + * 定时更新时间戳的副作用 + * 根据 isRunningTimestamp 和 showMilliseconds 控制定时器的启停和间隔 + */ useEffect(() => { - const timerId = isRunningTimestamp ? setInterval(() => { - setCurrentTimestamp(Math.floor(Date.now())); - }, showMilliseconds ? 100 : 1000) : null; + // 清除之前的定时器 + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } - return () => clearInterval(timerId); + // 如果需要运行,创建新的定时器 + if (isRunningTimestamp) { + const interval = showMilliseconds ? 100 : 1000; + timerRef.current = setInterval(() => { + setCurrentTimestamp(Math.floor(Date.now())); + }, interval); + } + + // 清理函数 + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + }; }, [isRunningTimestamp, showMilliseconds]); - const toggleUnit = () => setShowMilliseconds(prev => !prev); - const toggleTimestamp = () => setIsRunningTimestamp(prev => !prev); + /** + * 切换时间戳显示单位(毫秒/秒) + * @type {function(): void} + */ + const toggleUnit = useCallback(() => { + setShowMilliseconds(prev => !prev); + }, []); - return (
-

当前时间戳

-

- {Math.floor(currentTimestamp / (showMilliseconds ? 1 : 1000))} - {showMilliseconds ? '毫秒' : '秒'} -

-
- - - + /** + * 切换时间戳自动更新状态(启动/停止) + * @type {function(): void} + */ + const toggleTimestamp = useCallback(() => { + setIsRunningTimestamp(prev => !prev); + }, []); + + /** + * 切换单位按钮的辅助文本 + * @type {string} + */ + const unitButtonLabel = showMilliseconds ? '切换为秒显示' : '切换为毫秒显示'; + + /** + * 启动/停止按钮的辅助文本 + * @type {string} + */ + const toggleButtonLabel = isRunningTimestamp ? '停止时间戳自动更新' : '开始时间戳自动更新'; + + /** + * 启动/停止按钮的显示文本 + * @type {string} + */ + const toggleButtonText = isRunningTimestamp ? '停止' : '开始'; + + return ( +
+
+ {displayTimestamp} + {unitText} +
+ +
+ + + + + +
-
) + ); } \ No newline at end of file diff --git a/src/components/TimestampToDatetime.js b/src/components/TimestampToDatetime.js index 5b50936..8f14bcc 100644 --- a/src/components/TimestampToDatetime.js +++ b/src/components/TimestampToDatetime.js @@ -1,66 +1,183 @@ -import {useMemo, useState} from "react"; +import {useState, useCallback} from "react"; import {formatWithZone, TimezoneOptions} from "../utils/timeUtils"; +/** + * 时间戳转日期时间组件 + * + * 功能特性: + * 1. 将时间戳转换为日期时间字符串 + * 2. 支持多种时区选择 + * 3. 支持毫秒和秒单位切换 + * 4. 提供输入验证和错误提示 + * + * @component + * @example + * ```jsx + * + * ``` + * + * @returns {JSX.Element} 时间戳转日期时间组件 + */ + +// 常用时区列表 +const TIME_ZONE_LIST = [ + 'America/New_York', + 'America/Chicago', + 'America/Denver', + 'America/Los_Angeles', + 'America/Anchorage', + 'America/Honolulu', + 'Europe/London', + 'Europe/Paris', + 'Europe/Berlin', + 'Europe/Moscow', + 'Asia/Tokyo', + 'Asia/Shanghai', + 'Asia/Hong_Kong', + 'Asia/Singapore', + 'Asia/Dubai', + 'Asia/Kolkata', + 'Australia/Sydney', + 'Pacific/Auckland', +]; + +// 时间戳单位选项 +const TIMESTAMP_UNITS = [ + {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'); - const timeZoneList = useMemo(() => ['America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'America/Anchorage', 'America/Honolulu', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Moscow', 'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Singapore', 'Asia/Dubai', 'Asia/Kolkata', 'Australia/Sydney', 'Pacific/Auckland',], []); - function handleConvertTimestampToDate() { - if (!timestampValue) { - setTimestampResult('请输入时间戳'); - return; + /** @type {[string, function]} 错误信息 */ + const [error, setError] = useState(''); + + /** + * 转换时间戳为日期时间 + * @type {function(): void} + */ + 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) { + setError('转换失败,请检查输入格式'); + setTimestampResult(''); } - - const result = formatWithZone(timestampValue, selectedZone, unit); - setTimestampResult(result); - } + }, [timestampValue, selectedZone, unit]); - const handleInputChange = (e) => setTimestampValue(e.target.value); + /** + * 处理时间戳输入变化 + * @type {function(React.ChangeEvent): void} + */ + const handleInputChange = useCallback((e) => { + setTimestampValue(e.target.value); + setError(''); // 清除错误信息 + }, []); + + /** + * 处理时区选择变化 + * @type {function(React.ChangeEvent): void} + */ + const handleZoneChange = useCallback((e) => { + setSelectedZone(e.target.value); + }, []); + + /** + * 处理时间戳单位变化 + * @type {function(React.ChangeEvent): void} + */ + const handleUnitChange = useCallback((e) => { + setUnit(e.target.value); + }, []); return ( -
-

时间戳转日期时间

-
-
+
+

时间戳转日期时间

+ +
+
-
-
-
+
diff --git a/src/index.css b/src/index.css index ec2585e..c0da152 100644 --- a/src/index.css +++ b/src/index.css @@ -11,3 +11,8 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +h1, h2, h3, h4, h5, h6 { + font-size: inherit; /* 先全部重置为继承大小 */ + font-weight: inherit; +}