import React, { useState, useEffect, useCallback, useMemo } from 'react'; import dayjs from '@/utils/dayjs'; import { TextField, Select, MenuItem, Stack, Typography, Box, IconButton, alpha, Tooltip, Theme, Container, Fade, Divider, } from '@mui/material'; import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import CheckIcon from '@mui/icons-material/Check'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; import Button from '@/components/Button'; // ================= 常量配置 ================= const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss'; const ZONES = ['Asia/Shanghai', 'America/New_York', 'Europe/London'] as const; type UnitType = 'ms' | 's'; type ZoneType = (typeof ZONES)[number]; const INPUT_STYLE = { '& .MuiOutlinedInput-root': { bgcolor: 'background.paper', borderRadius: 3.5, border: '1px solid', borderColor: 'grey.100', transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)', '& fieldset': { border: 'none' }, '&:hover': { borderColor: 'grey.300', bgcolor: 'grey.50' }, '&.Mui-focused': { bgcolor: '#fff', borderColor: 'primary.main', boxShadow: (theme: Theme) => `0 0 0 4px ${alpha(theme.palette.primary.main, 0.1)}`, }, '&.Mui-error': { borderColor: 'error.main', boxShadow: (theme: Theme) => `0 0 0 4px ${alpha(theme.palette.error.main, 0.1)}`, }, }, '& .MuiInputBase-input': { py: 1.4, px: 2, fontSize: '0.9rem', fontFamily: 'monospace', fontWeight: 600 }, }; // ================= 子组件:实时时钟 (优化交互) ================= interface LiveClockProps { unit: UnitType; onCopy: (val: string) => void; onUseNow: (val: number) => void; onUnitChange: (u: UnitType) => void; } const LiveClock = React.memo(({ unit, onCopy, onUseNow, onUnitChange }: LiveClockProps) => { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const t = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(t); }, []); const displayVal = useMemo(() => String(Math.floor(now / (unit === 'ms' ? 1 : 1000))), [now, unit]); return ( 当前时间戳 {displayVal} {/* 胶囊式单位切换器 */} {(['ms', 's'] as const).map((u) => ( onUnitChange(u)} sx={{ px: 1.2, py: 0.35, borderRadius: 2, cursor: 'pointer', fontSize: '0.65rem', fontWeight: 900, transition: 'all 0.2s', bgcolor: unit === u ? '#fff' : 'transparent', color: unit === u ? 'primary.main' : alpha('#2196f3', 0.4), boxShadow: unit === u ? '0 2px 6px rgba(33, 150, 243, 0.2)' : 'none', }} > {u.toUpperCase()} ))} onUseNow(now)} sx={{ color: 'primary.main', bgcolor: '#fff', boxShadow: '0 2px 4px rgba(0,0,0,0.05)', '&:hover': { bgcolor: 'primary.main', color: '#fff' } }} > onCopy(displayVal)} sx={{ color: 'grey.400', '&:hover': { color: 'primary.main' } }} > ); }); LiveClock.displayName = 'LiveClock'; // ================= 子组件:多维度结果展示 ================= interface ResultViewProps { result: string; mode: 'ts2dt' | 'dt2ts'; unit: UnitType; zone: string; onCopy: (val: string) => void; } const ResultView = React.memo(({ result, mode, unit, zone, onCopy }: ResultViewProps) => { const [copied, setCopied] = useState(false); const handleCopy = useCallback(() => { onCopy(result); setCopied(true); setTimeout(() => setCopied(false), 1500); }, [onCopy, result]); const extraInfo = useMemo(() => { if (!result) return null; const d = mode === 'ts2dt' ? dayjs(result, DATE_FORMAT).tz(zone) : (unit === 'ms' ? dayjs(Number(result)) : dayjs.unix(Number(result))); return { relative: d.fromNow(), iso: d.toISOString(), utc: d.utc().format(DATE_FORMAT) + ' UTC', }; }, [result, mode, zone, unit]); if (!result) return null; return ( 转换结果 {result} {copied ? : } {[ { label: '相对时间', value: extraInfo?.relative }, { label: 'ISO 8601', value: extraInfo?.iso }, { label: 'UTC 时间', value: extraInfo?.utc }, ].map((item) => ( {item.label} { if (item.value) onCopy(item.value); }} sx={{ fontFamily: 'monospace', color: 'text.secondary', fontWeight: 600, fontSize: '0.65rem', cursor: 'pointer', '&:hover': { color: 'primary.main' } }} > {item.value} ))} ); }); ResultView.displayName = 'ResultView'; // ================= 主页面组件 ================= export default function TimestampPage() { const [mode, setMode] = useState<'ts2dt' | 'dt2ts'>('ts2dt'); const [tsInput, setTsInput] = useState(() => String(Date.now())); const [dtInput, setDtInput] = useState(() => dayjs().format(DATE_FORMAT)); const [unit, setUnit] = useState('ms'); const [zone, setZone] = useState('Asia/Shanghai'); const [result, setResult] = useState(''); const [error, setError] = useState(''); const { snackbarProps, showMessage } = useSnackbar({ autoHideDuration: 1500 }); const copy = useCallback(async (text: string) => { try { await navigator.clipboard.writeText(text); showMessage('已复制', { severity: 'success' }); } catch { showMessage('复制失败', { severity: 'error' }); } }, [showMessage]); const convert = useCallback(() => { if (mode === 'ts2dt') { const rawInput = tsInput.trim(); if (!rawInput) return; const num = Number(rawInput); if (isNaN(num)) { setError('无效数字'); return; } const d = unit === 'ms' ? dayjs(num) : dayjs.unix(num); if (!d.isValid()) { setError('无效时间戳'); return; } setError(''); setResult(d.tz(zone).format(DATE_FORMAT)); } else { const rawInput = dtInput.trim(); if (!rawInput) return; const d = dayjs.tz(rawInput, DATE_FORMAT, zone); if (!d.isValid()) { setError('格式错误'); return; } setError(''); const ms = d.valueOf(); setResult(unit === 'ms' ? String(ms) : String(Math.floor(ms / 1000))); } }, [mode, tsInput, dtInput, unit, zone]); useEffect(() => { const timer = setTimeout(convert, 400); return () => clearTimeout(timer); }, [convert]); const handleUseNow = useCallback((now: number) => { if (mode === 'ts2dt') { setTsInput(String(unit === 'ms' ? now : Math.floor(now / 1000))); } else { setDtInput(dayjs(now).tz(zone).format(DATE_FORMAT)); } }, [mode, unit, zone]); return ( {/* Header with Icon */} 时间戳转换 Unix 毫秒数转换与格式化 {/* Live Clock Card */} {/* Mode Switcher */} {(['ts2dt', 'dt2ts'] as const).map((m) => ( { setMode(m); setError(''); setResult(''); }} sx={{ flex: 1, py: 1, textAlign: 'center', position: 'relative', zIndex: 1, cursor: 'pointer', fontWeight: 800, fontSize: '0.75rem', color: mode === m ? 'primary.main' : 'text.secondary', transition: 'color 0.3s' }} > {m === 'ts2dt' ? '时间戳 → 日期' : '日期 → 时间戳'} ))} {/* Input Area */} { const val = e.target.value; if (mode === 'ts2dt') { setTsInput(val); } else { setDtInput(val); } setError(''); }} error={!!error} helperText={error} fullWidth sx={INPUT_STYLE} /> {/* 优化后的单位选择按钮组 */} {(['ms', 's'] as const).map((u) => ( setUnit(u)} sx={{ flex: 1, py: 0.8, textAlign: 'center', borderRadius: 3, cursor: 'pointer', fontSize: '0.75rem', fontWeight: 800, transition: 'all 0.2s', bgcolor: unit === u ? '#fff' : 'transparent', color: unit === u ? 'primary.main' : 'text.disabled', boxShadow: unit === u ? '0 2px 8px rgba(0,0,0,0.05)' : 'none', }} > {u === 'ms' ? '毫秒 (ms)' : '秒 (s)'} ))} {/* Main Action */} {/* Result View */} ); }