更新 README 文档,添加 JSON 预览功能的示例图片;新增 JsonTreeNode 组件以支持树形结构展示 JSON 数据

This commit is contained in:
lirui
2026-02-09 20:48:22 +08:00
parent 7f3eb2db49
commit 23bf383c04
5 changed files with 155 additions and 16 deletions

View File

@@ -105,3 +105,10 @@ npm run preview
- 日期字段会尽量兼容 Excel 序列日期(数字日期) - 日期字段会尽量兼容 Excel 序列日期(数字日期)
-`excludeIfEmpty = true` 时,空值字段不会出现在输出 JSON 中 -`excludeIfEmpty = true` 时,空值字段不会出现在输出 JSON 中
- 若目标字段包含 `.`,会按路径写入嵌套对象 - 若目标字段包含 `.`,会按路径写入嵌套对象
## <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼ
![<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼ 1](doc/images/image1.png)
![<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼ 2](doc/images/image2.png)

BIN
doc/images/image1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
doc/images/image2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import JsonTreeNode from './JsonTreeNode.svelte';
let { json, onupdate }: { let { json, onupdate }: {
json: Record<string, unknown>[]; json: Record<string, unknown>[];
onupdate?: (edited: string) => void; onupdate?: (edited: string) => void;
@@ -6,20 +8,21 @@
const MAX_PREVIEW_CHARS = 2000; const MAX_PREVIEW_CHARS = 2000;
const MAX_PREVIEW_LINES = 50; const MAX_PREVIEW_LINES = 50;
const MAX_TREE_ITEMS = 50;
let editing = $state(false); type ViewMode = 'tree' | 'raw' | 'edit';
let viewMode = $state<ViewMode>('tree');
let editText = $state(''); let editText = $state('');
let parseError = $state(''); let parseError = $state('');
const jsonString = $derived(JSON.stringify(json, null, 2)); const jsonString = $derived(JSON.stringify(json, null, 2));
// Truncation for preview // Truncation for raw preview
const truncated = $derived.by(() => { const truncated = $derived.by(() => {
const lines = jsonString.split('\n'); const lines = jsonString.split('\n');
if (jsonString.length <= MAX_PREVIEW_CHARS && lines.length <= MAX_PREVIEW_LINES) { if (jsonString.length <= MAX_PREVIEW_CHARS && lines.length <= MAX_PREVIEW_LINES) {
return { text: jsonString, isTruncated: false, totalChars: jsonString.length, totalLines: lines.length }; return { text: jsonString, isTruncated: false, totalChars: jsonString.length, totalLines: lines.length };
} }
// Truncate by line first, then by char
let preview = lines.slice(0, MAX_PREVIEW_LINES).join('\n'); let preview = lines.slice(0, MAX_PREVIEW_LINES).join('\n');
if (preview.length > MAX_PREVIEW_CHARS) { if (preview.length > MAX_PREVIEW_CHARS) {
preview = preview.slice(0, MAX_PREVIEW_CHARS); preview = preview.slice(0, MAX_PREVIEW_CHARS);
@@ -27,9 +30,13 @@
return { text: preview, isTruncated: true, totalChars: jsonString.length, totalLines: lines.length }; return { text: preview, isTruncated: true, totalChars: jsonString.length, totalLines: lines.length };
}); });
// Tree view shows limited items for performance
const treeData = $derived(json.length > MAX_TREE_ITEMS ? json.slice(0, MAX_TREE_ITEMS) : json);
const treeTruncated = $derived(json.length > MAX_TREE_ITEMS);
// Sync from upstream when not editing // Sync from upstream when not editing
$effect(() => { $effect(() => {
if (!editing) { if (viewMode !== 'edit') {
editText = jsonString; editText = jsonString;
} }
}); });
@@ -48,12 +55,12 @@
function startEdit() { function startEdit() {
editText = jsonString; editText = jsonString;
editing = true; viewMode = 'edit';
parseError = ''; parseError = '';
} }
function cancelEdit() { function cancelEdit() {
editing = false; viewMode = 'tree';
parseError = ''; parseError = '';
editText = jsonString; editText = jsonString;
} }
@@ -62,7 +69,7 @@
try { try {
JSON.parse(editText); JSON.parse(editText);
parseError = ''; parseError = '';
editing = false; viewMode = 'tree';
onupdate?.(editText); onupdate?.(editText);
} catch { } catch {
parseError = 'JSON 格式错误,请检查语法'; parseError = 'JSON 格式错误,请检查语法';
@@ -71,13 +78,14 @@
</script> </script>
<div class="flex h-full flex-col overflow-hidden"> <div class="flex h-full flex-col overflow-hidden">
<!-- Header -->
<div class="flex flex-shrink-0 items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-2"> <div class="flex flex-shrink-0 items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-2">
<h3 class="text-sm font-semibold text-gray-700"> <h3 class="text-sm font-semibold text-gray-700">
JSON 预览 JSON
<span class="ml-2 text-xs font-normal text-gray-400">{json.length}记录</span> <span class="ml-1 text-xs font-normal text-gray-400">{json.length}</span>
</h3> </h3>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1">
{#if editing} {#if viewMode === 'edit'}
<button onclick={applyEdit} class="rounded bg-blue-600 px-2 py-1 text-xs text-white hover:bg-blue-700 cursor-pointer"> <button onclick={applyEdit} class="rounded bg-blue-600 px-2 py-1 text-xs text-white hover:bg-blue-700 cursor-pointer">
应用 应用
</button> </button>
@@ -85,7 +93,24 @@
取消 取消
</button> </button>
{:else} {:else}
<button onclick={startEdit} class="rounded border border-gray-300 px-2 py-1 text-xs text-gray-500 hover:bg-gray-200 cursor-pointer"> <!-- View mode tabs -->
<div class="flex rounded border border-gray-300 text-xs">
<button
onclick={() => (viewMode = 'tree')}
class="px-2 py-1 cursor-pointer {viewMode === 'tree' ? 'bg-gray-200 text-gray-800 font-medium' : 'bg-white text-gray-500 hover:bg-gray-50'}"
style="border-radius: 3px 0 0 3px"
>
树形
</button>
<button
onclick={() => (viewMode = 'raw')}
class="border-l border-gray-300 px-2 py-1 cursor-pointer {viewMode === 'raw' ? 'bg-gray-200 text-gray-800 font-medium' : 'bg-white text-gray-500 hover:bg-gray-50'}"
style="border-radius: 0 3px 3px 0"
>
源码
</button>
</div>
<button onclick={startEdit} class="ml-1 rounded border border-gray-300 px-2 py-1 text-xs text-gray-500 hover:bg-gray-200 cursor-pointer">
编辑 编辑
</button> </button>
{/if} {/if}
@@ -96,21 +121,30 @@
<div class="flex-shrink-0 bg-red-50 px-4 py-1.5 text-xs text-red-600">{parseError}</div> <div class="flex-shrink-0 bg-red-50 px-4 py-1.5 text-xs text-red-600">{parseError}</div>
{/if} {/if}
<!-- Content -->
<div class="json-preview flex-1 overflow-hidden"> <div class="json-preview flex-1 overflow-hidden">
{#if editing} {#if viewMode === 'edit'}
<!-- Plain textarea for editing — no overlay highlighting to avoid lag -->
<textarea <textarea
bind:value={editText} bind:value={editText}
oninput={() => { parseError = ''; }} oninput={() => { parseError = ''; }}
spellcheck="false" spellcheck="false"
class="json-textarea h-full w-full resize-none overflow-auto border-0 bg-transparent font-mono text-xs leading-relaxed text-gray-700 outline-none" class="json-textarea h-full w-full resize-none overflow-auto border-0 bg-transparent font-mono text-xs leading-relaxed text-gray-700 outline-none"
></textarea> ></textarea>
{:else if viewMode === 'tree'}
<div class="h-full overflow-auto px-4 py-3">
<JsonTreeNode value={treeData} defaultOpen={true} />
{#if treeTruncated}
<div class="mt-2 border-t border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700">
&#x26A0;&#xFE0F; 树形视图仅展示前 {MAX_TREE_ITEMS} 条记录(共 {json.length.toLocaleString()} 条)。请下载或复制查看完整数据。
</div>
{/if}
</div>
{:else} {:else}
<div class="h-full overflow-auto"> <div class="h-full overflow-auto">
<pre class="json-content whitespace-pre font-mono text-xs leading-relaxed text-gray-500">{@html highlight(truncated.text)}</pre> <pre class="json-content whitespace-pre font-mono text-xs leading-relaxed text-gray-500">{@html highlight(truncated.text)}</pre>
{#if truncated.isTruncated} {#if truncated.isTruncated}
<div class="truncation-notice sticky bottom-0 border-t border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-700"> <div class="sticky bottom-0 border-t border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-700">
&#x26A0;&#xFE0F; 预览已截断以提升性能(显示 {Math.min(MAX_PREVIEW_CHARS, truncated.text.length).toLocaleString()}/{truncated.totalChars.toLocaleString()} 字符,{Math.min(MAX_PREVIEW_LINES, truncated.totalLines)}/{truncated.totalLines.toLocaleString()} 行)。请点击下载或复制查看完整数据。 &#x26A0;&#xFE0F; 预览已截断(显示 {Math.min(MAX_PREVIEW_CHARS, truncated.text.length).toLocaleString()}/{truncated.totalChars.toLocaleString()} 字符,{Math.min(MAX_PREVIEW_LINES, truncated.totalLines)}/{truncated.totalLines.toLocaleString()} 行)。请下载或复制查看完整数据。
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -0,0 +1,98 @@
<script lang="ts">
import JsonTreeNode from './JsonTreeNode.svelte';
let {
value,
keyName = undefined,
depth = 0,
defaultOpen = false
}: {
value: unknown;
keyName?: string | number;
depth?: number;
defaultOpen?: boolean;
} = $props();
let open = $state(defaultOpen);
const valueType = $derived(
value === null ? 'null'
: Array.isArray(value) ? 'array'
: typeof value === 'object' ? 'object'
: typeof value
);
const isExpandable = $derived(valueType === 'object' || valueType === 'array');
const entries = $derived(
isExpandable
? Object.entries(value as Record<string, unknown>)
: []
);
const childCount = $derived(entries.length);
const preview = $derived(
valueType === 'array' ? `Array(${childCount})`
: valueType === 'object' ? `{${childCount}}`
: ''
);
</script>
<div class="tree-node" style="padding-left: {depth > 0 ? 16 : 0}px">
<div class="node-row flex items-baseline gap-1 py-0.5">
{#if isExpandable}
<button
onclick={() => (open = !open)}
class="toggle flex-shrink-0 cursor-pointer border-0 bg-transparent p-0 font-mono text-[10px] leading-none text-gray-400 hover:text-gray-700"
aria-label={open ? '折叠' : '展开'}
>
{open ? '▼' : '▶'}
</button>
{:else}
<span class="w-[10px] flex-shrink-0"></span>
{/if}
{#if keyName !== undefined}
<span class="node-key font-mono text-xs font-medium text-gray-800">
{typeof keyName === 'number' ? keyName : `"${keyName}"`}</span><span class="text-xs text-gray-400">:&nbsp;</span>
{/if}
{#if isExpandable}
<button
onclick={() => (open = !open)}
class="cursor-pointer border-0 bg-transparent p-0 font-mono text-xs text-gray-400 hover:text-gray-600"
>
{#if open}
{valueType === 'array' ? '[' : '{'}
{:else}
<span class="text-gray-400">{preview}</span>
{/if}
</button>
{:else if valueType === 'string'}
<span class="font-mono text-xs text-green-700">"{String(value)}"</span>
{:else if valueType === 'number'}
<span class="font-mono text-xs text-blue-700">{String(value)}</span>
{:else if valueType === 'boolean'}
<span class="font-mono text-xs text-red-600">{String(value)}</span>
{:else if valueType === 'null'}
<span class="font-mono text-xs italic text-gray-400">null</span>
{:else}
<span class="font-mono text-xs text-gray-600">{String(value)}</span>
{/if}
</div>
{#if isExpandable && open}
<div class="tree-children border-l border-gray-200">
{#each entries as [childKey, childVal], i (childKey)}
<JsonTreeNode
value={childVal}
keyName={valueType === 'array' ? i : childKey}
depth={depth + 1}
defaultOpen={depth < 1}
/>
{/each}
</div>
<div style="padding-left: {depth > 0 ? 16 : 0}px">
<span class="font-mono text-xs text-gray-400">{valueType === 'array' ? ']' : '}'}</span>
</div>
{/if}
</div>