mirror of
https://github.com/justhtmls/html-tools.git
synced 2026-09-01 15:32:50 +08:00
Add JSON Formatter tool
Features: - JSON formatting/beautifying with custom indentation (2/4 spaces or tab) - JSON minifying/compression - JSON syntax validation with error messages - Sort keys option - Statistics: character count, line count, file size - Copy and download functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+20
@@ -22,6 +22,26 @@
|
||||
"featured": true,
|
||||
"repo": "https://github.com/justhtmls/html-tools"
|
||||
},
|
||||
{
|
||||
"id": "json-formatter",
|
||||
"name": "JSON 格式化工具",
|
||||
"slug": "json-formatter",
|
||||
"category": "converter",
|
||||
"tags": ["json", "formatter", "validator", "beautifier"],
|
||||
"author": "JustHTMLs",
|
||||
"authorUrl": "https://github.com/justhtmls",
|
||||
"version": "1.0.0",
|
||||
"description": "格式化、压缩和验证 JSON 数据",
|
||||
"longDescription": "JSON 格式化工具可以帮助你美化、压缩和验证 JSON 数据,支持多种缩进样式、键名排序,显示字符数、行数、文件大小等统计信息。",
|
||||
"icon": "✨",
|
||||
"color": "#667eea",
|
||||
"entry": "tools/json-formatter/app.html",
|
||||
"detail": "tools/json-formatter/index.html",
|
||||
"createdAt": "2025-12-23",
|
||||
"updatedAt": "2025-12-23",
|
||||
"featured": true,
|
||||
"repo": "https://github.com/justhtmls/html-tools"
|
||||
},
|
||||
{
|
||||
"id": "base64-encode",
|
||||
"name": "Base64 编码解码",
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JSON 格式化工具</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.option-item label {
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.option-item select,
|
||||
.option-item input[type="number"] {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.option-item select:focus,
|
||||
.option-item input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: white;
|
||||
border: 2px solid #e0e0e0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.editor-textarea {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 450px;
|
||||
padding: 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.editor-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
background: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
color: #c62828;
|
||||
margin-bottom: 15px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-msg.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: white;
|
||||
padding: 8px 15px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.editor-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.editor-textarea {
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>JSON 格式化工具</h1>
|
||||
<p>格式化、压缩和验证 JSON 数据</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="error-msg" id="error-msg"></div>
|
||||
|
||||
<div class="action-bar">
|
||||
<div class="btn-group">
|
||||
<button class="btn" onclick="formatJSON()">格式化</button>
|
||||
<button class="btn btn-success" onclick="minifyJSON()">压缩</button>
|
||||
<button class="btn btn-secondary" onclick="loadSample()">示例</button>
|
||||
<button class="btn btn-secondary" onclick="clearAll()">清空</button>
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
<div class="option-item">
|
||||
<label>缩进:</label>
|
||||
<select id="indent-style" onchange="formatJSON()">
|
||||
<option value="2">2 空格</option>
|
||||
<option value="4" selected>4 空格</option>
|
||||
<option value="tab">Tab</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<label>
|
||||
<input type="checkbox" id="sort-keys" onchange="formatJSON()">
|
||||
排序键名
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<div class="editor-panel">
|
||||
<div class="editor-header">
|
||||
<span class="editor-title">输入 JSON</span>
|
||||
<div class="editor-actions">
|
||||
<button class="icon-btn" onclick="pasteToInput()">粘贴</button>
|
||||
<button class="icon-btn" onclick="document.getElementById('input').value=''">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="editor-textarea" id="input" placeholder='在此输入 JSON,例如:
|
||||
{
|
||||
"name": "John",
|
||||
"age": 30
|
||||
}'></textarea>
|
||||
</div>
|
||||
|
||||
<div class="editor-panel">
|
||||
<div class="editor-header">
|
||||
<span class="editor-title">输出结果</span>
|
||||
<div class="editor-actions">
|
||||
<button class="icon-btn" onclick="copyOutput()">复制</button>
|
||||
<button class="icon-btn" onclick="downloadOutput()">下载</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="editor-textarea" id="output" readonly placeholder="格式化后的 JSON 将显示在这里..."></textarea>
|
||||
<div class="stats">
|
||||
<div class="stat-item">字符: <span class="stat-value" id="char-count">0</span></div>
|
||||
<div class="stat-item">行数: <span class="stat-value" id="line-count">0</span></div>
|
||||
<div class="stat-item">大小: <span class="stat-value" id="size-count">0 B</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function formatJSON() {
|
||||
const input = document.getElementById('input').value.trim();
|
||||
const output = document.getElementById('output');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
const indentStyle = document.getElementById('indent-style').value;
|
||||
const sortKeys = document.getElementById('sort-keys').checked;
|
||||
|
||||
if (!input) {
|
||||
output.value = '';
|
||||
updateStats('');
|
||||
errorMsg.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let jsonObj = JSON.parse(input);
|
||||
|
||||
// 排序键名
|
||||
if (sortKeys) {
|
||||
jsonObj = sortObjectKeys(jsonObj);
|
||||
}
|
||||
|
||||
// 格式化
|
||||
let indent;
|
||||
if (indentStyle === 'tab') {
|
||||
indent = '\t';
|
||||
} else {
|
||||
indent = parseInt(indentStyle);
|
||||
}
|
||||
|
||||
output.value = JSON.stringify(jsonObj, null, indent);
|
||||
errorMsg.classList.remove('active');
|
||||
updateStats(output.value);
|
||||
} catch (error) {
|
||||
errorMsg.innerHTML = `<strong>JSON 格式错误:</strong> ${error.message}<br><small>位置: ${error.message.match(/position (\d+)/) ? '字符 ' + error.message.match(/position (\d+)/)[1] : '未知'}</small>`;
|
||||
errorMsg.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function minifyJSON() {
|
||||
const input = document.getElementById('input').value.trim();
|
||||
const output = document.getElementById('output');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
|
||||
if (!input) {
|
||||
output.value = '';
|
||||
updateStats('');
|
||||
errorMsg.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonObj = JSON.parse(input);
|
||||
output.value = JSON.stringify(jsonObj);
|
||||
errorMsg.classList.remove('active');
|
||||
updateStats(output.value);
|
||||
} catch (error) {
|
||||
errorMsg.innerHTML = `<strong>JSON 格式错误:</strong> ${error.message}`;
|
||||
errorMsg.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function sortObjectKeys(obj) {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(sortObjectKeys);
|
||||
}
|
||||
|
||||
const sorted = {};
|
||||
Object.keys(obj).sort().forEach(key => {
|
||||
sorted[key] = sortObjectKeys(obj[key]);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function updateStats(text) {
|
||||
document.getElementById('char-count').textContent = text.length.toLocaleString();
|
||||
document.getElementById('line-count').textContent = text.split('\n').length.toLocaleString();
|
||||
document.getElementById('size-count').textContent = formatBytes(new Blob([text]).size);
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
async function pasteToInput() {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
document.getElementById('input').value = text;
|
||||
formatJSON();
|
||||
} catch (err) {
|
||||
showToast('无法访问剪贴板');
|
||||
}
|
||||
}
|
||||
|
||||
function copyOutput() {
|
||||
const output = document.getElementById('output').value;
|
||||
if (!output) {
|
||||
showToast('没有可复制的内容');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(output).then(() => {
|
||||
showToast('已复制到剪贴板');
|
||||
});
|
||||
}
|
||||
|
||||
function downloadOutput() {
|
||||
const output = document.getElementById('output').value;
|
||||
if (!output) {
|
||||
showToast('没有可下载的内容');
|
||||
return;
|
||||
}
|
||||
|
||||
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';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('已开始下载');
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
document.getElementById('input').value = '';
|
||||
document.getElementById('output').value = '';
|
||||
document.getElementById('error-msg').classList.remove('active');
|
||||
updateStats('');
|
||||
}
|
||||
|
||||
function loadSample() {
|
||||
document.getElementById('input').value = `{
|
||||
"name": "张三",
|
||||
"age": 28,
|
||||
"city": "北京",
|
||||
"hobbies": ["阅读", "旅游", "编程"],
|
||||
"address": {
|
||||
"street": "长安街",
|
||||
"number": 123
|
||||
},
|
||||
"isActive": true,
|
||||
"balance": 1234.56
|
||||
}`;
|
||||
formatJSON();
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
z-index: 1000;
|
||||
animation: fadeInOut 2s ease;
|
||||
`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 2000);
|
||||
}
|
||||
|
||||
// Add animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeInOut {
|
||||
0% { opacity: 0; transform: translateX(-50%) translateY(20px); }
|
||||
15% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
85% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
100% { opacity: 0; transform: translateX(-50%) translateY(-20px); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Auto format on paste
|
||||
document.getElementById('input').addEventListener('paste', () => {
|
||||
setTimeout(formatJSON, 100);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JSON 格式化工具 - JustHTMLs</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.features {
|
||||
list-style: none;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.features li {
|
||||
padding: 10px 0;
|
||||
color: #555;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.features li:before {
|
||||
content: "✓";
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 14px 30px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
margin: 10px 10px 10px 0;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preview iframe {
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>JSON 格式化工具</h1>
|
||||
<p>格式化、压缩和验证 JSON 数据</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>关于这个工具</h2>
|
||||
<p>JSON 格式化工具可以帮助你美化、压缩和验证 JSON 数据。支持多种缩进样式、键名排序等功能,是处理 JSON 数据的必备工具。</p>
|
||||
|
||||
<h2>功能特点</h2>
|
||||
<ul class="features">
|
||||
<li>JSON 格式化/美化</li>
|
||||
<li>JSON 压缩/最小化</li>
|
||||
<li>JSON 语法验证</li>
|
||||
<li>支持 2 空格、4 空格、Tab 缩进</li>
|
||||
<li>支持键名排序</li>
|
||||
<li>显示字符数、行数、文件大小</li>
|
||||
<li>一键复制或下载结果</li>
|
||||
<li>完全在浏览器本地运行</li>
|
||||
</ul>
|
||||
|
||||
<h2>使用场景</h2>
|
||||
<ul class="features">
|
||||
<li>格式化 API 返回的 JSON 数据</li>
|
||||
<li>压缩 JSON 减少文件大小</li>
|
||||
<li>验证 JSON 语法正确性</li>
|
||||
<li>调试和阅读 JSON 配置文件</li>
|
||||
<li>JSON 数据预处理</li>
|
||||
</ul>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<a href="app.html" class="btn">开始使用</a>
|
||||
<a href="https://github.com/justhtmls/html-tools/tree/main/tools/json-formatter" target="_blank" class="btn btn-secondary">查看源码</a>
|
||||
<a href="../../index.html" class="btn btn-secondary">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>快速预览</h2>
|
||||
<div class="preview">
|
||||
<iframe src="app.html" loading="lazy"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user