Skip to content

Commit 7d465f0

Browse files
authored
Add files via upload
1 parent 401baf3 commit 7d465f0

3 files changed

Lines changed: 676 additions & 0 deletions

File tree

index.html

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!DOCTYPE html>
2+
<html lang="zh-CN">
3+
<head>
4+
<meta charset="UTF-8"/>
5+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
6+
<title>JSON 树形结构解析器</title>
7+
<link rel="stylesheet" href="style.css"/>
8+
</head>
9+
<body>
10+
<div class="container">
11+
<h1>JSON 树状结构解析器</h1>
12+
13+
<!-- 1. 单选输入方式 -->
14+
<div class="radio-group">
15+
<label><input type="radio" name="source" value="file" id="source-file"> 文件上传</label>
16+
<label><input type="radio" name="source" value="url" id="source-url"> URL 拉取</label>
17+
<label><input type="radio" name="source" value="text" id="source-text" checked> 直接输入</label>
18+
</div>
19+
20+
<!-- 2. 对应输入面板 -->
21+
<div class="input-panel" id="panel-file">
22+
<input type="file" id="fileInput" accept=".json" title="选择 JSON 文件">
23+
</div>
24+
<div class="input-panel" id="panel-url">
25+
<input type="url" id="urlInput" placeholder="输入 JSON 文件 URL">
26+
</div>
27+
<div class="input-panel active" id="panel-text">
28+
<textarea id="jsonInput" placeholder="粘贴或输入 JSON 内容">[{"v": "1.0.4"}]</textarea>
29+
</div>
30+
<button onclick="handleImport()">导入并解析</button>
31+
<div class="loading" id="loading">解析 JSON 数据中...</div>
32+
<div class="error" id="error"></div>
33+
34+
<!-- 3. 结果区域 -->
35+
<div class="output-section" id="output">
36+
<input id="searchBox" placeholder="搜索键/值…" class="input-search"/>
37+
<h3>解析结果</h3>
38+
<div class="json-tree" id="treeView"></div>
39+
</div>
40+
</div>
41+
42+
<script src="script.js"></script>
43+
</body>
44+
</html>

script.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/* ========= 单选逻辑 ========= */
2+
const radios = document.querySelectorAll('input[name="source"]');
3+
const panels = {
4+
file : document.getElementById('panel-file'),
5+
url : document.getElementById('panel-url'),
6+
text : document.getElementById('panel-text')
7+
};
8+
radios.forEach(r => r.addEventListener('change', () => {
9+
Object.values(panels).forEach(p => p.classList.remove('active'));
10+
panels[r.value] && panels[r.value].classList.add('active');
11+
}));
12+
13+
/* ========= 工具函数 ========= */
14+
const $ = id => document.getElementById(id);
15+
function showError(msg){
16+
$('error').textContent = msg;
17+
$('error').style.display = 'block';
18+
}
19+
function clearError(){
20+
$('error').style.display = 'none';
21+
}
22+
function showLoading(){
23+
$('loading').style.display = 'block';
24+
clearError();
25+
}
26+
function hideLoading(){
27+
$('loading').style.display = 'none';
28+
}
29+
30+
/* ========= 导入逻辑 ========= */
31+
async function handleImport(){
32+
const source = [...radios].find(r => r.checked).value;
33+
let json = null;
34+
clearError();
35+
if(source === 'file'){
36+
const file = $('fileInput').files[0];
37+
if(!file){showError('请选择 JSON 文件'); return; }
38+
showLoading();
39+
try{
40+
json = JSON.parse(await file.text());
41+
}catch(e){
42+
showError('文件解析错误: ' + e.message);
43+
}finally{
44+
hideLoading();
45+
}
46+
}else if(source === 'url'){
47+
const url = $('urlInput').value.trim();
48+
if(!url){showError('请输入有效的 URL'); return; }
49+
if(!url.startsWith('http')){showError('URL 必须以 http:// 或 https:// 开头'); return; }
50+
showLoading();
51+
try{
52+
const res = await fetch(url);
53+
if(!res.ok) throw new Error(`请求失败: ${res.status} ${res.statusText}`);
54+
json = await res.json();
55+
}catch(err){
56+
showError('URL 拉取失败: ' + err.message);
57+
}finally{
58+
hideLoading();
59+
}
60+
}else if(source === 'text'){
61+
const txt = $('jsonInput').value.trim();
62+
if(!txt){showError('请输入 JSON 文本'); return; }
63+
showLoading();
64+
try{
65+
json = JSON.parse(txt);
66+
}catch(e){
67+
showError('JSON 解析错误: ' + e.message);
68+
}finally{
69+
hideLoading();
70+
}
71+
}
72+
if(json) {
73+
displayTree(json);
74+
}
75+
}
76+
77+
/* ========= 渲染树形结构 ========= */
78+
function displayTree(data) {
79+
const treeView = $('treeView');
80+
treeView.innerHTML = '';
81+
if (!data || typeof data !== 'object') {
82+
treeView.innerHTML = '<div class="tree-node">无效的 JSON 数据</div>';
83+
$('output').style.display = 'block';
84+
return;
85+
}
86+
87+
// 创建根节点
88+
const rootNode = document.createElement('div');
89+
rootNode.className = 'tree-node';
90+
91+
// 根据数据类型创建根节点内容
92+
let rootType = Array.isArray(data) ? 'array' : 'object';
93+
let rootSummary = rootType === 'array' ? `[共 ${data.length} 项]` : `[共 ${Object.keys(data).length} 属性]`;
94+
const rootContent = document.createElement('div');
95+
rootContent.className = 'tree-node-content';
96+
rootContent.innerHTML = `
97+
<span class="toggle" onclick="toggleNode(this)">−</span>
98+
<span class="tree-value">${rootSummary}</span>
99+
<span class="tree-type type-${rootType}">${rootType}</span>
100+
<span class="tree-path">(根节点)</span>
101+
`;
102+
rootNode.appendChild(rootContent);
103+
104+
// 添加子节点容器
105+
const childrenContainer = document.createElement('div');
106+
childrenContainer.className = 'tree-children';
107+
rootNode.appendChild(childrenContainer);
108+
109+
// 递归构建子节点
110+
buildTree(data, childrenContainer, '', true);
111+
treeView.appendChild(rootNode);
112+
$('output').style.display = 'block';
113+
114+
// 默认展开第一级
115+
rootNode.classList.add('expanded');
116+
}
117+
118+
function buildTree(obj, container, parentPath = '', isRoot = false) {
119+
if (!obj || typeof obj !== 'object') return;
120+
const entries = Array.isArray(obj) ?
121+
obj.map((item, index) => [index, item]) :
122+
Object.entries(obj);
123+
entries.forEach(([key, value]) => {
124+
const node = document.createElement('div');
125+
node.className = 'tree-node';
126+
const currentPath = parentPath ? `${parentPath}.${key}` : key;
127+
const isArrayItem = Array.isArray(obj);
128+
const displayKey = isArrayItem ? `[${key}]` : key;
129+
let type, displayValue, hasChildren;
130+
if (value === null) {
131+
type = 'null';
132+
displayValue = 'null';
133+
hasChildren = false;
134+
} else if (Array.isArray(value)) {
135+
type = 'array';
136+
displayValue = `[共 ${value.length} 项]`;
137+
hasChildren = value.length > 0;
138+
} else if (typeof value === 'object') {
139+
type = 'object';
140+
displayValue = `[共 ${Object.keys(value).length} 属性]`;
141+
hasChildren = Object.keys(value).length > 0;
142+
} else {
143+
type = typeof value;
144+
displayValue = type === 'string' ? `"${value}"` : value;
145+
hasChildren = false;
146+
}
147+
const nodeContent = document.createElement('div');
148+
nodeContent.className = 'tree-node-content';
149+
150+
// 对于可折叠节点添加切换按钮
151+
const toggleBtn = hasChildren ?
152+
`<span class="toggle" onclick="toggleNode(this)">−</span>` :
153+
'<span style="display:inline-block;width:18px"></span>';
154+
nodeContent.innerHTML = `
155+
${toggleBtn}
156+
<span class="tree-key">${displayKey}:<span class="tree-value type-${type}">${displayValue}</span></span>
157+
<span class="tree-type type-${type}">${type}</span>
158+
<button class="copy-btn" onclick="copyPath('${currentPath}')">复制⿻</button>`;
159+
node.appendChild(nodeContent);
160+
161+
// 添加子节点容器
162+
if (hasChildren) {
163+
const childrenContainer = document.createElement('div');
164+
childrenContainer.className = 'tree-children';
165+
node.appendChild(childrenContainer);
166+
167+
// 递归构建子节点
168+
buildTree(value, childrenContainer, currentPath);
169+
node.classList.add('expanded');
170+
}
171+
container.appendChild(node);
172+
});
173+
}
174+
175+
// 复制到剪贴板
176+
function copyPath(path) {
177+
navigator.clipboard.writeText(path)
178+
.then(() => alert('已复制:' + path))
179+
.catch(() => alert('复制失败,请手动选择'));
180+
}
181+
182+
// 即时搜索高亮
183+
$('searchBox').addEventListener('input', e=>{
184+
const kw = e.target.value.trim().toLowerCase();
185+
document.querySelectorAll('.tree-node-content').forEach(n=>{
186+
const txt = n.textContent.toLowerCase();
187+
n.style.background = kw && txt.includes(kw) ? '#fff9c4' : '';
188+
});
189+
});
190+
191+
/* ========= 节点折叠/展开 ========= */
192+
function toggleNode(toggleElement) {
193+
const node = toggleElement.closest('.tree-node');
194+
if (!node) return;
195+
node.classList.toggle('expanded');
196+
toggleElement.textContent = node.classList.contains('expanded') ? '−' : '+';
197+
}
198+
199+
// 页面加载时默认解析示例
200+
window.addEventListener('DOMContentLoaded', () => {
201+
// 解析示例JSON
202+
const sampleData = JSON.parse($('jsonInput').value);
203+
displayTree(sampleData);
204+
});
205+
window.addEventListener('DOMContentLoaded', ()=>{
206+
const params = new URLSearchParams(location.search);
207+
const autoUrl = params.get('url');
208+
if(autoUrl){
209+
$('urlInput').value = autoUrl;
210+
radios[1].click();
211+
handleImport();
212+
}
213+
});

0 commit comments

Comments
 (0)