mirror of
https://github.com/snowykami/web-tools.git
synced 2025-09-01 00:36:23 +00:00
feat: 添加 JSON 格式化工具组件及相关依赖
Some checks failed
Build and Push Container Image, Deploy to Host / build-and-push-and-deploy (push) Failing after 1m15s
Some checks failed
Build and Push Container Image, Deploy to Host / build-and-push-and-deploy (push) Failing after 1m15s
This commit is contained in:
@ -19,6 +19,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html-to-image": "^1.11.13",
|
||||
"lucide-react": "^0.539.0",
|
||||
"next": "15.4.6",
|
||||
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -38,6 +38,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
highlight.js:
|
||||
specifier: ^11.11.1
|
||||
version: 11.11.1
|
||||
html-to-image:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
@ -1544,6 +1547,10 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
highlight.js@11.11.1:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
html-to-image@1.11.13:
|
||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||
|
||||
@ -3876,6 +3883,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
html-to-image@1.11.13: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
232
src/app/json-formatter/page.tsx
Normal file
232
src/app/json-formatter/page.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import hljs from "highlight.js/lib/core";
|
||||
import json from "highlight.js/lib/languages/json";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
|
||||
// 注册 JSON 语言
|
||||
hljs.registerLanguage("json", json);
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Clipboard, Download, AlertTriangle } from "lucide-react";
|
||||
|
||||
interface FormatOptions {
|
||||
indent: number;
|
||||
sortKeys: boolean;
|
||||
colorize: boolean;
|
||||
}
|
||||
|
||||
export default function JSONFormatter() {
|
||||
const [input, setInput] = useState("");
|
||||
const [output, setOutput] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [options, setOptions] = useState<FormatOptions>({
|
||||
indent: 2,
|
||||
sortKeys: false,
|
||||
colorize: true,
|
||||
});
|
||||
const [highlightedCode, setHighlightedCode] = useState("");
|
||||
|
||||
const formatJSON = useCallback(() => {
|
||||
try {
|
||||
if (!input.trim()) {
|
||||
setOutput("");
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedJSON = JSON.parse(input);
|
||||
|
||||
if (options.sortKeys) {
|
||||
parsedJSON = sortObjectKeys(parsedJSON);
|
||||
}
|
||||
|
||||
const formatted = JSON.stringify(parsedJSON, null, options.indent);
|
||||
setOutput(formatted);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
setOutput("");
|
||||
}
|
||||
}, [input, options]);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.colorize && output) {
|
||||
const highlighted = hljs.highlight(output, { language: "json" }).value;
|
||||
setHighlightedCode(highlighted);
|
||||
}
|
||||
}, [output, options.colorize]);
|
||||
|
||||
const sortObjectKeys = (obj: any): any => {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(sortObjectKeys);
|
||||
}
|
||||
|
||||
return Object.keys(obj)
|
||||
.sort()
|
||||
.reduce((result: Record<string, any>, key) => {
|
||||
result[key] = sortObjectKeys(obj[key]);
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(output);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadJSON = () => {
|
||||
const blob = new Blob([output], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "formatted.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center p-4">
|
||||
<Card className="w-full max-w-6xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>JSON 格式化工具</CardTitle>
|
||||
<CardDescription>
|
||||
将 JSON 文本格式化,支持语法高亮、键值排序等功能
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label>配置选项</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="indent">缩进空格数</Label>
|
||||
<Input
|
||||
id="indent"
|
||||
type="number"
|
||||
min={0}
|
||||
max={8}
|
||||
value={options.indent}
|
||||
onChange={(e) =>
|
||||
setOptions({
|
||||
...options,
|
||||
indent: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="sort-keys"
|
||||
checked={options.sortKeys}
|
||||
onCheckedChange={(checked) =>
|
||||
setOptions({ ...options, sortKeys: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="sort-keys">键值排序</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="colorize"
|
||||
checked={options.colorize}
|
||||
onCheckedChange={(checked) =>
|
||||
setOptions({ ...options, colorize: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="colorize">语法高亮</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>输入 JSON</Label>
|
||||
<Textarea
|
||||
placeholder="在此输入 JSON 文本..."
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
formatJSON();
|
||||
}}
|
||||
className="h-[500px] font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label>格式化结果</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={copyToClipboard}
|
||||
disabled={!output}
|
||||
>
|
||||
<Clipboard className="w-4 h-4 mr-2" />
|
||||
复制
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={downloadJSON}
|
||||
disabled={!output}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{error ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<pre
|
||||
className={cn(
|
||||
"block p-4 rounded-lg border bg-muted h-[500px] overflow-auto font-mono"
|
||||
)}
|
||||
>
|
||||
{options.colorize ? (
|
||||
<code
|
||||
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||
/>
|
||||
) : (
|
||||
<code>{output}</code>
|
||||
)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
66
src/components/ui/alert.tsx
Normal file
66
src/components/ui/alert.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
@ -9,5 +9,10 @@ export const tools: Tool[] = [
|
||||
title: "Rail Transit Guide",
|
||||
description: "轨道交通导视生成器",
|
||||
href: "/rt-guide",
|
||||
},
|
||||
{
|
||||
title: "JSON formatter",
|
||||
description: "JSON 格式化工具",
|
||||
href: "/json-formatter",
|
||||
}
|
||||
];
|
||||
|
Reference in New Issue
Block a user