├── .browserslistrc ├── .editorconfig ├── .env ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── Screenshots ├── Screenshots1.png ├── Screenshots2.png ├── Screenshots3.png ├── Screenshots4.png ├── Screenshots5.png ├── Screenshots6.png ├── Screenshots7.png └── Screenshots8.png ├── babel.config.js ├── build ├── icon.icns ├── icon.ico ├── icon.png └── notarize.js ├── package.json ├── postcss.config.js ├── public ├── css-vars.css └── index.html ├── src ├── App.vue ├── assets │ └── icon.png ├── background.ts ├── components │ ├── contentDetail │ │ └── index.vue │ ├── customAce │ │ └── index.vue │ └── divider │ │ └── index.vue ├── i18n.ts ├── locales │ ├── en_GB.json │ └── zh_CN.json ├── main.ts ├── router │ └── index.ts ├── shims-vue.d.ts ├── store │ ├── getters.ts │ ├── index.ts │ └── modules │ │ ├── config.ts │ │ ├── keyList.ts │ │ ├── keyMenuAndTabBind.ts │ │ └── serverList.ts ├── styles │ └── styles.css ├── utils │ ├── checkIsJson.ts │ ├── cipherCode.ts │ ├── configStore.ts │ ├── contentLimit.ts │ ├── copyFromTable.ts │ ├── cutsomCommandFormat.ts │ ├── file.ts │ ├── formatCommandField.ts │ ├── formatTime.ts │ ├── log.ts │ ├── redis.ts │ ├── store.ts │ ├── switchColor.ts │ └── theme.ts └── views │ ├── Home │ └── index.vue │ ├── config │ ├── about.vue │ ├── content.vue │ ├── general.vue │ ├── hotKey.vue │ ├── index.vue │ ├── keyMenuFilterSymbol.vue │ ├── keyMenuStatus.vue │ ├── language.vue │ ├── theme.vue │ └── videoShow.vue │ ├── consolePane │ ├── commandMode.vue │ ├── doubleRowTable.vue │ ├── history.vue │ ├── index.ts │ ├── index.vue │ ├── result.vue │ ├── singleRowTable.vue │ └── stringTable.vue │ ├── historyLog │ └── index.vue │ ├── keyMenu │ ├── group.vue │ └── index.vue │ ├── keyTab │ └── index.vue │ ├── monitor │ ├── chart.vue │ ├── chartWithTwoLine.vue │ ├── clientCount.vue │ ├── cpu.vue │ ├── index.vue │ └── memory.vue │ ├── newKeyValue │ ├── hashType.vue │ ├── index.ts │ ├── index.vue │ ├── listType.vue │ ├── setType.vue │ ├── stringType.vue │ ├── topTab.vue │ └── zsetType.vue │ ├── pubAndSub │ ├── index.ts │ ├── index.vue │ ├── publishCom.vue │ ├── subChannelCard.vue │ ├── subContent.vue │ └── subscribeCom.vue │ ├── serverMenu │ ├── addForm.vue │ ├── editForm.vue │ ├── index.ts │ └── index.vue │ ├── serverTab │ ├── emptyPage.vue │ └── index.vue │ ├── valueContent │ ├── hashType.vue │ ├── index.ts │ ├── index.vue │ ├── listType.vue │ ├── setType.vue │ ├── stringType.vue │ ├── topTab.vue │ └── zsetType.vue │ └── videoTutorial │ └── index.vue ├── tailwind.config.js ├── tsconfig.json ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_I18N_LOCALE=en_GB 2 | VUE_APP_I18N_FALLBACK_LOCALE=en_GB 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | '@vue/standard', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | #Electron-builder output 26 | /dist_electron -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

RedisFish

5 | 6 |

7 | release 8 | RPs 9 | downloads 10 |

11 | 12 | 13 | 14 | ## Info 15 | 16 | `RedisFish` is a convenient, cross-platform, data-focused Redis GUI client🌈🌈🌈. Maybe you will like it. 17 | 18 | Since `v 1.0.0`, I have received a lot of useful feedback from friends, thank you very much! 19 | 20 | > Formerly RedFish, renamed due to the same name 21 | 22 | 23 | 24 | language: [简体中文](https://github.com/Kuari/RedisFish/blob/main/README.zh-CN.md) English 25 | 26 | download: [releases](https://github.com/Kuari/RedisFish/releases) 27 | 28 | 29 | 30 | ## Features 31 | 32 | * Redis basic type data processing 33 | * The status of data processing is distinguished by color. For example, in hash type data, blue represents new data, yellow represents edited data, etc. 34 | * Data multi-selection processing 35 | * The data in the table can be edited directly by `double-clicking`, or it can be enlarged by the shortcut key of `right-click` 36 | * JSON auto format 37 | * JSON viewing and editing 38 | * key list paging query 39 | * The key list can be filtered by prefix grouping, support`:`, `-`, `_`, `#`, `=`, `+`, select in settings 40 | * The key list supports two different modes: the list is displayed directly and classified into folders according to the prefix, which can be switched in the settings. 41 | * The console supports two different modes: single command execution and multi-command execution at the same time 42 | * Support log function, view execution command log 43 | * Database monitoring, supports cpu usage, memory usage, number of connected clients, number of clients waiting for blocking commands 44 | * Graphical operation of publish/subscribe function 45 | * TLS 46 | * Dark mode 47 | * Multi-language support, support Chinese, English, automatically switch according to the system 48 | * Automatic update (currently only on mac) 49 | 50 | 51 | 52 | ## HotKey 53 | 54 | * **Copy**: `Command/Ctrl` + `left-click` shortcut copy 55 | * **Data Zoom**: `Right-click` the data to open the data zoom editor, including text and json modes, can be automatically recognized 56 | 57 | 58 | 59 | ## Screenshot 60 | 61 | ![Screenshots](./Screenshots/Screenshots1.png) 62 | 63 | ![Screenshots](./Screenshots/Screenshots2.png) 64 | 65 | ![Screenshots](./Screenshots/Screenshots3.png) 66 | 67 | ![Screenshots](./Screenshots/Screenshots4.png) 68 | 69 | ![Screenshots](./Screenshots/Screenshots5.png) 70 | 71 | ![Screenshots](./Screenshots/Screenshots6.png) 72 | 73 | ![Screenshots](./Screenshots/Screenshots7.png) 74 | 75 | ![Screenshots](./Screenshots/Screenshots8.png) 76 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

RedisFish

5 | 6 |

7 | release 8 | RPs 9 | downloads 10 |

11 | 12 | 13 | 14 | ## 介绍 15 | 16 | `RedisFish`是一款便捷、跨平台、专注于数据的Redis GUI客户端🌈🌈🌈。或许你会喜欢~ 17 | 18 | 自从`v 1.0.0`后,收到不少小伙伴儿很有用的反馈,非常感谢! 19 | 20 | > 原RedFish,由于重名而改名 21 | 22 | 23 | 24 | 语言: 简体中文 [English](https://github.com/Kuari/RedisFish) 25 | 26 | 下载: [releases](https://github.com/Kuari/RedisFish/releases) 27 | 28 | 29 | 30 | ## 功能 31 | 32 | * redis基础类型数据处理 33 | * 通过颜色区别数据处理的状态,如hash类型的数据中,蓝色代表新增数据,黄色代表编辑过的数据等 34 | * 数据多选处理 35 | * 表格内数据直接`双击`编辑,也可通过`鼠标右击`快捷键放大编辑 36 | * JSON自动格式化 37 | * JSON查看和编辑 38 | * key列表分页查询 39 | * key列表可以通过前缀分组筛选,支持`:`、`-`、`_`、`#`、`=`、`+`分割,在设置中选择 40 | * key列表支持两种不同模式:列表直接展示和根据前缀分类成文件夹展示,在设置中切换 41 | * console支持两种不同模式:单条命令执行和多命令同时执行 42 | * 支持日志功能,查看执行命令日志 43 | * 数据库监控,支持cpu占用率、内存使用量、已连接客户端数、等待阻塞命令客户端数 44 | * 发布/订阅功能图形化操作 45 | * TLS 46 | * 暗黑模式 47 | * 多语言支持,支持中文、英文,根据系统自动切换 48 | * 自动更新(目前仅限MAC和Linux端) 49 | 50 | 51 | 52 | ## 快捷键 53 | 54 | * **复制**: `Command/Ctrl` + `鼠标左击`点击数据复制 55 | * **Data Zoom**: `鼠标右击`数据,打开Data Zoom,包括文本和json两种模式,可以自动识别 56 | 57 | 58 | 59 | ## 视频教程 60 | 61 | * [bilibili](https://www.bilibili.com/video/BV1Wf4y1d7JZ?share_source=copy_web) 62 | 63 | 64 | 65 | ## 截图 66 | 67 | ![Screenshots](./Screenshots/Screenshots1.png) 68 | 69 | ![Screenshots](./Screenshots/Screenshots2.png) 70 | 71 | ![Screenshots](./Screenshots/Screenshots3.png) 72 | 73 | ![Screenshots](./Screenshots/Screenshots4.png) 74 | 75 | ![Screenshots](./Screenshots/Screenshots5.png) 76 | 77 | ![Screenshots](./Screenshots/Screenshots6.png) 78 | 79 | ![Screenshots](./Screenshots/Screenshots7.png) 80 | 81 | ![Screenshots](./Screenshots/Screenshots8.png) 82 | -------------------------------------------------------------------------------- /Screenshots/Screenshots1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/Screenshots/Screenshots1.png -------------------------------------------------------------------------------- /Screenshots/Screenshots2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/Screenshots/Screenshots2.png -------------------------------------------------------------------------------- /Screenshots/Screenshots3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/Screenshots/Screenshots3.png -------------------------------------------------------------------------------- /Screenshots/Screenshots4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/Screenshots/Screenshots4.png -------------------------------------------------------------------------------- /Screenshots/Screenshots5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/Screenshots/Screenshots5.png -------------------------------------------------------------------------------- /Screenshots/Screenshots6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/Screenshots/Screenshots6.png -------------------------------------------------------------------------------- /Screenshots/Screenshots7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/Screenshots/Screenshots7.png -------------------------------------------------------------------------------- /Screenshots/Screenshots8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/Screenshots/Screenshots8.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/build/icon.png -------------------------------------------------------------------------------- /build/notarize.js: -------------------------------------------------------------------------------- 1 | import { notarize } from 'electron-notarize' 2 | 3 | export const notarizing = async () => { 4 | return await notarize({ 5 | appPath: 'RedisFish.app', // 应用的路径 xxx.app 结尾的 6 | appBundleId: 'com.kuari.RedisFish', // appid 7 | appleId: process.env.AppleID, // 苹果开发者 id 8 | appleIdPassword: process.env.AppleIdPassword, // 应用专用密码 9 | ascProvider: process.env.AscProvider // 证书提供者 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RedisFish", 3 | "version": "1.0.2", 4 | "author": "Kuari", 5 | "homepage": "https://github.com/Kuari/RedisFish", 6 | "scripts": { 7 | "electron:build": "vue-cli-service electron:build", 8 | "electron:build:mac": "vue-cli-service electron:build --macos --arm64 --x64", 9 | "electron:build:publish": "vue-cli-service electron:build -p always", 10 | "electron:serve": "vue-cli-service electron:serve" 11 | }, 12 | "main": "background.js", 13 | "dependencies": { 14 | "autoprefixer": "^9", 15 | "core-js": "^3.6.5", 16 | "redis": "4.0.0", 17 | "vue": "^3.0.0", 18 | "vue-i18n": "^9.1.0", 19 | "vue-router": "^4.0.0-0", 20 | "vuex": "^4.0.0-0" 21 | }, 22 | "devDependencies": { 23 | "@element-plus/icons-vue": "^0.2.4", 24 | "@intlify/vue-i18n-loader": "^3.0.0", 25 | "@tailwindcss/postcss7-compat": "^2.2.17", 26 | "@types/electron-devtools-installer": "^2.2.0", 27 | "@types/node": "^16.11.10", 28 | "@typescript-eslint/eslint-plugin": "^4.18.0", 29 | "@typescript-eslint/parser": "^4.18.0", 30 | "@vue/cli-plugin-babel": "~4.5.0", 31 | "@vue/cli-plugin-eslint": "~4.5.0", 32 | "@vue/cli-plugin-router": "~4.5.0", 33 | "@vue/cli-plugin-typescript": "~4.5.0", 34 | "@vue/cli-plugin-vuex": "~4.5.0", 35 | "@vue/cli-service": "~4.5.0", 36 | "@vue/compiler-sfc": "^3.0.0", 37 | "@vue/eslint-config-standard": "^5.1.2", 38 | "@vue/eslint-config-typescript": "^7.0.0", 39 | "ace-builds": "^1.4.13", 40 | "echarts": "^5.3.2", 41 | "electron": "^13.0.0", 42 | "electron-builder": "^23.0.3", 43 | "electron-devtools-installer": "^3.1.0", 44 | "electron-log": "^4.4.7", 45 | "electron-notarize": "^1.2.1", 46 | "electron-updater": "^5.0.1", 47 | "element-plus": "^2.2.0", 48 | "eslint": "^6.7.2", 49 | "eslint-plugin-import": "^2.20.2", 50 | "eslint-plugin-node": "^11.1.0", 51 | "eslint-plugin-promise": "^4.2.1", 52 | "eslint-plugin-standard": "^4.0.0", 53 | "eslint-plugin-vue": "^7.0.0", 54 | "postcss": "^7", 55 | "style-loader": "^3.3.1", 56 | "tailwindcss": "npm:@tailwindcss/postcss7-compat", 57 | "ts-clipboard": "^1.0.17", 58 | "typescript": "~4.1.5", 59 | "unplugin-vue-components": "^0.17.2", 60 | "vue-cli-plugin-electron-builder": "~2.1.1", 61 | "vue-cli-plugin-i18n": "~2.3.1" 62 | }, 63 | "resolutions": { 64 | "vue-cli-plugin-electron-builder/electron-builder": "^23.0.3" 65 | }, 66 | "license": "GPL" 67 | } 68 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/css-vars.css: -------------------------------------------------------------------------------- 1 | html.dark{color-scheme:dark;--el-color-primary:#409eff;--el-color-primary-light-3:#3375b9;--el-color-primary-light-5:#2a598a;--el-color-primary-light-7:#213d5b;--el-color-primary-light-8:#1d3043;--el-color-primary-light-9:#18222c;--el-color-primary-dark-2:#66b1ff;--el-color-success:#67c23a;--el-color-success-light-3:#4e8e2f;--el-color-success-light-5:#3e6b27;--el-color-success-light-7:#2d481f;--el-color-success-light-8:#25371c;--el-color-success-light-9:#1c2518;--el-color-success-dark-2:#85ce61;--el-color-warning:#e6a23c;--el-color-warning-light-3:#a77730;--el-color-warning-light-5:#7d5b28;--el-color-warning-light-7:#533f20;--el-color-warning-light-8:#3e301c;--el-color-warning-light-9:#292218;--el-color-warning-dark-2:#ebb563;--el-color-danger:#f56c6c;--el-color-danger-light-3:#b25252;--el-color-danger-light-5:#854040;--el-color-danger-light-7:#582e2e;--el-color-danger-light-8:#412626;--el-color-danger-light-9:#2b1d1d;--el-color-danger-dark-2:#f78989;--el-color-error:#f56c6c;--el-color-error-light-3:#b25252;--el-color-error-light-5:#854040;--el-color-error-light-7:#582e2e;--el-color-error-light-8:#412626;--el-color-error-light-9:#2b1d1d;--el-color-error-dark-2:#f78989;--el-color-info:#909399;--el-color-info-light-3:#6b6d71;--el-color-info-light-5:#525457;--el-color-info-light-7:#393a3c;--el-color-info-light-8:#2d2d2f;--el-color-info-light-9:#202121;--el-color-info-dark-2:#a6a9ad;--el-box-shadow:0px 12px 32px 4px rgba(0, 0, 0, 0.36),0px 8px 20px rgba(0, 0, 0, 0.72);--el-box-shadow-light:0px 0px 12px rgba(0, 0, 0, 0.72);--el-box-shadow-lighter:0px 0px 6px rgba(0, 0, 0, 0.72);--el-box-shadow-dark:0px 16px 48px 16px rgba(0, 0, 0, 0.72),0px 12px 32px #000000,0px 8px 16px -8px #000000;--el-bg-color-page:#0a0a0a;--el-bg-color:#141414;--el-bg-color-overlay:#1d1e1f;--el-text-color-primary:#E5EAF3;--el-text-color-regular:#CFD3DC;--el-text-color-secondary:#A3A6AD;--el-text-color-placeholder:#8D9095;--el-text-color-disabled:#6C6E72;--el-border-color-darker:#636466;--el-border-color-dark:#58585B;--el-border-color:#4C4D4F;--el-border-color-light:#414243;--el-border-color-lighter:#363637;--el-border-color-extra-light:#2B2B2C;--el-fill-color-darker:#424243;--el-fill-color-dark:#39393A;--el-fill-color:#303030;--el-fill-color-light:#262727;--el-fill-color-lighter:#1D1D1D;--el-fill-color-extra-light:#191919;--el-fill-color-blank:transparent;--el-mask-color:rgba(0, 0, 0, 0.8);--el-mask-color-extra-light:rgba(0, 0, 0, 0.3)}html.dark .el-button{--el-button-disabled-text-color:rgba(255, 255, 255, 0.5)}html.dark .el-card{--el-card-bg-color:var(--el-bg-color-overlay)}html.dark .el-empty{--el-empty-fill-color-0:var(--el-color-black);--el-empty-fill-color-1:#4b4b52;--el-empty-fill-color-2:#36383d;--el-empty-fill-color-3:#1e1e20;--el-empty-fill-color-4:#262629;--el-empty-fill-color-5:#202124;--el-empty-fill-color-6:#212224;--el-empty-fill-color-7:#1b1c1f;--el-empty-fill-color-8:#1c1d1f;--el-empty-fill-color-9:#18181a} -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RedisFish 8 | 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 64 | 65 | 82 | -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/src/assets/icon.png -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { app, protocol, BrowserWindow, screen } from 'electron' 4 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' 5 | import { autoUpdater } from 'electron-updater' 6 | import log from 'electron-log' 7 | const isDevelopment = process.env.NODE_ENV !== 'production' 8 | 9 | // Scheme must be registered before the app is ready 10 | protocol.registerSchemesAsPrivileged([ 11 | { scheme: 'app', privileges: { secure: true, standard: true } } 12 | ]) 13 | 14 | async function createWindow () { 15 | // Create the browser window. 16 | const win = new BrowserWindow({ 17 | autoHideMenuBar: true, 18 | width: screen.getPrimaryDisplay().workAreaSize.width, 19 | height: screen.getPrimaryDisplay().workAreaSize.height, 20 | // width: 1600, 21 | // height: 1000, 22 | webPreferences: { 23 | contextIsolation: false, 24 | nodeIntegration: true, 25 | nodeIntegrationInWorker: true, 26 | enableRemoteModule: true 27 | } 28 | }) 29 | 30 | win.maximize() 31 | 32 | if (process.env.WEBPACK_DEV_SERVER_URL) { 33 | // Load the url of the dev server if in development mode 34 | await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string) 35 | if (!process.env.IS_TEST) win.webContents.openDevTools() 36 | } else { 37 | createProtocol('app') 38 | // Load the index.html when not in development 39 | await win.loadURL('app://./index.html') 40 | log.transports.file.level = 'debug' 41 | autoUpdater.logger = log 42 | await autoUpdater.checkForUpdatesAndNotify() 43 | } 44 | } 45 | 46 | // 避免开启多个客户端窗口 47 | const gotTheLock = app.requestSingleInstanceLock() 48 | 49 | if (!gotTheLock) { 50 | app.quit() 51 | } 52 | 53 | // Quit when all windows are closed. 54 | app.on('window-all-closed', () => { 55 | // On macOS it is common for applications and their menu bar 56 | // to stay active until the user quits explicitly with Cmd + Q 57 | if (process.platform !== 'darwin') { 58 | app.quit() 59 | } 60 | }) 61 | 62 | app.on('activate', () => { 63 | // On macOS it's common to re-create a window in the app when the 64 | // dock icon is clicked and there are no other windows open. 65 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 66 | }) 67 | 68 | // This method will be called when Electron has finished 69 | // initialization and is ready to create browser windows. 70 | // Some APIs can only be used after this event occurs. 71 | app.on('ready', async () => { 72 | if (isDevelopment && !process.env.IS_TEST) { 73 | // Install Vue Devtools 74 | // try { 75 | // await installExtension(VUEJS3_DEVTOOLS) 76 | // } catch (e) { 77 | // console.error('Vue Devtools failed to install:', e.toString()) 78 | // } 79 | } 80 | createWindow() 81 | }) 82 | 83 | // Exit cleanly on request from parent process in development mode. 84 | if (isDevelopment) { 85 | if (process.platform === 'win32') { 86 | process.on('message', (data) => { 87 | if (data === 'graceful-exit') { 88 | app.quit() 89 | } 90 | }) 91 | } else { 92 | process.on('SIGTERM', () => { 93 | app.quit() 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/contentDetail/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 104 | -------------------------------------------------------------------------------- /src/components/customAce/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | -------------------------------------------------------------------------------- /src/components/divider/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n, LocaleMessages, VueMessageType } from 'vue-i18n' 2 | 3 | /** 4 | * Load locale messages 5 | * 6 | * The loaded `JSON` locale messages is pre-compiled by `@intlify/vue-i18n-loader`, which is integrated into `vue-cli-plugin-i18n`. 7 | * See: https://github.com/intlify/vue-i18n-loader#rocket-i18n-resource-pre-compilation 8 | */ 9 | function loadLocaleMessages (): LocaleMessages { 10 | const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i) 11 | const messages: LocaleMessages = {} 12 | locales.keys().forEach(key => { 13 | const matched = key.match(/([A-Za-z0-9-_]+)\./i) 14 | if (matched && matched.length > 1) { 15 | const locale = matched[1] 16 | messages[locale] = locales(key).default 17 | } 18 | }) 19 | return messages 20 | } 21 | 22 | export default createI18n({ 23 | legacy: false, 24 | locale: process.env.VUE_APP_I18N_LOCALE || 'en_GB', 25 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en_GB', 26 | messages: loadLocaleMessages() 27 | }) 28 | -------------------------------------------------------------------------------- /src/locales/en_GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "English", 3 | "consolePane": { 4 | "index": { 5 | "checkBtn": "submit", 6 | "historyLabel": "commands history", 7 | "historyPaneLabel": "run result", 8 | "commandInputPlaceholder": "Enter to execute the command", 9 | "dialog": { 10 | "title": "tip", 11 | "content": "The following command will be executed:", 12 | "cancelBtn": "cancel", 13 | "runBtn": "run" 14 | }, 15 | "notification": { 16 | "title": "tip", 17 | "message": "No action to take" 18 | } 19 | }, 20 | "history": { 21 | "command": "run commands", 22 | "result": "run result", 23 | "createdAt": "run time" 24 | } 25 | }, 26 | "Home": {}, 27 | "keyMenu": { 28 | "searchInputPlaceholder": "enter to query", 29 | "btnGroup": { 30 | "monitor": "monitor", 31 | "log": "log", 32 | "sub": "Pub/Sub", 33 | "refresh": "refresh", 34 | "delete": "delete" 35 | }, 36 | "delDialog": { 37 | "title": "tip", 38 | "content": "The selected key is about to be deleted, do you want to execute it?", 39 | "cancel": "cancel", 40 | "submit": "run" 41 | }, 42 | "logDialog": { 43 | "title": "log", 44 | "clearLog": "clear" 45 | } 46 | }, 47 | "keyTab": {}, 48 | "newKeyValue": { 49 | "index": { 50 | "runDialog": { 51 | "title": "run commands", 52 | "cancel": "cancel", 53 | "run": "run", 54 | "running": "in progress", 55 | "submit": "close" 56 | }, 57 | "notification": { 58 | "title": "tip", 59 | "message": "Please fill in the key" 60 | } 61 | }, 62 | "btnGroup": { 63 | "add": "add", 64 | "delete": "delete", 65 | "submit": "submit", 66 | "addFromHeader": "add from header", 67 | "addFromFooter": "add from footer", 68 | "deleteFromHeader": "delete from header", 69 | "deleteFromFooter": "delete from footer" 70 | }, 71 | "notification": { 72 | "infoTitle": "tip", 73 | "dataDuplicationTitle": "data duplication", 74 | "dataDuplicationMessage": "Duplicate with existing data", 75 | "emptyContentMessage": "No action to take" 76 | } 77 | }, 78 | "serverMenu": { 79 | "index": { 80 | "title": "Database List", 81 | "addServerBtn": "add database", 82 | "empty": "The data is empty, click the + button in the upper right corner of the database list column to add", 83 | "addBtn": "add", 84 | "editBtn": "edit" 85 | }, 86 | "form": { 87 | "name": "name", 88 | "host": "host", 89 | "port": "port", 90 | "password": "password", 91 | "namePlaceholder": "example: db_redis", 92 | "hostPlaceholder": "192.168.1.10", 93 | "successTip": "connection succeeded", 94 | "defaultTip": "unable to connect", 95 | "cancelBtn": "cancel", 96 | "testBtn": "test connection", 97 | "deleteBtn": "delete", 98 | "saveBtn": "save" 99 | }, 100 | "notification": { 101 | "infoTitle": "tip", 102 | "addSuccessMessage": "Add database succeeded", 103 | "editSuccessMessage": "Edit database succeeded", 104 | "errorMessage": "Duplicate database name" 105 | } 106 | }, 107 | "serverTab": { 108 | "emptyPage": { 109 | "message0": "It's easier to use shortcut keys", 110 | "message1": "Why don't you have a cup of milk tea?", 111 | "message2": "It would be nice to have an ice cream", 112 | "message3": "Welcome to communicate in Issues", 113 | "message4": "If only I had a popsicle", 114 | "message5": "empty ~" 115 | } 116 | }, 117 | "valueContent": { 118 | "index": { 119 | "runDialog": { 120 | "title": "run commands", 121 | "cancel": "cancel", 122 | "run": "run", 123 | "running": "in progress", 124 | "submit": "ok" 125 | }, 126 | "notification": { 127 | "title": "tip", 128 | "message": "Please fill in the key" 129 | }, 130 | "unknownTypeValue": "unknown type" 131 | }, 132 | "searchInputPlaceholder": "enter to query", 133 | "btnGroup": { 134 | "refresh": "refresh", 135 | "add": "add", 136 | "delete": "delete", 137 | "submit": "submit", 138 | "addFromHeader": "add from header", 139 | "addFromFooter": "add from footer", 140 | "deleteFromHeader": "delete from header", 141 | "deleteFromFooter": "delete from footer" 142 | }, 143 | "notification": { 144 | "infoTitle": "Tip", 145 | "dataDuplicationTitle": "data duplication", 146 | "dataDuplicationMessage": "Duplicate with existing data", 147 | "emptyContentMessage": "No action to take", 148 | "copySuccessMessage": "Copy successfully" 149 | } 150 | }, 151 | "config": { 152 | "title": "Config", 153 | "tabGroup": { 154 | "general": "General", 155 | "hotKey": "HotKey", 156 | "about": "About" 157 | }, 158 | "theme": { 159 | "label": "Theme", 160 | "light": "Light", 161 | "dark": "Dark", 162 | "auto": "Auto" 163 | }, 164 | "videoTutorial": { 165 | "label": "show video tutorial", 166 | "yes": "show", 167 | "no": "hide" 168 | }, 169 | "language": { 170 | "label": "language", 171 | "zh": "简体中文", 172 | "en": "English", 173 | "auto": "follow the system" 174 | }, 175 | "keyMenuFilterSymbol": { 176 | "label": "key prefix separator" 177 | }, 178 | "keyMenuStatus": { 179 | "label": "key display mode", 180 | "group": "prefix group", 181 | "filter": "list display" 182 | }, 183 | "hotKey": { 184 | "click": "left mouse click", 185 | "rightClick": "right mouse click", 186 | "copyLabel": "Copy", 187 | "viewLabel": "Data zoom", 188 | "copyTip": "Use the command key (⌘) on Mac keyboards and the Ctrl key on Windows/Linux keyboards" 189 | } 190 | }, 191 | "contentDetail": { 192 | "submit": "save", 193 | "cancel": "cancel" 194 | }, 195 | "pubAndSub": { 196 | "send": "send", 197 | "clear": "clear", 198 | "inSub": "subscribing", 199 | "startSub": "start subscribing", 200 | "stopSub": "stop subscription", 201 | "emptyMessage": "No news yet" 202 | }, 203 | "monitor": { 204 | "cpu": "CPU consumption rate", 205 | "memory": "memory usage", 206 | "clientCount": "number of connected clients and number of clients waiting for blocking commands" 207 | }, 208 | "videoTutorial": "video tutorial" 209 | } 210 | -------------------------------------------------------------------------------- /src/locales/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "中文", 3 | "consolePane": { 4 | "index": { 5 | "checkBtn": "提交操作", 6 | "historyLabel": "执行历史", 7 | "historyPaneLabel": "执行结果", 8 | "commandInputPlaceholder": "回车以执行命令", 9 | "dialog": { 10 | "title": "提示", 11 | "content": "将要执行如下命令:", 12 | "cancelBtn": "取消", 13 | "runBtn": "执行" 14 | }, 15 | "notification": { 16 | "title": "提示", 17 | "message": "没有可执行的操作" 18 | } 19 | }, 20 | "history": { 21 | "command": "执行命令", 22 | "result": "执行结果", 23 | "createdAt": "执行时间" 24 | } 25 | }, 26 | "Home": {}, 27 | "keyMenu": { 28 | "searchInputPlaceholder": "回车查询搜索结果", 29 | "btnGroup": { 30 | "monitor": "性能监控", 31 | "log": "日志", 32 | "sub": "发布/订阅", 33 | "refresh": "刷新", 34 | "delete": "删除" 35 | }, 36 | "delDialog": { 37 | "title": "提示", 38 | "content": "将要删除所选key,是否执行?", 39 | "cancel": "取消", 40 | "submit": "执行" 41 | }, 42 | "logDialog": { 43 | "title": "日志", 44 | "clearLog": "清空日志" 45 | } 46 | }, 47 | "keyTab": {}, 48 | "newKeyValue": { 49 | "index": { 50 | "runDialog": { 51 | "title": "执行命令", 52 | "cancel": "取消", 53 | "run": "执行", 54 | "running": "执行中", 55 | "submit": "确定" 56 | }, 57 | "notification": { 58 | "title": "提示", 59 | "message": "请填写key" 60 | } 61 | }, 62 | "btnGroup": { 63 | "add": "添加", 64 | "delete": "删除", 65 | "submit": "提交操作", 66 | "addFromHeader": "头部添加", 67 | "addFromFooter": "尾部添加", 68 | "deleteFromHeader": "头部移出", 69 | "deleteFromFooter": "尾部移出" 70 | }, 71 | "notification": { 72 | "infoTitle": "提示", 73 | "dataDuplicationTitle": "数据重复", 74 | "dataDuplicationMessage": "与现有数据重复", 75 | "emptyContentMessage": "没有可执行的操作" 76 | } 77 | }, 78 | "serverMenu": { 79 | "index": { 80 | "title": "数据库列表", 81 | "addServerBtn": "添加数据库", 82 | "empty": "数据为空,点击数据库列表列右上角+按钮进行添加", 83 | "addBtn": "新增", 84 | "editBtn": "编辑" 85 | }, 86 | "form": { 87 | "name": "名称", 88 | "host": "地址", 89 | "port": "端口", 90 | "password": "密码", 91 | "namePlaceholder": "开发数据库", 92 | "hostPlaceholder": "192.168.1.10", 93 | "successTip": "连接成功", 94 | "defaultTip": "未成功连接", 95 | "cancelBtn": "取消", 96 | "testBtn": "测试连接", 97 | "deleteBtn": "删除", 98 | "saveBtn": "保存" 99 | }, 100 | "notification": { 101 | "infoTitle": "提示", 102 | "addSuccessMessage": "添加数据库成功", 103 | "editSuccessMessage": "编辑数据库成功", 104 | "errorMessage": "数据库名称重复" 105 | } 106 | }, 107 | "serverTab": { 108 | "emptyPage": { 109 | "message0": "使用快捷键会更便捷呦", 110 | "message1": "不如喝杯奶茶吧", 111 | "message2": "吃个冰淇淋好像也不错", 112 | "message3": "欢迎在Issues交流哦", 113 | "message4": "要是吃个棒冰就好了", 114 | "message5": "空空如也" 115 | } 116 | }, 117 | "valueContent": { 118 | "index": { 119 | "runDialog": { 120 | "title": "执行命令", 121 | "cancel": "取消", 122 | "run": "执行", 123 | "running": "执行中", 124 | "submit": "确定" 125 | }, 126 | "notification": { 127 | "title": "提示", 128 | "message": "请填写key" 129 | }, 130 | "unknownTypeValue": "未知类型" 131 | }, 132 | "searchInputPlaceholder": "回车查询搜索结果", 133 | "btnGroup": { 134 | "refresh": "刷新", 135 | "add": "添加", 136 | "delete": "删除", 137 | "submit": "提交操作", 138 | "addFromHeader": "头部添加", 139 | "addFromFooter": "尾部添加", 140 | "deleteFromHeader": "头部移出", 141 | "deleteFromFooter": "尾部移出" 142 | }, 143 | "notification": { 144 | "infoTitle": "提示", 145 | "dataDuplicationTitle": "数据重复", 146 | "dataDuplicationMessage": "与现有数据重复", 147 | "emptyContentMessage": "没有可执行的操作", 148 | "copySuccessMessage": "复制成功" 149 | } 150 | }, 151 | "config": { 152 | "title": "设置", 153 | "tabGroup": { 154 | "general": "常规", 155 | "hotKey": "快捷键", 156 | "about": "关于" 157 | }, 158 | "theme": { 159 | "label": "主题", 160 | "light": "浅色", 161 | "dark": "深色", 162 | "auto": "自动" 163 | }, 164 | "videoTutorial": { 165 | "label": "显示视频教程", 166 | "yes": "显示", 167 | "no": "隐藏" 168 | }, 169 | "language": { 170 | "label": "语言", 171 | "zh": "简体中文", 172 | "en": "English", 173 | "auto": "跟随系统" 174 | }, 175 | "keyMenuFilterSymbol": { 176 | "label": "key前缀分割符" 177 | }, 178 | "keyMenuStatus": { 179 | "label": "key展示模式", 180 | "group": "前缀分组", 181 | "filter": "列表展示" 182 | }, 183 | "hotKey": { 184 | "click": "鼠标左击", 185 | "rightClick": "鼠标右击", 186 | "copyLabel": "复制", 187 | "viewLabel": "数据放大编辑", 188 | "copyTip": "在Mac系统键盘上使用command键 (⌘), 在Windows/Linux系统键盘上使用Ctrl键" 189 | } 190 | }, 191 | "contentDetail": { 192 | "submit": "保存", 193 | "cancel": "关闭" 194 | }, 195 | "pubAndSub": { 196 | "send": "发送", 197 | "clear": "清空", 198 | "inSub": "订阅中", 199 | "startSub": "开始订阅", 200 | "stopSub": "结束订阅", 201 | "emptyMessage": "暂无消息" 202 | }, 203 | "monitor": { 204 | "cpu": "CPU消耗率", 205 | "memory": "内存使用量", 206 | "clientCount": "已连接客户端数(个)和等待阻塞命令客户端数(个)", 207 | "clientCountLegend1": "已连接客户端数", 208 | "clientCountLegend2": "等待阻塞命令客户端数" 209 | }, 210 | "videoTutorial": "视频教程" 211 | } 212 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | 6 | import './styles/styles.css' 7 | import 'element-plus/dist/index.css' 8 | import 'element-plus/theme-chalk/dark/css-vars.css' 9 | import i18n from './i18n' 10 | 11 | createApp(App).use(i18n).use(store).use(router).mount('#app') 12 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: Array = [ 4 | { 5 | path: '/', 6 | name: 'Home', 7 | component: () => import('../views/Home/index.vue') 8 | } 9 | ] 10 | 11 | const router = createRouter({ 12 | history: createWebHashHistory(process.env.BASE_URL), 13 | routes 14 | }) 15 | 16 | export default router 17 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/store/getters.ts: -------------------------------------------------------------------------------- 1 | const getters = { 2 | serverList: (state: any) => state.serverList.serverList, 3 | serverTabList: (state: any) => state.serverList.serverTabList, 4 | currentServerTab: (state: any) => state.serverList.currentServerTab, 5 | keyTabList: (state: any) => state.keyList.keyTabList, 6 | monitorList: (state: any) => state.keyMenuAndTabBind.monitorList, 7 | psList: (state: any) => state.keyMenuAndTabBind.psList, 8 | theme: (state: any) => state.config.theme, 9 | isVideoShow: (state: any) => state.config.isVideoShow, 10 | language: (state: any) => state.config.language, 11 | keyMenuFilterSymbol: (state: any) => state.config.keyMenuFilterSymbol, 12 | keyMenuFilterSymbolStr: (state: any) => state.config.keyMenuFilterSymbolStr, 13 | keyMenuStatus: (state: any) => state.config.keyMenuStatus 14 | } 15 | 16 | export default getters 17 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import getters from '@/store/getters' 3 | 4 | const modulesFiles = require.context('./modules', true, /\.ts$/) 5 | 6 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 7 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 8 | const value = modulesFiles(modulePath) 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-ignore 11 | modules[moduleName] = value.default 12 | return modules 13 | }, {}) 14 | 15 | const store = createStore({ 16 | getters, 17 | modules 18 | }) 19 | 20 | export default store 21 | -------------------------------------------------------------------------------- /src/store/modules/config.ts: -------------------------------------------------------------------------------- 1 | import { Commit } from 'vuex' 2 | import { configType, getConfig, setConfig, transKeyMenuFilterSymbol } from '@/utils/configStore' 3 | import { handleThemeChange } from '@/utils/theme' 4 | 5 | const state: configType = { 6 | theme: 3, 7 | isVideoShow: 1, 8 | language: 3, 9 | keyMenuStatus: 2, 10 | keyMenuFilterSymbol: 1, 11 | keyMenuFilterSymbolStr: ':' 12 | } 13 | 14 | const mutations = { 15 | initConfig (state: configType, config: configType): void { 16 | state.theme = config.theme 17 | state.isVideoShow = config.isVideoShow 18 | state.language = config.language 19 | state.keyMenuStatus = config.keyMenuStatus 20 | state.keyMenuFilterSymbol = config.keyMenuFilterSymbol 21 | handleThemeChange(config.theme) 22 | }, 23 | storeConfig (state: configType): void { 24 | setConfig(state) 25 | }, 26 | updateTheme (state: configType, theme: number): void { 27 | state.theme = theme 28 | }, 29 | updateVideoShow (state: configType, isVideoShow: number): void { 30 | state.isVideoShow = isVideoShow 31 | }, 32 | updateLanguage (state: configType, language: number): void { 33 | state.language = language 34 | }, 35 | updateKeyMenuStatus (state: configType, keyMenuStatus: number): void { 36 | state.keyMenuStatus = keyMenuStatus 37 | }, 38 | updateKeyMenuFilterSymbol (state: configType, keyMenuFilterSymbol: number): void { 39 | state.keyMenuFilterSymbol = keyMenuFilterSymbol 40 | state.keyMenuFilterSymbolStr = transKeyMenuFilterSymbol(keyMenuFilterSymbol) 41 | } 42 | } 43 | 44 | const actions = { 45 | async init ({ commit }: { commit: Commit }): Promise { 46 | const configData = await getConfig() 47 | commit('initConfig', configData) 48 | }, 49 | async updateTheme ({ commit }: { commit: Commit }, theme: number): Promise { 50 | commit('updateTheme', theme) 51 | commit('storeConfig') 52 | }, 53 | async updateVideoShow ({ commit }: { commit: Commit }, isVideoShow: number): Promise { 54 | commit('updateVideoShow', isVideoShow) 55 | commit('storeConfig') 56 | }, 57 | async updateLanguage ({ commit }: { commit: Commit }, theme: number): Promise { 58 | commit('updateLanguage', theme) 59 | commit('storeConfig') 60 | }, 61 | async updateKeyMenuStatus ({ commit }: { commit: Commit }, keyMenuStatus: number): Promise { 62 | commit('updateKeyMenuStatus', keyMenuStatus) 63 | commit('storeConfig') 64 | }, 65 | async updateKeyMenuFilterSymbol ({ commit }: { commit: Commit }, keyMenuFilterSymbol: number): Promise { 66 | commit('updateKeyMenuFilterSymbol', keyMenuFilterSymbol) 67 | commit('storeConfig') 68 | } 69 | } 70 | 71 | export default { 72 | namespaced: true, 73 | state, 74 | mutations, 75 | actions 76 | } 77 | -------------------------------------------------------------------------------- /src/store/modules/keyList.ts: -------------------------------------------------------------------------------- 1 | import { Commit } from 'vuex' 2 | 3 | export interface keyTabType { 4 | serverLabel: string 5 | values: string[] 6 | } 7 | 8 | interface stateType { 9 | keyTabList: keyTabType[] 10 | } 11 | 12 | interface paramType { 13 | serverLabel: string 14 | key: string 15 | } 16 | 17 | const state: stateType = { 18 | keyTabList: [] 19 | } 20 | 21 | const mutations = { 22 | addKeyTab (state: stateType, param: paramType): void { 23 | const targetKeyTabIndex = state.keyTabList.findIndex(item => item.serverLabel === param.serverLabel) 24 | if (targetKeyTabIndex !== -1) { 25 | const checkIndex = state.keyTabList[targetKeyTabIndex].values.findIndex((item: string) => item === param.key) 26 | if (checkIndex === -1) { 27 | state.keyTabList[targetKeyTabIndex].values.push(param.key) 28 | } 29 | } else { 30 | const values: string[] = [] 31 | values.push(param.key) 32 | state.keyTabList.push({ 33 | serverLabel: param.serverLabel, 34 | values: values 35 | }) 36 | } 37 | }, 38 | delKeyTab (state: stateType, param: paramType): void { 39 | const targetKeyTabIndex = state.keyTabList.findIndex(item => item.serverLabel === param.serverLabel) 40 | if (targetKeyTabIndex !== -1) { 41 | state.keyTabList[targetKeyTabIndex].values = state.keyTabList[targetKeyTabIndex].values.filter(item => item !== param.key) 42 | } 43 | } 44 | } 45 | 46 | const actions = { 47 | async add ({ commit }: { commit: Commit }, param: paramType): Promise { 48 | commit('addKeyTab', param) 49 | }, 50 | async del ({ commit }: { commit: Commit }, param: paramType): Promise { 51 | commit('delKeyTab', param) 52 | } 53 | } 54 | 55 | export default { 56 | namespaced: true, 57 | state, 58 | mutations, 59 | actions 60 | } 61 | -------------------------------------------------------------------------------- /src/store/modules/keyMenuAndTabBind.ts: -------------------------------------------------------------------------------- 1 | import { Commit } from 'vuex' 2 | import { serverTabType } from '@/store/modules/serverList' 3 | 4 | interface stateType { 5 | monitorList: string[] 6 | psList: string[] 7 | } 8 | 9 | const genKey = (serverTab: serverTabType): string => { 10 | return `${serverTab.name}_${serverTab.db}` 11 | } 12 | 13 | const state: stateType = { 14 | monitorList: [], 15 | psList: [] 16 | } 17 | 18 | const mutations = { 19 | monitorToggle (state: stateType, serverTab: serverTabType): void { 20 | const key = genKey(serverTab) 21 | 22 | if (state.monitorList.indexOf(key) === -1) { 23 | state.monitorList.push(key) 24 | } else { 25 | state.monitorList = state.monitorList.filter((item: string) => item !== key) 26 | } 27 | }, 28 | psToggle (state: stateType, serverTab: serverTabType): void { 29 | const key = genKey(serverTab) 30 | 31 | if (state.psList.indexOf(key) === -1) { 32 | state.psList.push(key) 33 | } else { 34 | state.psList = state.psList.filter((item: string) => item !== key) 35 | } 36 | } 37 | } 38 | 39 | const actions = { 40 | monitorToggle ({ commit }: { commit: Commit }, serverTab: serverTabType): void { 41 | commit('monitorToggle', serverTab) 42 | }, 43 | psToggle ({ commit }: { commit: Commit }, serverTab: serverTabType): void { 44 | commit('psToggle', serverTab) 45 | } 46 | } 47 | 48 | export default { 49 | namespaced: true, 50 | state, 51 | mutations, 52 | actions 53 | } 54 | -------------------------------------------------------------------------------- /src/store/modules/serverList.ts: -------------------------------------------------------------------------------- 1 | import { serverType, setStore } from '@/utils/store' 2 | import { Commit } from 'vuex' 3 | 4 | export interface serverTabType extends serverType { 5 | db: string 6 | } 7 | 8 | interface stateType { 9 | serverList: serverType[] 10 | serverTabList: serverTabType[] 11 | currentServerTab: string 12 | } 13 | 14 | const state = { 15 | serverList: [], 16 | serverTabList: [], 17 | currentServerTab: '' 18 | } 19 | 20 | const mutations = { 21 | setServerList (state: stateType): void { 22 | setStore(state.serverList) 23 | }, 24 | updateServer (state: stateType, serverList: serverType[]): void { 25 | state.serverList = serverList 26 | }, 27 | setServerTabList (state: stateType, serverTab: serverTabType): void { 28 | if (state.serverTabList.findIndex((item: serverTabType) => `${item.db} ${item.name}` === `${serverTab.db} ${serverTab.name}`) === -1) { 29 | state.serverTabList.push(serverTab) 30 | } 31 | }, 32 | delServerTabList (state: stateType, targetName: string): void { 33 | state.serverTabList = state.serverTabList.filter((item: serverTabType) => `${item.db} ${item.name}` !== targetName) 34 | }, 35 | delServerTabListWithServerName (state: stateType, serverName: string): void { 36 | state.serverTabList = state.serverTabList.filter((item: serverTabType) => { 37 | return item.name !== serverName 38 | }) 39 | }, 40 | setCurrentServerTab (state: stateType, serverTab: string): void { 41 | state.currentServerTab = serverTab 42 | } 43 | } 44 | 45 | const actions = { 46 | addTab ({ commit }: { commit: Commit }, serverTab: serverTabType): void { 47 | commit('setCurrentServerTab', `${serverTab.db} ${serverTab.name}`) 48 | commit('setServerTabList', serverTab) 49 | }, 50 | delTab ({ commit }: { commit: Commit }, targetName: string): void { 51 | commit('delServerTabList', targetName) 52 | 53 | if (state.serverTabList.length && targetName === state.currentServerTab) { 54 | const lastServerTab: serverTabType = state.serverTabList[state.serverTabList.length - 1] 55 | commit('setCurrentServerTab', `${lastServerTab.db} ${lastServerTab.name}`) 56 | } else if (!state.serverTabList.length) { 57 | commit('setCurrentServerTab', '') 58 | } 59 | }, 60 | delTabWithServerName ({ commit }: { commit: Commit }, serverName: string): void { 61 | commit('delServerTabListWithServerName', serverName) 62 | }, 63 | setServer ({ commit }: { commit: Commit }): void { 64 | commit('setServerList') 65 | }, 66 | updateServer ({ commit }: { commit: Commit }, server: serverType[]): void { 67 | commit('updateServer', server) 68 | } 69 | } 70 | 71 | export default { 72 | namespaced: true, 73 | state, 74 | mutations, 75 | actions 76 | } 77 | -------------------------------------------------------------------------------- /src/styles/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | .el-tabs__nav-scroll { 11 | width: 100%; 12 | } 13 | 14 | .container { 15 | width: 100%; 16 | max-width: 100%; 17 | min-width: 100%; 18 | } 19 | 20 | ::-webkit-scrollbar{ 21 | width: 5px; 22 | height: 5px; 23 | } 24 | ::-webkit-scrollbar-thumb{ 25 | border-radius: 1em; 26 | background-color: rgba(50,50,50,.3); 27 | } 28 | ::-webkit-scrollbar-track{ 29 | border-radius: 1em; 30 | background-color: rgba(50,50,50,.1); 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/checkIsJson.ts: -------------------------------------------------------------------------------- 1 | export const checkIsJSON = (str: string): boolean => { 2 | try { 3 | const obj = JSON.parse(str) 4 | return !!(typeof obj === 'object' && obj) 5 | } catch (e) { 6 | return false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/cipherCode.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | const algorithm = 'aes-128-cbc' 4 | 5 | export const encrypt = async (data: string): Promise => { 6 | const password = crypto.randomBytes(16).toString('hex') // password是用于生产密钥的密码 7 | const salt = crypto.randomBytes(16).toString('hex') // 生成盐值 8 | const iv = crypto.randomBytes(8).toString('hex') // 初始化向量 9 | 10 | return new Promise((resolve, reject) => { 11 | crypto.scrypt(password, salt, 16, (err, derivedKey) => { 12 | if (err) { 13 | reject(err) 14 | } else { 15 | const cipher = crypto.createCipheriv(algorithm, derivedKey, iv) 16 | 17 | // 加密数据 18 | let cipherText = cipher.update(data, 'utf8', 'hex') 19 | cipherText += cipher.final('hex') 20 | cipherText += (password + salt + iv) 21 | 22 | resolve(cipherText) 23 | } 24 | }) 25 | }) 26 | } 27 | 28 | export const decrypt = async (cipherText: string): Promise => { 29 | const iv = cipherText.slice(-16) // 获取初始化向量 30 | const salt = cipherText.slice(-48, -16) // 获取盐值 31 | const password = cipherText.slice(-80, -48) // 获取密钥密码 32 | const data = cipherText.slice(0, -80) // 获取密文 33 | 34 | return new Promise((resolve, reject) => { 35 | crypto.scrypt(password, salt, 16, (err, derivedKey) => { 36 | if (err) { 37 | reject(err) 38 | } else { 39 | const decipher = crypto.createDecipheriv(algorithm, derivedKey, iv) 40 | 41 | // 解密数据 42 | let txt = decipher.update(data, 'hex', 'utf8') 43 | txt += decipher.final('utf8') 44 | 45 | resolve(txt) 46 | } 47 | }) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/configStore.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { remote } from 'electron' 3 | import { writeFile, readFile, existsFile, mkdirFolder } from '@/utils/file' 4 | import { checkIsJSON } from '@/utils/checkIsJson' 5 | 6 | const storeFolderPath = path.join(remote.app.getPath('home'), '.myRedisClient') 7 | const storeFilePath = path.join(remote.app.getPath('home'), '.myRedisClient/.config') 8 | 9 | export interface configType { 10 | theme: number // 1 light 2 dark 3 auto 11 | isVideoShow: number 12 | language: number // 1 zh 2 en 3 auto 13 | keyMenuStatus: number // 1 group 2 filter 14 | keyMenuFilterSymbol: number // 1 : 2 _ 3 - 4 # 5 = 6 + 15 | keyMenuFilterSymbolStr: string 16 | } 17 | 18 | export const getConfig = async (): Promise => { 19 | await initConfigFile() 20 | const result = await readFile(storeFilePath) 21 | if (checkIsJSON(result)) { 22 | return JSON.parse(result) 23 | } else { 24 | return { 25 | theme: 3, 26 | isVideoShow: 1, 27 | language: 3, 28 | keyMenuStatus: 1, 29 | keyMenuFilterSymbol: 1, 30 | keyMenuFilterSymbolStr: ':' 31 | } 32 | } 33 | } 34 | 35 | export const initConfigFile = async (): Promise => { 36 | const isFolderExists = await existsFile(storeFolderPath) 37 | const isFileExists = await existsFile(storeFilePath) 38 | if (!isFolderExists) { 39 | await mkdirFolder(storeFolderPath) 40 | await writeFile(storeFilePath, JSON.stringify({})) 41 | } else if (isFolderExists && !isFileExists) { 42 | await writeFile(storeFilePath, JSON.stringify({})) 43 | } 44 | } 45 | 46 | export const setConfig = (content: configType): void => { 47 | writeFile(storeFilePath, JSON.stringify(content)) 48 | } 49 | 50 | export const transKeyMenuFilterSymbol = (keyMenuFilterSymbol: number): string => { 51 | switch (keyMenuFilterSymbol) { 52 | case 1: 53 | return ':' 54 | case 2: 55 | return '_' 56 | case 3: 57 | return '-' 58 | case 4: 59 | return '#' 60 | case 5: 61 | return '=' 62 | case 6: 63 | return '+' 64 | default: 65 | return ':' 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/contentLimit.ts: -------------------------------------------------------------------------------- 1 | export const contentLimit = (content: string): string => { 2 | return content && content.length > 200 ? content.substring(0, 200) + '...' : content 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/copyFromTable.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | import { Clipboard } from 'ts-clipboard' 3 | 4 | export const copyKey = async (text: string, message: string): Promise => { 5 | await Clipboard.copy(text) 6 | ElMessage({ message: message, type: 'success' }) 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/cutsomCommandFormat.ts: -------------------------------------------------------------------------------- 1 | export const handleCommandFormat = async (command: string): Promise => { 2 | const arr: string[] = command.split(' ') 3 | const newArr: string[] = [] 4 | let tmpStr = '' 5 | let lock = false 6 | let sign = '' 7 | arr.forEach((item: string) => { 8 | if (item.startsWith('\'') && item.endsWith('\'') && !lock && !sign.length) { 9 | newArr.push(item.replace(/(^'|'$)/g, '')) 10 | } else if (item.startsWith('"') && item.endsWith('"') && !lock && !sign.length) { 11 | newArr.push(item.replace(/(^"|"$)/g, '')) 12 | } else if (item.startsWith('\'') && !sign.length) { 13 | lock = true 14 | sign = '\'' 15 | tmpStr += `${item} ` 16 | } else if (item.startsWith('"') && !sign.length) { 17 | lock = true 18 | sign = '"' 19 | tmpStr += `${item} ` 20 | } else if (item.endsWith(sign)) { 21 | lock = false 22 | tmpStr += `${item} ` 23 | if (sign === '"') { 24 | newArr.push(tmpStr.trim().replace(/(^"|"$)/g, '')) 25 | } else { 26 | newArr.push(tmpStr.trim().replace(/(^'|'$)/g, '')) 27 | } 28 | sign = '' 29 | tmpStr = '' 30 | } else if (lock) { 31 | tmpStr += `${item} ` 32 | } else { 33 | newArr.push(item) 34 | } 35 | }) 36 | return newArr 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import { encrypt, decrypt } from '@/utils/cipherCode' 3 | 4 | export const writeFile = async (path: string, content: string): Promise => { 5 | const encryptContent = await encrypt(content) 6 | fs.writeFileSync(path, encryptContent) 7 | } 8 | 9 | export const appendFile = async (path: string, content: string): Promise => { 10 | const encryptContent = await encrypt(content) 11 | fs.appendFileSync(path, encryptContent) 12 | } 13 | 14 | export const readFile = async (path: string): Promise => { 15 | const content = fs.readFileSync(path, { encoding: 'utf8', flag: 'r' }) 16 | return await decrypt(content) 17 | } 18 | 19 | export const existsFile = async (path: string): Promise => { 20 | return fs.existsSync(path) 21 | } 22 | 23 | export const mkdirFolder = async (path: string): Promise => { 24 | fs.mkdirSync(path) 25 | } 26 | 27 | export const readFileWithoutEn = async (path: string): Promise => { 28 | return fs.readFileSync(path, 'ascii') 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/formatCommandField.ts: -------------------------------------------------------------------------------- 1 | export const FormatCommandField = (field: string): string => { 2 | // if (field.indexOf(' ') >= 0) { 3 | // if (field.indexOf('\'') >= 0) return `"${field}"` 4 | // return `'${field}'` 5 | // } 6 | 7 | // if (field.includes(' ')) { 8 | // return `'${field}'` 9 | // } 10 | 11 | return field 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/formatTime.ts: -------------------------------------------------------------------------------- 1 | export const dateFormat = (): string => { 2 | let ret 3 | let fmt = 'YYYY-mm-dd HH:MM' 4 | const date = new Date() 5 | const opt: {'Y+': string, 'm+': string, 'd+': string, 'H+': string, 'M+': string, 'S+': string} = { 6 | 'Y+': date.getFullYear().toString(), 7 | 'm+': (date.getMonth() + 1).toString(), 8 | 'd+': date.getDate().toString(), 9 | 'H+': date.getHours().toString(), 10 | 'M+': date.getMinutes().toString(), 11 | 'S+': date.getSeconds().toString() 12 | } 13 | for (const k in opt) { 14 | ret = new RegExp('(' + k + ')').exec(fmt) 15 | if (ret) { 16 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 17 | // @ts-ignore 18 | fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, '0'))) 19 | } 20 | } 21 | return fmt 22 | } 23 | 24 | export const timeFormat = (): string => { 25 | let ret 26 | let fmt = 'HH:MM:SS' 27 | const date = new Date() 28 | const opt: {'H+': string, 'M+': string, 'S+': string} = { 29 | 'H+': date.getHours().toString(), 30 | 'M+': date.getMinutes().toString(), 31 | 'S+': date.getSeconds().toString() 32 | } 33 | for (const k in opt) { 34 | ret = new RegExp('(' + k + ')').exec(fmt) 35 | if (ret) { 36 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 37 | // @ts-ignore 38 | fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, '0'))) 39 | } 40 | } 41 | return fmt 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { remote } from 'electron' 3 | import { existsFile } from '@/utils/file' 4 | import fs from 'fs' 5 | import { serverTabType } from '@/store/modules/serverList' 6 | 7 | const genFilePath = async (serverTab: serverTabType): Promise => { 8 | return path.join(remote.app.getPath('home'), `.myRedisClient/.${serverTab.name}_${serverTab.db}.log`) 9 | } 10 | 11 | export const initLogFile = async (serverTab: serverTabType): Promise => { 12 | const storeFilePath = await genFilePath(serverTab) 13 | const isFileExists = await existsFile(storeFilePath) 14 | if (!isFileExists) { 15 | await fs.writeFileSync(storeFilePath, '') 16 | } 17 | } 18 | 19 | export const clearLog = async (serverTab: serverTabType): Promise => { 20 | const storeFilePath = await genFilePath(serverTab) 21 | fs.writeFileSync(storeFilePath, '') 22 | } 23 | 24 | export const setLog = async (serverTab: serverTabType, command: string[], createAt: string): Promise => { 25 | const storeFilePath = await genFilePath(serverTab) 26 | await fs.appendFileSync(storeFilePath, `\r\n${createAt}###${command.map((item: string) => item.includes(' ') ? `'${item}'` : item).join(' ')}`) 27 | } 28 | 29 | export const readLog = async (serverTab: serverTabType): Promise => { 30 | const storeFilePath = await genFilePath(serverTab) 31 | const data = fs.readFileSync(storeFilePath, { encoding: 'utf8', flag: 'r' }) 32 | return data.split('\r\n').filter((item: string) => item && item.length) 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/redis.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | import { serverType } from '@/utils/store' 3 | import { serverTabType } from '@/store/modules/serverList' 4 | 5 | export const getClient = (data: serverType): any => { 6 | if (data.tls?.tlsCaCertFile) { 7 | return createClient({ 8 | password: data.password, 9 | socket: { tls: true, rejectUnauthorized: false, ca: data.tls.tlsCaCertFile, cert: data.tls.tlsCertFile, key: data.tls.tlsKeyFile, host: data.host, port: data.port } 10 | }) 11 | } 12 | return createClient({ url: `redis://:${data.password}@${data.host}:${data.port}` }) 13 | } 14 | 15 | export const getClientWithDB = (data: serverTabType): any => { 16 | const databaseExp = /\d+/.exec(data.db) 17 | const database = Number(databaseExp?.[0]) 18 | if (data.tls?.tlsCaCertFile) { 19 | return createClient({ 20 | password: data.password, 21 | database: database, 22 | socket: { tls: true, rejectUnauthorized: false, ca: data.tls.tlsCaCertFile, cert: data.tls.tlsCertFile, key: data.tls.tlsKeyFile, host: data.host, port: data.port } 23 | }) 24 | } 25 | return createClient({ url: `redis://:${data.password}@${data.host}:${data.port}`, database: database }) 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/store.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { remote } from 'electron' 3 | import { writeFile, readFile, existsFile, mkdirFolder } from '@/utils/file' 4 | import { checkIsJSON } from '@/utils/checkIsJson' 5 | 6 | const storeFolderPath = path.join(remote.app.getPath('home'), '.myRedisClient') 7 | const storeFilePath = path.join(remote.app.getPath('home'), '.myRedisClient/.info') 8 | 9 | export interface tlsType { 10 | tlsCertFilePath: string 11 | tlsKeyFilePath: string 12 | tlsCaCertFilePath: string 13 | tlsCertFile: string 14 | tlsKeyFile: string 15 | tlsCaCertFile: string 16 | } 17 | 18 | export interface serverType { 19 | id: number 20 | name: string 21 | host: string 22 | port: number 23 | password: string 24 | children?: string[] 25 | tls?: tlsType 26 | } 27 | 28 | export const getStore = async (): Promise => { 29 | const result = await readFile(storeFilePath) 30 | if (checkIsJSON(result)) { 31 | return JSON.parse(result) 32 | } else { 33 | return [] 34 | } 35 | } 36 | 37 | export const initStoreFile = async (): Promise => { 38 | const isFolderExists = await existsFile(storeFolderPath) 39 | const isFileExists = await existsFile(storeFilePath) 40 | if (!isFolderExists) { 41 | await mkdirFolder(storeFolderPath) 42 | await writeFile(storeFilePath, JSON.stringify([])) 43 | } else if (isFolderExists && !isFileExists) { 44 | await writeFile(storeFilePath, JSON.stringify([])) 45 | } 46 | } 47 | 48 | export const setStore = async (content: serverType[]): Promise => { 49 | await writeFile(storeFilePath, JSON.stringify(content)) 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/switchColor.ts: -------------------------------------------------------------------------------- 1 | export const SwitchColor = (type: string): string => { 2 | let color: string 3 | switch (type) { 4 | case 'edit': 5 | color = '#E6A23C' 6 | break 7 | case 'add': 8 | color = '#409EFF' 9 | break 10 | default: 11 | color = '' 12 | } 13 | return color 14 | } 15 | 16 | export const SwitchColorWithRepeat = (isRepeat: boolean, type: string): string => { 17 | if (isRepeat) return '#F56C6C' 18 | return SwitchColor(type) 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | const html = document.documentElement 2 | 3 | const configLight = (): void => { 4 | html.classList.remove('dark') 5 | } 6 | const configDark = (): void => { 7 | html.classList.add('dark') 8 | } 9 | const configAuto = (): void => { 10 | const currentHour = new Date().getHours() 11 | if (currentHour < 8 || currentHour > 20) { 12 | configDark() 13 | } else { 14 | configLight() 15 | } 16 | } 17 | 18 | export const handleThemeChange = (theme: number): void => { 19 | if (theme === 1) { 20 | configLight() 21 | } else if (theme === 2) { 22 | configDark() 23 | } else { 24 | configAuto() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/views/Home/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /src/views/config/about.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /src/views/config/content.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /src/views/config/general.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /src/views/config/hotKey.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 22 | 23 | 32 | -------------------------------------------------------------------------------- /src/views/config/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /src/views/config/keyMenuFilterSymbol.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /src/views/config/keyMenuStatus.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | -------------------------------------------------------------------------------- /src/views/config/language.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | -------------------------------------------------------------------------------- /src/views/config/theme.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /src/views/config/videoShow.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | -------------------------------------------------------------------------------- /src/views/consolePane/commandMode.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 112 | 113 | 145 | -------------------------------------------------------------------------------- /src/views/consolePane/doubleRowTable.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /src/views/consolePane/history.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 32 | -------------------------------------------------------------------------------- /src/views/consolePane/index.ts: -------------------------------------------------------------------------------- 1 | export interface historyCommandType { 2 | command: string 3 | result: string 4 | resultData: string 5 | createdAt: string 6 | } 7 | 8 | export interface outputPaneType { 9 | command: string 10 | result: any 11 | } 12 | 13 | export interface outputPaneResultType { 14 | value: string 15 | } 16 | 17 | export interface outputPaneResultDoubleRowType { 18 | field: string 19 | value: string 20 | } 21 | 22 | export interface commandHistoryItemType { 23 | command: string 24 | results: string[] 25 | createAt: string 26 | } 27 | -------------------------------------------------------------------------------- /src/views/consolePane/index.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 229 | 230 | 235 | -------------------------------------------------------------------------------- /src/views/consolePane/result.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 60 | -------------------------------------------------------------------------------- /src/views/consolePane/singleRowTable.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | -------------------------------------------------------------------------------- /src/views/consolePane/stringTable.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/views/historyLog/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 56 | 57 | 68 | -------------------------------------------------------------------------------- /src/views/keyTab/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 162 | 163 | 168 | -------------------------------------------------------------------------------- /src/views/monitor/chart.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 97 | 98 | 105 | -------------------------------------------------------------------------------- /src/views/monitor/chartWithTwoLine.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 110 | -------------------------------------------------------------------------------- /src/views/monitor/clientCount.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 77 | 78 | 87 | -------------------------------------------------------------------------------- /src/views/monitor/cpu.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 81 | 82 | 91 | -------------------------------------------------------------------------------- /src/views/monitor/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 32 | -------------------------------------------------------------------------------- /src/views/monitor/memory.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 73 | 74 | 83 | -------------------------------------------------------------------------------- /src/views/newKeyValue/index.ts: -------------------------------------------------------------------------------- 1 | export const selectOptions: { label: string, value: string }[] = [ 2 | { 3 | label: 'string', 4 | value: 'string' 5 | }, 6 | { 7 | label: 'hash', 8 | value: 'hash' 9 | }, 10 | { 11 | label: 'list', 12 | value: 'list' 13 | }, 14 | { 15 | label: 'set', 16 | value: 'set' 17 | }, 18 | { 19 | label: 'zset', 20 | value: 'zset' 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /src/views/newKeyValue/index.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 129 | -------------------------------------------------------------------------------- /src/views/newKeyValue/listType.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 178 | -------------------------------------------------------------------------------- /src/views/newKeyValue/setType.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 170 | -------------------------------------------------------------------------------- /src/views/newKeyValue/stringType.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 128 | -------------------------------------------------------------------------------- /src/views/newKeyValue/topTab.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 57 | 58 | 63 | -------------------------------------------------------------------------------- /src/views/newKeyValue/zsetType.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 200 | -------------------------------------------------------------------------------- /src/views/pubAndSub/index.ts: -------------------------------------------------------------------------------- 1 | export interface subMessageType { 2 | isSend: boolean // 是否用户发发送的数据 3 | message: string 4 | time: string 5 | } 6 | 7 | export interface subItemType { 8 | label: string 9 | isSub: boolean 10 | messages: subMessageType[] 11 | } 12 | -------------------------------------------------------------------------------- /src/views/pubAndSub/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /src/views/pubAndSub/publishCom.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 90 | -------------------------------------------------------------------------------- /src/views/pubAndSub/subChannelCard.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 62 | 63 | 70 | -------------------------------------------------------------------------------- /src/views/pubAndSub/subContent.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 78 | 79 | 98 | -------------------------------------------------------------------------------- /src/views/pubAndSub/subscribeCom.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 116 | 117 | 131 | -------------------------------------------------------------------------------- /src/views/serverMenu/addForm.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 210 | 211 | 217 | -------------------------------------------------------------------------------- /src/views/serverMenu/editForm.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 209 | 210 | 216 | -------------------------------------------------------------------------------- /src/views/serverMenu/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunter-ji/RedisFish/846de215215fc24320ee98be762c29d89a2c7c47/src/views/serverMenu/index.ts -------------------------------------------------------------------------------- /src/views/serverMenu/index.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 206 | 207 | 228 | -------------------------------------------------------------------------------- /src/views/serverTab/emptyPage.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 50 | -------------------------------------------------------------------------------- /src/views/serverTab/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 56 | -------------------------------------------------------------------------------- /src/views/valueContent/index.ts: -------------------------------------------------------------------------------- 1 | export interface keyMenuType { 2 | label: string 3 | value: number 4 | type: string 5 | } 6 | 7 | export interface keyMenuWithGroupChildType { 8 | label: string 9 | value: number 10 | isKey: boolean 11 | } 12 | 13 | export interface keyMenuWithGroupType { 14 | label: string 15 | value: number 16 | count: number 17 | isKey: boolean 18 | children: keyMenuWithGroupChildType[] 19 | } 20 | 21 | export interface commandObjectType { 22 | command: string[] 23 | result?: string 24 | } 25 | 26 | export interface hashTableValueType { 27 | id: number 28 | field: string 29 | oldField: string 30 | value: string 31 | oldValue: string 32 | type: string 33 | isRepeat: boolean 34 | } 35 | 36 | export interface listTableValueType { 37 | id: number 38 | value: string 39 | oldValue: string 40 | type: string 41 | isFront: boolean 42 | } 43 | 44 | export interface setTableValueType { 45 | id: number 46 | value: string 47 | oldValue: string 48 | type: string 49 | isRepeat: boolean 50 | } 51 | 52 | export interface zsetTableValueType { 53 | id: number 54 | field: string 55 | oldField: string 56 | value: string 57 | oldValue: string 58 | type: string 59 | isRepeat: boolean 60 | } 61 | 62 | export const stringTypeSelectOptions: { label: string, value: string }[] = [ 63 | { 64 | label: 'text', 65 | value: 'text' 66 | }, 67 | { 68 | label: 'json', 69 | value: 'json' 70 | } 71 | ] 72 | -------------------------------------------------------------------------------- /src/views/valueContent/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 140 | -------------------------------------------------------------------------------- /src/views/valueContent/stringType.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 188 | -------------------------------------------------------------------------------- /src/views/valueContent/topTab.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | 42 | -------------------------------------------------------------------------------- /src/views/videoTutorial/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | 29 | 43 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {} 6 | }, 7 | variants: { 8 | extend: {} 9 | }, 10 | plugins: [] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "resolveJsonModule": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const Components = require('unplugin-vue-components/webpack') 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const { ElementPlusResolver } = require('unplugin-vue-components/resolvers') 5 | 6 | module.exports = { 7 | productionSourceMap: false, 8 | configureWebpack: { 9 | plugins: [ 10 | Components({ 11 | resolvers: [ElementPlusResolver()] 12 | }) 13 | ] 14 | }, 15 | pluginOptions: { 16 | electronBuilder: { 17 | nodeIntegration: true, 18 | disableMainProcessTypescript: false, 19 | mainProcessTypeChecking: false, 20 | productName: 'RedisFish', 21 | appId: 'com.kuari.RedisFish', 22 | copyright: '© Kuari 2022', 23 | afterSign: 'build/notarize.js', 24 | publish: [ 25 | { 26 | provider: 'github', 27 | owner: 'Kuari', 28 | repo: 'RedisFish', 29 | releaseType: 'draft' 30 | } 31 | ], 32 | nsis: { 33 | oneClick: false, 34 | allowElevation: true, 35 | allowToChangeInstallationDirectory: true, 36 | installerIcon: 'build/icon.ico', 37 | uninstallerIcon: 'build/icon.ico', 38 | installerHeaderIcon: 'build/icon.ico', 39 | createDesktopShortcut: true, 40 | createStartMenuShortcut: true, 41 | shortcutName: 'RedisFish' 42 | }, 43 | dmg: { 44 | contents: [ 45 | { 46 | x: 410, 47 | y: 150, 48 | type: 'link', 49 | path: '/Applications' 50 | }, 51 | { 52 | x: 130, 53 | y: 150, 54 | type: 'file' 55 | } 56 | ] 57 | }, 58 | mas: { 59 | icon: 'build/icon.icns', 60 | hardenedRuntime: true, 61 | entitlements: 'electron-builder/entitlements.mas.plist', 62 | entitlementsInherit: 'electron-builder/entitlements.mas.plist' 63 | }, 64 | mac: { 65 | target: [ 66 | { 67 | target: 'dmg', 68 | arch: 'universal' 69 | }, 70 | { 71 | target: 'zip', 72 | arch: 'universal' 73 | } 74 | ], 75 | icon: 'build/icon.icns', 76 | hardenedRuntime: true, 77 | entitlements: 'electron-builder/entitlements.plist', 78 | entitlementsInherit: 'electron-builder/entitlements.plist', 79 | provisioningProfile: 'electron-builder/comalibabaslobs.provisionprofile' 80 | }, 81 | win: { 82 | icon: 'build/icon.ico', 83 | target: [ 84 | { 85 | target: 'nsis' 86 | } 87 | ] 88 | }, 89 | linux: { 90 | icon: 'build/icon.ico' 91 | } 92 | }, 93 | i18n: { 94 | locale: 'en', 95 | fallbackLocale: 'en', 96 | localeDir: 'locales', 97 | enableLegacy: false, 98 | runtimeOnly: false, 99 | compositionOnly: false, 100 | fullInstall: true 101 | } 102 | } 103 | } 104 | --------------------------------------------------------------------------------