diff --git a/components/nvs-editor/NvsEditor.vue b/components/nvs-editor/NvsEditor.vue index b64516d..6bf1a4a 100644 --- a/components/nvs-editor/NvsEditor.vue +++ b/components/nvs-editor/NvsEditor.vue @@ -12,7 +12,7 @@ import { validatePartition, generateEntryId, normalizePartition, reconcileBlobTypes, checkBlobCompatibility, parseBinary, serializeBinary, parseCsv, serializeCsv, - MAX_KEY_LENGTH, + MAX_KEY_LENGTH, PAGE_SIZE, } from '../../lib/nvs'; const props = defineProps<{ @@ -44,12 +44,8 @@ const sortProp = ref<'namespace' | 'key' | 'value' | null>(null); const sortOrder = ref<'ascending' | 'descending' | null>(null); // File input refs -const openBinInput = ref(); -const mergeBinInput = ref(); -const openCsvInput = ref(); -const mergeCsvInput = ref(); -const openJsonInput = ref(); -const mergeJsonInput = ref(); +const openInput = ref(); +const mergeInput = ref(); const blobUploadInput = ref(); const blobUploadEntryId = ref(''); @@ -525,19 +521,92 @@ function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | // ── Actions: File I/O ────────────────────────────────────────────── -async function onOpenBinChange(e: Event) { +async function detectFileType(file: File): Promise<'bin' | 'csv' | 'json'> { + const ext = file.name.split('.').pop()?.toLowerCase(); + if (ext === 'bin') return 'bin'; + if (ext === 'csv') return 'csv'; + if (ext === 'json') return 'json'; + // Fallback: size multiple of page size → binary; first byte '{' → json; else csv + if (file.size > 0 && file.size % PAGE_SIZE === 0) return 'bin'; + const firstByte = new Uint8Array(await file.slice(0, 1).arrayBuffer())[0]; + return firstByte === 0x7b ? 'json' : 'csv'; +} + +async function handleImport(file: File, mode: 'open' | 'merge') { + const type = await detectFileType(file); + try { + if (type === 'bin') { + const buffer = await file.arrayBuffer(); + const data = new Uint8Array(buffer); + const incoming = parseBinary(data); + if (mode === 'open') { + partition.value = incoming; + targetSize.value = data.byteLength; + showStatus(`已加载 ${file.name} (${data.byteLength} 字节)`, 'success'); + } else { + partition.value = mergePartitions(partition.value, incoming, mergeMode.value); + showStatus(`已合并 ${file.name} (${incoming.entries.length} 条记录)`, 'success'); + const blobWarnings = checkBlobCompatibility(partition.value.entries, partition.value.version); + if (blobWarnings.length > 0) showStatus(`${blobWarnings.length} 个 blob 超出大小限制,请查看验证面板`, 'info'); + } + } else if (type === 'csv') { + const text = await file.text(); + const incoming = parseCsv(text); + if (mode === 'open') { + partition.value = incoming; + showStatus(`已加载 ${file.name}`, 'success'); + } else { + partition.value = mergePartitions(partition.value, incoming, mergeMode.value); + showStatus(`已合并 ${file.name} (${incoming.entries.length} 条记录)`, 'success'); + const blobWarnings = checkBlobCompatibility(partition.value.entries, partition.value.version); + if (blobWarnings.length > 0) showStatus(`${blobWarnings.length} 个 blob 超出大小限制,请查看验证面板`, 'info'); + } + } else { + const text = await file.text(); + const raw = partitionFromJson(text); + const { partition: incoming, dropped, clamped } = normalizePartition(raw); + if (mode === 'open') { + partition.value = incoming; + const parts: string[] = [`已加载 ${file.name}`]; + if (dropped > 0 || clamped > 0) { + const details: string[] = []; + if (dropped > 0) details.push(`丢弃 ${dropped} 条无效记录`); + if (clamped > 0) details.push(`${clamped} 条值被截断`); + parts.push(`(${incoming.entries.length} 条,${details.join(',')})`); + } + showStatus(parts.join(''), (dropped > 0 || clamped > 0) ? 'info' : 'success'); + } else { + partition.value = mergePartitions(partition.value, incoming, mergeMode.value); + const parts: string[] = [`已合并 ${file.name}(${incoming.entries.length} 条记录`]; + if (dropped > 0 || clamped > 0) { + const details: string[] = []; + if (dropped > 0) details.push(`丢弃 ${dropped} 条无效记录`); + if (clamped > 0) details.push(`${clamped} 条值被截断`); + parts.push(`,${details.join(',')}`); + } + parts.push(')'); + showStatus(parts.join(''), (dropped > 0 || clamped > 0) ? 'info' : 'success'); + const blobWarnings = checkBlobCompatibility(partition.value.entries, partition.value.version); + if (blobWarnings.length > 0) showStatus(`${blobWarnings.length} 个 blob 超出大小限制,请查看验证面板`, 'info'); + } + } + } catch (e: any) { + showStatus(`${mode === 'open' ? '加载' : '合并'}失败: ${e.message}`, 'error'); + } +} + +async function onOpenChange(e: Event) { const file = (e.target as HTMLInputElement).files?.[0]; (e.target as HTMLInputElement).value = ''; if (!file) return; - try { - const buffer = await file.arrayBuffer(); - const data = new Uint8Array(buffer); - partition.value = parseBinary(data); - targetSize.value = data.byteLength; - showStatus(`已加载 ${file.name} (${data.byteLength} 字节)`, 'success'); - } catch (e: any) { - showStatus(`加载失败: ${e.message}`, 'error'); - } + await handleImport(file, 'open'); +} + +async function onMergeChange(e: Event) { + const file = (e.target as HTMLInputElement).files?.[0]; + (e.target as HTMLInputElement).value = ''; + if (!file) return; + await handleImport(file, 'merge'); } function handleExportBinary() { @@ -552,37 +621,6 @@ function handleExportBinary() { } } -async function onMergeBinChange(e: Event) { - const file = (e.target as HTMLInputElement).files?.[0]; - (e.target as HTMLInputElement).value = ''; - if (!file) return; - try { - const buffer = await file.arrayBuffer(); - const incoming = parseBinary(new Uint8Array(buffer)); - partition.value = mergePartitions(partition.value, incoming, mergeMode.value); - showStatus(`已合并 ${file.name} (${incoming.entries.length} 条记录)`, 'success'); - const blobWarnings = checkBlobCompatibility(partition.value.entries, partition.value.version); - if (blobWarnings.length > 0) { - showStatus(`${blobWarnings.length} 个 blob 超出大小限制,请查看验证面板`, 'info'); - } - } catch (e: any) { - showStatus(`合并失败: ${e.message}`, 'error'); - } -} - -async function onOpenCsvChange(e: Event) { - const file = (e.target as HTMLInputElement).files?.[0]; - (e.target as HTMLInputElement).value = ''; - if (!file) return; - try { - const text = await file.text(); - partition.value = parseCsv(text); - showStatus(`已加载 ${file.name}`, 'success'); - } catch (e: any) { - showStatus(`加载失败: ${e.message}`, 'error'); - } -} - function handleExportCsv() { try { const text = serializeCsv(partition.value); @@ -593,46 +631,6 @@ function handleExportCsv() { } } -async function onMergeCsvChange(e: Event) { - const file = (e.target as HTMLInputElement).files?.[0]; - (e.target as HTMLInputElement).value = ''; - if (!file) return; - try { - const text = await file.text(); - const incoming = parseCsv(text); - partition.value = mergePartitions(partition.value, incoming, mergeMode.value); - showStatus(`已合并 ${file.name} (${incoming.entries.length} 条记录)`, 'success'); - const blobWarnings = checkBlobCompatibility(partition.value.entries, partition.value.version); - if (blobWarnings.length > 0) { - showStatus(`${blobWarnings.length} 个 blob 超出大小限制,请查看验证面板`, 'info'); - } - } catch (e: any) { - showStatus(`合并失败: ${e.message}`, 'error'); - } -} - -async function onOpenJsonChange(e: Event) { - const file = (e.target as HTMLInputElement).files?.[0]; - (e.target as HTMLInputElement).value = ''; - if (!file) return; - try { - const text = await file.text(); - const raw = partitionFromJson(text); - const { partition: incoming, dropped, clamped } = normalizePartition(raw); - partition.value = incoming; - const parts: string[] = [`已加载 ${file.name}`]; - if (dropped > 0 || clamped > 0) { - const details: string[] = []; - if (dropped > 0) details.push(`丢弃 ${dropped} 条无效记录`); - if (clamped > 0) details.push(`${clamped} 条值被截断`); - parts.push(`(${incoming.entries.length} 条,${details.join(',')})`); - } - showStatus(parts.join(''), (dropped > 0 || clamped > 0) ? 'info' : 'success'); - } catch (e: any) { - showStatus(`加载失败: ${e.message}`, 'error'); - } -} - function handleExportJson() { try { const text = partitionToJson(partition.value); @@ -643,34 +641,6 @@ function handleExportJson() { } } -async function onMergeJsonChange(e: Event) { - const file = (e.target as HTMLInputElement).files?.[0]; - (e.target as HTMLInputElement).value = ''; - if (!file) return; - try { - const text = await file.text(); - const raw = partitionFromJson(text); - const { partition: incoming, dropped, clamped } = normalizePartition(raw); - partition.value = mergePartitions(partition.value, incoming, mergeMode.value); - const parts: string[] = [`已合并 ${file.name}(${incoming.entries.length} 条记录`]; - if (dropped > 0 || clamped > 0) { - const details: string[] = []; - if (dropped > 0) details.push(`丢弃 ${dropped} 条无效记录`); - if (clamped > 0) details.push(`${clamped} 条值被截断`); - parts.push(`,${details.join(',')}`); - } - parts.push(')'); - showStatus(parts.join(''), (dropped > 0 || clamped > 0) ? 'info' : 'success'); - const blobWarnings = checkBlobCompatibility(partition.value.entries, partition.value.version); - if (blobWarnings.length > 0) { - showStatus(`${blobWarnings.length} 个 blob 超出大小限制,请查看验证面板`, 'info'); - } - } catch (e: any) { - showStatus(`合并失败: ${e.message}`, 'error'); - } -} - - async function onBlobUploadChange(e: Event) { const file = (e.target as HTMLInputElement).files?.[0]; (e.target as HTMLInputElement).value = ''; @@ -713,12 +683,8 @@ function copyToClipboard(text: string) {