feat: binary key support

- New src/utils/binary.ts: isBinaryKey + formatKeyName
- KeyList: display [Hex] prefix for binary keys in list/tree/preview
- KeyDetail: binary key display + copy hex button
- i18n keys (zh-CN + en)
- P2 complete!
This commit is contained in:
2026-07-07 22:21:19 +08:00
parent 6ac5636944
commit b802fc6928
6 changed files with 51 additions and 7 deletions
+1 -1
View File
@@ -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 下载
+23 -1
View File
@@ -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() {
<div class="detail-header">
<div class="key-info">
<el-icon class="key-icon" :style="{ color: typeColor }"><Document /></el-icon>
<span class="key-name">{{ keyStore.selectedKey }}</span>
<span class="key-name">{{ formatKeyName(keyStore.selectedKey) }}</span>
<el-button size="small" text class="copy-btn" @click="copyKeyName">
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button
v-if="isBinaryKey(keyStore.selectedKey)"
size="small"
text
class="copy-btn"
@click="copyHexKeyName"
>
<el-icon><CopyDocument /></el-icon>
<span class="copy-hex-label">{{ t('key.copyHex') }}</span>
</el-button>
<span class="type-badge" :style="{ color: typeColor, borderColor: typeColor }">
{{ keyStore.selectedType?.toUpperCase() }}
</span>
+6 -5
View File
@@ -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 => `<div class="batch-preview-key">${escapeHtml(k)}</div>`).join('')
const keyListHtml = previewKeys.map(k => `<div class="batch-preview-key">${escapeHtml(formatKeyName(k))}</div>`).join('')
const hintText = t('key.batchPreviewHint', { n: count })
const andMore = remaining > 0 ? `<div class="batch-preview-more">${t('key.andMore', { n: remaining })}</div>` : ''
@@ -475,7 +476,7 @@ defineExpose({ deselectAll })
@click.stop
/>
<el-icon class="key-icon"><Document /></el-icon>
<span class="key-name">{{ filteredKeys[vItem.index] }}</span>
<span class="key-name">{{ formatKeyName(filteredKeys[vItem.index]) }}</span>
</div>
</div>
</div>
@@ -500,7 +501,7 @@ defineExpose({ deselectAll })
@click.stop
/>
<el-icon class="key-icon"><Document /></el-icon>
<span class="key-name">{{ keyName }}</span>
<span class="key-name">{{ formatKeyName(keyName) }}</span>
</div>
</template>
</div>
@@ -518,14 +519,14 @@ defineExpose({ deselectAll })
<ArrowRight />
</el-icon>
<el-icon class="tree-icon folder-icon"><Folder /></el-icon>
<span class="tree-name">{{ node.name }}</span>
<span class="tree-name">{{ formatKeyName(node.name) }}</span>
<span class="tree-count">{{ node.count }}</span>
</div>
<div v-else class="tree-children" :class="{ hidden: !isFolderExpanded(node.fullPath) }">
<div class="tree-item leaf-item" @click="selectKey(node.fullPath)">
<span class="tree-spacer"></span>
<el-icon class="tree-icon"><Document /></el-icon>
<span class="tree-name">{{ node.name }}</span>
<span class="tree-name">{{ formatKeyName(node.name) }}</span>
</div>
</div>
</template>
+2
View File
@@ -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',
+2
View File
@@ -110,6 +110,8 @@ export default {
exportDone: '已导出 {n} 个键',
importDone: '已导入 {n} 个键',
importFailed: '导入失败: {error}',
binaryKey: '二进制键',
copyHex: '复制 Hex',
},
types: {
string: '字符串',
+17
View File
@@ -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
}