feat: multi-format viewer (Text/Hex/JSON/Binary/Gzip/Deflate/Brotli)
- New format.ts: decodeValue + detectFormat using Node.js zlib - IPC: redis:decodeValue, redis:detectFormat - StringEditor: format selector dropdown with auto-detect - Monaco editor shows decoded value, read-only for compressed formats - Auto-detect: JSON, Gzip (1f 8b), Deflate (78 xx), non-printable → Hex - i18n keys for all formats (zh-CN + en)
This commit is contained in:
@@ -5,47 +5,93 @@ import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useAppStore } from '@renderer/stores/app'
|
||||
import { useI18n } from '@renderer/i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const connStore = useConnectionStore()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const editedValue = ref('')
|
||||
const displayValue = ref('')
|
||||
const isJson = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const selectedFormat = ref('auto')
|
||||
const detectedFormat = ref('text')
|
||||
|
||||
const monacoTheme = computed(() => {
|
||||
if (appStore.theme === 'light') return 'vs'
|
||||
if (appStore.theme === 'dark') return 'vs-dark'
|
||||
return 'vs-dark' // system default (will be refined)
|
||||
return 'vs-dark'
|
||||
})
|
||||
|
||||
function formatValue(val: string | null) {
|
||||
if (val === null) return ''
|
||||
try {
|
||||
const parsed = JSON.parse(val)
|
||||
isJson.value = true
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
const formatOptions = [
|
||||
{ value: 'auto', labelKey: 'editor.formatAuto' },
|
||||
{ value: 'text', labelKey: 'editor.formatText' },
|
||||
{ value: 'hex', labelKey: 'editor.formatHex' },
|
||||
{ value: 'json', labelKey: 'editor.formatJson' },
|
||||
{ value: 'binary', labelKey: 'editor.formatBinary' },
|
||||
{ value: 'gzip', labelKey: 'editor.formatGzip' },
|
||||
{ value: 'deflate', labelKey: 'editor.formatDeflate' },
|
||||
{ value: 'brotli', labelKey: 'editor.formatBrotli' },
|
||||
]
|
||||
|
||||
const effectiveFormat = computed(() => {
|
||||
if (selectedFormat.value === 'auto') return detectedFormat.value
|
||||
return selectedFormat.value
|
||||
})
|
||||
|
||||
const monacoLanguage = computed(() => {
|
||||
return effectiveFormat.value === 'json' ? 'json' : 'plaintext'
|
||||
})
|
||||
|
||||
async function decodeAndDisplay(raw: string) {
|
||||
if (!raw) {
|
||||
displayValue.value = ''
|
||||
isJson.value = false
|
||||
return val
|
||||
return
|
||||
}
|
||||
// Auto-detect
|
||||
if (selectedFormat.value === 'auto') {
|
||||
detectedFormat.value = await window.electronAPI.redis.detectFormat(raw)
|
||||
}
|
||||
const fmt = effectiveFormat.value
|
||||
if (fmt === 'text' || fmt === 'json') {
|
||||
// For text/json, try local JSON format first
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
isJson.value = true
|
||||
displayValue.value = JSON.stringify(parsed, null, 2)
|
||||
return
|
||||
} catch {
|
||||
isJson.value = false
|
||||
displayValue.value = raw
|
||||
return
|
||||
}
|
||||
}
|
||||
// For other formats, use IPC decode
|
||||
displayValue.value = await window.electronAPI.redis.decodeValue(raw, fmt)
|
||||
isJson.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => keyStore.keyData,
|
||||
(newVal) => {
|
||||
if (typeof newVal === 'string') {
|
||||
editedValue.value = formatValue(newVal)
|
||||
editedValue.value = newVal
|
||||
decodeAndDisplay(newVal)
|
||||
isEditing.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(selectedFormat, () => {
|
||||
if (editedValue.value) decodeAndDisplay(editedValue.value)
|
||||
})
|
||||
|
||||
const editorOptions = computed(() => ({
|
||||
readOnly: !isEditing.value,
|
||||
readOnly: !isEditing.value || effectiveFormat.value !== 'json' && effectiveFormat.value !== 'text',
|
||||
minimap: { enabled: false },
|
||||
fontSize: appStore.fontSize,
|
||||
wordWrap: 'on' as const,
|
||||
@@ -54,7 +100,6 @@ const editorOptions = computed(() => ({
|
||||
tabSize: 2,
|
||||
lineNumbers: 'on' as const,
|
||||
folding: true,
|
||||
renderWhitespace: 'none',
|
||||
scrollbar: {
|
||||
vertical: 'auto' as const,
|
||||
horizontal: 'auto' as const,
|
||||
@@ -80,17 +125,29 @@ async function save() {
|
||||
<div class="string-editor">
|
||||
<div class="editor-toolbar">
|
||||
<span class="type-badge">STRING</span>
|
||||
<el-select v-model="selectedFormat" size="small" style="width:140px">
|
||||
<el-option
|
||||
v-for="opt in formatOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
:label="t(opt.labelKey)"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedFormat === 'auto'" class="format-detected">
|
||||
{{ t('editor.format') }}: {{ detectedFormat }}
|
||||
</span>
|
||||
<span class="spacer" />
|
||||
<el-button v-if="!isEditing" size="small" type="primary" @click="isEditing = true">Edit</el-button>
|
||||
<template v-else>
|
||||
<el-button size="small" type="primary" @click="save">Save</el-button>
|
||||
<el-button size="small" @click="isEditing = false; editedValue = formatValue(keyStore.keyData as string)">Cancel</el-button>
|
||||
<el-button size="small" @click="isEditing = false; decodeAndDisplay(editedValue)">Cancel</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="editor-content">
|
||||
<vue-monaco-editor
|
||||
v-model:value="editedValue"
|
||||
v-model:value="displayValue"
|
||||
:theme="monacoTheme"
|
||||
:language="isJson ? 'json' : 'plaintext'"
|
||||
:language="monacoLanguage"
|
||||
:options="editorOptions"
|
||||
height="100%"
|
||||
/>
|
||||
@@ -124,6 +181,15 @@ async function save() {
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.format-detected {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -132,6 +132,15 @@ export default {
|
||||
entryId: 'ID (use * for auto)',
|
||||
fields: 'Fields',
|
||||
maxLength: 'Max Length',
|
||||
format: 'Format',
|
||||
formatAuto: 'Auto Detect',
|
||||
formatText: 'Text',
|
||||
formatHex: 'Hex',
|
||||
formatJson: 'JSON',
|
||||
formatBinary: 'Binary',
|
||||
formatGzip: 'Gzip',
|
||||
formatDeflate: 'Deflate',
|
||||
formatBrotli: 'Brotli',
|
||||
consumerGroups: 'Consumer Groups',
|
||||
group: 'Group',
|
||||
consumers: 'Consumers',
|
||||
|
||||
@@ -132,6 +132,15 @@ export default {
|
||||
entryId: 'ID (使用 * 自动生成)',
|
||||
fields: '字段',
|
||||
maxLength: '最大长度',
|
||||
format: '格式',
|
||||
formatAuto: '自动检测',
|
||||
formatText: '文本',
|
||||
formatHex: 'Hex',
|
||||
formatJson: 'JSON',
|
||||
formatBinary: '二进制',
|
||||
formatGzip: 'Gzip',
|
||||
formatDeflate: 'Deflate',
|
||||
formatBrotli: 'Brotli',
|
||||
consumerGroups: '消费者组',
|
||||
group: '组名',
|
||||
consumers: '消费者',
|
||||
|
||||
Reference in New Issue
Block a user