Initial release of Web Tools 0.0.5

This commit is contained in:
mid
2026-08-07 21:51:15 +08:00
commit fa14e9e5b9
71 changed files with 6365 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
<script setup lang="ts">
import { AlertCircle, Calculator, CheckCircle2, Copy, FileUp, Trash2, Upload } from '@lucide/vue'
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
import ToolPage from '@/components/ToolPage.vue'
import { copyText } from '@/lib/browser'
import { CRC_PRESETS, type CrcParameters } from '@/lib/crc'
import type { CrcBlockResult, CrcWorkerRequest, 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 fileInput = ref<HTMLInputElement>()
const firmware = shallowRef<FirmwareData>()
const dragging = ref(false)
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 parsing = ref(false)
const calculating = ref(false)
const error = ref('')
const success = ref('')
let worker: Worker | undefined
let requestSequence = 0
let activeParseRequest: number | undefined
let activeCalculationRequest: number | undefined
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() {
combinedResult.value = ''
blockResults.value = []
success.value = ''
}
function invalidateResults() {
activeCalculationRequest = undefined
calculating.value = false
clearResults()
error.value = ''
}
function destroyWorker() {
worker?.terminate()
worker = undefined
activeParseRequest = undefined
activeCalculationRequest = undefined
parsing.value = false
calculating.value = false
}
function createWorker(): Worker {
const nextWorker = new Worker(new URL('../workers/crc.worker.ts', import.meta.url), { type: 'module' })
nextWorker.onmessage = (event: MessageEvent<CrcWorkerResponse>) => {
if (nextWorker !== worker) return
const response = event.data
if (response.type === 'parsed') {
if (response.id !== activeParseRequest) return
activeParseRequest = undefined
parsing.value = false
firmware.value = {
fileName: pendingFileName,
fileSize: pendingFileSize,
format: response.format,
totalBytes: response.totalBytes,
blockCount: response.blockCount,
warnings: response.warnings,
}
success.value = `已解析 ${response.blockCount} 个数据块`
return
}
if (response.type === 'calculated') {
if (response.id !== activeCalculationRequest) return
activeCalculationRequest = undefined
calculating.value = false
combinedResult.value = response.combinedResult
blockResults.value = response.blockResults
success.value = `${activeCalculationName} 计算完成`
return
}
if (response.operation === 'parse' && response.id === activeParseRequest) {
activeParseRequest = undefined
parsing.value = false
firmware.value = undefined
error.value = response.error
} else if (response.operation === 'calculate' && response.id === activeCalculationRequest) {
activeCalculationRequest = undefined
calculating.value = false
error.value = response.error
}
}
nextWorker.onerror = () => {
if (nextWorker !== worker) return
destroyWorker()
firmware.value = undefined
clearResults()
error.value = 'CRC 后台线程异常终止,请重新选择文件'
}
worker = nextWorker
return nextWorker
}
let pendingFileName = ''
let pendingFileSize = 0
let activeCalculationName = ''
function selectFile(selected?: File) {
if (!selected) return
destroyWorker()
firmware.value = undefined
clearResults()
error.value = ''
if (selected.size === 0) { error.value = '文件为空'; return }
if (selected.size > MAX_FIRMWARE_FILE_SIZE) { error.value = '文件不能超过 64 MiB'; return }
pendingFileName = selected.name
pendingFileSize = selected.size
const id = ++requestSequence
activeParseRequest = id
parsing.value = true
const request: CrcWorkerRequest = { type: 'parse', id, file: selected }
createWorker().postMessage(request)
}
function onFileInput(event: Event) {
const input = event.target as HTMLInputElement
const selected = input.files?.[0]
input.value = ''
selectFile(selected)
}
function onDrop(event: DragEvent) {
dragging.value = false
void selectFile(event.dataTransfer?.files[0])
}
function calculate() {
invalidateResults()
if (!firmware.value) { error.value = '请先选择 BIN 或 Intel HEX 文件'; return }
try {
const parameters = getParameters()
if (!worker) throw new Error('文件数据已失效,请重新选择文件')
const id = ++requestSequence
activeCalculationRequest = id
activeCalculationName = activeParameterName.value
calculating.value = true
const request: CrcWorkerRequest = { type: 'calculate', id, parameters }
worker.postMessage(request)
} catch (reason) {
activeCalculationRequest = undefined
calculating.value = false
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() {
destroyWorker()
invalidateResults()
firmware.value = undefined
if (fileInput.value) fileInput.value.value = ''
}
onBeforeUnmount(destroyWorker)
</script>
<template>
<ToolPage category="校验计算" title="CRC" description="解析 BIN 或 Intel HEX 有效数据,并使用预设或自定义参数计算 CRC。">
<label v-if="!firmware" class="upload-zone compact-upload" :class="{ 'is-dragging': dragging }" @dragover.prevent="dragging = true" @dragleave.prevent="dragging = false" @drop.prevent="onDrop">
<input ref="fileInput" type="file" accept=".hex,.ihx,.bin,application/octet-stream" :disabled="parsing" @change="onFileInput" />
<span><FileUp :size="28" aria-hidden="true" /><strong>{{ parsing ? '正在解析文件…' : '选择或拖入 HEX / BIN 文件' }}</strong><span>Intel HEX 会校验记录并识别多个数据块</span></span>
</label>
<div v-else class="firmware-summary">
<div><strong>{{ firmware.fileName }}</strong><span>{{ firmware.format }} · {{ firmware.totalBytes.toLocaleString() }} 有效字节 · {{ firmware.blockCount }} 个 block</span></div>
<label class="action-button"><Upload :size="15" />更换文件<input class="sr-only-file" type="file" accept=".hex,.ihx,.bin,application/octet-stream" @change="onFileInput" /></label>
<button class="action-button" type="button" @click="clearFile"><Trash2 :size="15" />清空</button>
</div>
<div class="crc-config">
<label class="parameter-field preset-field"><span>参数预设</span>
<select 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 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 v-model="polynomial" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
<label class="parameter-field"><span>Init (Hex)</span><input v-model="initialValue" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
<label class="parameter-field"><span>XorOut (Hex)</span><input v-model="xorOut" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
<label class="check-field crc-check"><input v-model="inputReflected" type="checkbox" :disabled="calculating" @change="markCustom" />RefIn</label>
<label class="check-field crc-check"><input v-model="outputReflected" type="checkbox" :disabled="calculating" @change="markCustom" />RefOut</label>
</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>起始地址</th><th>结束地址</th><th>字节数</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>