feat: P3 final batch - zoom, fonts, dark mode, tech debt cleanup

- Page zoom control (0.5-2.0x, localStorage)
- Custom monospace font selector (6 presets + custom)
- Element Plus dark mode final fixes (dropdown, message-box, switch, popper)
- AGENTS.md updated with correct paths + new modules/components/IPC channels
- ROADMAP: window state persistence already done via win-state.ts
- ROADMAP: tech debt items marked done
This commit is contained in:
2026-07-07 22:53:18 +08:00
parent 75c3a854d6
commit 71b2bd1771
7 changed files with 186 additions and 20 deletions
+41 -11
View File
@@ -8,7 +8,7 @@ Fork of [AnotherRedisDesktopManager](https://github.com/qishibo/AnotherRedisDesk
- **Shell:** Electron 43 (`electron/main/index.ts`)
- **Preload:** `electron/preload/index.ts` (contextBridge, typed API)
- **Renderer:** Vue 3 + Pinia + Element Plus + Vue Router (`src/renderer/`)
- **Renderer:** Vue 3 + Pinia + Element Plus + Vue Router (`src/`)
- **Shared:** `electron/shared/types.ts` (cross-process types)
- **Build:** electron-vite + Vite 6 + TypeScript 5
- **Redis client:** ioredis 5 (main process only)
@@ -41,20 +41,21 @@ Electron main (electron/main/)
│ ├── set.ts # members, add, remove
│ ├── zset.ts # range, add, remove
│ ├── stream.ts # info, range, add, trim
── server.ts # info, select, ping, slowlog
── server.ts # info, select, ping, slowlog
│ ├── pubsub.ts # SUBSCRIBE/UNSUBSCRIBE
│ ├── commandLogger.ts # command execution log
│ └── format.ts # value decode/format detection
├── store.ts # electron-store (connections, settings)
├── credential.ts # safeStorage encrypt/decrypt
├── win-state.ts # window position persistence
├── updater.ts # electron-updater (check/download/install)
└── ipc-handlers.ts # all IPC channel handlers
Preload (electron/preload/)
├── index.ts # contextBridge.exposeInMainWorld
└── index.d.ts # ElectronAPI type declarations
Shared (electron/shared/)
└── types.ts # Cross-process type definitions
Renderer (src/renderer/src/)
Renderer (src/)
├── main.ts # Vue 3 bootstrap (Pinia, Element Plus)
├── App.vue # Shell layout (TitleBar + Sidebar + MainArea + StatusBar)
├── stores/
@@ -82,14 +83,24 @@ Renderer (src/renderer/src/)
│ ├── StreamEditor.vue # stream entries
│ ├── CliView.vue # interactive terminal
│ ├── SlowLogView.vue # slow log table
── SettingsView.vue # theme, language, scan count
── SettingsView.vue # theme, language, scan count
│ ├── MemoryAnalysis.vue # MEMORY USAGE scan & sort
│ ├── CommandLog.vue # command execution history
│ └── ReJsonEditor.vue # JSON.GET / JSON.SET editing
├── i18n/
│ ├── index.ts # useI18n composable
│ └── locales/
│ ├── en.ts # English
│ └── zh-CN.ts # 中文
── composables/
└── useKeyboard.ts # keyboard shortcut handler
── composables/
└── useKeyboard.ts # keyboard shortcut handler
├── styles/
│ ├── variables.css # CSS custom properties
│ └── global.css # global overrides
├── data/
│ └── commands.ts # Redis command definitions
└── utils/
└── binary.ts # hex ↔ buffer conversion
```
## IPC Channels
@@ -120,6 +131,22 @@ Renderer (src/renderer/src/)
| `redis:dbSize` | invoke | DBSIZE |
| `redis:slowLogGet` | invoke | SLOWLOG GET |
| `redis:memoryUsage` | invoke | MEMORY USAGE |
| `redis:subscribe` | invoke | SUBSCRIBE to channels |
| `redis:unsubscribe` | invoke | UNSUBSCRIBE from channels |
| `redis:monitor` | invoke | Start MONITOR stream |
| `redis:monitorStop` | invoke | Stop MONITOR stream |
| `redis:getCommandLog` | invoke | Get command execution log |
| `redis:clearCommandLog` | invoke | Clear command log |
| `redis:decodeValue` | invoke | Decode value by format |
| `redis:detectFormat` | invoke | Auto-detect value format |
| `redis:customFormat` | invoke | Custom external formatter |
| `redis:streamGroups` | invoke | XINFO GROUPS |
| `redis:streamConsumers` | invoke | XINFO CONSUMERS |
| `redis:streamGroupCreate` | invoke | XGROUP CREATE |
| `redis:streamGroupDestroy` | invoke | XGROUP DESTROY |
| `redis:streamAck` | invoke | XACK messages |
| `redis:rejsonGet` | invoke | JSON.GET value |
| `redis:rejsonSet` | invoke | JSON.SET value |
| `storage:getConnections` | invoke | Read connections |
| `storage:saveConnection` | invoke | Save connection |
| `storage:deleteConnection` | invoke | Delete connection |
@@ -131,6 +158,9 @@ Renderer (src/renderer/src/)
| `window:minimize` | send | Minimize window |
| `window:maximize` | send | Toggle maximize |
| `window:close` | send | Close window |
| `updater:check` | invoke | Check for updates |
| `updater:download` | invoke | Download update |
| `updater:install` | invoke | Quit and install update |
## Keyboard Shortcuts
@@ -152,8 +182,8 @@ Renderer (src/renderer/src/)
- Root `tsconfig.json` references `tsconfig.node.json` (main + preload) and `tsconfig.web.json` (renderer)
- Node target includes `src/main/**/*`, `src/preload/**/*`, `electron.vite.config.ts`
- Web target includes `src/renderer/**/*`
- `@renderer` alias resolves to `src/renderer/src`
- Web target includes `src/**/*`
- `@renderer` alias resolves to `src`
## Security
+8 -8
View File
@@ -54,8 +54,8 @@
- [x] **DB 数量自动检测** — CONFIG GET databases + INFO keyspace
- [x] **DB 记忆** — 每个连接记住最后选择的数据库
- [ ] **更多语言** — de/es/fr/it/ko/pt/ru/tr/ua/vi/tw(当前仅 en/zh-CN
- [ ] **页面缩放** — 0.5x2.0x 缩放控制
- [ ] **自定义字体** — 用户可选等宽字体
- [x] **页面缩放** — 0.5x2.0x 缩放控制
- [x] **自定义字体** — 用户可选等宽字体
- [x] **SlowLog 配置显示** — slowlog-log-slower-than / slowlog-max-len
- [ ] **Cluster SlowLog** — 每个 master 节点分别查询
- [ ] **Cluster Keyspace 表** — 集群模式显示各节点 key 数量
@@ -68,17 +68,17 @@
- [x] **Zset 排序切换** — ASC/DESC 单选按钮
- [x] **CLI 自动同步** — CLI 执行 SELECT 后自动同步 UI
- [x] **CLI 执行后刷新 key 列表** — set/hset/del 等命令后自动刷新
- [ ] **标签页滚轮切换** — 鼠标滚轮切换标签
- [ ] **标签页右键菜单** — 关闭/关闭其他/关闭左侧/关闭右侧
- [ ] **Element Plus 深色适配** — 所有组件无白底
- [ ] **窗口状态持久化** — 大小/位置/最大化状态记忆
- [x] **标签页滚轮切换** — 鼠标滚轮切换标签
- [x] **标签页右键菜单** — 关闭/关闭其他/关闭左侧/关闭右侧
- [x] **Element Plus 深色适配** — 所有组件无白底
- [x] **窗口状态持久化** — 大小/位置/最大化状态记忆
---
## 技术债务
- [ ] AGENTS.md 路径更新 — 文档写 `src/renderer/src/components/`,实际是 `src/components/`
- [ ] `electron/shared/types.ts` 残留字段 — SSH/Sentinel/Cluster/TLS 路径在类型中存在但从未在 UI 中实现
- [x] AGENTS.md 路径更新 — 文档写 `src/renderer/src/components/`,实际是 `src/components/`
- [x] `electron/shared/types.ts` 残留字段 — SSH/Sentinel/Cluster/TLS 路径在类型中存在但从未在 UI 中实现
- [ ] 单元测试 — 当前无任何测试
- [ ] E2E 测试 — 无端到端测试
- [ ] 构建产物清理 — `electron-dist/` 应加入 .gitignore(当前已加入)
+49
View File
@@ -17,6 +17,15 @@ const locales = [
{ value: 'en', label: 'English' },
{ value: 'zh-CN', label: '中文' },
]
const fontOptions = [
'Cascadia Code',
'Fira Code',
'JetBrains Mono',
'Consolas',
'Monaco',
'Courier New',
]
</script>
<template>
@@ -61,6 +70,40 @@ const locales = [
size="small"
/>
</div>
<div class="settings-group">
<label class="settings-label">{{ t('settings.zoom') }}</label>
<el-slider
:model-value="appStore.zoom"
@update:model-value="appStore.setZoom($event)"
:min="0.5"
:max="2.0"
:step="0.1"
show-input
size="small"
style="width: 200px"
/>
<span class="zoom-value">{{ Math.round(appStore.zoom * 100) }}%</span>
</div>
<div class="settings-group">
<label class="settings-label">{{ t('settings.fontFamily') }}</label>
<el-select
:model-value="appStore.fontFamily"
@change="appStore.setFontFamily($event)"
size="small"
style="width: 180px"
allow-create
filterable
>
<el-option
v-for="font in fontOptions"
:key="font"
:label="font"
:value="font"
/>
</el-select>
</div>
</div>
</template>
@@ -89,4 +132,10 @@ const locales = [
color: var(--text-secondary);
min-width: 140px;
}
.zoom-value {
font-size: 12px;
color: var(--text-muted);
min-width: 40px;
}
</style>
+2
View File
@@ -299,5 +299,7 @@ export default {
language: 'Language',
scanCount: 'Keys per SCAN',
fontSize: 'Font Size',
zoom: 'Page Zoom',
fontFamily: 'Monospace Font',
},
}
+2
View File
@@ -299,5 +299,7 @@ export default {
language: '语言',
scanCount: 'SCAN 每次数量',
fontSize: '字体大小',
zoom: '页面缩放',
fontFamily: '等宽字体',
},
}
+19 -1
View File
@@ -9,6 +9,8 @@ export const useAppStore = defineStore('app', () => {
const isDark = ref(true)
const fontSize = ref<number>(Number(localStorage.getItem('fontSize')) || 13)
const scanCount = ref<number>(Number(localStorage.getItem('scanCount')) || 100)
const zoom = ref<number>(Number(localStorage.getItem('zoom')) || 1.0)
const fontFamily = ref<string>(localStorage.getItem('fontFamily') || 'Cascadia Code')
function applyTheme(mode: ThemeMode, osDark?: boolean) {
theme.value = mode
@@ -45,5 +47,21 @@ export const useAppStore = defineStore('app', () => {
localStorage.setItem('scanCount', String(count))
}
return { theme, isDark, fontSize, scanCount, setTheme, setFontSize, setScanCount }
function setZoom(value: number) {
zoom.value = value
localStorage.setItem('zoom', String(value))
document.body.style.zoom = String(value)
}
function setFontFamily(value: string) {
fontFamily.value = value
localStorage.setItem('fontFamily', value)
document.documentElement.style.setProperty('--font-mono', value)
}
// Apply saved zoom and fontFamily on mount
document.body.style.zoom = String(zoom.value)
document.documentElement.style.setProperty('--font-mono', fontFamily.value)
return { theme, isDark, fontSize, scanCount, zoom, fontFamily, setTheme, setFontSize, setScanCount, setZoom, setFontFamily }
})
+65
View File
@@ -291,3 +291,68 @@ body {
border-color: var(--border-color) !important;
color: var(--text-primary) !important;
}
/* Dropdown Menu */
.el-dropdown-menu {
background: var(--bg-secondary) !important;
border-color: var(--border-color) !important;
}
.el-dropdown-menu__item {
color: var(--text-primary) !important;
}
.el-dropdown-menu__item:not(.is-disabled):hover {
background: var(--bg-hover) !important;
color: var(--accent-light) !important;
}
/* Message Box */
.el-message-box {
background: var(--bg-secondary) !important;
border-color: var(--border-color) !important;
}
.el-message-box__title {
color: var(--text-primary) !important;
}
.el-message-box__message {
color: var(--text-primary) !important;
}
.el-message-box__content {
color: var(--text-primary) !important;
}
/* Switch */
.el-switch {
--el-switch-on-color: var(--accent) !important;
--el-switch-off-color: var(--bg-tertiary) !important;
}
.el-switch__core {
background: var(--bg-tertiary) !important;
border-color: var(--border-color) !important;
}
.el-switch.is-checked .el-switch__core {
background: var(--accent) !important;
border-color: var(--accent) !important;
}
/* Loading Mask */
.el-loading-mask {
background: rgba(0, 0, 0, 0.7) !important;
}
/* Popper (all popper types) */
.el-popper {
background: var(--bg-secondary) !important;
border-color: var(--border-color) !important;
}
.el-popper__arrow::before {
background: var(--bg-secondary) !important;
border-color: var(--border-color) !important;
}