Files
i-tools/app/json-formatter/page.tsx
yfan 3d175d75af
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Sync to CNB / sync (push) Has been cancelled
Delete old workflow runs / del_runs (push) Has been cancelled
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
first commit
2026-01-30 16:57:44 +08:00

319 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState, useCallback } from 'react';
import {
Code, Minimize2, CheckCircle, Copy, FilePlus, Eraser, Check, XCircle
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner";
type Mode = 'format' | 'compress' | 'validate';
export default function JsonFormatterPage() {
const [mode, setMode] = useState<Mode>('format');
const [inputJson, setInputJson] = useState('');
const [outputJson, setOutputJson] = useState('');
const [isValid, setIsValid] = useState<boolean | null>(null);
const [errorMsg, setErrorMsg] = useState('');
const [charCount, setCharCount] = useState(0);
const validateJson = useCallback((jsonStr: string) => {
if (!jsonStr.trim()) {
setIsValid(null);
setErrorMsg('');
return null;
}
try {
const parsed = JSON.parse(jsonStr);
setIsValid(true);
setErrorMsg('');
return parsed;
} catch (err) {
const msg = err instanceof Error ? err.message : '未知错误';
setErrorMsg(msg);
setIsValid(false);
return null;
}
}, []);
const formatJson = useCallback(() => {
const parsed = validateJson(inputJson);
if (parsed !== null) {
const formatted = JSON.stringify(parsed, null, 2);
setOutputJson(formatted);
toast.success('格式化成功');
}
}, [inputJson, validateJson]);
const compressJson = useCallback(() => {
const parsed = validateJson(inputJson);
if (parsed !== null) {
const compressed = JSON.stringify(parsed);
setOutputJson(compressed);
toast.success('压缩成功');
}
}, [inputJson, validateJson]);
const validateOnly = useCallback(() => {
validateJson(inputJson);
if (inputJson.trim() && isValid) {
toast.success('JSON 格式正确');
}
}, [inputJson, validateJson, isValid]);
const handleAction = useCallback(() => {
switch (mode) {
case 'format':
formatJson();
break;
case 'compress':
compressJson();
break;
case 'validate':
validateOnly();
break;
}
}, [mode, formatJson, compressJson, validateOnly]);
const copyToClipboard = useCallback(async (text: string, type: string) => {
if (!text) {
toast.warning('没有可复制的内容');
return;
}
try {
await navigator.clipboard.writeText(text);
toast.success(`${type}已复制`);
} catch {
toast.error('复制失败');
}
}, []);
const clearAll = useCallback(() => {
setInputJson('');
setOutputJson('');
setIsValid(null);
setErrorMsg('');
setCharCount(0);
}, []);
const loadExample = useCallback(() => {
const example = {
name: "信奥工具箱",
version: "1.0.0",
features: ["JSON格式化", "压缩", "验证"],
config: {
theme: "cyan",
language: "zh-CN"
}
};
const exampleStr = JSON.stringify(example, null, 2);
setInputJson(exampleStr);
setCharCount(exampleStr.length);
validateJson(exampleStr);
}, [validateJson]);
const handleInputChange = useCallback((value: string) => {
setInputJson(value);
setCharCount(value.length);
if (value.trim()) {
validateJson(value);
} else {
setIsValid(null);
setErrorMsg('');
}
}, [validateJson]);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center justify-between border-b pb-4">
<div className="flex items-center space-x-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-cyan-500 to-blue-600 shadow-lg">
<Code className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">JSON </h1>
<p className="text-muted-foreground">JSON使</p>
</div>
</div>
<Tabs value={mode} onValueChange={(v) => setMode(v as Mode)} className="w-75">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="format"></TabsTrigger>
<TabsTrigger value="compress"></TabsTrigger>
<TabsTrigger value="validate"></TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Action Bar */}
<Card>
<CardContent className="flex flex-wrap items-center gap-4 p-4">
<Button
onClick={handleAction}
disabled={!inputJson.trim()}
className="gap-2"
>
{mode === 'format' && <Code className="h-4 w-4" />}
{mode === 'compress' && <Minimize2 className="h-4 w-4" />}
{mode === 'validate' && <CheckCircle className="h-4 w-4" />}
{mode === 'format' ? '格式化' : mode === 'compress' ? '压缩' : '验证'}
</Button>
<Button
variant="secondary"
onClick={compressJson}
disabled={!inputJson.trim() || isValid === false}
className="gap-2"
>
<Minimize2 className="h-4 w-4" />
</Button>
<Button
variant="outline"
onClick={validateOnly}
disabled={!inputJson.trim()}
className="gap-2"
>
<CheckCircle className="h-4 w-4" />
</Button>
<Button variant="ghost" onClick={loadExample} className="gap-2">
<FilePlus className="h-4 w-4" />
</Button>
<Button variant="ghost" onClick={clearAll} className="gap-2 text-destructive hover:text-destructive/90 hover:bg-destructive/10">
<Eraser className="h-4 w-4" />
</Button>
</CardContent>
</Card>
{/* Main Content */}
<div className="grid gap-6 lg:grid-cols-2">
{/* Input Panel */}
<Card className="flex flex-col h-full">
<CardHeader className="py-3 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium flex items-center gap-2">
JSON
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => copyToClipboard(inputJson, '输入内容')}>
<Copy className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</TooltipProvider>
</CardTitle>
{isValid !== null && (
<Badge variant={isValid ? "default" : "destructive"} className={isValid ? "bg-emerald-500 hover:bg-emerald-600" : ""}>
{isValid ? <Check className="h-3 w-3 mr-1" /> : <XCircle className="h-3 w-3 mr-1" />}
{isValid ? '格式正确' : '格式错误'}
</Badge>
)}
</CardHeader>
<CardContent className="flex-1 p-0 relative">
<Textarea
value={inputJson}
onChange={(e) => handleInputChange(e.target.value)}
placeholder="请输入JSON数据..."
className="min-h-125 h-full border-0 rounded-none focus-visible:ring-0 resize-none font-mono text-sm leading-relaxed p-4 bg-transparent"
/>
{errorMsg && (
<div className="absolute bottom-0 left-0 right-0 bg-destructive/10 text-destructive text-xs p-2 border-t border-destructive/20">
{errorMsg}
</div>
)}
</CardContent>
<div className="p-2 border-t bg-muted/30 text-xs text-muted-foreground flex justify-end">
: {charCount}
</div>
</Card>
{/* Output Panel */}
<Card className="flex flex-col h-full">
<CardHeader className="py-3 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => copyToClipboard(outputJson, '结果')}>
<Copy className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</TooltipProvider>
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-0 bg-muted/30">
<Textarea
value={outputJson}
readOnly
placeholder="处理结果将显示在这里..."
className="min-h-125 h-full border-0 rounded-none focus-visible:ring-0 resize-none font-mono text-sm leading-relaxed p-4 bg-transparent"
/>
</CardContent>
</Card>
</div>
{/* Usage Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>JSON结构</li>
<li></li>
<li>便</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>JSON语法正确性</li>
<li></li>
<li></li>
</ul>
</div>
</div>
<div className="mt-4 p-3 bg-muted rounded-md text-xs text-muted-foreground">
💡 null值
</div>
</CardContent>
</Card>
</div>
);
}