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() {
@@ -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
+}