223 lines
10 KiB
Vue
223 lines
10 KiB
Vue
<script setup lang="ts">
|
||
import { AlertCircle, Calculator, CheckCircle2, Copy, FileUp } from '@lucide/vue'
|
||
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
|
||
import FileDropField from '@/components/FileDropField.vue'
|
||
import ToolPage from '@/components/ToolPage.vue'
|
||
import { useTransactionalFileWorker } from '@/composables/useTransactionalFileWorker'
|
||
import { copyText } from '@/lib/browser'
|
||
import { CRC_PRESETS, type CrcParameters } from '@/lib/crc'
|
||
import type { CrcBlockResult, CrcWorkerCalculateRequest, CrcWorkerResponse } from '@/lib/crcWorker'
|
||
import { MAX_FIRMWARE_FILE_SIZE, type FirmwareFormat } from '@/lib/firmware'
|
||
|
||
interface FirmwareData {
|
||
fileName: string
|
||
fileSize: number
|
||
format: FirmwareFormat
|
||
blockCount: number
|
||
totalBytes: number
|
||
warnings: string[]
|
||
}
|
||
|
||
const defaultPreset = CRC_PRESETS[0]
|
||
const firmware = shallowRef<FirmwareData>()
|
||
const selectedPreset = ref(defaultPreset.id)
|
||
const width = ref(defaultPreset.width)
|
||
const polynomial = ref(defaultPreset.polynomial.toString(16).toUpperCase())
|
||
const initialValue = ref(defaultPreset.initialValue.toString(16).toUpperCase())
|
||
const xorOut = ref(defaultPreset.xorOut.toString(16).toUpperCase())
|
||
const inputReflected = ref(defaultPreset.inputReflected)
|
||
const outputReflected = ref(defaultPreset.outputReflected)
|
||
const combinedResult = ref('')
|
||
const blockResults = ref<CrcBlockResult[]>([])
|
||
const error = ref('')
|
||
const success = ref('')
|
||
|
||
const activeParameterName = computed(() => CRC_PRESETS.find((preset) => preset.id === selectedPreset.value)?.name ?? '自定义参数')
|
||
|
||
function formatAddress(address: number): string {
|
||
return `0x${address.toString(16).toUpperCase().padStart(8, '0')}`
|
||
}
|
||
|
||
function applyPreset() {
|
||
const preset = CRC_PRESETS.find((item) => item.id === selectedPreset.value)
|
||
if (!preset) return
|
||
width.value = preset.width
|
||
polynomial.value = preset.polynomial.toString(16).toUpperCase()
|
||
initialValue.value = preset.initialValue.toString(16).toUpperCase()
|
||
xorOut.value = preset.xorOut.toString(16).toUpperCase()
|
||
inputReflected.value = preset.inputReflected
|
||
outputReflected.value = preset.outputReflected
|
||
invalidateResults()
|
||
}
|
||
|
||
function markCustom() {
|
||
selectedPreset.value = 'custom'
|
||
invalidateResults()
|
||
}
|
||
|
||
function parseParameter(value: string, label: string): bigint {
|
||
const normalized = value.trim().replace(/^0x/i, '')
|
||
if (!normalized || !/^[0-9a-f]+$/i.test(normalized)) throw new Error(`${label} 必须是十六进制整数`)
|
||
return BigInt(`0x${normalized}`)
|
||
}
|
||
|
||
function getParameters(): CrcParameters {
|
||
return {
|
||
width: width.value,
|
||
polynomial: parseParameter(polynomial.value, '多项式(Poly)'),
|
||
initialValue: parseParameter(initialValue.value, '初始值(Init)'),
|
||
inputReflected: inputReflected.value,
|
||
outputReflected: outputReflected.value,
|
||
xorOut: parseParameter(xorOut.value, '异或输出(XorOut)'),
|
||
}
|
||
}
|
||
|
||
function clearResults() {
|
||
invalidateCalculation()
|
||
combinedResult.value = ''
|
||
blockResults.value = []
|
||
success.value = ''
|
||
}
|
||
|
||
function invalidateResults() {
|
||
clearResults()
|
||
error.value = ''
|
||
}
|
||
|
||
const {
|
||
parsing,
|
||
calculating,
|
||
parseFile,
|
||
calculate: calculateInWorker,
|
||
invalidateCalculation,
|
||
destroyWorkers,
|
||
} = useTransactionalFileWorker<Omit<CrcWorkerCalculateRequest, 'type' | 'id'>, CrcWorkerResponse>({
|
||
createWorker: () => new Worker(new URL('../workers/crc.worker.ts', import.meta.url), { type: 'module' }),
|
||
onParsed: (response, selected) => {
|
||
clearResults()
|
||
firmware.value = {
|
||
fileName: selected.name,
|
||
fileSize: selected.size,
|
||
format: response.format,
|
||
totalBytes: response.totalBytes,
|
||
blockCount: response.blockCount,
|
||
warnings: response.warnings,
|
||
}
|
||
success.value = `已解析 ${response.blockCount} 个数据块`
|
||
},
|
||
onCalculated: (response) => {
|
||
combinedResult.value = response.combinedResult
|
||
blockResults.value = response.blockResults
|
||
success.value = `${activeCalculationName} 计算完成`
|
||
},
|
||
onParseError: (message) => { error.value = message },
|
||
onCalculationError: (message) => { error.value = message },
|
||
onPendingWorkerError: (hasCurrentFile) => {
|
||
error.value = hasCurrentFile
|
||
? '新文件解析失败,当前文件未更改'
|
||
: 'CRC 后台线程异常终止,请重新选择文件'
|
||
},
|
||
onActiveWorkerError: () => {
|
||
firmware.value = undefined
|
||
clearResults()
|
||
error.value = 'CRC 后台线程异常终止,请重新选择文件'
|
||
},
|
||
})
|
||
|
||
let activeCalculationName = ''
|
||
|
||
function selectFile(selected: File) {
|
||
if (selected.size === 0) { error.value = '文件为空'; return }
|
||
if (selected.size > MAX_FIRMWARE_FILE_SIZE) { error.value = '文件不能超过 64 MiB'; return }
|
||
if (!/\.(?:hex|ihx|bin)$/i.test(selected.name)) { error.value = '请选择 .hex、.ihx 或 .bin 文件'; return }
|
||
error.value = ''
|
||
success.value = ''
|
||
parseFile(selected)
|
||
}
|
||
|
||
function calculate() {
|
||
invalidateResults()
|
||
if (!firmware.value) { error.value = '请先选择 BIN 或 Intel HEX 文件'; return }
|
||
try {
|
||
const parameters = getParameters()
|
||
activeCalculationName = activeParameterName.value
|
||
const startStatus = calculateInWorker({ parameters })
|
||
if (startStatus === 'unavailable') throw new Error('文件数据已失效,请重新选择文件')
|
||
} catch (reason) {
|
||
error.value = reason instanceof Error ? reason.message : 'CRC 计算失败'
|
||
}
|
||
}
|
||
|
||
async function copyResult() {
|
||
try { await copyText(combinedResult.value); success.value = '总 CRC 已复制'; error.value = '' }
|
||
catch { error.value = '复制失败' }
|
||
}
|
||
|
||
function clearFile() {
|
||
destroyWorkers()
|
||
invalidateResults()
|
||
firmware.value = undefined
|
||
}
|
||
|
||
onBeforeUnmount(destroyWorkers)
|
||
</script>
|
||
|
||
<template>
|
||
<ToolPage category="校验计算" title="CRC" description="解析 BIN 或 Intel HEX 有效数据,并使用预设或自定义参数计算 CRC。">
|
||
<FileDropField
|
||
input-id="crc-file"
|
||
replacement-input-id="crc-replacement-file"
|
||
accept=".hex,.ihx,.bin,application/octet-stream"
|
||
:has-file="Boolean(firmware)"
|
||
:busy="parsing"
|
||
initial-label="选择或拖入 HEX / BIN 文件"
|
||
initial-busy-label="正在解析文件…"
|
||
hint="Intel HEX 会校验记录并识别多个数据块"
|
||
compact
|
||
@select="selectFile"
|
||
@clear="clearFile"
|
||
>
|
||
<template #icon><FileUp :size="28" aria-hidden="true" /></template>
|
||
<template v-if="firmware"><strong>{{ firmware.fileName }}</strong><span>{{ firmware.format }} · {{ firmware.totalBytes.toLocaleString() }} 有效字节 · {{ firmware.blockCount }} 个 block</span></template>
|
||
</FileDropField>
|
||
|
||
<div class="crc-config">
|
||
<label class="parameter-field preset-field"><span>参数预设(Preset)</span>
|
||
<select id="crc-preset" name="crc-preset" v-model="selectedPreset" class="select-input" :disabled="calculating" @change="applyPreset">
|
||
<option v-for="preset in CRC_PRESETS" :key="preset.id" :value="preset.id">{{ preset.name }}</option>
|
||
<option value="custom">自定义参数</option>
|
||
</select>
|
||
</label>
|
||
<label class="parameter-field"><span>位宽(Width)</span>
|
||
<select id="crc-width" name="crc-width" v-model.number="width" class="select-input" :disabled="calculating" @change="markCustom"><option :value="8">8</option><option :value="16">16</option><option :value="32">32</option><option :value="64">64</option></select>
|
||
</label>
|
||
<label class="parameter-field"><span>多项式(Poly,Hex)</span><input id="crc-polynomial" name="crc-polynomial" v-model="polynomial" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
|
||
<label class="parameter-field"><span>初始值(Init,Hex)</span><input id="crc-initial-value" name="crc-initial-value" v-model="initialValue" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
|
||
<label class="parameter-field"><span>异或输出(XorOut,Hex)</span><input id="crc-xor-out" name="crc-xor-out" v-model="xorOut" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
|
||
<div class="crc-reflection-options" role="group" aria-label="位反转选项">
|
||
<label class="check-field crc-check"><input id="crc-ref-in" name="crc-ref-in" v-model="inputReflected" type="checkbox" :disabled="calculating" @change="markCustom" />输入位反转(RefIn)</label>
|
||
<label class="check-field crc-check"><input id="crc-ref-out" name="crc-ref-out" v-model="outputReflected" type="checkbox" :disabled="calculating" @change="markCustom" />输出位反转(RefOut)</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-row">
|
||
<button class="action-button primary" type="button" :disabled="!firmware || parsing || calculating" @click="calculate"><Calculator :size="15" />{{ calculating ? '正在计算…' : '计算 CRC' }}</button>
|
||
<span class="action-spacer" />
|
||
<button class="action-button ghost" type="button" :disabled="!combinedResult" @click="copyResult"><Copy :size="15" />复制总 CRC</button>
|
||
</div>
|
||
|
||
<div v-if="combinedResult" class="crc-results">
|
||
<div class="crc-total"><span>总有效数据 · 按地址升序拼接,不填充空洞</span><strong>{{ combinedResult }}</strong></div>
|
||
<div class="result-table-wrap">
|
||
<table class="result-table">
|
||
<thead><tr><th>数据块(Block)</th><th>起始地址(Start Address)</th><th>结束地址(End Address)</th><th>字节数(Length)</th><th>CRC</th></tr></thead>
|
||
<tbody><tr v-for="(block, index) in blockResults" :key="block.startAddress"><td>#{{ index + 1 }}</td><td>{{ formatAddress(block.startAddress) }}</td><td>{{ formatAddress(block.endAddress) }}</td><td>{{ block.length.toLocaleString() }}</td><td class="crc-value">{{ block.crc }}</td></tr></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div v-if="firmware?.warnings.length" class="status-message warning"><AlertCircle :size="16" />{{ firmware.warnings.join(';') }}</div>
|
||
<div v-if="error" class="status-message error"><AlertCircle :size="16" />{{ error }}</div>
|
||
<div v-else-if="success" class="status-message success"><CheckCircle2 :size="16" />{{ success }}</div>
|
||
</ToolPage>
|
||
</template>
|