feat(phase5): stream editor, i18n, settings, key management

- StreamEditor: view/add/trim/delete entries with field management
- KeyDetail: TTL editor dialog, rename dialog, copy key name button
- i18n: English + Chinese locale system with composable
- SettingsView: theme selector, language switch, scan count, font size
- MainArea: added Settings tab
- RedisService: stream/rename/exists operations
- Preload: stream/rename/exists IPC channels
This commit is contained in:
2026-07-04 18:26:51 +08:00
parent 4c66263477
commit 6b9c6a1ed5
14 changed files with 903 additions and 8 deletions
+102 -4
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
// src/renderer/src/components/KeyDetail.vue
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useKeyStore } from '@renderer/stores/key'
import { useConnectionStore } from '@renderer/stores/connection'
import { ElMessage, ElMessageBox } from 'element-plus'
@@ -9,10 +9,16 @@ import HashEditor from './HashEditor.vue'
import ListEditor from './ListEditor.vue'
import SetEditor from './SetEditor.vue'
import ZsetEditor from './ZsetEditor.vue'
import StreamEditor from './StreamEditor.vue'
const keyStore = useKeyStore()
const connStore = useConnectionStore()
const showTtlDialog = ref(false)
const ttlValue = ref(0)
const showRenameDialog = ref(false)
const newName = ref('')
const typeColor = computed(() => {
const colors: Record<string, string> = {
string: 'var(--color-green)',
@@ -34,6 +40,63 @@ const ttlDisplay = computed(() => {
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`
})
function openTtlDialog() {
ttlValue.value = keyStore.selectedTTL < 0 ? 0 : keyStore.selectedTTL
showTtlDialog.value = true
}
async function saveTtl() {
if (!connStore.activeId || !keyStore.selectedKey) return
try {
await window.electronAPI.redis.setTTL(connStore.activeId, keyStore.selectedKey, ttlValue.value)
keyStore.selectedTTL = ttlValue.value
showTtlDialog.value = false
ElMessage.success('TTL updated')
} catch (err: any) {
ElMessage.error(err.message)
}
}
function openRenameDialog() {
newName.value = keyStore.selectedKey || ''
showRenameDialog.value = true
}
async function saveRename() {
if (!connStore.activeId || !keyStore.selectedKey || !newName.value) return
if (newName.value === keyStore.selectedKey) {
showRenameDialog.value = false
return
}
try {
const exists = await window.electronAPI.redis.existsKey(connStore.activeId, newName.value)
if (exists) {
ElMessage.error('Key already 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')
// Update in key list
const idx = keyStore.keys.findIndex(k => k.name === oldKey)
if (idx >= 0) keyStore.keys[idx].name = newName.value
} catch (err: any) {
ElMessage.error(err.message)
}
}
async function copyKeyName() {
if (!keyStore.selectedKey) return
try {
await navigator.clipboard.writeText(keyStore.selectedKey)
ElMessage.success('Copied')
} catch {
ElMessage.error('Copy failed')
}
}
async function deleteKey() {
if (!connStore.activeId || !keyStore.selectedKey) return
try {
@@ -59,12 +122,18 @@ async function refreshKey() {
<div class="key-info">
<el-icon class="key-icon" :style="{ color: typeColor }"><Document /></el-icon>
<span class="key-name">{{ keyStore.selectedKey }}</span>
<el-button size="small" text class="copy-btn" @click="copyKeyName">
<el-icon><CopyDocument /></el-icon>
</el-button>
<span class="type-badge" :style="{ color: typeColor, borderColor: typeColor }">
{{ keyStore.selectedType?.toUpperCase() }}
</span>
<span class="ttl-badge">{{ ttlDisplay }}</span>
<el-button size="small" text class="ttl-btn" @click="openTtlDialog">
{{ ttlDisplay }}
</el-button>
</div>
<div class="key-actions">
<el-button size="small" @click="openRenameDialog">Rename</el-button>
<el-button size="small" @click="refreshKey">Refresh</el-button>
<el-button size="small" type="danger" @click="deleteKey">Delete</el-button>
</div>
@@ -76,11 +145,36 @@ async function refreshKey() {
<ListEditor v-else-if="keyStore.selectedType === 'list'" />
<SetEditor v-else-if="keyStore.selectedType === 'set'" />
<ZsetEditor v-else-if="keyStore.selectedType === 'zset'" />
<StreamEditor v-else-if="keyStore.selectedType === 'stream'" />
<div v-else class="unsupported">
<el-icon><Warning /></el-icon>
<p>Type "{{ keyStore.selectedType }}" viewer not yet implemented</p>
</div>
</div>
<!-- TTL Dialog -->
<el-dialog v-model="showTtlDialog" title="Set TTL" width="320px">
<el-form label-position="top">
<el-form-item label="Seconds (-1 = no expiry, 0 = expire now)">
<el-input-number v-model="ttlValue" :min="-1" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showTtlDialog = false">Cancel</el-button>
<el-button type="primary" @click="saveTtl">Save</el-button>
</template>
</el-dialog>
<!-- Rename Dialog -->
<el-dialog v-model="showRenameDialog" title="Rename Key" width="400px">
<el-form-item label="New name">
<el-input v-model="newName" />
</el-form-item>
<template #footer>
<el-button @click="showRenameDialog = false">Cancel</el-button>
<el-button type="primary" @click="saveRename">Rename</el-button>
</template>
</el-dialog>
</div>
<div v-else class="no-selection">
@@ -109,7 +203,7 @@ async function refreshKey() {
.key-info {
display: flex;
align-items: center;
gap: 10px;
gap: 6px;
min-width: 0;
}
@@ -128,6 +222,10 @@ async function refreshKey() {
max-width: 300px;
}
.copy-btn {
padding: 2px 4px;
}
.type-badge {
font-size: 10px;
font-weight: 700;
@@ -137,7 +235,7 @@ async function refreshKey() {
letter-spacing: 0.05em;
}
.ttl-badge {
.ttl-btn {
font-size: 11px;
color: var(--text-muted);
}
+11 -1
View File
@@ -6,10 +6,11 @@ import StatusView from './StatusView.vue'
import KeyBrowser from './KeyBrowser.vue'
import CliView from './CliView.vue'
import SlowLogView from './SlowLogView.vue'
import SettingsView from './SettingsView.vue'
const connStore = useConnectionStore()
type Tab = 'status' | 'keys' | 'cli' | 'slowlog'
type Tab = 'status' | 'keys' | 'cli' | 'slowlog' | 'settings'
const activeTab = ref<Tab>('status')
watch(
@@ -57,12 +58,21 @@ watch(
<el-icon><Clock /></el-icon>
Slow Log
</button>
<button
class="tab-item"
:class="{ active: activeTab === 'settings' }"
@click="activeTab = 'settings'"
>
<el-icon><Setting /></el-icon>
Settings
</button>
</div>
<div class="tab-content">
<StatusView v-if="activeTab === 'status'" />
<KeyBrowser v-else-if="activeTab === 'keys'" />
<CliView v-else-if="activeTab === 'cli'" />
<SlowLogView v-else-if="activeTab === 'slowlog'" />
<SettingsView v-else-if="activeTab === 'settings'" />
</div>
</div>
@@ -0,0 +1,92 @@
<script setup lang="ts">
// src/renderer/src/components/SettingsView.vue
import { useAppStore } from '@renderer/stores/app'
import { useI18n } from '@renderer/i18n'
import { ref } from 'vue'
const appStore = useAppStore()
const { t, locale } = useI18n()
const themes = [
{ value: 'dark', labelKey: 'settings.themeDark' },
{ value: 'light', labelKey: 'settings.themeLight' },
{ value: 'system', labelKey: 'settings.themeSystem' },
]
const locales = [
{ value: 'en', label: 'English' },
{ value: 'zh-CN', label: '中文' },
]
</script>
<template>
<div class="settings-view">
<h2 class="settings-title">{{ t('settings.title') }}</h2>
<div class="settings-group">
<label class="settings-label">{{ t('settings.theme') }}</label>
<el-radio-group :model-value="appStore.theme" @change="appStore.setTheme($event)">
<el-radio-button v-for="item in themes" :key="item.value" :value="item.value">
{{ t(item.labelKey) }}
</el-radio-button>
</el-radio-group>
</div>
<div class="settings-group">
<label class="settings-label">{{ t('settings.language') }}</label>
<el-select :model-value="locale" @change="locale = $event" size="small" style="width: 140px">
<el-option v-for="l in locales" :key="l.value" :label="l.label" :value="l.value" />
</el-select>
</div>
<div class="settings-group">
<label class="settings-label">{{ t('settings.scanCount') }}</label>
<el-input-number
:model-value="appStore.scanCount"
@change="appStore.setScanCount($event || 100)"
:min="10"
:max="10000"
:step="10"
size="small"
/>
</div>
<div class="settings-group">
<label class="settings-label">{{ t('settings.fontSize') }}</label>
<el-input-number
:model-value="appStore.fontSize"
@change="appStore.setFontSize($event || 13)"
:min="10"
:max="20"
size="small"
/>
</div>
</div>
</template>
<style scoped>
.settings-view {
padding: 24px;
max-width: 500px;
}
.settings-title {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 24px;
}
.settings-group {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.settings-label {
font-size: 13px;
color: var(--text-secondary);
min-width: 140px;
}
</style>
@@ -0,0 +1,280 @@
<script setup lang="ts">
// src/renderer/src/components/StreamEditor.vue
import { ref, watch } from 'vue'
import { useKeyStore } from '@renderer/stores/key'
import { useConnectionStore } from '@renderer/stores/connection'
import { ElMessage, ElMessageBox } from 'element-plus'
const keyStore = useKeyStore()
const connStore = useConnectionStore()
interface StreamEntry {
id: string
fields: Record<string, string>
}
const entries = ref<StreamEntry[]>([])
const loading = ref(false)
const showAddDialog = ref(false)
const newEntryId = ref('*')
const newFields = ref<{ key: string; value: string }[]>([{ key: '', value: '' }])
const trimming = ref(false)
const trimMaxLen = ref(1000)
async function loadEntries() {
if (!connStore.activeId || !keyStore.selectedKey) return
loading.value = true
try {
const data = await window.electronAPI.redis.streamRange(
connStore.activeId, keyStore.selectedKey, '-', '+', 100
)
entries.value = data.map((item: any) => ({
id: item[0],
fields: parseFields(item[1]),
}))
} finally {
loading.value = false
}
}
function parseFields(raw: any[]): Record<string, string> {
const result: Record<string, string> = {}
for (let i = 0; i < raw.length; i += 2) {
result[raw[i]] = raw[i + 1]
}
return result
}
watch(
() => keyStore.selectedKey,
() => { if (keyStore.selectedType === 'stream') loadEntries() },
{ immediate: true }
)
function addField() {
newFields.value.push({ key: '', value: '' })
}
function removeField(index: number) {
if (newFields.value.length > 1) {
newFields.value.splice(index, 1)
}
}
async function addEntry() {
if (!connStore.activeId || !keyStore.selectedKey) return
const fieldValues: string[] = []
for (const f of newFields.value) {
if (f.key && f.value) {
fieldValues.push(f.key, f.value)
}
}
if (fieldValues.length === 0) {
ElMessage.warning('Add at least one field')
return
}
try {
const id = await window.electronAPI.redis.streamAdd(
connStore.activeId, keyStore.selectedKey, newEntryId.value, fieldValues
)
entries.value.push({
id,
fields: Object.fromEntries(newFields.value.filter(f => f.key).map(f => [f.key, f.value])),
})
showAddDialog.value = false
newFields.value = [{ key: '', value: '' }]
ElMessage.success('Added')
} catch (err: any) {
ElMessage.error(err.message)
}
}
async function deleteEntry(entry: StreamEntry) {
if (!connStore.activeId || !keyStore.selectedKey) return
try {
await ElMessageBox.confirm(`Delete entry "${entry.id}"?`, 'Confirm')
await window.electronAPI.redis.streamDel(connStore.activeId, keyStore.selectedKey, [entry.id])
entries.value = entries.value.filter(e => e.id !== entry.id)
ElMessage.success('Deleted')
} catch (err: any) {
if (err !== 'cancel') ElMessage.error(err.message)
}
}
async function trimStream() {
if (!connStore.activeId || !keyStore.selectedKey) return
try {
await ElMessageBox.confirm(`Trim stream to max ${trimMaxLen.value} entries?`, 'Confirm')
trimming.value = true
const removed = await window.electronAPI.redis.streamTrim(
connStore.activeId, keyStore.selectedKey, trimMaxLen.value
)
ElMessage.success(`Removed ${removed} entries`)
await loadEntries()
} catch (err: any) {
if (err !== 'cancel') ElMessage.error(err.message)
} finally {
trimming.value = false
}
}
</script>
<template>
<div class="stream-editor">
<div class="editor-toolbar">
<span class="type-badge">STREAM</span>
<span class="item-count">{{ entries.length }} entries</span>
<el-button size="small" type="primary" @click="showAddDialog = true">Add</el-button>
<el-popconfirm title="Trim stream?">
<template #reference>
<el-button size="small" :loading="trimming">Trim</el-button>
</template>
</el-popconfirm>
<el-button size="small" @click="loadEntries" :loading="loading">Refresh</el-button>
</div>
<div class="stream-content">
<div v-for="entry in entries" :key="entry.id" class="stream-entry">
<div class="entry-header">
<span class="entry-id">{{ entry.id }}</span>
<el-button size="small" type="danger" text @click="deleteEntry(entry)">Del</el-button>
</div>
<div class="entry-fields">
<div v-for="(val, key) in entry.fields" :key="key" class="field-row">
<span class="field-key">{{ key }}</span>
<span class="field-value">{{ val }}</span>
</div>
</div>
</div>
<div v-if="entries.length === 0 && !loading" class="empty">Empty stream</div>
</div>
<el-dialog v-model="showAddDialog" title="Add Stream Entry" width="500px">
<el-form label-position="top">
<el-form-item label="ID (use * for auto)">
<el-input v-model="newEntryId" />
</el-form-item>
<el-form-item label="Fields">
<div v-for="(_, index) in newFields" :key="index" class="field-input-row">
<el-input v-model="newFields[index].key" placeholder="Field" size="small" />
<el-input v-model="newFields[index].value" placeholder="Value" size="small" />
<el-button size="small" type="danger" text @click="removeField(index)"
:disabled="newFields.length === 1">
<el-icon><Delete /></el-icon>
</el-button>
</div>
<el-button size="small" type="primary" text @click="addField">+ Add field</el-button>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddDialog = false">Cancel</el-button>
<el-button type="primary" @click="addEntry">Add</el-button>
</template>
</el-dialog>
<el-dialog v-model="trimming" title="Trim Stream" width="300px">
<el-form-item label="Max length">
<el-input-number v-model="trimMaxLen" :min="1" />
</el-form-item>
</el-dialog>
</div>
</template>
<style scoped>
.stream-editor {
height: 100%;
display: flex;
flex-direction: column;
}
.editor-toolbar {
padding: 12px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.type-badge {
font-size: 10px;
font-weight: 700;
color: var(--accent-light);
background: var(--accent-bg);
padding: 2px 8px;
border-radius: 4px;
letter-spacing: 0.05em;
}
.item-count {
font-size: 11px;
color: var(--text-muted);
}
.stream-content {
flex: 1;
overflow-y: auto;
}
.stream-entry {
border-bottom: 1px solid var(--border-subtle);
padding: 10px 12px;
}
.stream-entry:hover {
background: var(--bg-hover);
}
.entry-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.entry-id {
font-size: 11px;
font-family: 'Cascadia Code', monospace;
color: var(--accent-light);
font-weight: 600;
}
.entry-fields {
display: grid;
gap: 4px;
}
.field-row {
display: flex;
gap: 12px;
font-size: 12px;
}
.field-key {
color: var(--color-yellow);
font-family: 'Cascadia Code', monospace;
min-width: 100px;
}
.field-value {
color: var(--text-primary);
font-family: 'Cascadia Code', monospace;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.field-input-row {
display: flex;
gap: 8px;
margin-bottom: 6px;
}
.empty {
padding: 40px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
</style>
+52
View File
@@ -0,0 +1,52 @@
// src/renderer/src/i18n/index.ts
import { ref, computed } from 'vue'
import en from './locales/en'
import zhCN from './locales/zh-CN'
type Messages = typeof en
type NestedKeyOf<T> = {
[K in keyof T & string]: T[K] extends object
? `${K}.${NestedKeyOf<T[K]>}`
: K
}[keyof T & string]
const messages: Record<string, Messages> = {
en,
'zh-CN': zhCN,
}
const currentLocale = ref<string>(
localStorage.getItem('locale') || 'en'
)
export function useI18n() {
const locale = computed({
get: () => currentLocale.value,
set: (val: string) => {
currentLocale.value = val
localStorage.setItem('locale', val)
},
})
function t(path: string, params?: Record<string, string | number>): string {
const keys = path.split('.')
let result: any = messages[currentLocale.value] || messages.en
for (const key of keys) {
result = result?.[key]
}
if (typeof result !== 'string') return path
if (params) {
return Object.entries(params).reduce(
(str, [k, v]) => str.replace(`{${k}}`, String(v)),
result
)
}
return result
}
return { t, locale, currentLocale }
}
+123
View File
@@ -0,0 +1,123 @@
// src/renderer/src/i18n/locales/en.ts
export default {
common: {
ok: 'OK',
cancel: 'Cancel',
save: 'Save',
delete: 'Delete',
edit: 'Edit',
add: 'Add',
refresh: 'Refresh',
search: 'Search',
confirm: 'Confirm',
success: 'Success',
error: 'Error',
loading: 'Loading...',
noData: 'No data',
},
app: {
title: 'JRedisDesktop',
subtitle: 'A modern Redis desktop manager',
placeholder: 'Select or create a connection to get started',
},
connection: {
title: 'Connections',
new: 'New Connection',
edit: 'Edit Connection',
name: 'Name',
host: 'Host',
port: 'Port',
password: 'Password',
username: 'Username',
tls: 'Enable TLS',
ssh: 'SSH Tunnel',
connect: 'Connect',
disconnect: 'Disconnect',
deleteConfirm: 'Delete this connection?',
testSuccess: 'Connected successfully',
testFailed: 'Connection failed',
connected: 'Connected',
disconnected: 'Disconnected',
},
key: {
title: 'Keys',
filter: 'Filter keys...',
loadMore: 'Load more...',
count: '{n} keys',
deleteConfirm: 'Delete key "{key}"?',
deleted: 'Key deleted',
rename: 'Rename',
renameTitle: 'Rename Key',
newName: 'New name',
renamed: 'Key renamed',
copyName: 'Copy key name',
copied: 'Copied',
ttl: 'TTL',
ttlTitle: 'Set TTL',
ttlHint: '-1 = no expiry, 0 = expire now',
ttlUpdated: 'TTL updated',
exists: 'Key already exists',
batchDelete: 'Delete Selected',
batchConfirm: 'Delete {n} key(s)?',
batchDeleted: 'Deleted {n} key(s)',
selected: '{n} selected',
},
types: {
string: 'STRING',
hash: 'HASH',
list: 'LIST',
set: 'SET',
zset: 'ZSET',
stream: 'STREAM',
},
editor: {
save: 'Save',
cancel: 'Cancel',
edit: 'Edit',
saved: 'Saved',
push: 'Push',
addField: 'Add Field',
addMember: 'Add Member',
filter: 'Filter...',
refresh: 'Refresh',
elements: '{n} elements',
members: '{n} members',
empty: 'Empty',
field: 'Field',
value: 'Value',
score: 'Score',
remove: 'Remove',
removed: 'Removed',
added: 'Added',
},
cli: {
title: 'CLI',
welcome: 'Redis CLI - type commands below',
placeholder: 'Enter Redis command...',
},
slowlog: {
title: 'Slow Log',
time: 'Time',
duration: 'Duration',
command: 'Command',
},
status: {
title: 'Status',
database: 'Database',
keys: 'Keys',
refresh: 'Refresh',
},
db: {
label: 'DB',
},
settings: {
title: 'Settings',
theme: 'Theme',
themeDark: 'Dark',
themeLight: 'Light',
themeSystem: 'System',
language: 'Language',
scanCount: 'Keys per SCAN',
fontSize: 'Font Size',
},
}
+123
View File
@@ -0,0 +1,123 @@
// src/renderer/src/i18n/locales/zh-CN.ts
export default {
common: {
ok: '确定',
cancel: '取消',
save: '保存',
delete: '删除',
edit: '编辑',
add: '添加',
refresh: '刷新',
search: '搜索',
confirm: '确认',
success: '成功',
error: '错误',
loading: '加载中...',
noData: '暂无数据',
},
app: {
title: 'JRedisDesktop',
subtitle: '现代 Redis 桌面管理器',
placeholder: '选择或创建连接开始使用',
},
connection: {
title: '连接',
new: '新建连接',
edit: '编辑连接',
name: '名称',
host: '主机',
port: '端口',
password: '密码',
username: '用户名',
tls: '启用 TLS',
ssh: 'SSH 隧道',
connect: '连接',
disconnect: '断开',
deleteConfirm: '删除此连接?',
testSuccess: '连接成功',
testFailed: '连接失败',
connected: '已连接',
disconnected: '未连接',
},
key: {
title: '键',
filter: '过滤键...',
loadMore: '加载更多...',
count: '{n} 个键',
deleteConfirm: '删除键 "{key}"',
deleted: '键已删除',
rename: '重命名',
renameTitle: '重命名键',
newName: '新名称',
renamed: '键已重命名',
copyName: '复制键名',
copied: '已复制',
ttl: 'TTL',
ttlTitle: '设置 TTL',
ttlHint: '-1 = 永不过期, 0 = 立即过期',
ttlUpdated: 'TTL 已更新',
exists: '键已存在',
batchDelete: '删除选中',
batchConfirm: '删除 {n} 个键?',
batchDeleted: '已删除 {n} 个键',
selected: '已选 {n} 项',
},
types: {
string: '字符串',
hash: '哈希',
list: '列表',
set: '集合',
zset: '有序集合',
stream: '流',
},
editor: {
save: '保存',
cancel: '取消',
edit: '编辑',
saved: '已保存',
push: '推入',
addField: '添加字段',
addMember: '添加成员',
filter: '过滤...',
refresh: '刷新',
elements: '{n} 个元素',
members: '{n} 个成员',
empty: '空',
field: '字段',
value: '值',
score: '分数',
remove: '移除',
removed: '已移除',
added: '已添加',
},
cli: {
title: 'CLI',
welcome: 'Redis CLI - 在下方输入命令',
placeholder: '输入 Redis 命令...',
},
slowlog: {
title: '慢日志',
time: '时间',
duration: '耗时',
command: '命令',
},
status: {
title: '状态',
database: '数据库',
keys: '键数',
refresh: '刷新',
},
db: {
label: '数据库',
},
settings: {
title: '设置',
theme: '主题',
themeDark: '深色',
themeLight: '浅色',
themeSystem: '跟随系统',
language: '语言',
scanCount: 'SCAN 每次数量',
fontSize: '字体大小',
},
}
+15 -2
View File
@@ -5,8 +5,10 @@ import { ref } from 'vue'
export type ThemeMode = 'system' | 'dark' | 'light'
export const useAppStore = defineStore('app', () => {
const theme = ref<ThemeMode>('system')
const theme = ref<ThemeMode>((localStorage.getItem('theme') as ThemeMode) || 'system')
const isDark = ref(true)
const fontSize = ref<number>(Number(localStorage.getItem('fontSize')) || 13)
const scanCount = ref<number>(Number(localStorage.getItem('scanCount')) || 100)
function applyTheme(mode: ThemeMode, osDark?: boolean) {
theme.value = mode
@@ -29,8 +31,19 @@ export const useAppStore = defineStore('app', () => {
function setTheme(mode: ThemeMode) {
applyTheme(mode)
localStorage.setItem('theme', mode)
window.electronAPI?.theme.set(mode)
}
return { theme, isDark, setTheme }
function setFontSize(size: number) {
fontSize.value = size
localStorage.setItem('fontSize', String(size))
}
function setScanCount(count: number) {
scanCount.value = count
localStorage.setItem('scanCount', String(count))
}
return { theme, isDark, fontSize, scanCount, setTheme, setFontSize, setScanCount }
})
+9
View File
@@ -150,6 +150,15 @@ export const useKeyStore = defineStore('key', () => {
keyData.value = { size }
break
}
case 'stream': {
try {
const info = await window.electronAPI.redis.streamInfo(connId, key)
keyData.value = { length: info.length }
} catch {
keyData.value = { length: 0 }
}
break
}
default:
keyData.value = null
}