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') }}
+
@@ -480,4 +571,10 @@ onUnmounted(() => {
font-family: 'Cascadia Code', 'Fira Code', monospace;
margin-left: 6px;
}
+
+.import-cmd-btn {
+ flex-shrink: 0;
+ margin-left: 6px;
+ font-size: 11px;
+}
diff --git a/src/components/KeyList.vue b/src/components/KeyList.vue
index 85bc3f2..3c1d4f1 100644
--- a/src/components/KeyList.vue
+++ b/src/components/KeyList.vue
@@ -1,6 +1,7 @@