mirror of
https://github.com/snowykami/web-tools.git
synced 2025-09-03 19:56:27 +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",
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"next": "15.4.6",
|
"next": "15.4.6",
|
||||||
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -38,6 +38,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
highlight.js:
|
||||||
|
specifier: ^11.11.1
|
||||||
|
version: 11.11.1
|
||||||
html-to-image:
|
html-to-image:
|
||||||
specifier: ^1.11.13
|
specifier: ^1.11.13
|
||||||
version: 1.11.13
|
version: 1.11.13
|
||||||
@ -1544,6 +1547,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
html-to-image@1.11.13:
|
||||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||||
|
|
||||||
@ -3876,6 +3883,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
highlight.js@11.11.1: {}
|
||||||
|
|
||||||
html-to-image@1.11.13: {}
|
html-to-image@1.11.13: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
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",
|
title: "Rail Transit Guide",
|
||||||
description: "轨道交通导视生成器",
|
description: "轨道交通导视生成器",
|
||||||
href: "/rt-guide",
|
href: "/rt-guide",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "JSON formatter",
|
||||||
|
description: "JSON 格式化工具",
|
||||||
|
href: "/json-formatter",
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
Reference in New Issue
Block a user