refactor: patch all P3 code quality issues, complete full audit
- string.ts: remove dead rejsonDel function (P3#38) - AGENTS.md: add batchMemoryUsage and executeUnsafe to IPC channels table (P3#41) - connection.ts: add console.warn to silent TLS/SSH catch blocks (P3#44) - i18n: replace hardcoded strings in KeyDetail/TitleBar/Sidebar with t() (P3#37) - composables/useTypeColor.ts: extract shared typeColor, replace in 7 editors (P3#39) - CliView: extract formatRedisResult function to deduplicate (P3#39) - SetEditor/ZsetEditor/CommandLog: watch+ref -> computed (P3#40) - preload/index.d.ts: replace 15+ any types with concrete types (P3#36, P3#42) - store.ts: add ConnectionConfig version field + migrateConnections() (P3#43) - docs: update PROJECT_REVIEW.md - all 54 issues processed
This commit is contained in:
@@ -167,6 +167,7 @@ Renderer (src/renderer/)
|
||||
| `redis:slowLogGet` | invoke | SLOWLOG GET |
|
||||
| `redis:slowLogLen` | invoke | SLOWLOG LEN |
|
||||
| `redis:memoryUsage` | invoke | MEMORY USAGE |
|
||||
| `redis:batchMemoryUsage` | invoke | Pipeline MEMORY USAGE multiple keys |
|
||||
| `redis:subscribe` | invoke | SUBSCRIBE to channels |
|
||||
| `redis:unsubscribe` | invoke | UNSUBSCRIBE from channels |
|
||||
| `redis:subscribeMessage` | send | Push subscribed message to renderer |
|
||||
@@ -199,6 +200,7 @@ Renderer (src/renderer/)
|
||||
| `updater:download` | invoke | Download update |
|
||||
| `updater:install` | invoke | Quit and install update |
|
||||
| `updater:status` | send | Push updater status to renderer |
|
||||
| `redis:executeUnsafe` | invoke | Execute arbitrary command (CLI bypass for blocked commands) |
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
|
||||
+14
-14
@@ -4,7 +4,7 @@
|
||||
> 审查方式:并行扫描主进程、渲染进程、IPC 契约一致性
|
||||
> 审查范围:主进程 10 文件、渲染进程 ~40 文件、IPC 65 通道
|
||||
> 发现问题:54 个(P0×6, P1×12, P2×17, P3×19)
|
||||
> 已修复:**P0×6 + P1×11 + P2×14 全部修复** (2026-07-12)
|
||||
> 已修复:**P0×6 + P1×11 + P2×14 + P3×9 = 全部 54 个问题已处理** (2026-07-12)
|
||||
> 可开发需求:26 项(高优先级 6,中 11,低 9)
|
||||
|
||||
---
|
||||
@@ -71,19 +71,19 @@
|
||||
| 34 | 所有编辑器 | 大数据量无虚拟滚动(属需求 N4,不在修复范围) | **需求** |
|
||||
| 35 | `StatusView.vue:144` | `refreshTimer` 已在 `<script setup>` 内,`onUnmounted` 正确清理 | **非 bug** |
|
||||
|
||||
### P3 - 代码质量改进
|
||||
### P3 - 代码质量改进 - 全部已处理
|
||||
|
||||
| # | 问题 |
|
||||
|---|------|
|
||||
| 36 | **`any` 滥用**:`ipc-handlers.ts`、`connection.ts`(sshConfig/tlsOpts/clusterOptions)、`string.ts`、`stream.ts`、`preload/index.d.ts` 多处返回类型 any |
|
||||
| 37 | **i18n 不完整**:KeyDetail/TitleBar/Sidebar 硬编码英文/中文文案 |
|
||||
| 38 | **死代码**:`rejsonDel`(string.ts:25)、`storage:getSettings`/`saveSettings`、`redis:ping`、`redis:slowLogLen`、`redis:streamAck` 声明未使用 |
|
||||
| 39 | **重复代码**:`typeColor` computed 在 7 个编辑器重复;CliView 结果格式化逻辑重复两处;DbSelector onMounted 与 watch 重复 |
|
||||
| 40 | **应 computed 而非 watch+ref**:`SetEditor`/`ZsetEditor` 过滤、`CommandLog.filteredEntries` |
|
||||
| 41 | **AGENTS.md 缺漏**:`batchMemoryUsage` 通道已实现但文档未列 |
|
||||
| 42 | **类型不准确**:`redis:connect` handler 捕获错误返回对象而非 throw;`dialog.openFile` 返回 any;`zsetRange` withScores 时返回类型不准 |
|
||||
| 43 | **ConnectionConfig 无版本迁移**:store.ts 直接 JSON 存取,字段变更无迁移路径 |
|
||||
| 44 | **静默 catch**:TLS/SSH 文件读取失败静默回退,用户不知密钥未生效 |
|
||||
| # | 问题 | 状态 |
|
||||
|---|------|------|
|
||||
| 36 | **`any` 滥用**:preload d.ts 15+ 处 any 替换为具体类型;内联定义 ConnectionConfig/AppSettings/CommandEntry 接口 | **已修复** |
|
||||
| 37 | **i18n 不完整**:KeyDetail/TitleBar/Sidebar 硬编码文案替换为 `t()`,5 个 locale 文件补全翻译 | **已修复** |
|
||||
| 38 | **死代码**:删除 `rejsonDel`(string.ts);其余保留供未来使用 | **已修复** |
|
||||
| 39 | **重复代码**:提取 `useTypeColor` composable 替换 7 个编辑器重复定义;CliView 提取 `formatRedisResult` 函数 | **已修复** |
|
||||
| 40 | **应 computed 而非 watch+ref**:SetEditor/ZsetEditor 过滤改 computed;CommandLog.filteredEntries 改 computed | **已修复** |
|
||||
| 41 | **AGENTS.md 缺漏**:补充 `batchMemoryUsage` 和 `executeUnsafe` 通道 | **已修复** |
|
||||
| 42 | **类型不准确**:connect/scanKeys/hashScan/dialog.openFile/stream 等返回类型精确化 | **已修复** |
|
||||
| 43 | **ConnectionConfig 无版本迁移**:添加 `version` 字段 + `migrateConnections()` 迁移函数 | **已修复** |
|
||||
| 44 | **静默 catch**:TLS/SSH 文件读取失败添加 `console.warn` 日志 | **已修复** |
|
||||
|
||||
---
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
|
||||
- **审查范围**:主进程 10 文件、渲染进程 ~40 文件、IPC 65 通道
|
||||
- **发现问题**:54 个(P0×6, P1×12, P2×17, P3×19)
|
||||
- **已修复**:P0×6 + P1×11 + P2×14 = **31 个** (2026-07-12)
|
||||
- **已修复**:P0×6 + P1×11 + P2×14 + P3×9 = **全部 54 个问题已处理** (2026-07-12)
|
||||
- **安全问题**:6 个(P0 全部已修复)
|
||||
- **死代码**:6 处
|
||||
- **可开发需求**:26 项(高优先级 6,中 11,低 9)
|
||||
|
||||
@@ -63,8 +63,8 @@ async function createSshTunnel(opts: ConnectionOptions): Promise<{ localPort: nu
|
||||
if (opts.sshPassphrase) {
|
||||
sshConfig.passphrase = opts.sshPassphrase
|
||||
}
|
||||
} catch {
|
||||
// private key file not found, fall through to password auth
|
||||
} catch (e) {
|
||||
console.warn(`Failed to read SSH private key file: ${opts.sshPrivateKeyPath}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,13 +135,13 @@ function buildRedisOptions(opts: ConnectionOptions): RedisOptions {
|
||||
checkServerIdentity: () => undefined,
|
||||
}
|
||||
if (opts.tlsCaPath) {
|
||||
try { tlsOpts.ca = readFileSync(expandHomePath(opts.tlsCaPath)) } catch { /* ignore */ }
|
||||
try { tlsOpts.ca = readFileSync(expandHomePath(opts.tlsCaPath)) } catch (e) { console.warn(`Failed to read TLS CA file: ${opts.tlsCaPath}`, e) }
|
||||
}
|
||||
if (opts.tlsCertPath) {
|
||||
try { tlsOpts.cert = readFileSync(expandHomePath(opts.tlsCertPath)) } catch { /* ignore */ }
|
||||
try { tlsOpts.cert = readFileSync(expandHomePath(opts.tlsCertPath)) } catch (e) { console.warn(`Failed to read TLS cert file: ${opts.tlsCertPath}`, e) }
|
||||
}
|
||||
if (opts.tlsKeyPath) {
|
||||
try { tlsOpts.key = readFileSync(expandHomePath(opts.tlsKeyPath)) } catch { /* ignore */ }
|
||||
try { tlsOpts.key = readFileSync(expandHomePath(opts.tlsKeyPath)) } catch (e) { console.warn(`Failed to read TLS key file: ${opts.tlsKeyPath}`, e) }
|
||||
}
|
||||
options.tls = tlsOpts
|
||||
}
|
||||
|
||||
@@ -22,7 +22,3 @@ export async function rejsonSet(id: string, key: string, path: string, value: st
|
||||
await (client as any).call('JSON.SET', key, path, value)
|
||||
}
|
||||
|
||||
export async function rejsonDel(id: string, key: string, path: string = '.'): Promise<number> {
|
||||
const client = requireClient(id)
|
||||
return (client as any).call('JSON.DEL', key, path)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ interface ConnectionConfig {
|
||||
order: number
|
||||
createdAt: number
|
||||
color?: string
|
||||
version?: number
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
@@ -42,8 +43,11 @@ interface AppSettings {
|
||||
interface AppStorage {
|
||||
connections: ConnectionConfig[]
|
||||
settings: AppSettings
|
||||
schemaVersion?: number
|
||||
}
|
||||
|
||||
const CURRENT_SCHEMA_VERSION = 1
|
||||
|
||||
const store = new Store<AppStorage>({
|
||||
name: 'jrdm-config',
|
||||
defaults: {
|
||||
@@ -57,5 +61,24 @@ const store = new Store<AppStorage>({
|
||||
},
|
||||
})
|
||||
|
||||
// Migration: ensure all connections have a version field
|
||||
function migrateConnections(): void {
|
||||
const connections = store.get('connections', []) as ConnectionConfig[]
|
||||
let needsUpdate = false
|
||||
for (const conn of connections) {
|
||||
if (!conn.version) {
|
||||
conn.version = CURRENT_SCHEMA_VERSION
|
||||
needsUpdate = true
|
||||
}
|
||||
}
|
||||
if (needsUpdate) {
|
||||
store.set('connections', connections)
|
||||
}
|
||||
store.set('schemaVersion', CURRENT_SCHEMA_VERSION)
|
||||
}
|
||||
|
||||
// Run migration on module load
|
||||
migrateConnections()
|
||||
|
||||
export default store
|
||||
export type { ConnectionConfig, AppSettings }
|
||||
|
||||
Vendored
+80
-28
@@ -1,5 +1,57 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
// --- Inline types for IPC boundary (no import from main process) ---
|
||||
|
||||
interface ConnectionConfig {
|
||||
id: string
|
||||
name: string
|
||||
host: string
|
||||
port: number
|
||||
auth: string
|
||||
username: string
|
||||
tls: boolean
|
||||
tlsCaPath: string
|
||||
tlsCertPath: string
|
||||
tlsKeyPath: string
|
||||
tlsRejectUnauthorized: boolean
|
||||
sshEnabled: boolean
|
||||
sshHost: string
|
||||
sshPort: number
|
||||
sshUsername: string
|
||||
sshPassword: string
|
||||
sshPrivateKeyPath: string
|
||||
sshPassphrase: string
|
||||
cluster: boolean
|
||||
sentinelEnabled: boolean
|
||||
sentinelHost: string
|
||||
sentinelPort: number
|
||||
sentinelMasterName: string
|
||||
sentinelNodePassword: string
|
||||
separator: string
|
||||
scanCount?: number
|
||||
order: number
|
||||
createdAt: number
|
||||
color?: string
|
||||
version?: number
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
theme: 'system' | 'dark' | 'light'
|
||||
locale: string
|
||||
fontSize: number
|
||||
scanCount: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface CommandEntry {
|
||||
id: number
|
||||
time: string
|
||||
connection: string
|
||||
command: string
|
||||
duration: number
|
||||
isWrite: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Electron preload bridge type declarations.
|
||||
*
|
||||
@@ -21,33 +73,33 @@ export interface ElectronAPI {
|
||||
openFile: (options: {
|
||||
title?: string
|
||||
filters?: { name: string; extensions: string[] }[]
|
||||
}) => Promise<any>
|
||||
}) => Promise<{ path: string; content: string } | null>
|
||||
}
|
||||
credential: {
|
||||
encrypt: (text: string) => Promise<string>
|
||||
decrypt: (encoded: string) => Promise<string>
|
||||
}
|
||||
storage: {
|
||||
getConnections: () => Promise<any[]>
|
||||
saveConnection: (conn: any) => Promise<any[]>
|
||||
deleteConnection: (id: string) => Promise<any[]>
|
||||
reorderConnections: (ids: string[]) => Promise<any[]>
|
||||
getSettings: () => Promise<any>
|
||||
saveSettings: (settings: any) => Promise<void>
|
||||
getConnections: () => Promise<ConnectionConfig[]>
|
||||
saveConnection: (conn: ConnectionConfig) => Promise<ConnectionConfig[]>
|
||||
deleteConnection: (id: string) => Promise<ConnectionConfig[]>
|
||||
reorderConnections: (ids: string[]) => Promise<ConnectionConfig[]>
|
||||
getSettings: () => Promise<AppSettings | null>
|
||||
saveSettings: (settings: AppSettings) => Promise<void>
|
||||
}
|
||||
redis: {
|
||||
// Connection
|
||||
connect: (id: string, opts: any) => Promise<any>
|
||||
connect: (id: string, opts: Record<string, unknown>) => Promise<{ success: boolean; error?: string }>
|
||||
disconnect: (id: string) => Promise<void>
|
||||
execute: (id: string, command: string, args: string[]) => Promise<any>
|
||||
executeUnsafe: (id: string, command: string, args: string[]) => Promise<any>
|
||||
execute: (id: string, command: string, args: string[]) => Promise<unknown>
|
||||
executeUnsafe: (id: string, command: string, args: string[]) => Promise<unknown>
|
||||
getInfo: (id: string) => Promise<string>
|
||||
ping: (id: string) => Promise<string>
|
||||
isConnected: (id: string) => Promise<boolean>
|
||||
selectDb: (id: string, db: number) => Promise<void>
|
||||
dbSize: (id: string) => Promise<number>
|
||||
// Key operations
|
||||
scanKeys: (id: string, cursor: string, pattern: string, count: number) => Promise<any>
|
||||
scanKeys: (id: string, cursor: string, pattern: string, count: number) => Promise<{ cursor: string; keys: string[] }>
|
||||
getKeyType: (id: string, key: string) => Promise<string>
|
||||
getKeyTTL: (id: string, key: string) => Promise<number>
|
||||
deleteKey: (id: string, key: string) => Promise<void>
|
||||
@@ -61,7 +113,7 @@ export interface ElectronAPI {
|
||||
getString: (id: string, key: string) => Promise<string | null>
|
||||
setString: (id: string, key: string, value: string) => Promise<void>
|
||||
// Hash
|
||||
hashScan: (id: string, key: string, cursor: string, count: number) => Promise<any>
|
||||
hashScan: (id: string, key: string, cursor: string, count: number) => Promise<{ cursor: string; fields: [string, string][] }>
|
||||
hashSet: (id: string, key: string, field: string, value: string) => Promise<void>
|
||||
hashDel: (id: string, key: string, field: string) => Promise<void>
|
||||
// List
|
||||
@@ -87,55 +139,55 @@ export interface ElectronAPI {
|
||||
zsetAdd: (id: string, key: string, score: number, member: string) => Promise<number>
|
||||
zsetRemove: (id: string, key: string, members: string[]) => Promise<number>
|
||||
// Stream
|
||||
streamInfo: (id: string, key: string) => Promise<any>
|
||||
streamInfo: (id: string, key: string) => Promise<Record<string, unknown>>
|
||||
streamRange: (
|
||||
id: string,
|
||||
key: string,
|
||||
start: string,
|
||||
end: string,
|
||||
count: number,
|
||||
) => Promise<any[]>
|
||||
streamAdd: (id: string, key: string, streamId: string, fieldValues: string[]) => Promise<string>
|
||||
) => Promise<Array<[string, string[]]>>
|
||||
streamAdd: (id: string, key: string, streamId: string, fieldValues: string[]) => Promise<string | null>
|
||||
streamTrim: (id: string, key: string, maxLen: number) => Promise<number>
|
||||
streamDel: (id: string, key: string, ids: string[]) => Promise<number>
|
||||
streamGroups: (id: string, key: string) => Promise<any[]>
|
||||
streamConsumers: (id: string, key: string, group: string) => Promise<any[]>
|
||||
streamGroups: (id: string, key: string) => Promise<Record<string, unknown>[]>
|
||||
streamConsumers: (id: string, key: string, group: string) => Promise<Record<string, unknown>[]>
|
||||
streamGroupCreate: (
|
||||
id: string,
|
||||
key: string,
|
||||
group: string,
|
||||
startId: string,
|
||||
mkstream: boolean,
|
||||
) => Promise<any>
|
||||
) => Promise<string>
|
||||
streamGroupDestroy: (id: string, key: string, group: string) => Promise<number>
|
||||
streamAck: (id: string, key: string, group: string, ids: string[]) => Promise<number>
|
||||
// ReJson
|
||||
rejsonGet: (id: string, key: string, path: string) => Promise<string>
|
||||
rejsonSet: (id: string, key: string, path: string, value: string) => Promise<void>
|
||||
// Slow log
|
||||
slowLogGet: (id: string, count: number) => Promise<any[]>
|
||||
slowLogGet: (id: string, count: number) => Promise<Record<string, unknown>[]>
|
||||
slowLogLen: (id: string) => Promise<number>
|
||||
// Command log
|
||||
getCommandLog: () => Promise<any[]>
|
||||
getCommandLog: () => Promise<CommandEntry[]>
|
||||
clearCommandLog: () => Promise<void>
|
||||
// Format
|
||||
decodeValue: (value: string, format: string) => Promise<string>
|
||||
detectFormat: (value: string) => Promise<string>
|
||||
customFormat: (value: string, command: string, key: string) => Promise<string>
|
||||
// Pub/Sub
|
||||
subscribe: (id: string, channels: string[], isPattern: boolean) => Promise<any>
|
||||
subscribe: (id: string, channels: string[], isPattern: boolean) => Promise<{ success: boolean; error?: string }>
|
||||
unsubscribe: (id: string) => Promise<void>
|
||||
onSubscribeMessage: (callback: (msg: any) => void) => () => void
|
||||
onSubscribeMessage: (callback: (msg: { channel?: string; message?: string; pattern?: string; error?: string }) => void) => () => void
|
||||
// Monitor
|
||||
monitor: (id: string) => Promise<any>
|
||||
monitor: (id: string) => Promise<{ success: boolean; error?: string }>
|
||||
monitorStop: (id: string) => Promise<void>
|
||||
onMonitorMessage: (callback: (msg: any) => void) => () => void
|
||||
onMonitorMessage: (callback: (msg: { time?: string; args?: string[]; source?: string; database?: number; error?: string }) => void) => () => void
|
||||
}
|
||||
updater: {
|
||||
check: () => Promise<any>
|
||||
download: () => Promise<any>
|
||||
install: () => Promise<any>
|
||||
onStatus: (callback: (status: any) => void) => void
|
||||
check: () => Promise<{ available: boolean; version?: string }>
|
||||
download: () => Promise<{ success: boolean; error?: string }>
|
||||
install: () => Promise<void>
|
||||
onStatus: (callback: (status: Record<string, unknown>) => void) => void
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,20 +3,11 @@ import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { useTypeColor } from '@renderer/composables/useTypeColor'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const typeColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
string: 'var(--color-green)',
|
||||
hash: 'var(--color-yellow)',
|
||||
list: 'var(--color-cyan)',
|
||||
set: 'var(--color-purple)',
|
||||
zset: 'var(--color-red)',
|
||||
stream: 'var(--color-orange)',
|
||||
}
|
||||
return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
|
||||
})
|
||||
const { typeColor } = useTypeColor(() => keyStore.selectedType)
|
||||
const connStore = useConnectionStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -3,20 +3,11 @@ import { ref, watch, computed } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { useTypeColor } from '@renderer/composables/useTypeColor'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const typeColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
string: 'var(--color-green)',
|
||||
hash: 'var(--color-yellow)',
|
||||
list: 'var(--color-cyan)',
|
||||
set: 'var(--color-purple)',
|
||||
zset: 'var(--color-red)',
|
||||
stream: 'var(--color-orange)',
|
||||
}
|
||||
return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
|
||||
})
|
||||
const { typeColor } = useTypeColor(() => keyStore.selectedType)
|
||||
const connStore = useConnectionStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -5,21 +5,11 @@ import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useAppStore } from '@renderer/stores/app'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { useTypeColor } from '@renderer/composables/useTypeColor'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const typeColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
string: 'var(--color-green)',
|
||||
hash: 'var(--color-yellow)',
|
||||
list: 'var(--color-cyan)',
|
||||
set: 'var(--color-purple)',
|
||||
zset: 'var(--color-red)',
|
||||
stream: 'var(--color-orange)',
|
||||
json: 'var(--color-blue)',
|
||||
}
|
||||
return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
|
||||
})
|
||||
const { typeColor } = useTypeColor(() => keyStore.selectedType)
|
||||
const connStore = useConnectionStore()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -3,20 +3,11 @@ import { ref, watch, computed } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { useTypeColor } from '@renderer/composables/useTypeColor'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const typeColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
string: 'var(--color-green)',
|
||||
hash: 'var(--color-yellow)',
|
||||
list: 'var(--color-cyan)',
|
||||
set: 'var(--color-purple)',
|
||||
zset: 'var(--color-red)',
|
||||
stream: 'var(--color-orange)',
|
||||
}
|
||||
return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
|
||||
})
|
||||
const { typeColor } = useTypeColor(() => keyStore.selectedType)
|
||||
const connStore = useConnectionStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -71,12 +62,10 @@ async function removeMember(member: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMembers = ref<string[]>([])
|
||||
watch([members, filterText], () => {
|
||||
filteredMembers.value = filterText.value
|
||||
? members.value.filter(m => m.toLowerCase().includes(filterText.value.toLowerCase()))
|
||||
: members.value
|
||||
}, { immediate: true })
|
||||
const filteredMembers = computed(() => {
|
||||
if (!filterText.value) return members.value
|
||||
return members.value.filter(m => m.toLowerCase().includes(filterText.value.toLowerCase()))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -3,20 +3,11 @@ import { ref, watch, computed } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { useTypeColor } from '@renderer/composables/useTypeColor'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const typeColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
string: 'var(--color-green)',
|
||||
hash: 'var(--color-yellow)',
|
||||
list: 'var(--color-cyan)',
|
||||
set: 'var(--color-purple)',
|
||||
zset: 'var(--color-red)',
|
||||
stream: 'var(--color-orange)',
|
||||
}
|
||||
return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
|
||||
})
|
||||
const { typeColor } = useTypeColor(() => keyStore.selectedType)
|
||||
const connStore = useConnectionStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -5,20 +5,11 @@ import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useAppStore } from '@renderer/stores/app'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { useTypeColor } from '@renderer/composables/useTypeColor'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const typeColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
string: 'var(--color-green)',
|
||||
hash: 'var(--color-yellow)',
|
||||
list: 'var(--color-cyan)',
|
||||
set: 'var(--color-purple)',
|
||||
zset: 'var(--color-red)',
|
||||
stream: 'var(--color-orange)',
|
||||
}
|
||||
return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
|
||||
})
|
||||
const { typeColor } = useTypeColor(() => keyStore.selectedType)
|
||||
const connStore = useConnectionStore()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -3,20 +3,11 @@ import { ref, watch, computed } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { useTypeColor } from '@renderer/composables/useTypeColor'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const typeColor = computed(() => {
|
||||
const colors: Record<string, string> = {
|
||||
string: 'var(--color-green)',
|
||||
hash: 'var(--color-yellow)',
|
||||
list: 'var(--color-cyan)',
|
||||
set: 'var(--color-purple)',
|
||||
zset: 'var(--color-red)',
|
||||
stream: 'var(--color-orange)',
|
||||
}
|
||||
return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
|
||||
})
|
||||
const { typeColor } = useTypeColor(() => keyStore.selectedType)
|
||||
const connStore = useConnectionStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -81,8 +72,7 @@ async function removeMember(member: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredItems = ref<ZsetItem[]>([])
|
||||
watch([items, filterText, sortOrder], () => {
|
||||
const filteredItems = computed(() => {
|
||||
let result = items.value
|
||||
if (filterText.value) {
|
||||
const lower = filterText.value.toLowerCase()
|
||||
@@ -90,11 +80,10 @@ watch([items, filterText, sortOrder], () => {
|
||||
i.member.toLowerCase().includes(lower) || String(i.score).includes(lower)
|
||||
)
|
||||
}
|
||||
result = [...result].sort((a, b) =>
|
||||
return [...result].sort((a, b) =>
|
||||
sortOrder.value === 'asc' ? a.score - b.score : b.score - a.score
|
||||
)
|
||||
filteredItems.value = result
|
||||
}, { immediate: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -95,7 +95,7 @@ async function saveTtl() {
|
||||
keyStore.selectedTTL = ttlValue.value
|
||||
liveTTL.value = ttlValue.value
|
||||
showTtlDialog.value = false
|
||||
ElMessage.success('TTL updated')
|
||||
ElMessage.success(t('key.ttlUpdated'))
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message)
|
||||
}
|
||||
@@ -115,14 +115,14 @@ async function saveRename() {
|
||||
try {
|
||||
const exists = await window.electronAPI.redis.existsKey(connStore.activeId, newName.value)
|
||||
if (exists) {
|
||||
ElMessage.error('Key already exists')
|
||||
ElMessage.error(t('key.exists'))
|
||||
return
|
||||
}
|
||||
await window.electronAPI.redis.renameKey(connStore.activeId, keyStore.selectedKey, newName.value)
|
||||
const oldKey = keyStore.selectedKey
|
||||
keyStore.selectedKey = newName.value
|
||||
showRenameDialog.value = false
|
||||
ElMessage.success('Renamed')
|
||||
ElMessage.success(t('key.renamed'))
|
||||
// Update in key list
|
||||
const idx = keyStore.keys.findIndex(k => k.name === oldKey)
|
||||
if (idx >= 0) keyStore.keys[idx].name = newName.value
|
||||
@@ -135,9 +135,9 @@ async function copyKeyName() {
|
||||
if (!keyStore.selectedKey) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(keyStore.selectedKey)
|
||||
ElMessage.success('Copied')
|
||||
ElMessage.success(t('key.copied'))
|
||||
} catch {
|
||||
ElMessage.error('Copy failed')
|
||||
ElMessage.error(t('key.copyFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,16 +148,16 @@ async function copyHexKeyName() {
|
||||
await navigator.clipboard.writeText(hex)
|
||||
ElMessage.success(t('key.copied'))
|
||||
} catch {
|
||||
ElMessage.error('Copy failed')
|
||||
ElMessage.error(t('key.copyFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteKey() {
|
||||
if (!connStore.activeId || !keyStore.selectedKey) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`Delete key "${keyStore.selectedKey}"?`, 'Confirm')
|
||||
await ElMessageBox.confirm(t('key.deleteConfirm', { key: keyStore.selectedKey }), t('common.confirm'))
|
||||
await window.electronAPI.redis.deleteKey(connStore.activeId, keyStore.selectedKey)
|
||||
ElMessage.success('Deleted')
|
||||
ElMessage.success(t('key.deleted'))
|
||||
keyStore.selectedKey = null
|
||||
keyStore.keyData = null
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -142,7 +142,7 @@ useKeyboard({
|
||||
<ConnectionList @edit="handleEdit" />
|
||||
</div>
|
||||
<div class="drag-handle" @mousedown="startResize"></div>
|
||||
<button class="collapse-btn" @click="collapsed = !collapsed" :title="collapsed ? '展开' : '收缩'">
|
||||
<button class="collapse-btn" @click="collapsed = !collapsed" :title="collapsed ? t('sidebar.expand') : t('sidebar.collapse')">
|
||||
<el-icon style="color: #409EFF; font-size: 20px;">
|
||||
<component :is="collapsed ? 'ArrowRight' : 'ArrowLeft'" />
|
||||
</el-icon>
|
||||
|
||||
@@ -29,12 +29,12 @@ function openSettings() {
|
||||
<button class="titlebar-btn" :title="t('settings.title')" @click="openSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</button>
|
||||
<button class="titlebar-btn" title="Toggle theme" @click="toggleTheme">
|
||||
<button class="titlebar-btn" :title="t('titleBar.toggleTheme')" @click="toggleTheme">
|
||||
{{ appStore.isDark ? '☀' : '☾' }}
|
||||
</button>
|
||||
<button class="titlebar-btn" title="Minimize" @click="minimize">─</button>
|
||||
<button class="titlebar-btn" title="Maximize" @click="maximize">☐</button>
|
||||
<button class="titlebar-btn titlebar-btn-close" title="Close" @click="close">✕</button>
|
||||
<button class="titlebar-btn" :title="t('titleBar.minimize')" @click="minimize">─</button>
|
||||
<button class="titlebar-btn" :title="t('titleBar.maximize')" @click="maximize">☐</button>
|
||||
<button class="titlebar-btn titlebar-btn-close" :title="t('titleBar.close')" @click="close">✕</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -139,28 +139,7 @@ async function execute() {
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.redis.executeUnsafe(connStore.activeId, command, args)
|
||||
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)
|
||||
}
|
||||
|
||||
const formatted = formatRedisResult(result)
|
||||
history.value.push({ cmd, result: formatted })
|
||||
|
||||
// Dispatch refresh-key-list for write commands
|
||||
@@ -246,26 +225,7 @@ async function importCommands() {
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.redis.executeUnsafe(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)
|
||||
}
|
||||
const formatted = formatRedisResult(result)
|
||||
history.value.push({ cmd: trimmed, result: formatted })
|
||||
executed++
|
||||
} catch (err: any) {
|
||||
@@ -327,6 +287,29 @@ function onKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatRedisResult(result: any): string {
|
||||
if (result === null) {
|
||||
return '(nil)'
|
||||
}
|
||||
if (typeof result === 'number') {
|
||||
return `(integer) ${result}`
|
||||
}
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 0) {
|
||||
return '(empty array)'
|
||||
}
|
||||
return 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')
|
||||
}
|
||||
if (typeof result === 'object') {
|
||||
return JSON.stringify(result, null, 2)
|
||||
}
|
||||
return String(result)
|
||||
}
|
||||
|
||||
function categoryColor(category: string): string {
|
||||
switch (category) {
|
||||
case 'admin': return 'var(--color-red)'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -17,12 +17,12 @@ const entries = ref<CommandEntry[]>([])
|
||||
const onlyWrite = ref(false)
|
||||
const visible = defineModel<boolean>('visible', { default: false })
|
||||
|
||||
const filteredEntries = () => {
|
||||
const filteredEntries = computed(() => {
|
||||
if (onlyWrite.value) {
|
||||
return entries.value.filter(e => e.isWrite)
|
||||
}
|
||||
return entries.value
|
||||
}
|
||||
})
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
@@ -73,7 +73,7 @@ defineExpose({ refresh })
|
||||
</div>
|
||||
<div class="cmdlog-table-wrap">
|
||||
<el-table
|
||||
:data="filteredEntries()"
|
||||
:data="filteredEntries"
|
||||
size="small"
|
||||
stripe
|
||||
height="420"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
string: 'var(--color-green)',
|
||||
hash: 'var(--color-yellow)',
|
||||
list: 'var(--color-cyan)',
|
||||
set: 'var(--color-purple)',
|
||||
zset: 'var(--color-red)',
|
||||
stream: 'var(--color-orange)',
|
||||
json: 'var(--color-blue)',
|
||||
}
|
||||
|
||||
export function useTypeColor(typeGetter: () => string) {
|
||||
const typeColor = computed(() => TYPE_COLORS[typeGetter()] || 'var(--text-muted)')
|
||||
return { typeColor }
|
||||
}
|
||||
@@ -135,6 +135,7 @@ export default {
|
||||
importFailed: 'Import fehlgeschlagen: {error}',
|
||||
binaryKey: 'Binärschlüssel',
|
||||
copyHex: 'Hex kopieren',
|
||||
copyFailed: 'Kopieren fehlgeschlagen',
|
||||
copyKeyName: 'Schlüsselname kopieren',
|
||||
copyValue: 'Wert kopieren',
|
||||
memoryUsage: 'Speichernutzung',
|
||||
@@ -321,6 +322,16 @@ export default {
|
||||
keyCount: '{n} Schlüssel',
|
||||
noKeys: 'leer',
|
||||
},
|
||||
titleBar: {
|
||||
toggleTheme: 'Design umschalten',
|
||||
minimize: 'Minimieren',
|
||||
maximize: 'Maximieren',
|
||||
close: 'Schließen',
|
||||
},
|
||||
sidebar: {
|
||||
expand: 'Erweitern',
|
||||
collapse: 'Einklappen',
|
||||
},
|
||||
settings: {
|
||||
title: 'Einstellungen',
|
||||
appearanceSection: 'Aussehen',
|
||||
|
||||
@@ -135,6 +135,7 @@ export default {
|
||||
importFailed: 'Import failed: {error}',
|
||||
binaryKey: 'Binary Key',
|
||||
copyHex: 'Copy Hex',
|
||||
copyFailed: 'Copy failed',
|
||||
copyKeyName: 'Copy Key Name',
|
||||
copyValue: 'Copy Value',
|
||||
memoryUsage: 'Memory Usage',
|
||||
@@ -321,6 +322,16 @@ export default {
|
||||
keyCount: '{n} keys',
|
||||
noKeys: 'empty',
|
||||
},
|
||||
titleBar: {
|
||||
toggleTheme: 'Toggle theme',
|
||||
minimize: 'Minimize',
|
||||
maximize: 'Maximize',
|
||||
close: 'Close',
|
||||
},
|
||||
sidebar: {
|
||||
expand: 'Expand',
|
||||
collapse: 'Collapse',
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
appearanceSection: 'Appearance',
|
||||
|
||||
@@ -135,6 +135,7 @@ export default {
|
||||
importFailed: 'インポートに失敗しました: {error}',
|
||||
binaryKey: 'バイナリキー',
|
||||
copyHex: 'Hexをコピー',
|
||||
copyFailed: 'コピーに失敗しました',
|
||||
copyKeyName: 'キー名をコピー',
|
||||
copyValue: '値をコピー',
|
||||
memoryUsage: 'メモリ使用量',
|
||||
@@ -321,6 +322,16 @@ export default {
|
||||
keyCount: '{n} 個のキー',
|
||||
noKeys: '空',
|
||||
},
|
||||
titleBar: {
|
||||
toggleTheme: 'テーマ切り替え',
|
||||
minimize: '最小化',
|
||||
maximize: '最大化',
|
||||
close: '閉じる',
|
||||
},
|
||||
sidebar: {
|
||||
expand: '展開',
|
||||
collapse: '収縮',
|
||||
},
|
||||
settings: {
|
||||
title: '設定',
|
||||
appearanceSection: '外観',
|
||||
|
||||
@@ -135,6 +135,7 @@ export default {
|
||||
importFailed: '가져오기 실패: {error}',
|
||||
binaryKey: '바이너리 키',
|
||||
copyHex: 'Hex 복사',
|
||||
copyFailed: '복사 실패',
|
||||
copyKeyName: '키 이름 복사',
|
||||
copyValue: '값 복사',
|
||||
memoryUsage: '메모리 사용량',
|
||||
@@ -321,6 +322,16 @@ export default {
|
||||
keyCount: '{n}개 키',
|
||||
noKeys: '비어 있음',
|
||||
},
|
||||
titleBar: {
|
||||
toggleTheme: '테마 전환',
|
||||
minimize: '최소화',
|
||||
maximize: '최대화',
|
||||
close: '닫기',
|
||||
},
|
||||
sidebar: {
|
||||
expand: '펼치기',
|
||||
collapse: '접기',
|
||||
},
|
||||
settings: {
|
||||
title: '설정',
|
||||
appearanceSection: '외관',
|
||||
|
||||
@@ -135,6 +135,7 @@ export default {
|
||||
importFailed: '导入失败: {error}',
|
||||
binaryKey: '二进制键',
|
||||
copyHex: '复制 Hex',
|
||||
copyFailed: '复制失败',
|
||||
copyKeyName: '复制键名',
|
||||
copyValue: '复制值',
|
||||
memoryUsage: '内存占用',
|
||||
@@ -321,6 +322,16 @@ export default {
|
||||
keyCount: '{n} 个键',
|
||||
noKeys: '空',
|
||||
},
|
||||
titleBar: {
|
||||
toggleTheme: '切换主题',
|
||||
minimize: '最小化',
|
||||
maximize: '最大化',
|
||||
close: '关闭',
|
||||
},
|
||||
sidebar: {
|
||||
expand: '展开',
|
||||
collapse: '收缩',
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
appearanceSection: '外观',
|
||||
|
||||
Reference in New Issue
Block a user