feat(Timestamp): 重构时间戳转换功能,新增 ConverterForm 组件
- 在 Timestamp 页面中引入 ConverterForm 组件,简化输入和转换逻辑。 - 新增常量定义,包括模式选项、单位选项和输入占位符,提升代码可读性。 - 更新 useTimestampConverter 钩子,添加输入占位符支持。 - 引入 ToolCard 组件以统一样式和结构,优化页面布局。
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { MODE_OPTIONS, UNIT_OPTIONS, ZONES, type ZoneType } from '../constants';
|
||||||
|
import type { UseTimestampConverterReturn } from '../useTimestampConverter';
|
||||||
|
import ToolCard from './ToolCard';
|
||||||
|
|
||||||
|
type ConverterFormProps = Omit<UseTimestampConverterReturn, 'result' | 'handleUseNow'>;
|
||||||
|
|
||||||
|
export default function ConverterForm({
|
||||||
|
mode,
|
||||||
|
input,
|
||||||
|
unit,
|
||||||
|
zone,
|
||||||
|
error,
|
||||||
|
inputPlaceholder,
|
||||||
|
setMode,
|
||||||
|
setInput,
|
||||||
|
setUnit,
|
||||||
|
setZone,
|
||||||
|
}: ConverterFormProps) {
|
||||||
|
return (
|
||||||
|
<ToolCard className="gap-4">
|
||||||
|
<SwitchButtonGroup value={mode} options={MODE_OPTIONS} onChange={setMode} size="small" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={inputPlaceholder}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
className={cn(
|
||||||
|
'font-mono font-semibold h-10 shadow-sm placeholder:text-muted-foreground/60 focus:bg-background',
|
||||||
|
error && 'border-destructive focus-visible:ring-destructive',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <p className="text-destructive text-xs font-medium px-0.5">{error}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch gap-3 w-full">
|
||||||
|
<SwitchButtonGroup
|
||||||
|
value={unit}
|
||||||
|
options={UNIT_OPTIONS}
|
||||||
|
onChange={setUnit}
|
||||||
|
size="small"
|
||||||
|
className="sm:w-auto shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select value={zone} onValueChange={(v: string) => setZone(v as ZoneType)}>
|
||||||
|
<SelectTrigger className="flex-1 font-mono font-semibold h-9 shadow-sm bg-background">
|
||||||
|
<SelectValue placeholder="选择时区" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-64 font-mono">
|
||||||
|
{ZONES.map((z) => (
|
||||||
|
<SelectItem
|
||||||
|
key={z}
|
||||||
|
value={z}
|
||||||
|
className="text-xs font-semibold focus:bg-accent cursor-pointer"
|
||||||
|
>
|
||||||
|
{z}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</ToolCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { CARD_CLASS } from '../constants';
|
||||||
|
|
||||||
|
interface ToolCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolCard({ children, className, ...props }: ToolCardProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(CARD_CLASS, className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,24 @@ export type ModeType = 'ts2dt' | 'dt2ts';
|
|||||||
export type UnitType = 'ms' | 's';
|
export type UnitType = 'ms' | 's';
|
||||||
export type ZoneType = (typeof ZONES)[number];
|
export type ZoneType = (typeof ZONES)[number];
|
||||||
|
|
||||||
|
export const MODE_OPTIONS: { value: ModeType; label: string }[] = [
|
||||||
|
{ value: 'ts2dt', label: '时间戳转日期' },
|
||||||
|
{ value: 'dt2ts', label: '日期转时间戳' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const UNIT_OPTIONS: { value: UnitType; label: string }[] = [
|
||||||
|
{ value: 'ms', label: '毫秒' },
|
||||||
|
{ value: 's', label: '秒' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INPUT_PLACEHOLDERS: Record<ModeType, string> = {
|
||||||
|
ts2dt: '输入时间戳...',
|
||||||
|
dt2ts: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CARD_CLASS =
|
||||||
|
'p-5 rounded-xl border border-border bg-card text-card-foreground shadow-sm flex flex-col';
|
||||||
|
|
||||||
const DIVISORS: Record<UnitType, number> = { ms: 1, s: 1000 };
|
const DIVISORS: Record<UnitType, number> = { ms: 1, s: 1000 };
|
||||||
|
|
||||||
export function msToUnit(ms: number, unit: UnitType): number {
|
export function msToUnit(ms: number, unit: UnitType): number {
|
||||||
|
|||||||
@@ -1,108 +1,22 @@
|
|||||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
|
||||||
import { ZONES } from './constants';
|
|
||||||
import type { ModeType, UnitType, ZoneType } from './constants';
|
|
||||||
import LiveClock from './components/LiveClock';
|
import LiveClock from './components/LiveClock';
|
||||||
|
import ConverterForm from './components/ConverterForm';
|
||||||
import ResultView from './components/ResultView';
|
import ResultView from './components/ResultView';
|
||||||
|
import ToolCard from './components/ToolCard';
|
||||||
import { useTimestampConverter } from './useTimestampConverter';
|
import { useTimestampConverter } from './useTimestampConverter';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
|
|
||||||
const MODE_OPTIONS: { value: ModeType; label: string }[] = [
|
|
||||||
{ value: 'ts2dt', label: '时间戳转日期' },
|
|
||||||
{ value: 'dt2ts', label: '日期转时间戳' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const UNIT_OPTIONS: { value: UnitType; label: string }[] = [
|
|
||||||
{ value: 'ms', label: '毫秒' },
|
|
||||||
{ value: 's', label: '秒' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const {
|
const { result, handleUseNow, ...formProps } = useTimestampConverter();
|
||||||
mode,
|
|
||||||
input,
|
|
||||||
unit,
|
|
||||||
zone,
|
|
||||||
result,
|
|
||||||
error,
|
|
||||||
setMode,
|
|
||||||
setInput,
|
|
||||||
setUnit,
|
|
||||||
setZone,
|
|
||||||
handleUseNow,
|
|
||||||
} = useTimestampConverter();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground antialiased selection:bg-primary/20">
|
<div className="p-4 w-full flex flex-col space-y-4 select-none">
|
||||||
<div className="max-w-7xl mx-auto p-4 sm:p-6 space-y-4">
|
<LiveClock unit={formProps.unit} onUseNow={handleUseNow} />
|
||||||
<LiveClock unit={unit} onUseNow={handleUseNow} />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
|
||||||
<div className="p-5 rounded-xl border border-border bg-card text-card-foreground shadow-sm flex flex-col justify-between">
|
<ConverterForm {...formProps} />
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<SwitchButtonGroup
|
|
||||||
value={mode}
|
|
||||||
options={MODE_OPTIONS}
|
|
||||||
onChange={setMode}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<ToolCard className="h-full">
|
||||||
<Input
|
<ResultView result={result} showEmptyPlaceholder />
|
||||||
type="text"
|
</ToolCard>
|
||||||
placeholder={mode === 'ts2dt' ? '输入时间戳...' : 'YYYY-MM-DD HH:mm:ss'}
|
|
||||||
value={input}
|
|
||||||
onChange={(e) => setInput(e.target.value)}
|
|
||||||
className={cn(
|
|
||||||
'font-mono font-semibold h-10 shadow-sm placeholder:text-muted-foreground/60 focus:bg-background',
|
|
||||||
error && 'border-destructive focus-visible:ring-destructive',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{error && <p className="text-destructive text-xs font-medium px-0.5">{error}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-stretch gap-3 w-full">
|
|
||||||
<SwitchButtonGroup
|
|
||||||
value={unit}
|
|
||||||
options={UNIT_OPTIONS}
|
|
||||||
onChange={setUnit}
|
|
||||||
size="small"
|
|
||||||
className="sm:w-auto shrink-0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select value={zone} onValueChange={(v: string) => setZone(v as ZoneType)}>
|
|
||||||
<SelectTrigger className="flex-1 font-mono font-semibold h-9 shadow-sm bg-background">
|
|
||||||
<SelectValue placeholder="选择时区" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="max-h-64 font-mono">
|
|
||||||
{ZONES.map((z) => (
|
|
||||||
<SelectItem
|
|
||||||
key={z}
|
|
||||||
value={z}
|
|
||||||
className="text-xs font-semibold focus:bg-accent cursor-pointer"
|
|
||||||
>
|
|
||||||
{z}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-5 rounded-xl border border-border bg-card text-card-foreground shadow-sm h-full flex flex-col">
|
|
||||||
<ResultView result={result} showEmptyPlaceholder />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import dayjs from '@/utils/dayjs';
|
import dayjs from '@/utils/dayjs';
|
||||||
import type { UnitType, ZoneType, ModeType } from './constants';
|
import type { UnitType, ZoneType, ModeType } from './constants';
|
||||||
import { DATE_FORMAT, msToUnit, dayjsFromTimestamp } from './constants';
|
import { DATE_FORMAT, INPUT_PLACEHOLDERS, msToUnit, dayjsFromTimestamp } from './constants';
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
import { useContextMenuData } from '@/utils/useContextMenuData';
|
||||||
|
|
||||||
export interface UseTimestampConverterReturn {
|
export interface UseTimestampConverterReturn {
|
||||||
@@ -11,6 +11,7 @@ export interface UseTimestampConverterReturn {
|
|||||||
zone: ZoneType;
|
zone: ZoneType;
|
||||||
result: string;
|
result: string;
|
||||||
error: string;
|
error: string;
|
||||||
|
inputPlaceholder: string;
|
||||||
|
|
||||||
setMode: (mode: ModeType) => void;
|
setMode: (mode: ModeType) => void;
|
||||||
setInput: (value: string) => void;
|
setInput: (value: string) => void;
|
||||||
@@ -100,6 +101,7 @@ export function useTimestampConverter(): UseTimestampConverterReturn {
|
|||||||
zone,
|
zone,
|
||||||
result,
|
result,
|
||||||
error,
|
error,
|
||||||
|
inputPlaceholder: INPUT_PLACEHOLDERS[mode],
|
||||||
setMode: handleSetMode,
|
setMode: handleSetMode,
|
||||||
setInput,
|
setInput,
|
||||||
setUnit,
|
setUnit,
|
||||||
|
|||||||
Reference in New Issue
Block a user