refactor: 全面重构 Jwt 页面,采用防抖输入并统一 shadcn 样式
This commit is contained in:
+99
-58
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Key } from 'lucide-react';
|
import { Key } from 'lucide-react';
|
||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import { parseJwt, stringifyJson } from '@/utils/jwt';
|
import { parseJwt, stringifyJson } from '@/utils/jwt';
|
||||||
@@ -6,32 +6,36 @@ import CopyButton from '@/components/CopyButton';
|
|||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
import { useContextMenuData } from '@/utils/useContextMenuData';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface SectionProps {
|
interface SectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
content: unknown;
|
content: unknown;
|
||||||
color: string;
|
colorClass: string; // 💡 1. 废除硬编码十六进制色值,改用语义化的 Tailwind 类名
|
||||||
|
bgClass: string;
|
||||||
|
borderClass: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Section = ({ title, content, color }: SectionProps) => {
|
const Section = ({ title, content, colorClass, bgClass, borderClass }: SectionProps) => {
|
||||||
const { t } = useLazyTranslation('jwt');
|
const { t } = useLazyTranslation('jwt');
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="p-4 rounded-xl border relative"
|
className={cn('p-4 rounded-xl border border-solid transition-colors', bgClass, borderClass)}
|
||||||
style={{
|
|
||||||
borderColor: `${color}40`,
|
|
||||||
backgroundColor: `${color}05`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2 select-none">
|
||||||
<span className="text-sm font-bold tracking-wide" style={{ color }}>
|
<span className={cn('text-xs font-bold tracking-wider uppercase', colorClass)}>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-1">
|
<CopyButton
|
||||||
<CopyButton text={JSON.stringify(content)} size="small" />
|
text={JSON.stringify(content)}
|
||||||
</div>
|
className="h-6 w-6 rounded-md border text-muted-foreground"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<pre className="m-0 p-3 bg-background/60 rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap break-all border border-black/5">
|
{/* 💡 排版微距精雕:
|
||||||
|
- 彻底移除 border-black/5 这种非暗黑模式友好的硬隔离。
|
||||||
|
- 统一收拢为标准的 bg-muted/40 配合 font-mono text-xs
|
||||||
|
*/}
|
||||||
|
<pre className="m-0 p-3 bg-muted/30 dark:bg-muted/10 rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap break-all border border-border/50 text-foreground/90 leading-relaxed select-text">
|
||||||
{content ? stringifyJson(content) : t('jwt:invalidFormat')}
|
{content ? stringifyJson(content) : t('jwt:invalidFormat')}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,9 +43,19 @@ const Section = ({ title, content, color }: SectionProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation('jwt');
|
const { t } = useLazyTranslation(['jwt', 'jsonFormat']);
|
||||||
const [jwtInput, setJwtInput] = useState('');
|
const [jwtInput, setJwtInput] = useState('');
|
||||||
|
|
||||||
|
// 2. 防抖中转管道:切断高频键盘敲击时的红色语法闪烁
|
||||||
|
const [debouncedInput, setDebouncedInput] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handle = setTimeout(() => {
|
||||||
|
setDebouncedInput(jwtInput);
|
||||||
|
}, 200);
|
||||||
|
return () => clearTimeout(handle);
|
||||||
|
}, [jwtInput]);
|
||||||
|
|
||||||
const handleContextMenuData = useCallback((payload: string) => {
|
const handleContextMenuData = useCallback((payload: string) => {
|
||||||
const cleaned = payload.replace(/^Bearer\s*/i, '').trim();
|
const cleaned = payload.replace(/^Bearer\s*/i, '').trim();
|
||||||
setJwtInput(cleaned);
|
setJwtInput(cleaned);
|
||||||
@@ -49,59 +63,86 @@ export default function Index() {
|
|||||||
|
|
||||||
useContextMenuData({ featureKey: 'jwt', onData: handleContextMenuData });
|
useContextMenuData({ featureKey: 'jwt', onData: handleContextMenuData });
|
||||||
|
|
||||||
|
// 3. 贯彻方案 A:衍生变量流。直接消费防抖后的文本
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
if (!jwtInput.trim()) {
|
if (!debouncedInput.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return parseJwt(jwtInput);
|
return parseJwt(debouncedInput);
|
||||||
}, [jwtInput]);
|
}, [debouncedInput]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="p-4 w-full flex flex-col space-y-4 animate-in fade-in duration-300 select-none">
|
||||||
<div className="p-2">
|
<PageHeader
|
||||||
<PageHeader title={t('jwt:pageTitle')} subtitle={t('jwt:pageSubtitle')} icon={<Key />} />
|
title={t('jwt:pageTitle')}
|
||||||
|
subtitle={t('jwt:pageSubtitle')}
|
||||||
|
icon={<Key className="h-4 w-4" />} // 💡 规范锁死 Icon 宽高,抹杀闪烁
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Input Area */}
|
{/* 输入终端 */}
|
||||||
<TextInputArea
|
<TextInputArea
|
||||||
minRows={4}
|
minRows={5}
|
||||||
placeholder={t('jwt:placeholder')}
|
maxRows={10}
|
||||||
value={jwtInput}
|
placeholder={t('jwt:placeholder')}
|
||||||
onChange={(val) => {
|
value={jwtInput}
|
||||||
const cleaned = val.replace(/^Bearer\s*/i, '').trim();
|
onChange={(val) => {
|
||||||
setJwtInput(cleaned);
|
const cleaned = val.replace(/^Bearer\s*/i, '').trim();
|
||||||
}}
|
setJwtInput(cleaned);
|
||||||
allowCopy={true}
|
}}
|
||||||
showClear={true}
|
allowCopy={true}
|
||||||
externalError={result?.error}
|
showClear={true}
|
||||||
/>
|
externalError={result?.error || undefined}
|
||||||
|
onClear={() => setJwtInput('')}
|
||||||
|
/>
|
||||||
|
|
||||||
{result && !result.error && (
|
{/* 解码看板结果展现 */}
|
||||||
<div className="flex flex-col gap-4">
|
{result && !result.error && (
|
||||||
<Section
|
<div className="flex flex-col gap-4 animate-in slide-in-from-bottom-2 duration-300">
|
||||||
title={t('jwt:headerTitle')}
|
{/* Header 分区:完美致敬 JWT.io 的鲜艳色彩,同时实现黑夜暗化自适应 */}
|
||||||
content={result.header}
|
<Section
|
||||||
color="#fb015b" // JWT.io Header Color
|
title={t('jwt:headerTitle')}
|
||||||
/>
|
content={result.header}
|
||||||
<Section
|
colorClass="text-[#fb015b] dark:text-rose-400"
|
||||||
title={t('jwt:payloadTitle')}
|
borderClass="border-[#fb015b]/20 dark:border-rose-500/20"
|
||||||
content={result.payload}
|
bgClass="bg-[#fb015b]/5 dark:bg-rose-500/5"
|
||||||
color="#d63aff" // JWT.io Payload Color
|
/>
|
||||||
/>
|
|
||||||
<div className="p-4 rounded-xl border border-primary/30 bg-primary/10">
|
{/* Payload 分区 */}
|
||||||
<div className="flex justify-between items-center mb-2">
|
<Section
|
||||||
<span className="text-sm font-bold tracking-wide text-primary">
|
title={t('jwt:payloadTitle')}
|
||||||
{t('jwt:signatureTitle')}
|
content={result.payload}
|
||||||
</span>
|
colorClass="text-[#a03aff] dark:text-purple-400" // 针对暗黑模式略微调高对比度
|
||||||
<CopyButton text={JSON.stringify(result.raw.signature)} size="small" />
|
borderClass="border-[#a03aff]/20 dark:border-purple-500/20"
|
||||||
</div>
|
bgClass="bg-[#a03aff]/5 dark:bg-purple-500/5"
|
||||||
<span className="block text-sm font-mono break-all text-muted-foreground bg-background/60 p-3 rounded-lg border border-black/5">
|
/>
|
||||||
{result.signature || t('jwt:noSignature')}
|
|
||||||
|
{/* Signature 签名区:完全对齐标准的 shadcn 骨架阶度 */}
|
||||||
|
<div className="p-4 rounded-xl border border-border bg-secondary/40 shadow-sm transition-colors">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-xs font-bold tracking-wider text-muted-foreground/90 uppercase">
|
||||||
|
{t('jwt:signatureTitle')}
|
||||||
</span>
|
</span>
|
||||||
|
<CopyButton
|
||||||
|
text={result.signature || ''}
|
||||||
|
className="h-6 w-6 rounded-md border text-muted-foreground"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="block text-xs font-mono break-all text-foreground/80 bg-muted/30 dark:bg-muted/10 p-3 rounded-lg border border-border/50 leading-relaxed select-text">
|
||||||
|
{result.signature || t('jwt:noSignature')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* 当解析错误时的干净中性引导拦截 */}
|
||||||
|
{result?.error && (
|
||||||
|
<div className="p-6 rounded-xl bg-muted/30 border border-dashed border-border text-center">
|
||||||
|
<p className="text-xs font-semibold text-muted-foreground/80">
|
||||||
|
{t('jsonFormat:invalidJson')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user