diff --git a/ROADMAP.md b/ROADMAP.md index 15ac347..33cf621 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -30,7 +30,7 @@ - [x] **自动格式检测** — 检查内容 magic bytes 自动选择查看器 - [x] **自定义格式化器** — 外部可执行文件 + 模板参数 {KEY} {VALUE} {HEX} {HEX_FILE} - [x] **ReJson / TairJson 支持** — JSON.GET / JSON.SET / JSON.DEL 编辑 -- [ ] **二进制 key 支持** — [Hex] 前缀输入,hex ↔ buffer 双向转换 +- [x] **二进制 key 支持** — [Hex] 前缀输入,hex ↔ buffer 双向转换 - [x] **虚拟化滚动** — 大 key 集合(200k+)的性能优化,KeyList 树形视图虚拟化 - [x] **批量删除预览** — 先扫描预览,再确认删除,显示 key 数量和总大小 - [x] **Key 导出** — DUMP + PTTL → CSV 下载 diff --git a/src/components/KeyDetail.vue b/src/components/KeyDetail.vue index d1d1f77..4043b6c 100644 --- a/src/components/KeyDetail.vue +++ b/src/components/KeyDetail.vue @@ -4,6 +4,7 @@ import { computed, ref } from 'vue' import { useKeyStore } from '@renderer/stores/key' import { useConnectionStore } from '@renderer/stores/connection' import { useI18n } from '@renderer/i18n' +import { formatKeyName, isBinaryKey } from '@renderer/utils/binary' import { ElMessage, ElMessageBox } from 'element-plus' import StringEditor from './StringEditor.vue' import HashEditor from './HashEditor.vue' @@ -100,6 +101,17 @@ async function copyKeyName() { } } +async function copyHexKeyName() { + if (!keyStore.selectedKey) return + try { + const hex = Array.from(keyStore.selectedKey).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('') + await navigator.clipboard.writeText(hex) + ElMessage.success(t('key.copied')) + } catch { + ElMessage.error('Copy failed') + } +} + async function deleteKey() { if (!connStore.activeId || !keyStore.selectedKey) return try { @@ -124,10 +136,20 @@ async function refreshKey() {
- {{ keyStore.selectedKey }} + {{ formatKeyName(keyStore.selectedKey) }} + + + {{ t('key.copyHex') }} + {{ keyStore.selectedType?.toUpperCase() }} diff --git a/src/components/KeyList.vue b/src/components/KeyList.vue index 3c1d4f1..93fec4a 100644 --- a/src/components/KeyList.vue +++ b/src/components/KeyList.vue @@ -5,6 +5,7 @@ import { useVirtualizer } from '@tanstack/vue-virtual' import { useKeyStore } from '@renderer/stores/key' import { useConnectionStore } from '@renderer/stores/connection' import { useI18n } from '@renderer/i18n' +import { formatKeyName } from '@renderer/utils/binary' import { ElMessage, ElMessageBox } from 'element-plus' const keyStore = useKeyStore() @@ -218,7 +219,7 @@ async function batchDelete() { const previewKeys = keysArray.slice(0, 100) const remaining = count - 100 - const keyListHtml = previewKeys.map(k => `
${escapeHtml(k)}
`).join('') + const keyListHtml = previewKeys.map(k => `
${escapeHtml(formatKeyName(k))}
`).join('') const hintText = t('key.batchPreviewHint', { n: count }) const andMore = remaining > 0 ? `
${t('key.andMore', { n: remaining })}
` : '' @@ -475,7 +476,7 @@ defineExpose({ deselectAll }) @click.stop /> - {{ filteredKeys[vItem.index] }} + {{ formatKeyName(filteredKeys[vItem.index]) }}
@@ -500,7 +501,7 @@ defineExpose({ deselectAll }) @click.stop /> - {{ keyName }} + {{ formatKeyName(keyName) }} @@ -518,14 +519,14 @@ defineExpose({ deselectAll }) - {{ node.name }} + {{ formatKeyName(node.name) }} {{ node.count }}
- {{ node.name }} + {{ formatKeyName(node.name) }}
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 5f5e729..27be39a 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -110,6 +110,8 @@ export default { exportDone: 'Exported {n} keys', importDone: 'Imported {n} keys', importFailed: 'Import failed: {error}', + binaryKey: 'Binary Key', + copyHex: 'Copy Hex', }, types: { string: 'STRING', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 0488313..d10d3d3 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -110,6 +110,8 @@ export default { exportDone: '已导出 {n} 个键', importDone: '已导入 {n} 个键', importFailed: '导入失败: {error}', + binaryKey: '二进制键', + copyHex: '复制 Hex', }, types: { string: '字符串', diff --git a/src/utils/binary.ts b/src/utils/binary.ts new file mode 100644 index 0000000..64fd5f2 --- /dev/null +++ b/src/utils/binary.ts @@ -0,0 +1,17 @@ +// src/utils/binary.ts +export function isBinaryKey(key: string): boolean { + // Check for non-printable characters (outside 0x20-0x7E, not standard whitespace) + for (let i = 0; i < key.length; i++) { + const code = key.charCodeAt(i) + if (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) return true + if (code > 0x7e) return true + } + return false +} + +export function formatKeyName(key: string): string { + if (isBinaryKey(key)) { + return '[Hex] ' + Array.from(key).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('') + } + return key +}