import React, { useState, useEffect, useCallback, useMemo } from 'react'; import dayjs from '@/utils/dayjs'; import { TextField, Select, MenuItem, Paper, Stack, Typography, Box, IconButton, Snackbar, Alert, InputAdornment, alpha, Tooltip, Theme, } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; 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: 'grey.50', borderRadius: 3, transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)', '& fieldset': { border: 'none' }, '&:hover': { bgcolor: 'grey.100' }, '&.Mui-focused': { bgcolor: '#fff', boxShadow: (theme: Theme) => `0 0 0 2px ${alpha(theme.palette.primary.main, 0.2)}, 0 4px 12px rgba(0,0,0,0.03)`, }, '&.Mui-error': { boxShadow: (theme: Theme) => `0 0 0 2px ${alpha(theme.palette.error.main, 0.2)}`, }, }, '& .MuiInputBase-input': { py: 1.5, fontFamily: 'monospace' }, }; // ================= 子组件:实时时钟 ================= interface LiveClockProps { unit: UnitType; onCopy: (val: string) => void; onUseNow: (val: number) => void; } const LiveClock = React.memo(({ unit, onCopy, onUseNow }: 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} {unit} onUseNow(now)} sx={{ color: 'primary.main', transition: 'all 0.2s', '&:hover': { bgcolor: alpha('#2563eb', 0.08) } }} > onCopy(displayVal)} sx={{ color: 'grey.400', transition: 'all 0.2s', '&:hover': { color: 'primary.main', transform: 'scale(1.1)' } }} > ); }); 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 ( 转换结果 {copied ? : } ), }, }} sx={{ ...INPUT_STYLE, mb: 2, '& .MuiOutlinedInput-root': { ...INPUT_STYLE['& .MuiOutlinedInput-root'], bgcolor: alpha('#2563eb', 0.03), }, }} /> {/* 辅助信息预览 */} {[ { 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.primary', cursor: 'pointer', '&:hover': { color: 'primary.main', textDecoration: 'underline' } }} > {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 [snack, setSnack] = useState<{ open: boolean; msg: string }>({ open: false, msg: '' }); const copy = useCallback(async (text: string) => { try { await navigator.clipboard.writeText(text); setSnack({ open: true, msg: '已复制' }); } catch { setSnack({ open: true, msg: '复制失败' }); } }, []); 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]); // 智能实时转换 (Debounce Effect) 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 ( {/* 1. 实时时钟 */} { setUnit((u) => (u === 'ms' ? 's' : 'ms')); }} sx={{ position: 'absolute', right: 80, top: 4, color: 'grey.400', transition: 'transform 0.3s ease', '&:hover': { transform: 'rotate(180deg)', color: 'primary.main' } }} > {/* 2. 模式切换 */} {(['ts2dt', 'dt2ts'] as const).map((m) => ( ))} {/* 3. 输入与设置 */} { const val = e.target.value; if (mode === 'ts2dt') { setTsInput(val); } else { setDtInput(val); } setError(''); }} error={!!error} helperText={error} fullWidth sx={INPUT_STYLE} /> {/* 4. 转换操作 (作为手动确认) */} {/* 5. 结果展示 */} { setSnack((s) => ({ ...s, open: false })); }} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > {snack.msg} ); }