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