From 6ac56369448cc18c7dc0952d7eea389658e6b8fe Mon Sep 17 00:00:00 2001 From: Jokul Date: Tue, 7 Jul 2026 22:14:22 +0800 Subject: [PATCH] feat: key export/import, command import, virtualized scrolling - P2-6: KeyList virtualized scrolling using @tanstack/vue-virtual (>100 keys) - P2-8: Key export to CSV (DUMP + PTTL, hex-encoded) - P2-9: Key import from CSV (RESTORE ... REPLACE) - P2-10: CLI command import from text file - i18n keys for all features (zh-CN + en) --- ROADMAP.md | 8 +- package-lock.json | 27 +++++ package.json | 1 + src/components/CliView.vue | 97 ++++++++++++++++ src/components/KeyList.vue | 220 +++++++++++++++++++++++++++++++++---- src/i18n/locales/en.ts | 7 ++ src/i18n/locales/zh-CN.ts | 7 ++ 7 files changed, 339 insertions(+), 28 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index ce2e203..15ac347 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,11 +31,11 @@ - [x] **自定义格式化器** — 外部可执行文件 + 模板参数 {KEY} {VALUE} {HEX} {HEX_FILE} - [x] **ReJson / TairJson 支持** — JSON.GET / JSON.SET / JSON.DEL 编辑 - [ ] **二进制 key 支持** — [Hex] 前缀输入,hex ↔ buffer 双向转换 -- [ ] **虚拟化滚动** — 大 key 集合(200k+)的性能优化,KeyList 树形视图虚拟化 +- [x] **虚拟化滚动** — 大 key 集合(200k+)的性能优化,KeyList 树形视图虚拟化 - [x] **批量删除预览** — 先扫描预览,再确认删除,显示 key 数量和总大小 -- [ ] **Key 导出** — DUMP + PTTL → CSV 下载 -- [ ] **Key 导入** — CSV (hex_key, hex_value, ttl) → RESTORE -- [ ] **命令导入** — 文本文件逐行执行 Redis 命令 +- [x] **Key 导出** — DUMP + PTTL → CSV 下载 +- [x] **Key 导入** — CSV (hex_key, hex_value, ttl) → RESTORE +- [x] **命令导入** — 文本文件逐行执行 Redis 命令 - [x] **FlushDB** — 清空当前数据库,双重确认 - [x] **Key 搜索历史** — 最近 200 条搜索模式自动补全 - [x] **取消扫描** — 中断正在进行的 SCAN diff --git a/package-lock.json b/package-lock.json index bf3370a..d828694 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "dependencies": { "@guolao/vue-monaco-editor": "^1.6.0", + "@tanstack/vue-virtual": "^3.13.31", "electron-store": "^11.0.2", "electron-updater": "^6.8.9", "ioredis": "^5.7.0", @@ -1881,6 +1882,32 @@ "node": ">=10" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.3.tgz", + "integrity": "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.31", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.31.tgz", + "integrity": "sha512-wZMEoSf852jQqaf3Ika1J7PiBae6341LNy/2CxmIyn0XKDQXMuK41wVX+xp6G0yx8jyR95Ef+Tdr13DK7mbJtQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", diff --git a/package.json b/package.json index 49223db..46b71df 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ }, "dependencies": { "@guolao/vue-monaco-editor": "^1.6.0", + "@tanstack/vue-virtual": "^3.13.31", "electron-store": "^11.0.2", "electron-updater": "^6.8.9", "ioredis": "^5.7.0", diff --git a/src/components/CliView.vue b/src/components/CliView.vue index 51a6bca..9919796 100644 --- a/src/components/CliView.vue +++ b/src/components/CliView.vue @@ -165,6 +165,94 @@ async function execute() { } } +async function importCommands() { + if (!connStore.activeId) return + + const result = await window.electronAPI.dialog.openFile({ + title: t('cli.importCommands'), + filters: [{ name: 'Text Files', extensions: ['txt', 'conf'] }], + }) + + if (!result || !result.content) return + + const lines = result.content.split('\n').filter(line => line.trim() && !line.trim().startsWith('#')) + let executed = 0 + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) continue + + // Parse command respecting quoted strings + const args: string[] = [] + let current = '' + let inQuote = false + let quoteChar = '' + + for (const ch of trimmed) { + if (inQuote) { + if (ch === quoteChar) { + inQuote = false + if (current) args.push(current) + current = '' + } else { + current += ch + } + } else if (ch === '"' || ch === "'") { + inQuote = true + quoteChar = ch + } else if (ch === ' ') { + if (current) { + args.push(current) + current = '' + } + } else { + current += ch + } + } + if (current) args.push(current) + + if (args.length === 0) continue + + const command = args[0].toUpperCase() + const cmdArgs = args.slice(1) + + try { + const result = await window.electronAPI.redis.execute(connStore.activeId, command, cmdArgs) + let formatted = '' + if (result === null) { + formatted = '(nil)' + } else if (typeof result === 'number') { + formatted = `(integer) ${result}` + } else if (Array.isArray(result)) { + if (result.length === 0) { + formatted = '(empty array)' + } else { + formatted = result.map((item: any, i: number) => { + if (item === null) return `${i + 1}) (nil)` + if (typeof item === 'number') return `${i + 1}) (integer) ${item}` + return `${i + 1}) "${item}"` + }).join('\n') + } + } else if (typeof result === 'object') { + formatted = JSON.stringify(result, null, 2) + } else { + formatted = String(result) + } + history.value.push({ cmd: trimmed, result: formatted }) + executed++ + } catch (err: any) { + history.value.push({ cmd: trimmed, result: `ERROR: ${err.message}`, error: true }) + } + } + + history.value.push({ cmd: '', result: `--- ${t('cli.importCmdDone', { n: executed })} ---` }) + + await nextTick() + if (outputRef.value) { + outputRef.value.scrollTop = outputRef.value.scrollHeight + } +} + function onKeydown(e: KeyboardEvent) { if (showSuggestions.value) { if (e.key === 'ArrowDown') { @@ -310,6 +398,9 @@ onUnmounted(() => { + + {{ t('cli.importCommands') }} +