Files
jisuxiang/src/app/tools/json_formatter/page.tsx
T
2025-04-10 18:06:45 +08:00

1058 lines
39 KiB
TypeScript

'use client';
import React, { useState, useRef, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faCode, faCopy, faCheck, faCompress, faExpand,
faSearch, faTrash, faSync, faFolderOpen, faFolder, faSpinner,
faSave, faHistory, faTimes, faEdit, faStar, faTrashAlt
} from '@fortawesome/free-solid-svg-icons';
import dynamic from 'next/dynamic';
import BackToTop from '@/components/BackToTop';
import ToolHeader from '@/components/ToolHeader';
import { useLanguage } from '@/context/LanguageContext';
// 动态导入@uiw/react-json-view组件,避免SSR问题
const ReactJson = dynamic(() => import('@uiw/react-json-view'), { ssr: false });
// 定义历史记录条目类型
interface JsonHistoryItem {
id: string;
title: string;
json: string;
timestamp: number;
isFavorite?: boolean;
}
export default function JsonFormatter() {
const { t } = useLanguage();
const [jsonInput, setJsonInput] = useState('');
const jsonInputRef = useRef<HTMLTextAreaElement>(null);
const jsonPathInputRef = useRef<HTMLInputElement>(null);
// 状态管理
const [jsonOutput, setJsonOutput] = useState<string>('');
const [jsonPath, setJsonPath] = useState<string>('');
const [pathResult, setPathResult] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>('');
const [isCompressed, setIsCompressed] = useState<boolean>(false);
const [isFoldable, setIsFoldable] = useState<boolean>(true);
const [copied, setCopied] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLargeJson, setIsLargeJson] = useState<boolean>(false);
const [validationResult, setValidationResult] = useState<{
isValid: boolean;
message: string;
}>({ isValid: false, message: '' });
// 历史记录相关状态
const [historyItems, setHistoryItems] = useState<JsonHistoryItem[]>([]);
const [isHistoryOpen, setIsHistoryOpen] = useState<boolean>(false);
const [savingTitle, setSavingTitle] = useState<string>('');
const [isSaveModalOpen, setIsSaveModalOpen] = useState<boolean>(false);
const [editingItem, setEditingItem] = useState<JsonHistoryItem | null>(null);
// 参考值,确保能在格式化过程中保持加载状态
const processingRef = useRef<boolean>(false);
// 从本地存储加载历史记录
useEffect(() => {
const savedHistory = localStorage.getItem('json_formatter_history');
if (savedHistory) {
try {
const parsedHistory = JSON.parse(savedHistory) as JsonHistoryItem[];
setHistoryItems(parsedHistory);
} catch (e) {
console.error(t('tools.json_formatter.load_history_error'), e);
}
}
}, [t]);
// 保存历史记录到本地存储
const saveHistoryToLocalStorage = (items: JsonHistoryItem[]) => {
localStorage.setItem('json_formatter_history', JSON.stringify(items));
};
// 格式化JSON
const formatJson = (json: string, compress = false) => {
if (!json.trim()) {
setJsonOutput('');
setValidationResult({ isValid: false, message: '' });
return;
}
// 检查JSON大小
const isLarge = json.length > 100000;
setIsLargeJson(isLarge);
// 设置加载状态和处理参考值
setIsLoading(true);
processingRef.current = true;
// 使用setTimeout确保UI先更新,但不添加不必要的延迟
setTimeout(() => {
try {
// 处理可能的JS对象文本 (将单引号转为双引号)
const processedJson = json
.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2":') // 键名标准化
.replace(/'/g, '"'); // 单引号转双引号
try {
let parsed;
try {
parsed = JSON.parse(processedJson);
} catch (e) {
// 尝试使用eval处理JS对象(不安全,但为了更好的兼容性)
try {
// eslint-disable-next-line no-eval
parsed = eval('(' + json + ')');
} catch (/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
_) {
throw e; // 如果eval也失败了,抛出原始错误
}
}
// 根据模式输出不同格式
let formattedJson;
if (compress) {
formattedJson = JSON.stringify(parsed);
} else {
formattedJson = JSON.stringify(parsed, null, 2);
}
// 设置输出
setJsonOutput(formattedJson);
// 计算大小
const sizeKB = (formattedJson.length / 1024).toFixed(1);
const largeJsonMessage = t('tools.json_formatter.large_json_processed').replace('{size}', sizeKB);
setValidationResult({
isValid: true,
message: isLarge ? largeJsonMessage : t('tools.json_formatter.json_valid')
});
setErrorMessage('');
// 如果有JSONPath查询,执行查询
if (jsonPath) {
queryJsonPath(parsed, jsonPath);
}
// 完成后取消加载状态和处理参考值
setIsLoading(false);
processingRef.current = false;
} catch (error) {
if (error instanceof Error) {
setErrorMessage(error.message);
setValidationResult({ isValid: false, message: t('tools.json_formatter.json_invalid') });
setIsLoading(false);
processingRef.current = false;
}
}
} catch (error) {
if (error instanceof Error) {
setErrorMessage(error.message);
setValidationResult({ isValid: false, message: t('tools.json_formatter.json_invalid') });
setIsLoading(false);
processingRef.current = false;
}
}
}, 0);
};
// 取消正在进行的格式化操作
const cancelFormatting = () => {
processingRef.current = false;
setIsLoading(false);
};
// 清除组件卸载时可能的处理操作
useEffect(() => {
return () => {
processingRef.current = false;
};
}, []);
// 压缩/美化切换
const toggleCompression = () => {
setIsCompressed(!isCompressed);
formatJson(jsonInput, !isCompressed);
};
// 切换折叠功能
const toggleFoldable = () => {
setIsFoldable(!isFoldable);
};
// 复制结果到剪贴板
const copyToClipboard = () => {
if (jsonOutput) {
navigator.clipboard.writeText(jsonOutput)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(err => console.error(t('tools.json_formatter.copy_failed'), err));
}
};
// 清空输入
const clearInput = () => {
// 如果正在处理,先取消
if (isLoading) {
cancelFormatting();
}
setJsonInput('');
setJsonOutput('');
setErrorMessage('');
setValidationResult({ isValid: false, message: '' });
setPathResult('');
if (jsonInputRef.current) {
jsonInputRef.current.focus();
}
};
// 处理输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
setJsonInput(value);
};
// 处理粘贴事件
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
// 获取粘贴的内容
const pastedText = e.clipboardData.getData('text');
if (pastedText && pastedText.trim().length > 0) {
// 如果正在处理,先取消
if (isLoading) {
cancelFormatting();
}
// 更新输入内容
setJsonInput(pastedText);
// 立即设置加载状态但延迟执行格式化,确保UI更新
setIsLoading(true);
processingRef.current = true;
setTimeout(() => formatJson(pastedText, isCompressed), 100);
}
};
// 路径查询变化
const handlePathChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setJsonPath(value);
// 如果有有效的JSON和路径,执行查询
if (value && jsonOutput) {
try {
const parsed = JSON.parse(jsonOutput);
queryJsonPath(parsed, value);
} catch (/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
_error) {
// JSON解析错误,忽略
}
} else {
setPathResult('');
}
};
// 执行JSONPath查询
const queryJsonPath = (json: Record<string, unknown>, path: string) => {
if (!path) {
setPathResult('');
return;
}
try {
// 简单的路径解析,支持点符号和方括号
const segments = path
.replace(/\[(\w+)\]/g, '.$1') // 将[abc]转换为.abc
.replace(/^\./, '') // 移除开头的点
.split('.');
let result: unknown = json;
for (const segment of segments) {
if (typeof result === 'object' && result !== null && segment in result) {
result = (result as Record<string, unknown>)[segment];
} else {
throw new Error(`路径 '${path}' 不存在`);
}
}
// 格式化结果
if (typeof result === 'object' && result !== null) {
setPathResult(JSON.stringify(result, null, 2));
} else {
setPathResult(String(result));
}
} catch (error) {
if (error instanceof Error) {
setPathResult(`查询错误: ${error.message}`);
}
}
};
// 加载示例JSON
const loadExample = () => {
// 如果正在处理,先取消
if (isLoading) {
cancelFormatting();
}
const example = {
name: "极速箱",
version: "1.0.0",
description: "高效开发工具集成平台",
author: {
name: "JiSuXiang开发团队",
email: "[email protected]"
},
features: [
"JSON格式化与验证",
"时间戳转换",
"编码转换工具",
"正则表达式测试"
],
statistics: {
tools: 2400,
users: 10000000,
rating: 4.9
},
isOpenSource: true,
lastUpdate: "2063-12-01T08:00:00Z"
};
const exampleJson = JSON.stringify(example);
setJsonInput(exampleJson);
formatJson(exampleJson, isCompressed);
};
// 重新格式化
const reformat = () => {
// 如果正在处理,先取消
if (isLoading) {
cancelFormatting();
}
formatJson(jsonInput, isCompressed);
};
// 渲染可折叠的JSON
const renderFoldableJson = (jsonStr: string) => {
if (!jsonStr) return null;
try {
const jsonObj = JSON.parse(jsonStr);
return (
<div className="json-viewer-theme">
<ReactJson
value={jsonObj}
style={{
backgroundColor: 'transparent',
fontFamily: 'monospace',
fontSize: '0.95rem',
lineHeight: '1.7',
color: 'rgb(var(--color-text-primary))'
}}
displayObjectSize={true}
enableClipboard={false}
displayDataTypes={false}
/>
</div>
);
} catch (/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
_error) {
// 如果解析失败,回退到普通模式
return <pre className="whitespace-pre-wrap m-0">{jsonStr}</pre>;
}
};
// 处理输入/输出区域的样式
const getTextareaClasses = (hasError: boolean) => {
return `w-full p-4 font-mono text-sm rounded-md border ${
hasError ? 'border-[rgb(var(--color-error))]' : 'border-[rgba(var(--color-primary),0.2)]'
} focus:outline-none focus:border-[rgb(var(--color-primary))] focus:ring-1 focus:ring-[rgb(var(--color-primary))] min-h-[350px] transition-all bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-primary))]`;
};
// 右侧输出区域样式
const outputAreaClasses = `${getTextareaClasses(false)} overflow-auto relative flex-grow json-output-area`;
// 工具栏按钮样式
const toolbarButtonClass = "px-3 py-1.5 rounded text-sm flex items-center gap-1 transition-all border border-transparent hover:border-[rgba(var(--color-primary),0.3)]";
// 历史记录项目样式
const historyItemClass = "p-3 rounded-md border border-[rgba(var(--color-primary),0.15)] hover:border-[rgba(var(--color-primary),0.4)] transition-all cursor-pointer bg-[rgb(var(--color-bg-secondary))] flex justify-between items-center mb-2";
// 自动格式化(仅在首次输入更改后)
useEffect(() => {
if (jsonInput) {
formatJson(jsonInput, isCompressed);
}
}, []);
// 保存当前JSON到历史记录
const saveToHistory = () => {
if (!jsonOutput || !jsonOutput.trim()) return;
if (editingItem) {
// 更新现有项目
const updatedItem = {
...editingItem,
title: savingTitle || `未命名 ${new Date().toLocaleString()}`,
json: jsonOutput,
timestamp: Date.now()
};
const updatedHistory = historyItems.map(item =>
item.id === editingItem.id ? updatedItem : item
);
setHistoryItems(updatedHistory);
saveHistoryToLocalStorage(updatedHistory);
} else {
// 创建新项目
const newItem: JsonHistoryItem = {
id: Date.now().toString(),
title: savingTitle || `未命名 ${new Date().toLocaleString()}`,
json: jsonOutput,
timestamp: Date.now()
};
const updatedHistory = [newItem, ...historyItems];
setHistoryItems(updatedHistory);
saveHistoryToLocalStorage(updatedHistory);
}
// 重置状态
setSavingTitle('');
setIsSaveModalOpen(false);
setEditingItem(null);
};
// 加载历史记录中的JSON
const loadFromHistory = (item: JsonHistoryItem) => {
setJsonInput(item.json);
formatJson(item.json, isCompressed);
setIsHistoryOpen(false);
};
// 删除历史记录项目
const deleteHistoryItem = (id: string, e: React.MouseEvent) => {
e.stopPropagation(); // 防止触发父元素的点击事件
const updatedHistory = historyItems.filter(item => item.id !== id);
setHistoryItems(updatedHistory);
saveHistoryToLocalStorage(updatedHistory);
};
// 编辑历史记录项目标题
const startEditingTitle = (item: JsonHistoryItem, e: React.MouseEvent) => {
e.stopPropagation(); // 防止触发父元素的点击事件
setEditingItem(item);
setSavingTitle(item.title);
setIsSaveModalOpen(true);
};
// 切换收藏状态
const toggleFavorite = (id: string, e: React.MouseEvent) => {
e.stopPropagation(); // 防止触发父元素的点击事件
const updatedHistory = historyItems.map(item => {
if (item.id === id) {
return { ...item, isFavorite: !item.isFavorite };
}
return item;
});
setHistoryItems(updatedHistory);
saveHistoryToLocalStorage(updatedHistory);
};
return (
<div className="min-h-screen flex flex-col max-w-[1440px] mx-auto p-4 md:p-6">
{/* 使用自定义样式 */}
<style jsx global>{`
.json-viewer-theme {
--w-rjv-border-left-color: rgba(var(--color-primary), 0.5);
--w-rjv-border-left-width: 1px;
--w-rjv-color: rgb(var(--color-text-primary));
--w-rjv-key-string: rgb(var(--color-primary-light));
--w-rjv-type-string-color: rgb(var(--color-success));
--w-rjv-type-int-color: rgb(var(--color-warning));
--w-rjv-type-float-color: rgb(var(--color-warning));
--w-rjv-type-boolean-color: rgb(var(--color-primary-hover));
--w-rjv-arrow-color: rgb(var(--color-text-secondary));
--w-rjv-background-color: transparent;
}
/* 增强JSON展示区域样式 */
.json-output-area {
font-size: 0.95rem !important;
letter-spacing: 0.01em;
background-color: rgb(var(--color-bg-secondary)) !important;
}
.json-output-area pre {
font-size: 0.95rem !important;
color: rgb(var(--color-text-primary)) !important;
}
/* 增强JSON视图组件中的文本样式 */
.json-viewer-theme > div {
font-size: 0.95rem !important;
letter-spacing: 0.01em;
}
/* 优化字符串、数字和布尔值显示 */
.json-viewer-theme [data-type="string"] {
color: rgb(var(--color-success)) !important;
opacity: 0.95;
}
.json-viewer-theme [data-type="number"],
.json-viewer-theme [data-type="int"] {
color: rgb(var(--color-warning)) !important;
opacity: 0.95;
}
.json-viewer-theme [data-type="boolean"] {
color: rgb(var(--color-primary-hover)) !important;
opacity: 0.95;
}
/* 优化键名显示 */
.json-viewer-theme [data-key] {
color: rgb(var(--color-primary-light)) !important;
font-weight: 500;
opacity: 0.9;
}
/* 优化项目计数显示 */
.json-viewer-theme .w-rjv-objects {
color: rgb(var(--color-text-secondary)) !important;
opacity: 0.9;
font-size: 0.9em;
}
/* 优化同级元素对齐 */
.json-viewer-theme .w-rjv-object-key,
.json-viewer-theme .w-rjv-array-key {
position: relative;
display: inline-flex;
min-width: 1em;
}
/* 使展开/折叠按钮更加合理 */
.json-viewer-theme .w-rjv-item {
position: relative;
}
.json-viewer-theme .w-rjv-arrow {
position: absolute;
left: -15px;
top: 3px;
}
/* 减弱引号的显示 */
.json-viewer-theme .w-rjv-qoute {
opacity: 0.5;
color: rgb(var(--color-text-tertiary)) !important;
}
/* 增强非折叠模式下的JSON显示 */
.json-output-area pre.text-base {
padding: 0.5rem;
}
/* 增强JSONPath查询结果区域 */
.card pre {
font-size: 0.95rem !important;
color: rgb(var(--color-text-primary)) !important;
}
/* 暗色主题特定样式 */
[data-theme="dark"] .json-output-area pre {
text-shadow: none;
}
[data-theme="dark"] .json-viewer-theme [data-type="string"] {
text-shadow: none;
}
[data-theme="dark"] .json-viewer-theme [data-type="number"],
[data-theme="dark"] .json-viewer-theme [data-type="int"] {
text-shadow: none;
}
[data-theme="dark"] .json-viewer-theme [data-type="boolean"] {
text-shadow: none;
}
/* 历史记录面板样式 */
.history-panel {
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: 350px;
background-color: rgb(var(--color-bg-primary));
border-left: 1px solid rgba(var(--color-primary), 0.2);
z-index: 50;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1);
padding: 1rem;
overflow-y: auto;
}
.history-panel.open {
transform: translateX(0);
}
.history-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 40;
backdrop-filter: blur(2px);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease-in-out;
}
.history-overlay.open {
opacity: 1;
pointer-events: auto;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 60;
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(3px);
}
.modal-container {
background-color: rgb(var(--color-bg-primary));
border-radius: 0.5rem;
padding: 1.5rem;
width: 90%;
max-width: 500px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(var(--color-primary), 0.2);
}
/* 收藏图标样式 */
.favorite-icon {
color: rgba(var(--color-text-tertiary));
transition: color 0.2s ease-in-out;
}
.favorite-icon.active {
color: rgb(var(--color-warning));
}
`}</style>
{/* 使用 ToolHeader 组件 */}
<ToolHeader
toolCode="json_formatter"
icon={faCode}
title={t('tools.json_formatter.title')}
description={t('tools.json_formatter.description')}
/>
{/* 验证状态显示 */}
<div className="text-center mb-4">
{validationResult.message && !isLoading && (
<span className={validationResult.isValid ? 'text-[rgb(var(--color-success))]' : 'text-[rgb(var(--color-error))]'}>
{validationResult.message}
</span>
)}
{isLoading && (
<span className="flex items-center justify-center" style={{color: 'rgb(var(--color-text-tertiary))'}}>
<FontAwesomeIcon icon={faSpinner} className="animate-spin mr-1" />
<span>{isLargeJson ? t('tools.json_formatter.processing_large_json') : t('tools.json_formatter.parsing_json')}</span>
</span>
)}
</div>
{/* 工具栏 */}
<div className="flex flex-wrap gap-2 mb-4">
<button
className={`${toolbarButtonClass} bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-secondary))] hover:text-[rgb(var(--color-text-primary))]`}
onClick={toggleCompression}
disabled={isLoading}
>
<FontAwesomeIcon icon={isCompressed ? faExpand : faCompress} />
<span>{isCompressed ? t('tools.json_formatter.beautify') : t('tools.json_formatter.compress')}</span>
</button>
<button
className={`${toolbarButtonClass} bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-secondary))] hover:text-[rgb(var(--color-text-primary))]`}
onClick={toggleFoldable}
disabled={isLoading || !jsonOutput}
>
<FontAwesomeIcon icon={isFoldable ? faFolder : faFolderOpen} />
<span>{isFoldable ? t('tools.json_formatter.normal_mode') : t('tools.json_formatter.fold_mode')}</span>
</button>
<button
className={`${toolbarButtonClass} bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-secondary))] hover:text-[rgb(var(--color-text-primary))]`}
onClick={copyToClipboard}
disabled={!jsonOutput || isLoading}
>
<FontAwesomeIcon icon={copied ? faCheck : faCopy} />
<span>{copied ? t('common.copySuccess') : t('tools.json_formatter.copy')}</span>
</button>
<button
className={`${toolbarButtonClass} bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-secondary))] hover:text-[rgb(var(--color-text-primary))]`}
onClick={clearInput}
disabled={isLoading}
>
<FontAwesomeIcon icon={faTrash} />
<span>{t('tools.json_formatter.clear')}</span>
</button>
<button
className={`${toolbarButtonClass} bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-secondary))] hover:text-[rgb(var(--color-text-primary))]`}
onClick={loadExample}
disabled={isLoading}
>
<FontAwesomeIcon icon={faCode} />
<span>{t('tools.json_formatter.load_example')}</span>
</button>
<button
className={`${toolbarButtonClass} bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-secondary))] hover:text-[rgb(var(--color-text-primary))]`}
onClick={reformat}
disabled={!jsonInput || isLoading}
>
<FontAwesomeIcon icon={faSync} className={isLoading ? 'animate-spin' : ''} />
<span>{isLoading ? t('tools.json_formatter.processing') : t('tools.json_formatter.reformat')}</span>
</button>
{/* 新增的保存和历史记录按钮 */}
<button
className={`${toolbarButtonClass} bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-secondary))] hover:text-[rgb(var(--color-text-primary))]`}
onClick={() => setIsSaveModalOpen(true)}
disabled={!jsonOutput || isLoading}
>
<FontAwesomeIcon icon={faSave} />
<span>{t('tools.json_formatter.save')}</span>
</button>
<button
className={`${toolbarButtonClass} bg-[rgb(var(--color-bg-secondary))] text-[rgb(var(--color-text-secondary))] hover:text-[rgb(var(--color-text-primary))]`}
onClick={() => setIsHistoryOpen(true)}
>
<FontAwesomeIcon icon={faHistory} />
<span>{t('tools.json_formatter.history')}</span>
</button>
{isLoading && (
<button
className="px-3 py-1.5 rounded text-sm flex items-center gap-1 transition-all border"
style={{
backgroundColor: 'rgb(var(--color-bg-secondary))',
color: 'rgb(var(--color-text-secondary))',
borderColor: 'rgb(var(--color-primary))',
opacity: 0.8
}}
onClick={cancelFormatting}
>
<FontAwesomeIcon icon={faSpinner} className="animate-spin" />
<span>{t('tools.json_formatter.cancel')}</span>
</button>
)}
</div>
{/* 主内容区 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6 flex-grow">
{/* 输入区域 */}
<div className="flex flex-col h-full">
<div className="mb-2 flex items-center justify-between">
<label className="text-sm font-medium" style={{color: 'rgb(var(--color-text-secondary))'}}>{t('tools.json_formatter.input_json')}</label>
<div className="text-xs" style={{color: 'rgb(var(--color-text-tertiary))'}}>{t('tools.json_formatter.paste_json_here')}</div>
</div>
<div className="flex-grow flex flex-col">
<textarea
ref={jsonInputRef}
className={getTextareaClasses(!!errorMessage) + " flex-grow"}
value={jsonInput}
onChange={handleInputChange}
onBlur={() => jsonInput && !isLoading && formatJson(jsonInput, isCompressed)}
onPaste={handlePaste}
placeholder={t('tools.json_formatter.paste_json_placeholder')}
disabled={isLoading}
/>
{errorMessage && (
<div className="mt-2 text-sm text-[rgb(var(--color-error))]">
{errorMessage}
</div>
)}
</div>
</div>
{/* 输出区域 */}
<div className="flex flex-col h-full">
<div className="mb-2 flex items-center justify-between">
<label className="text-sm font-medium" style={{color: 'rgb(var(--color-text-secondary))'}}>{t('tools.json_formatter.output')}</label>
<div className="text-xs" style={{color: 'rgb(var(--color-text-tertiary))'}}>
{jsonOutput && !isLoading && `${jsonOutput.length.toLocaleString()} ${t('tools.json_formatter.characters')}`}
{isLoading && (
<span className="flex items-center">
<FontAwesomeIcon icon={faSpinner} className="animate-spin mr-1" />
<span>{isLargeJson ? t('tools.json_formatter.processing_large_json') : t('tools.json_formatter.processing')}</span>
</span>
)}
</div>
</div>
<div className={outputAreaClasses}>
{isLoading ? (
<div className="absolute inset-0 flex items-center justify-center backdrop-blur-sm"
style={{backgroundColor: 'rgba(var(--color-bg-secondary), 0.7)'}}>
<div className="flex flex-col items-center space-y-2">
<FontAwesomeIcon icon={faSpinner} className="animate-spin text-2xl" style={{color: 'rgb(var(--color-primary))'}} />
<span style={{color: 'rgb(var(--color-text-secondary))'}} className="text-center">
{isLargeJson ?
t('tools.json_formatter.processing_large_json_message') :
t('tools.json_formatter.parsing_json')}
</span>
<button
className="mt-3 px-3 py-1.5 text-xs rounded transition-all border"
style={{
backgroundColor: 'rgb(var(--color-bg-secondary))',
color: 'rgb(var(--color-text-secondary))',
borderColor: 'rgba(var(--color-primary), 0.5)'
}}
onClick={cancelFormatting}
>
{t('tools.json_formatter.cancel_processing')}
</button>
</div>
</div>
) : isFoldable ? (
renderFoldableJson(jsonOutput)
) : (
<pre className="whitespace-pre-wrap m-0 text-base leading-7" style={{color: 'rgb(var(--color-text-primary))'}}>{jsonOutput}</pre>
)}
</div>
</div>
</div>
{/* JSONPath查询 */}
<div className="mt-4 card p-4">
<div className="mb-2">
<label className="text-sm font-medium" style={{color: 'rgb(var(--color-text-secondary))'}}>{t('tools.json_formatter.jsonpath_query')}</label>
<div className="text-xs mt-1" style={{color: 'rgb(var(--color-text-tertiary))'}}>
{t('tools.json_formatter.enter_jsonpath')}
</div>
</div>
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<FontAwesomeIcon
icon={faSearch}
className="absolute left-3 top-1/2 -translate-y-1/2"
style={{color: 'rgb(var(--color-text-tertiary))'}}
/>
<input
ref={jsonPathInputRef}
type="text"
value={jsonPath}
onChange={handlePathChange}
placeholder={t('tools.json_formatter.jsonpath_placeholder')}
className="search-input pl-10"
disabled={isLoading || !jsonOutput}
/>
</div>
</div>
<div className="flex-1">
<div className="p-3 rounded-md min-h-[40px] text-sm"
style={{
backgroundColor: 'rgb(var(--color-bg-secondary))',
color: 'rgb(var(--color-text-secondary))'
}}>
<pre className="whitespace-pre-wrap">{pathResult || t('tools.json_formatter.query_result_placeholder')}</pre>
</div>
</div>
</div>
</div>
{/* 历史记录侧边栏 */}
<div className={`history-overlay ${isHistoryOpen ? 'open' : ''}`} onClick={() => setIsHistoryOpen(false)}></div>
<div className={`history-panel ${isHistoryOpen ? 'open' : ''}`}>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium" style={{color: 'rgb(var(--color-text-primary))'}}>{t('tools.json_formatter.history')}</h3>
<button
className="p-2 rounded-full hover:bg-[rgba(var(--color-primary),0.1)]"
onClick={() => setIsHistoryOpen(false)}
>
<FontAwesomeIcon icon={faTimes} style={{color: 'rgb(var(--color-text-secondary))'}} />
</button>
</div>
{historyItems.length === 0 ? (
<div className="text-center py-8" style={{color: 'rgb(var(--color-text-tertiary))'}}>
<p>{t('tools.json_formatter.no_saved_records')}</p>
<p className="text-sm mt-2">{t('tools.json_formatter.save_first_record')}</p>
</div>
) : (
<div>
{/* 收藏的项目 */}
{historyItems.some(item => item.isFavorite) && (
<div className="mb-4">
<h4 className="text-sm font-medium mb-2" style={{color: 'rgb(var(--color-text-secondary))'}}>{t('tools.json_formatter.favorites')}</h4>
{historyItems
.filter(item => item.isFavorite)
.map(item => (
<div key={item.id} className={historyItemClass} onClick={() => loadFromHistory(item)}>
<div className="flex-1 truncate">
<div className="font-medium truncate">{item.title}</div>
<div className="text-xs" style={{color: 'rgb(var(--color-text-tertiary))'}}>
{new Date(item.timestamp).toLocaleString()}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={(e) => toggleFavorite(item.id, e)}
title={item.isFavorite ? t('tools.json_formatter.remove_favorite') : t('tools.json_formatter.add_favorite')}
>
<FontAwesomeIcon
icon={faStar}
className={`favorite-icon ${item.isFavorite ? 'active' : ''}`}
/>
</button>
<button onClick={(e) => startEditingTitle(item, e)} title={t('tools.json_formatter.edit_title')}>
<FontAwesomeIcon
icon={faEdit}
style={{color: 'rgb(var(--color-text-tertiary))'}}
/>
</button>
<button onClick={(e) => deleteHistoryItem(item.id, e)} title={t('tools.json_formatter.delete')}>
<FontAwesomeIcon
icon={faTrashAlt}
style={{color: 'rgb(var(--color-text-tertiary))'}}
/>
</button>
</div>
</div>
))}
</div>
)}
{/* 全部历史记录 */}
<h4 className="text-sm font-medium mb-2" style={{color: 'rgb(var(--color-text-secondary))'}}>{t('tools.json_formatter.all_history')}</h4>
{historyItems.map(item => (
<div key={item.id} className={historyItemClass} onClick={() => loadFromHistory(item)}>
<div className="flex-1 truncate">
<div className="font-medium truncate">{item.title}</div>
<div className="text-xs" style={{color: 'rgb(var(--color-text-tertiary))'}}>
{new Date(item.timestamp).toLocaleString()}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={(e) => toggleFavorite(item.id, e)}
title={item.isFavorite ? t('tools.json_formatter.remove_favorite') : t('tools.json_formatter.add_favorite')}
>
<FontAwesomeIcon
icon={faStar}
className={`favorite-icon ${item.isFavorite ? 'active' : ''}`}
/>
</button>
<button onClick={(e) => startEditingTitle(item, e)} title={t('tools.json_formatter.edit_title')}>
<FontAwesomeIcon
icon={faEdit}
style={{color: 'rgb(var(--color-text-tertiary))'}}
/>
</button>
<button onClick={(e) => deleteHistoryItem(item.id, e)} title={t('tools.json_formatter.delete')}>
<FontAwesomeIcon
icon={faTrashAlt}
style={{color: 'rgb(var(--color-text-tertiary))'}}
/>
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* 保存模态框 */}
{isSaveModalOpen && (
<div className="modal-overlay">
<div className="modal-container">
<h3 className="text-lg font-medium mb-4" style={{color: 'rgb(var(--color-text-primary))'}}>
{editingItem ? t('tools.json_formatter.edit_saved_json') : t('tools.json_formatter.save_to_history')}
</h3>
<div className="mb-4">
<label className="block text-sm font-medium mb-1" style={{color: 'rgb(var(--color-text-secondary))'}}>
{t('tools.json_formatter.title')}
</label>
<input
type="text"
value={savingTitle}
onChange={(e) => setSavingTitle(e.target.value)}
placeholder={t('tools.json_formatter.enter_title')}
className="search-input w-full"
/>
</div>
<div className="flex justify-end gap-2">
<button
className="px-4 py-2 rounded-md text-sm"
style={{
backgroundColor: 'rgb(var(--color-bg-secondary))',
color: 'rgb(var(--color-text-secondary))'
}}
onClick={() => {
setIsSaveModalOpen(false);
setEditingItem(null);
setSavingTitle('');
}}
>
{t('tools.json_formatter.cancel')}
</button>
<button
className="px-4 py-2 rounded-md text-sm"
style={{
backgroundColor: 'rgb(var(--color-primary))',
color: 'white'
}}
onClick={saveToHistory}
>
{editingItem ? t('tools.json_formatter.update') : t('tools.json_formatter.save')}
</button>
</div>
</div>
</div>
)}
{/* 说明信息 */}
<div className="mt-8 text-sm" style={{color: 'rgb(var(--color-text-tertiary))'}}>
<h3 className="mb-2 font-medium" style={{color: 'rgb(var(--color-text-secondary))'}}>{t('tools.json_formatter.usage_guide')}</h3>
<ul className="list-disc pl-5 space-y-1">
<li>{t('tools.json_formatter.guide_1')}</li>
<li>{t('tools.json_formatter.guide_2')}</li>
<li>{t('tools.json_formatter.guide_3')}</li>
<li>{t('tools.json_formatter.guide_4')}</li>
<li>{t('tools.json_formatter.guide_5')}</li>
<li>{t('tools.json_formatter.guide_6')}</li>
<li>{t('tools.json_formatter.guide_7')}</li>
</ul>
</div>
{/* 回到顶部按钮 */}
<BackToTop position="bottom-right" offset={30} size="medium" />
</div>
);
}