refactor: 重构 Timestamp 页面使用响应式 Hook 统一状态并全面迁移至 shadcn 样式

This commit is contained in:
雨霖铃
2026-05-22 21:03:48 +08:00
parent 94158f016b
commit 02aef0c7b1
4 changed files with 252 additions and 184 deletions
+49 -87
View File
@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import dayjs from '@/utils/dayjs';
import type { UnitType, ZoneType } from '@/config/pageTheme';
import { DATE_FORMAT } from '@/config/pageTheme';
@@ -6,148 +6,111 @@ import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { useContextMenuData } from '@/utils/useContextMenuData';
export interface UseTimestampConverterReturn {
// State
mode: 'ts2dt' | 'dt2ts';
tsInput: string;
dtInput: string;
input: string; // 统一为单一受控输入源
unit: UnitType;
zone: ZoneType;
result: string;
error: string;
// Actions
setMode: (mode: 'ts2dt' | 'dt2ts') => void;
setInput: (value: string) => void;
setUnit: (unit: UnitType) => void;
setZone: (zone: ZoneType) => void;
handleUseNow: (now: number) => void;
convert: () => void;
}
/**
* 判断输入是否为时间戳(纯数字或长度 >= 10 的数字字符串)
*/
function isTimestampLike(input: string): boolean {
const trimmed = input.trim();
if (!/^\d+$/.test(trimmed)) return false;
return trimmed.length >= 10;
return /^\d+$/.test(trimmed) && trimmed.length >= 10;
}
export function useTimestampConverter(): UseTimestampConverterReturn {
const { t } = useLazyTranslation('timestamp');
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<UnitType>('ms');
const [zone, setZone] = useState<ZoneType>('Asia/Shanghai');
const [result, setResult] = useState('');
const [error, setError] = useState('');
const convert = useCallback(() => {
// 1. 唯一受控源:不再区分 ts/dt,输入框在当前模式下展现的就是它的值
const [input, setInput] = useState(() => String(Date.now()));
// 2. 核心魔法:利用 useMemo 达成 0 延迟响应式转换 (Reactive Pipeline)
// 只要 input, mode, unit, zone 任何一个发生改变,结果和错误信息自动流出,废除 convert 按钮
const conversionPipeline = useMemo(() => {
const rawInput = input.trim();
if (!rawInput) return { result: '', error: '' };
if (mode === 'ts2dt') {
const rawInput = tsInput.trim();
if (!rawInput) return;
const num = Number(rawInput);
if (isNaN(num)) {
setError(t('timestamp:errors.invalidNumber'));
return;
return { result: '', error: t('timestamp:errors.invalidNumber') };
}
const d = unit === 'ms' ? dayjs(num) : dayjs.unix(num);
if (!d.isValid()) {
setError(t('timestamp:errors.invalidTimestamp'));
return;
return { result: '', error: t('timestamp:errors.invalidTimestamp') };
}
setError('');
setResult(d.tz(zone).format(DATE_FORMAT));
return { result: d.tz(zone).format(DATE_FORMAT), error: '' };
} else {
const rawInput = dtInput.trim();
if (!rawInput) return;
const d = dayjs.tz(rawInput, DATE_FORMAT, zone);
if (!d.isValid()) {
setError(t('timestamp:errors.invalidFormat'));
return;
return { result: '', error: t('timestamp:errors.invalidFormat') };
}
setError('');
const ms = d.valueOf();
setResult(unit === 'ms' ? String(ms) : String(Math.floor(ms / 1000)));
const outputTs = unit === 'ms' ? String(ms) : String(Math.floor(ms / 1000));
return { result: outputTs, error: '' };
}
}, [mode, tsInput, dtInput, unit, zone, t]);
}, [input, mode, unit, zone, t]);
// 处理右键菜单传递的数据
const handleContextMenuData = useCallback(
(payload: string) => {
const trimmed = payload.trim();
if (isTimestampLike(trimmed)) {
// 看起来是时间戳,切换到 ts2dt 模式
setMode('ts2dt');
setTsInput(trimmed);
// 如果是 13 位毫秒级时间戳,自动选择 ms 单位
const detectedUnit: UnitType = trimmed.length >= 13 ? 'ms' : 's';
setUnit(detectedUnit);
// 直接执行转换
const num = Number(trimmed);
if (!isNaN(num)) {
const d = detectedUnit === 'ms' ? dayjs(num) : dayjs.unix(num);
if (d.isValid()) {
setError('');
setResult(d.tz(zone).format(DATE_FORMAT));
}
}
const { result, error } = conversionPipeline;
// 3. 处理右键菜单联动(直接修改唯一的 input,衍生转换自动触发)
const handleContextMenuData = useCallback((payload: string) => {
const trimmed = payload.trim();
if (isTimestampLike(trimmed)) {
setMode('ts2dt');
const detectedUnit: UnitType = trimmed.length >= 13 ? 'ms' : 's';
setUnit(detectedUnit);
setInput(trimmed);
} else {
const d = dayjs(trimmed);
if (d.isValid()) {
setMode('dt2ts');
setInput(d.format(DATE_FORMAT));
} else {
// 尝试作为日期时间解析
const d = dayjs(trimmed);
if (d.isValid()) {
setMode('dt2ts');
setDtInput(d.format(DATE_FORMAT));
// 直接执行转换
const ms = d.valueOf();
setError('');
setResult(String(ms));
} else {
// 无法识别,作为时间戳处理
setMode('ts2dt');
setTsInput(trimmed);
}
setMode('ts2dt');
setInput(trimmed);
}
},
[zone],
);
}
}, []);
useContextMenuData({ featureKey: 'timestamp', onData: handleContextMenuData });
const handleUseNow = useCallback(
(now: number) => {
if (mode === 'ts2dt') {
setTsInput(String(unit === 'ms' ? now : Math.floor(now / 1000)));
setInput(String(unit === 'ms' ? now : Math.floor(now / 1000)));
} else {
setDtInput(dayjs(now).tz(zone).format(DATE_FORMAT));
setInput(dayjs(now).tz(zone).format(DATE_FORMAT));
}
},
[mode, unit, zone],
);
const handleSetMode = useCallback((newMode: 'ts2dt' | 'dt2ts') => {
setMode(newMode);
setError('');
setResult('');
}, []);
const setInput = useCallback(
(value: string) => {
if (mode === 'ts2dt') {
setTsInput(value);
} else {
setDtInput(value);
// 4. 尊享级连贯交互:切换 Mode 时,自动把上一个模式计算出的结果喂进输入框
// 比如:输入时间戳 -> 得到日期 -> 切换模式 -> 日期直接进入输入框,方便用户进行微调反向转换
const handleSetMode = useCallback(
(newMode: 'ts2dt' | 'dt2ts') => {
setMode(newMode);
if (result && !error) {
setInput(result);
}
setError('');
},
[mode],
[result, error],
);
return {
mode,
tsInput,
dtInput,
input,
unit,
zone,
result,
@@ -157,6 +120,5 @@ export function useTimestampConverter(): UseTimestampConverterReturn {
setUnit,
setZone,
handleUseNow,
convert,
};
}