mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
feat: redis支持zset、redis数据操作界面优化
This commit is contained in:
@@ -23,13 +23,13 @@
|
||||
"monaco-sql-languages": "^0.11.0",
|
||||
"monaco-themes": "^0.4.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.33",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.13.0",
|
||||
"sql-formatter": "^12.1.2",
|
||||
"vue": "^3.2.47",
|
||||
"vue-clipboard3": "^1.0.1",
|
||||
"vue-router": "^4.1.6",
|
||||
"pinia": "^2.0.33",
|
||||
"xterm": "^5.1.0",
|
||||
"xterm-addon-fit": "^0.7.0"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.4.1'
|
||||
version: 'v1.4.2'
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="monaco-editor" style="border: 1px solid #ccc;">
|
||||
<div ref="monacoTextarea" :style="{ height: height }"></div>
|
||||
<div class="monaco-editor-content" ref="monacoTextarea" :style="{ height: height }"></div>
|
||||
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
|
||||
<el-option v-for="mode in languages" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
|
||||
</el-select>
|
||||
|
||||
@@ -244,6 +244,7 @@ export async function initRouter() {
|
||||
}
|
||||
|
||||
let SysWs: any;
|
||||
let loadRouter = false;
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
@@ -283,8 +284,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
if (!SysWs && to.path != '/machine/terminal') {
|
||||
SysWs = sockets.sysMsgSocket();
|
||||
}
|
||||
if (useRoutesList().routesList.length == 0) {
|
||||
// 不存在路由(避免刷新页面找不到路由)并且未加载过(避免token过期,导致获取权限接口报权限不足,无限获取),则重新初始化路由
|
||||
if (useRoutesList().routesList.length == 0 && !loadRouter) {
|
||||
await initRouter();
|
||||
loadRouter = true;
|
||||
next({ path: to.path, query: to.query });
|
||||
} else {
|
||||
next();
|
||||
|
||||
@@ -328,6 +328,10 @@ body,
|
||||
float: left;
|
||||
}
|
||||
|
||||
.fr {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 3px;
|
||||
|
||||
@@ -46,26 +46,16 @@
|
||||
<el-form-item>
|
||||
<el-button @click="searchKey()" type="success" icon="search" plain></el-button>
|
||||
<el-button @click="scan()" icon="bottom" plain>scan</el-button>
|
||||
<el-popover placement="right" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="primary" icon="plus" plain v-auth="'redis:data:save'"></el-button>
|
||||
</template>
|
||||
<el-tag @click="onAddData('string')" :color="getTypeColor('string')"
|
||||
style="cursor: pointer">string</el-tag>
|
||||
<el-tag @click="onAddData('hash')" :color="getTypeColor('hash')" class="ml5"
|
||||
style="cursor: pointer">hash</el-tag>
|
||||
<el-tag @click="onAddData('set')" :color="getTypeColor('set')" class="ml5"
|
||||
style="cursor: pointer">set</el-tag>
|
||||
<!-- <el-tag @click="onAddData('list')" :color="getTypeColor('list')" class="ml5" style="cursor: pointer">list</el-tag> -->
|
||||
</el-popover>
|
||||
<el-button @click="showNewKeyDialog" type="primary" icon="plus" plain
|
||||
v-auth="'redis:data:save'"></el-button>
|
||||
</el-form-item>
|
||||
<div style="float: right">
|
||||
<span>keys: {{ state.dbsize }}</span>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-table v-loading="state.loading" :data="state.keys" :height="tableHeight" stripe :highlight-current-row="true"
|
||||
style="cursor: pointer">
|
||||
<el-table v-loading="state.loading" :data="state.keys" :height="tableHeight" stripe
|
||||
:highlight-current-row="true" style="cursor: pointer">
|
||||
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
|
||||
<el-table-column prop="type" label="type" width="80">
|
||||
<template #default="scope">
|
||||
@@ -79,7 +69,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button @click="getValue(scope.row)" type="success" icon="search" plain size="small">查看
|
||||
<el-button @click="showKeyDetail(scope.row)" type="success" icon="search" plain
|
||||
size="small">查看
|
||||
</el-button>
|
||||
<el-button v-auth="'redis:data:del'" @click="del(scope.row.key)" type="danger" icon="delete"
|
||||
plain size="small">删除
|
||||
@@ -93,21 +84,37 @@
|
||||
|
||||
<div style="text-align: center; margin-top: 10px"></div>
|
||||
|
||||
<hash-value v-model:visible="hashValueDialog.visible" :operationType="dataEdit.operationType"
|
||||
:title="dataEdit.title" :keyInfo="dataEdit.keyInfo" :redisId="scanParam.id" :db="scanParam.db"
|
||||
@cancel="onCancelDataEdit" @valChange="searchKey" />
|
||||
<el-dialog title="Key详情" v-model="keyDetailDialog.visible" width="800px" :destroy-on-close="true"
|
||||
:close-on-click-modal="false">
|
||||
<key-detail :redisId="scanParam.id" :db="scanParam.db" :key-info="keyDetailDialog.keyInfo"
|
||||
@change-key="searchKey()" />
|
||||
</el-dialog>
|
||||
|
||||
<string-value v-model:visible="stringValueDialog.visible" :operationType="dataEdit.operationType"
|
||||
:title="dataEdit.title" :keyInfo="dataEdit.keyInfo" :redisId="scanParam.id" :db="scanParam.db"
|
||||
@cancel="onCancelDataEdit" @valChange="searchKey" />
|
||||
<el-dialog title="新增Key" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true"
|
||||
:close-on-click-modal="false">
|
||||
<el-form ref="keyForm" label-width="50px">
|
||||
<el-form-item prop="key" label="键名">
|
||||
<el-input v-model.trim="keyDetailDialog.keyInfo.key" placeholder="请输入键名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="type" label="类型">
|
||||
<el-select v-model="keyDetailDialog.keyInfo.type" default-first-option style="width: 100%"
|
||||
placeholder="请选择类型">
|
||||
<el-option key="string" label="string" value="string"></el-option>
|
||||
<el-option key="hash" label="hash" value="hash"></el-option>
|
||||
<el-option key="set" label="set" value="set"></el-option>
|
||||
<el-option key="zset" label="zset" value="zset"></el-option>
|
||||
<el-option key="list" label="list" value="list"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<set-value v-model:visible="setValueDialog.visible" :title="dataEdit.title" :keyInfo="dataEdit.keyInfo"
|
||||
:redisId="scanParam.id" :db="scanParam.db" :operationType="dataEdit.operationType" @valChange="searchKey"
|
||||
@cancel="onCancelDataEdit" />
|
||||
|
||||
<list-value v-model:visible="listValueDialog.visible" :title="dataEdit.title" :keyInfo="dataEdit.keyInfo"
|
||||
:redisId="scanParam.id" :db="scanParam.db" :operationType="dataEdit.operationType" @valChange="searchKey"
|
||||
@cancel="onCancelDataEdit" />
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancelNewKey()">取 消</el-button>
|
||||
<el-button v-auth="'machine:script:save'" type="primary" @click="newKey">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -119,10 +126,7 @@ import { isTrue, notBlank, notNull } from '@/common/assert';
|
||||
import { TagTreeNode } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
|
||||
const HashValue = defineAsyncComponent(() => import('./HashValue.vue'));
|
||||
const StringValue = defineAsyncComponent(() => import('./StringValue.vue'));
|
||||
const SetValue = defineAsyncComponent(() => import('./SetValue.vue'));
|
||||
const ListValue = defineAsyncComponent(() => import('./ListValue.vue'));
|
||||
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
|
||||
|
||||
/**
|
||||
* 树节点类型
|
||||
@@ -144,31 +148,20 @@ const state = reactive({
|
||||
scanParam: {
|
||||
id: null as any,
|
||||
mode: '',
|
||||
db: '',
|
||||
db: 0,
|
||||
match: null,
|
||||
count: 10,
|
||||
cursor: {},
|
||||
},
|
||||
dataEdit: {
|
||||
keyDetailDialog: {
|
||||
visible: false,
|
||||
title: '新增数据',
|
||||
operationType: 1,
|
||||
keyInfo: {
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
key: '',
|
||||
},
|
||||
},
|
||||
hashValueDialog: {
|
||||
visible: false,
|
||||
},
|
||||
stringValueDialog: {
|
||||
visible: false,
|
||||
},
|
||||
setValueDialog: {
|
||||
visible: false,
|
||||
},
|
||||
listValueDialog: {
|
||||
newKeyDialog: {
|
||||
visible: false,
|
||||
},
|
||||
keys: [],
|
||||
@@ -178,11 +171,8 @@ const state = reactive({
|
||||
const {
|
||||
tableHeight,
|
||||
scanParam,
|
||||
dataEdit,
|
||||
hashValueDialog,
|
||||
stringValueDialog,
|
||||
setValueDialog,
|
||||
listValueDialog,
|
||||
keyDetailDialog,
|
||||
newKeyDialog,
|
||||
} = toRefs(state)
|
||||
|
||||
|
||||
@@ -339,50 +329,56 @@ const resetScanParam = () => {
|
||||
state.scanParam.cursor = {};
|
||||
};
|
||||
|
||||
const getValue = async (row: any) => {
|
||||
const showKeyDetail = async (row: any) => {
|
||||
const type = row.type;
|
||||
|
||||
state.dataEdit.keyInfo.type = type;
|
||||
state.dataEdit.keyInfo.timed = row.ttl;
|
||||
state.dataEdit.keyInfo.key = row.key;
|
||||
state.dataEdit.operationType = 2;
|
||||
state.dataEdit.title = '查看数据';
|
||||
|
||||
if (type == 'hash') {
|
||||
state.hashValueDialog.visible = true;
|
||||
} else if (type == 'string') {
|
||||
state.stringValueDialog.visible = true;
|
||||
} else if (type == 'set') {
|
||||
state.setValueDialog.visible = true;
|
||||
} else if (type == 'list') {
|
||||
state.listValueDialog.visible = true;
|
||||
} else {
|
||||
ElMessage.warning('暂不支持该类型');
|
||||
}
|
||||
state.keyDetailDialog.keyInfo.type = type;
|
||||
state.keyDetailDialog.keyInfo.timed = row.ttl;
|
||||
state.keyDetailDialog.keyInfo.key = row.key;
|
||||
state.keyDetailDialog.visible = true;
|
||||
};
|
||||
|
||||
const onAddData = (type: string) => {
|
||||
const closeKeyDetail = () => {
|
||||
|
||||
// state.keyDetailDialog.visible = false;
|
||||
}
|
||||
|
||||
const showNewKeyDialog = () => {
|
||||
notNull(state.scanParam.id, '请先选择redis');
|
||||
state.dataEdit.operationType = 1;
|
||||
state.dataEdit.title = '新增数据';
|
||||
state.dataEdit.keyInfo.type = type;
|
||||
state.dataEdit.keyInfo.timed = -1;
|
||||
if (type == 'hash') {
|
||||
state.hashValueDialog.visible = true;
|
||||
} else if (type == 'string') {
|
||||
state.stringValueDialog.visible = true;
|
||||
} else if (type == 'set') {
|
||||
state.setValueDialog.visible = true;
|
||||
} else if (type == 'list') {
|
||||
state.listValueDialog.visible = true;
|
||||
} else {
|
||||
ElMessage.warning('暂不支持该类型');
|
||||
}
|
||||
};
|
||||
notNull(state.scanParam.db, "请选择要操作的库")
|
||||
resetKeyDetailInfo();
|
||||
state.newKeyDialog.visible = true;
|
||||
}
|
||||
|
||||
const onCancelDataEdit = () => {
|
||||
state.dataEdit.keyInfo = {} as any;
|
||||
};
|
||||
const cancelNewKey = () => {
|
||||
resetKeyDetailInfo();
|
||||
state.newKeyDialog.visible = false;
|
||||
}
|
||||
|
||||
const newKey = async () => {
|
||||
const keyInfo = state.keyDetailDialog.keyInfo
|
||||
const keyType = keyInfo.type
|
||||
const key = keyInfo.key;
|
||||
notBlank(key, "键名不能为空");
|
||||
|
||||
if (keyType == 'string') {
|
||||
await redisApi.setString.request({
|
||||
id: state.scanParam.id,
|
||||
db: state.scanParam.db,
|
||||
key: key,
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
state.newKeyDialog.visible = false;
|
||||
state.keyDetailDialog.visible = true;
|
||||
searchKey();
|
||||
}
|
||||
|
||||
const resetKeyDetailInfo = () => {
|
||||
state.keyDetailDialog.keyInfo.key = '';
|
||||
state.keyDetailDialog.keyInfo.type = 'string';
|
||||
state.keyDetailDialog.keyInfo.timed = -1;
|
||||
}
|
||||
|
||||
const del = (key: string) => {
|
||||
ElMessageBox.confirm(`确定删除[ ${key} ] 该key?`, '提示', {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<div style="width: 100%;">
|
||||
<el-input @input="onInput" type="textarea" v-model="modelValue" :autosize="autosize" :rows="rows" />
|
||||
<div style="padding: 3px; float: right" class="mr5 format-btns">
|
||||
<div>
|
||||
<el-button @click="showFormatDialog()" :underline="false" type="success" icon="MagicStick" size="small">
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog @opened="opened" width="60%" :title="title" v-model="formatDialog.visible"
|
||||
:close-on-click-modal="false">
|
||||
<monaco-editor ref="monacoEditorRef" :canChangeMode="true" v-model="formatDialog.value" language="json" />
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="formatDialog.visible = false">取 消</el-button>
|
||||
<el-button @click="onConfirmValue" type="primary">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch, toRefs, onMounted } from 'vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
},
|
||||
autosize: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const monacoEditorRef: any = ref(null)
|
||||
|
||||
const state = reactive({
|
||||
rows: 2,
|
||||
autosize: {},
|
||||
modelValue: '',
|
||||
formatDialog: {
|
||||
visible: false,
|
||||
value: '',
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
rows,
|
||||
autosize,
|
||||
modelValue,
|
||||
formatDialog,
|
||||
} = toRefs(state)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: any) => {
|
||||
state.modelValue = val;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
state.modelValue = props.modelValue as any;
|
||||
state.autosize = props.autosize as any;
|
||||
state.rows = props.rows as any;
|
||||
})
|
||||
|
||||
const showFormatDialog = () => {
|
||||
state.formatDialog.visible = true;
|
||||
state.formatDialog.value = state.modelValue;
|
||||
}
|
||||
|
||||
const opened = () => {
|
||||
monacoEditorRef.value.format();
|
||||
};
|
||||
|
||||
const onConfirmValue = () => {
|
||||
// 尝试压缩json
|
||||
try {
|
||||
state.modelValue = JSON.stringify(JSON.parse(state.formatDialog.value));
|
||||
} catch (e) {
|
||||
state.modelValue = state.formatDialog.value;
|
||||
}
|
||||
emit('update:modelValue', state.modelValue);
|
||||
state.formatDialog.visible = false;
|
||||
}
|
||||
|
||||
const onInput = (value: any) => {
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.format-btns {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 5px;
|
||||
top: 4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
</style>
|
||||
104
mayfly_go_web/src/views/ops/redis/FormatViewer.vue
Normal file
104
mayfly_go_web/src/views/ops/redis/FormatViewer.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="format-viewer-container">
|
||||
<div class="mb5 fr">
|
||||
<el-select v-model="selectedView" class='format-selector' size='mini' placeholder='Text'>
|
||||
<template #prefix>
|
||||
<SvgIcon name="view" />
|
||||
</template>
|
||||
<el-option v-for="item of Object.keys(viewers)" :key="item" :label="item" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<component ref='viewerRef' :is='components[viewerComponent]' :content='state.content' :name="selectedView">
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, shallowReactive, watch, toRefs, onMounted } from 'vue';
|
||||
import ViewerText from './ViewerText.vue';
|
||||
import ViewerJson from './ViewerJson.vue';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
|
||||
const components = shallowReactive({
|
||||
ViewerText, ViewerJson
|
||||
})
|
||||
const viewerRef: any = ref(null)
|
||||
|
||||
const state = reactive({
|
||||
content: '',
|
||||
selectedView: 'Text',
|
||||
});
|
||||
|
||||
const viewers = {
|
||||
"Text": {
|
||||
value: 'ViewerText',
|
||||
},
|
||||
|
||||
"Json": {
|
||||
value: 'ViewerJson',
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
selectedView,
|
||||
} = toRefs(state)
|
||||
|
||||
const viewerComponent = computed(() => {
|
||||
return viewers[state.selectedView].value;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
(val: any) => {
|
||||
state.content = val;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
state.content = props.content as any;
|
||||
})
|
||||
|
||||
const getContent = () => {
|
||||
return viewerRef.value.getContent();
|
||||
}
|
||||
|
||||
defineExpose({ getContent })
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.format-selector {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.format-selector .el-input__inner {
|
||||
height: 22px !important;
|
||||
}
|
||||
|
||||
/*outline same with text viewer's .el-textarea__inner*/
|
||||
.format-viewer-container .text-formated-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
clear: both
|
||||
}
|
||||
|
||||
.format-viewer-container .formater-binary-tag {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
// 默认文本框样式
|
||||
.format-viewer-container .el-textarea textarea {
|
||||
font-size: 14px;
|
||||
height: calc(100vh - 536px);
|
||||
}
|
||||
|
||||
.format-viewer-container .monaco-editor-content {
|
||||
height: calc(100vh - 550px) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,266 +0,0 @@
|
||||
<template>
|
||||
<el-dialog class="el-table-z-index-inherit" :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
|
||||
<el-form label-width="85px">
|
||||
<el-form-item prop="key" label="key:">
|
||||
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="timed" label="过期时间:">
|
||||
<el-input v-model.number="key.timed" type="number"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dataType" label="数据类型:">
|
||||
<el-input v-model="key.type" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-row class="mt10">
|
||||
<el-form label-position="right" :inline="true">
|
||||
<el-form-item label="field" label-width="40px" v-if="operationType == 2">
|
||||
<el-input placeholder="支持*模糊field" style="width: 140px" v-model="scanParam.match" clearable
|
||||
size="small"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="count" v-if="operationType == 2">
|
||||
<el-input placeholder="count" style="width: 62px" v-model.number="scanParam.count" size="small">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="operationType == 2" @click="reHscan()" type="success" icon="search" plain
|
||||
size="small"></el-button>
|
||||
<el-button v-if="operationType == 2" @click="hscan()" icon="bottom" plain size="small">scan
|
||||
</el-button>
|
||||
<el-button @click="onAddHashValue" icon="plus" size="small" plain>添加</el-button>
|
||||
</el-form-item>
|
||||
<div v-if="operationType == 2" class="mt10" style="float: right">
|
||||
<span>fieldSize: {{ keySize }}</span>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-table :data="hashValues" stripe style="width: 100%;">
|
||||
<el-table-column prop="field" label="field" width>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.field" clearable size="small"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="value" label="value" min-width="200">
|
||||
<template #default="scope">
|
||||
<format-input :title="`type:【${key.type}】key:【${key.key}】field:【${scope.row.field}】`" v-model="scope.row.value"
|
||||
:autosize="{ minRows: 2, maxRows: 10 }" size="small"></format-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button v-if="operationType == 2" type="success" @click="hset(scope.row)" icon="check"
|
||||
size="small" plain></el-button>
|
||||
<el-button v-auth="'redis:data:del'" type="danger" @click="hdel(scope.row.field, scope.$index)" icon="delete" size="small"
|
||||
plain></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<template #footer v-if="operationType == 1">
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { isTrue, notEmpty } from '@/common/assert';
|
||||
import FormatInput from './FormatInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
// 操作类型,1:新增,2:修改
|
||||
operationType: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
db: {
|
||||
type: [String],
|
||||
require: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
hashValue: {
|
||||
type: [Array, Object],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'valChange'])
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
operationType: 1,
|
||||
redisId: 0,
|
||||
db: '0',
|
||||
key: {
|
||||
key: '',
|
||||
type: 'hash',
|
||||
timed: -1,
|
||||
},
|
||||
scanParam: {
|
||||
key: '',
|
||||
id: 0,
|
||||
db: '0',
|
||||
cursor: 0,
|
||||
match: '',
|
||||
count: 10,
|
||||
},
|
||||
keySize: 0,
|
||||
hashValues: [
|
||||
{
|
||||
field: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const {
|
||||
dialogVisible,
|
||||
operationType,
|
||||
key,
|
||||
scanParam,
|
||||
keySize,
|
||||
hashValues,
|
||||
} = toRefs(state)
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
setTimeout(() => {
|
||||
state.hashValues = [];
|
||||
state.key = {} as any;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
const visible = newValue.visible;
|
||||
state.redisId = newValue.redisId;
|
||||
state.db = newValue.db;
|
||||
state.key = newValue.keyInfo;
|
||||
state.operationType = newValue.operationType;
|
||||
|
||||
if (visible && state.operationType == 2) {
|
||||
state.scanParam.id = props.redisId as any;
|
||||
state.scanParam.key = state.key.key;
|
||||
await reHscan();
|
||||
}
|
||||
|
||||
state.dialogVisible = visible;
|
||||
});
|
||||
|
||||
const reHscan = async () => {
|
||||
state.scanParam.id = state.redisId;
|
||||
state.scanParam.db = state.db;
|
||||
state.scanParam.cursor = 0;
|
||||
hscan();
|
||||
};
|
||||
|
||||
const hscan = async () => {
|
||||
const match = state.scanParam.match;
|
||||
if (!match || match == '' || match == '*') {
|
||||
if (state.scanParam.count > 100) {
|
||||
ElMessage.error('match为空或者*时, count不能超过100');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (state.scanParam.count > 1000) {
|
||||
ElMessage.error('count不能超过1000');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const scanRes = await redisApi.hscan.request(state.scanParam);
|
||||
state.scanParam.cursor = scanRes.cursor;
|
||||
state.keySize = scanRes.keySize;
|
||||
|
||||
const keys = scanRes.keys;
|
||||
const hashValue = [];
|
||||
const fieldCount = keys.length / 2;
|
||||
let nextFieldIndex = 0;
|
||||
for (let i = 0; i < fieldCount; i++) {
|
||||
hashValue.push({ field: keys[nextFieldIndex++], value: keys[nextFieldIndex++] });
|
||||
}
|
||||
state.hashValues = hashValue;
|
||||
};
|
||||
|
||||
const hdel = async (field: any, index: any) => {
|
||||
// 如果是新增操作,则直接数组移除即可
|
||||
if (state.operationType == 1) {
|
||||
state.hashValues.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除[${field}]?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await redisApi.hdel.request({
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key.key,
|
||||
field,
|
||||
});
|
||||
ElMessage.success('删除成功');
|
||||
reHscan();
|
||||
};
|
||||
|
||||
const hset = async (row: any) => {
|
||||
await redisApi.saveHashValue.request({
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key.key,
|
||||
timed: state.key.timed,
|
||||
value: [
|
||||
{
|
||||
field: row.field,
|
||||
value: row.value,
|
||||
},
|
||||
],
|
||||
});
|
||||
ElMessage.success('保存成功');
|
||||
};
|
||||
|
||||
const onAddHashValue = () => {
|
||||
state.hashValues.unshift({ field: '', value: '' });
|
||||
};
|
||||
|
||||
const saveValue = async () => {
|
||||
notEmpty(state.key.key, 'key不能为空');
|
||||
isTrue(state.hashValues.length > 0, 'hash内容不能为空');
|
||||
const sv = { value: state.hashValues, id: state.redisId, db: state.db };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveHashValue.request(sv);
|
||||
ElMessage.success('保存成功');
|
||||
|
||||
cancel();
|
||||
emit('valChange');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
mayfly_go_web/src/views/ops/redis/KeyDetail.vue
Normal file
136
mayfly_go_web/src/views/ops/redis/KeyDetail.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-container direction="vertical" class="key-tab-container">
|
||||
<!-- key info -->
|
||||
<key-header ref="keyHeader" :redis-id="redisId" :db="db" :key-info="keyInfo" @refresh-content="refreshContent"
|
||||
@change-key="changeKey" class="key-header-info">
|
||||
</key-header>
|
||||
|
||||
<!-- key content -->
|
||||
<component ref="keyValueRef" :is="components[componentName]" :redis-id="redisId" :db="db" :key-info="keyInfo">
|
||||
</component>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref, shallowReactive, reactive, computed, toRefs } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import KeyHeader from './KeyHeader.vue'
|
||||
|
||||
const KeyValueString = defineAsyncComponent(() => import('./KeyValueString.vue'));
|
||||
const KeyValueHash = defineAsyncComponent(() => import('./KeyValueHash.vue'));
|
||||
const KeyValueSet = defineAsyncComponent(() => import('./KeyValueSet.vue'));
|
||||
const KeyValueList = defineAsyncComponent(() => import('./KeyValueList.vue'));
|
||||
const KeyValueZset = defineAsyncComponent(() => import('./KeyValueZset.vue'));
|
||||
|
||||
const components = shallowReactive({
|
||||
KeyValueString, KeyValueHash, KeyValueSet, KeyValueList, KeyValueZset
|
||||
})
|
||||
|
||||
const keyValueRef = ref(null) as any
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: Number
|
||||
},
|
||||
db: {
|
||||
type: Number
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'changeKey', 'valChange'])
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
});
|
||||
|
||||
const componentMap = {
|
||||
string: 'KeyValueString',
|
||||
hash: 'KeyValueHash',
|
||||
zset: 'KeyValueZset',
|
||||
set: 'KeyValueSet',
|
||||
list: 'KeyValueList',
|
||||
};
|
||||
|
||||
const componentName = computed(() => {
|
||||
const component = componentMap[props.keyInfo?.type]
|
||||
if (!component) {
|
||||
ElMessage.error("暂不支持该类型")
|
||||
return ''
|
||||
}
|
||||
return component;
|
||||
});
|
||||
|
||||
const refreshContent = () => {
|
||||
keyValueRef.value?.initData();
|
||||
}
|
||||
|
||||
const changeKey = () => {
|
||||
emit('changeKey');
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
} = toRefs(state)
|
||||
|
||||
// watch(
|
||||
// () => props.keyInfo,
|
||||
// (val) => {
|
||||
// state.keyInfo = val;
|
||||
// }
|
||||
// );
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.key-tab-container {
|
||||
/*padding-left: 5px;*/
|
||||
}
|
||||
|
||||
.key-header-info {
|
||||
// margin-top: 15px;
|
||||
}
|
||||
|
||||
.key-content-container {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
// .key-detail-filter-value {
|
||||
// width: 90%;
|
||||
// height: 24px;
|
||||
// padding: 0 5px;
|
||||
// }
|
||||
|
||||
/*tooltip in table width limit*/
|
||||
.el-tooltip__popper {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.content-more-container {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.content-more-container .content-more-btn {
|
||||
width: 95%;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/*data table list styles*/
|
||||
.key-content-container .el-table {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/*table list height*/
|
||||
.key-content-container .el-table .el-table__body td {
|
||||
padding: 0px 0px;
|
||||
}
|
||||
|
||||
/*table list border*/
|
||||
.key-content-container .el-table--border td,
|
||||
.key-content-container .el-table--border th {
|
||||
border-right-width: 0;
|
||||
}
|
||||
</style>
|
||||
200
mayfly_go_web/src/views/ops/redis/KeyHeader.vue
Normal file
200
mayfly_go_web/src/views/ops/redis/KeyHeader.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- key name -->
|
||||
<div class="key-header-item key-name-input">
|
||||
<el-input ref="keyNameInput" v-model="keyInfo.key" title="点击重命名" placeholder="KeyName">
|
||||
<template #prepend>
|
||||
<span class="key-detail-type">{{ keyInfo.type }}</span>
|
||||
</template>
|
||||
|
||||
<template #suffix>
|
||||
<SvgIcon v-auth="'redis:data:save'" @click="renameKey" title="点击重命名" name="check"
|
||||
class="cursor-pointer" />
|
||||
</template>
|
||||
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- key ttl -->
|
||||
<div class="key-header-item key-ttl-input">
|
||||
<el-input type="number" v-model.number="keyInfo.timed" placeholder="单位(秒),负数永久" title="点击修改过期时间">
|
||||
<template #prepend>
|
||||
<span slot="prepend">TTL</span>
|
||||
</template>
|
||||
|
||||
<template #suffix>
|
||||
<!-- save ttl -->
|
||||
<SvgIcon v-auth="'redis:data:save'" @click="ttlKey" title="点击修改过期时间" name="check" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- del & refresh btn -->
|
||||
<div class='key-header-item key-header-btn-con'>
|
||||
<el-button slot="reference" ref='refreshBtn' type="success" @click="refreshKey" icon="refresh"
|
||||
title="刷新"></el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch, toRefs, onMounted } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: Number
|
||||
},
|
||||
db: {
|
||||
type: Number
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['refreshContent', 'changeKey', 'valChange'])
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
keyInfo: {
|
||||
key: '',
|
||||
type: '',
|
||||
timed: -1,
|
||||
} as any,
|
||||
oldKey: '',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
state.keyInfo = props.keyInfo
|
||||
state.oldKey = props.keyInfo?.key
|
||||
})
|
||||
|
||||
const refreshKey = async () => {
|
||||
const ttl = await redisApi.keyTtl.request({
|
||||
id: props.redisId,
|
||||
db: props.db,
|
||||
key: state.oldKey,
|
||||
})
|
||||
state.keyInfo.timed = ttl;
|
||||
emit('refreshContent');
|
||||
}
|
||||
|
||||
const renameKey = async () => {
|
||||
if (!state.oldKey || state.keyInfo.key == state.oldKey) {
|
||||
return;
|
||||
}
|
||||
await redisApi.renameKey.request({
|
||||
id: props.redisId,
|
||||
db: props.db,
|
||||
newKey: state.keyInfo.key,
|
||||
key: state.oldKey
|
||||
});
|
||||
ElMessage.success("设置成功")
|
||||
emit('changeKey');
|
||||
}
|
||||
|
||||
const ttlKey = async () => {
|
||||
if (!state.oldKey) {
|
||||
return;
|
||||
}
|
||||
// ttl <= 0,则持久化该key
|
||||
if (state.keyInfo.timed <= 0) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定持久化该key?',
|
||||
'Warning',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
await persistKey();
|
||||
state.keyInfo.timed = -1;
|
||||
return
|
||||
}
|
||||
|
||||
await redisApi.expireKey.request({
|
||||
id: props.redisId,
|
||||
db: props.db,
|
||||
key: state.keyInfo.key,
|
||||
seconds: state.keyInfo.timed
|
||||
});
|
||||
ElMessage.success("设置成功")
|
||||
emit('changeKey');
|
||||
}
|
||||
|
||||
const persistKey = async () => {
|
||||
await redisApi.persistKey.request({
|
||||
id: props.redisId,
|
||||
db: props.db,
|
||||
key: state.keyInfo.key,
|
||||
});
|
||||
ElMessage.success("设置成功")
|
||||
emit('changeKey');
|
||||
}
|
||||
|
||||
const {
|
||||
keyInfo,
|
||||
oldKey,
|
||||
} = toRefs(state)
|
||||
|
||||
// watch(
|
||||
// () => props.keyInfo,
|
||||
// (val) => {
|
||||
// state.keyInfo = val;
|
||||
// state.keyName = state.keyInfo.key;
|
||||
// }
|
||||
// );
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.key-detail-type {
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
min-width: 34px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.key-header-item {
|
||||
/*padding-right: 15px;*/
|
||||
/*margin-bottom: 10px;*/
|
||||
float: left;
|
||||
}
|
||||
|
||||
.key-header-item.key-name-input {
|
||||
width: calc(100% - 332px);
|
||||
min-width: 220px;
|
||||
max-width: 800px;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.key-header-item.key-ttl-input {
|
||||
width: 220px;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/*hide number input button*/
|
||||
.key-header-item.key-ttl-input input::-webkit-inner-spin-button,
|
||||
.key-header-item.key-ttl-input input::-webkit-outer-spin-button {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.key-header-item.key-header-btn-con .el-button+.el-button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/*refresh btn rotating*/
|
||||
.key-header-info .key-header-btn-con .rotating .el-icon-refresh {
|
||||
animation: rotate 1.5s linear infinite;
|
||||
}
|
||||
</style>
|
||||
241
mayfly_go_web/src/views/ops/redis/KeyValueHash.vue
Normal file
241
mayfly_go_web/src/views/ops/redis/KeyValueHash.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
|
||||
<el-table size="small" border :data="hashValues" min-height=300 stripe>
|
||||
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100">
|
||||
</el-table-column>
|
||||
<el-table-column resizable sortable prop="field" label="field" show-overflow-tooltip min-width="100">
|
||||
</el-table-column>
|
||||
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #header>
|
||||
<el-input class="key-detail-filter-value" v-model="state.filterValue" @keyup.enter='hscan(true, true)'
|
||||
placeholder="输入关键词回车搜索" clearable size="small" />
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit"
|
||||
plain></el-link>
|
||||
<el-popconfirm title="确定删除?" @confirm="hdel(scope.row.field, scope.$index)">
|
||||
<template #reference>
|
||||
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small"
|
||||
plain class="ml5"></el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- load more content -->
|
||||
<div class='content-more-container'>
|
||||
<el-button size='small' @click='hscan()' :disabled='loadMoreDisable' class='content-more-btn'>
|
||||
加载更多
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true"
|
||||
:close-on-click-modal="false">
|
||||
<el-form>
|
||||
<el-form-item>
|
||||
<el-input v-model="editDialog.field" placeholder="field" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.value"></format-viewer>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="editDialog.visible = false">取 消</el-button>
|
||||
<el-button v-auth="'redis:data:save'" type="primary" @click="confirmEditData">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import FormatViewer from './FormatViewer.vue';
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0
|
||||
},
|
||||
db: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
})
|
||||
|
||||
const formatViewerRef = ref(null) as any;
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
db: 0,
|
||||
key: '',
|
||||
scanParam: {
|
||||
cursor: 0,
|
||||
count: 50,
|
||||
},
|
||||
filterValue: '',
|
||||
hashValues: [] as any,
|
||||
total: 0,
|
||||
loadMoreDisable: false,
|
||||
editDialog: {
|
||||
visible: false,
|
||||
field: '',
|
||||
value: '',
|
||||
dataRow: null as any,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
hashValues,
|
||||
total,
|
||||
loadMoreDisable,
|
||||
editDialog,
|
||||
} = toRefs(state)
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
state.redisId = props.redisId;
|
||||
state.db = props.db;
|
||||
state.key = props.keyInfo?.key;
|
||||
initData();
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
state.filterValue = '';
|
||||
hscan(true, true);
|
||||
}
|
||||
|
||||
const getScanMatch = () => {
|
||||
return state.filterValue ? `*${state.filterValue}*` : '*';
|
||||
}
|
||||
|
||||
const hscan = async (resetTableData = false, resetCursor = false) => {
|
||||
if (resetCursor) {
|
||||
state.scanParam.cursor = 0;
|
||||
}
|
||||
|
||||
const scanRes = await redisApi.hscan.request({
|
||||
...getBaseReqParam(),
|
||||
match: getScanMatch(),
|
||||
...state.scanParam
|
||||
});
|
||||
state.scanParam.cursor = scanRes.cursor;
|
||||
state.loadMoreDisable = scanRes.cursor == 0
|
||||
state.total = scanRes.keySize;
|
||||
|
||||
const keys = scanRes.keys;
|
||||
const hashValue = [];
|
||||
const fieldCount = keys.length / 2;
|
||||
let nextFieldIndex = 0;
|
||||
for (let i = 0; i < fieldCount; i++) {
|
||||
hashValue.push({ field: keys[nextFieldIndex++], value: keys[nextFieldIndex++] });
|
||||
}
|
||||
|
||||
if (resetTableData) {
|
||||
state.hashValues = hashValue;
|
||||
} else {
|
||||
state.hashValues.push(...hashValue)
|
||||
}
|
||||
};
|
||||
|
||||
const hdel = async (field: any, index: any) => {
|
||||
await redisApi.hdel.request({
|
||||
...getBaseReqParam(),
|
||||
field,
|
||||
});
|
||||
|
||||
ElMessage.success('删除成功');
|
||||
state.hashValues.splice(index, 1)
|
||||
state.total--;
|
||||
};
|
||||
|
||||
const showEditDialog = (row: any) => {
|
||||
state.editDialog.dataRow = row;
|
||||
state.editDialog.field = row ? row.field : '';
|
||||
state.editDialog.value = row ? row.value : '';
|
||||
state.editDialog.visible = true;
|
||||
}
|
||||
|
||||
const confirmEditData = async () => {
|
||||
const param = getBaseReqParam();
|
||||
|
||||
const field = state.editDialog.field;
|
||||
notBlank(field, "field不能为空");
|
||||
|
||||
// 存在数据行,则说明为修改,则要先删除旧数据后新增
|
||||
const dataRow = state.editDialog.dataRow
|
||||
if (dataRow) {
|
||||
await redisApi.hdel.request({
|
||||
...param,
|
||||
field: dataRow.field,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取hash value内容并新增
|
||||
const value = formatViewerRef.value.getContent()
|
||||
const res = await redisApi.hset.request({
|
||||
...param,
|
||||
value: [
|
||||
{
|
||||
field,
|
||||
value: value,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
ElMessage.success("保存成功");
|
||||
if (dataRow) {
|
||||
state.editDialog.dataRow.value = value;
|
||||
state.editDialog.dataRow.field = field;
|
||||
} else {
|
||||
// 响应0则为被覆盖,则重新scan
|
||||
if (res == 0) {
|
||||
hscan(true, true);
|
||||
} else {
|
||||
state.hashValues.unshift({ value, field });
|
||||
state.total++;
|
||||
}
|
||||
}
|
||||
state.editDialog.visible = false;
|
||||
state.editDialog.dataRow = null;
|
||||
}
|
||||
|
||||
const getBaseReqParam = () => {
|
||||
return {
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ initData })
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
199
mayfly_go_web/src/views/ops/redis/KeyValueList.vue
Normal file
199
mayfly_go_web/src/views/ops/redis/KeyValueList.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
|
||||
<el-table size="small" border :data="values" min-height=300 stripe>
|
||||
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100">
|
||||
</el-table-column>
|
||||
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit"
|
||||
plain></el-link>
|
||||
<el-popconfirm title="确定删除?" @confirm="lrem(scope.row, scope.$index)">
|
||||
<template #reference>
|
||||
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small"
|
||||
plain class="ml5"></el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- load more content -->
|
||||
<div class='content-more-container'>
|
||||
<el-button size='small' @click='getListValue(false)' :disabled='loadMoreDisable' class='content-more-btn'>
|
||||
加载更多
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true"
|
||||
:close-on-click-modal="false">
|
||||
<el-form>
|
||||
<el-form-item>
|
||||
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="editDialog.visible = false">取 消</el-button>
|
||||
<el-button v-auth="'redis:data:save'" type="primary" @click="confirmEditData">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, toRefs, onMounted } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import FormatViewer from './FormatViewer.vue';
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0,
|
||||
},
|
||||
db: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
})
|
||||
|
||||
const formatViewerRef = ref(null) as any;
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
db: 0,
|
||||
key: '',
|
||||
pageNum: 1,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
values: [] as any,
|
||||
loadMoreDisable: false,
|
||||
editDialog: {
|
||||
visible: false,
|
||||
content: '',
|
||||
dataRow: null as any,
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
total,
|
||||
values,
|
||||
loadMoreDisable,
|
||||
editDialog,
|
||||
} = toRefs(state)
|
||||
|
||||
onMounted(() => {
|
||||
state.redisId = props.redisId;
|
||||
state.db = props.db;
|
||||
state.key = props.keyInfo?.key;
|
||||
initData();
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
state.pageNum = 1;
|
||||
getListValue(true);
|
||||
}
|
||||
|
||||
const getListValue = async (resetTableData = false) => {
|
||||
const pageNum = state.pageNum;
|
||||
const pageSize = state.pageSize;
|
||||
const res = await redisApi.getListValue.request({
|
||||
...getBaseReqParam(),
|
||||
start: (pageNum - 1) * pageSize,
|
||||
stop: pageNum * pageSize - 1,
|
||||
});
|
||||
state.total = res.len;
|
||||
|
||||
const datas = res.list.map((x: any) => {
|
||||
return {
|
||||
value: x,
|
||||
};
|
||||
});
|
||||
if (resetTableData) {
|
||||
state.values = datas;
|
||||
} else {
|
||||
state.values.push(...datas)
|
||||
}
|
||||
state.pageNum++;
|
||||
state.loadMoreDisable = state.values.length === state.total
|
||||
};
|
||||
|
||||
const lset = async (row: any, rowIndex: number) => {
|
||||
await redisApi.setListValue.request({
|
||||
...getBaseReqParam(),
|
||||
index: (state.pageNum - 1) * state.pageSize + rowIndex,
|
||||
value: row.value,
|
||||
});
|
||||
ElMessage.success('数据保存成功');
|
||||
};
|
||||
|
||||
const showEditDialog = (row: any) => {
|
||||
state.editDialog.dataRow = row;
|
||||
state.editDialog.content = row ? row.value : '';
|
||||
state.editDialog.visible = true;
|
||||
}
|
||||
|
||||
const confirmEditData = async () => {
|
||||
const param = getBaseReqParam();
|
||||
|
||||
// 存在数据行,则说明为修改,则要先删除旧数据后新增
|
||||
const dataRow = state.editDialog.dataRow
|
||||
if (dataRow) {
|
||||
await redisApi.lrem.request({
|
||||
member: state.editDialog.dataRow.value,
|
||||
count: 1,
|
||||
...param
|
||||
});
|
||||
}
|
||||
|
||||
// 获取list member内容并新增
|
||||
const member = formatViewerRef.value.getContent()
|
||||
await redisApi.saveListValue.request({
|
||||
value: [member],
|
||||
...param
|
||||
});
|
||||
|
||||
ElMessage.success("保存成功");
|
||||
if (dataRow) {
|
||||
state.editDialog.dataRow.value = member;
|
||||
} else {
|
||||
state.values.push({ value: member });
|
||||
state.total++;
|
||||
}
|
||||
state.editDialog.visible = false;
|
||||
state.editDialog.dataRow = null;
|
||||
}
|
||||
|
||||
const lrem = async (row: any, index: any) => {
|
||||
await redisApi.lrem.request({
|
||||
...getBaseReqParam(),
|
||||
member: row.value,
|
||||
count: 1,
|
||||
})
|
||||
ElMessage.success("删除成功");
|
||||
state.values.splice(index, 1)
|
||||
state.total--;
|
||||
}
|
||||
|
||||
const getBaseReqParam = () => {
|
||||
return {
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ initData })
|
||||
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
205
mayfly_go_web/src/views/ops/redis/KeyValueSet.vue
Normal file
205
mayfly_go_web/src/views/ops/redis/KeyValueSet.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
|
||||
<el-table size="small" border :data="setDatas" min-height=300 stripe>
|
||||
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100">
|
||||
</el-table-column>
|
||||
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #header>
|
||||
<el-input class="key-detail-filter-value" v-model="state.filterValue"
|
||||
@keyup.enter='sscanData(true, true)' placeholder="输入关键词回车搜索" clearable size="small" />
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit"
|
||||
plain></el-link>
|
||||
<el-popconfirm title="确定删除?" @confirm="srem(scope.row, scope.$index)">
|
||||
<template #reference>
|
||||
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small"
|
||||
plain class="ml5"></el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- load more content -->
|
||||
<div class='content-more-container'>
|
||||
<el-button size='small' @click='sscanData(false)' :disabled='loadMoreDisable' class='content-more-btn'>
|
||||
加载更多
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true"
|
||||
:close-on-click-modal="false">
|
||||
<el-form>
|
||||
<el-form-item>
|
||||
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="editDialog.visible = false">取 消</el-button>
|
||||
<el-button v-auth="'redis:data:save'" type="primary" @click="confirmEditData">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, toRefs, onMounted } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import FormatViewer from './FormatViewer.vue';
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0,
|
||||
},
|
||||
db: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
})
|
||||
|
||||
const formatViewerRef = ref(null) as any;
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
db: 0,
|
||||
key: '',
|
||||
|
||||
filterValue: '',
|
||||
scanParam: {
|
||||
count: 50,
|
||||
cursor: 0,
|
||||
},
|
||||
total: 0,
|
||||
setDatas: [] as any,
|
||||
loadMoreDisable: false,
|
||||
value: [{ value: '' }],
|
||||
editDialog: {
|
||||
visible: false,
|
||||
content: '',
|
||||
dataRow: null as any,
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
total,
|
||||
setDatas,
|
||||
loadMoreDisable,
|
||||
editDialog,
|
||||
} = toRefs(state)
|
||||
|
||||
onMounted(() => {
|
||||
state.redisId = props.redisId;
|
||||
state.db = props.db;
|
||||
state.key = props.keyInfo?.key;
|
||||
initData();
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
state.filterValue = '';
|
||||
sscanData(true, true);
|
||||
getTotal();
|
||||
}
|
||||
|
||||
const getScanMatch = () => {
|
||||
return state.filterValue ? `*${state.filterValue}*` : '*';
|
||||
}
|
||||
|
||||
const sscanData = async (resetDatas = true, resetCursor = false) => {
|
||||
if (resetCursor) {
|
||||
state.scanParam.cursor = 0;
|
||||
}
|
||||
const res = await redisApi.sscan.request({
|
||||
...getBaseReqParam(),
|
||||
match: getScanMatch(),
|
||||
...state.scanParam
|
||||
});
|
||||
|
||||
if (resetDatas) {
|
||||
state.setDatas = [];
|
||||
}
|
||||
res.keys.forEach((x: any) => {
|
||||
state.setDatas.push({
|
||||
value: x,
|
||||
})
|
||||
})
|
||||
state.scanParam.cursor = res.cursor;
|
||||
state.loadMoreDisable = res.cursor == 0
|
||||
};
|
||||
|
||||
const getTotal = () => {
|
||||
redisApi.scard.request(getBaseReqParam()).then((res) => {
|
||||
state.total = res;
|
||||
});
|
||||
}
|
||||
|
||||
const showEditDialog = (row: any) => {
|
||||
state.editDialog.dataRow = row;
|
||||
state.editDialog.content = row ? row.value : '';
|
||||
state.editDialog.visible = true;
|
||||
}
|
||||
|
||||
const confirmEditData = async () => {
|
||||
const param = getBaseReqParam();
|
||||
|
||||
// 存在数据行,则说明为修改,则要先删除旧数据后新增
|
||||
const dataRow = state.editDialog.dataRow
|
||||
if (dataRow) {
|
||||
await redisApi.srem.request({
|
||||
member: state.editDialog.dataRow.value,
|
||||
...param
|
||||
});
|
||||
}
|
||||
|
||||
// 获取set member内容并新增
|
||||
const member = formatViewerRef.value.getContent()
|
||||
await redisApi.sadd.request({
|
||||
member,
|
||||
...param
|
||||
});
|
||||
|
||||
ElMessage.success("保存成功");
|
||||
if (dataRow) {
|
||||
state.editDialog.dataRow.value = member;
|
||||
} else {
|
||||
state.setDatas.unshift({ value: member });
|
||||
state.total++;
|
||||
}
|
||||
state.editDialog.visible = false;
|
||||
state.editDialog.dataRow = null;
|
||||
}
|
||||
|
||||
const srem = async (row: any, index: any) => {
|
||||
await redisApi.srem.request({
|
||||
...getBaseReqParam(),
|
||||
member: row.value,
|
||||
})
|
||||
ElMessage.success("删除成功");
|
||||
state.setDatas.splice(index, 1)
|
||||
state.total--;
|
||||
}
|
||||
|
||||
const getBaseReqParam = () => {
|
||||
return {
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ initData })
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
111
mayfly_go_web/src/views/ops/redis/KeyValueString.vue
Normal file
111
mayfly_go_web/src/views/ops/redis/KeyValueString.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form class='key-content-string' label-width="85px">
|
||||
<div>
|
||||
<format-viewer ref="formatViewerRef" :content="string.value"></format-viewer>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="mt10 fr">
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">保 存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch, toRefs, onMounted } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { notEmpty } from '@/common/assert';
|
||||
import FormatViewer from './FormatViewer.vue';
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0,
|
||||
},
|
||||
db: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
})
|
||||
|
||||
const formatViewerRef = ref(null) as any
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
db: 0,
|
||||
key: '',
|
||||
keyInfo: {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
},
|
||||
string: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
string,
|
||||
} = toRefs(state)
|
||||
|
||||
onMounted(() => {
|
||||
state.redisId = props.redisId
|
||||
state.db = props.db
|
||||
state.key = props.keyInfo?.key;
|
||||
initData();
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
getStringValue();
|
||||
}
|
||||
|
||||
const getStringValue = async () => {
|
||||
if (state.key) {
|
||||
state.string.value = await redisApi.getString.request(getBaseReqParam());
|
||||
}
|
||||
};
|
||||
|
||||
const saveValue = async () => {
|
||||
state.string.value = formatViewerRef.value.getContent();
|
||||
notEmpty(state.string.value, 'value不能为空');
|
||||
|
||||
await redisApi.setString.request({
|
||||
...getBaseReqParam(),
|
||||
value: state.string.value,
|
||||
});
|
||||
ElMessage.success('数据保存成功');
|
||||
};
|
||||
|
||||
const getBaseReqParam = () => {
|
||||
return {
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ initData })
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.key-content-string .format-viewer-container {
|
||||
min-height: calc(100vh - 453px);
|
||||
}
|
||||
|
||||
/*text viewer box*/
|
||||
.key-content-string .el-textarea textarea {
|
||||
font-size: 14px;
|
||||
height: calc(100vh - 436px);
|
||||
}
|
||||
|
||||
/*json in monaco editor*/
|
||||
.key-content-string .monaco-editor-content {
|
||||
height: calc(100vh - 450px) !important;
|
||||
}
|
||||
</style>
|
||||
257
mayfly_go_web/src/views/ops/redis/KeyValueZset.vue
Normal file
257
mayfly_go_web/src/views/ops/redis/KeyValueZset.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
|
||||
<el-table size="small" border :data="values" min-height=300 stripe>
|
||||
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100">
|
||||
</el-table-column>
|
||||
<el-table-column resizable sortable prop="score" label="score" show-overflow-tooltip min-width="100">
|
||||
</el-table-column>
|
||||
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #header>
|
||||
<el-input class="key-detail-filter-value" v-model="state.filterValue" @keyup.enter='zscanData(true)'
|
||||
placeholder="输入关键词回车搜索" clearable size="small" />
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit"
|
||||
plain></el-link>
|
||||
<el-popconfirm title="确定删除?" @confirm="zrem(scope.row, scope.$index)">
|
||||
<template #reference>
|
||||
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small"
|
||||
plain class="ml5"></el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- load more content -->
|
||||
<div class='content-more-container'>
|
||||
<el-button size='small' @click='loadDatas()' :disabled='loadMoreDisable' class='content-more-btn'>
|
||||
加载更多
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true"
|
||||
:close-on-click-modal="false">
|
||||
<el-form>
|
||||
<el-form-item>
|
||||
<el-input type="number" v-model.number="editDialog.score" placeholder="score" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="editDialog.visible = false">取 消</el-button>
|
||||
<el-button v-auth="'redis:data:save'" type="primary" @click="confirmEditData">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, toRefs, onMounted } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import FormatViewer from './FormatViewer.vue';
|
||||
|
||||
const props = defineProps({
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0,
|
||||
},
|
||||
db: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
default: 0
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
})
|
||||
|
||||
const formatViewerRef = ref(null) as any;
|
||||
|
||||
const state = reactive({
|
||||
redisId: 0,
|
||||
db: 0,
|
||||
key: '',
|
||||
filterValue: '',
|
||||
scanCursor: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
values: [] as any,
|
||||
loadMoreDisable: false,
|
||||
editDialog: {
|
||||
visible: false,
|
||||
score: 0,
|
||||
content: '',
|
||||
dataRow: null as any,
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
total,
|
||||
values,
|
||||
loadMoreDisable,
|
||||
editDialog,
|
||||
} = toRefs(state)
|
||||
|
||||
onMounted(() => {
|
||||
state.redisId = props.redisId;
|
||||
state.db = props.db;
|
||||
state.key = props.keyInfo?.key;
|
||||
initData();
|
||||
})
|
||||
|
||||
const initData = async () => {
|
||||
state.pageNum = 1;
|
||||
state.filterValue = '';
|
||||
await getTotal();
|
||||
await zrevrange(true);
|
||||
}
|
||||
|
||||
const loadDatas = (resetTableData = false) => {
|
||||
if (state.filterValue) {
|
||||
zscanData(resetTableData);
|
||||
return;
|
||||
}
|
||||
zrevrange(resetTableData);
|
||||
}
|
||||
|
||||
const zrevrange = async (resetTableData = false) => {
|
||||
const pageNum = state.pageNum;
|
||||
const pageSize = state.pageSize;
|
||||
const res = await redisApi.zrevrange.request({
|
||||
...getBaseReqParam(),
|
||||
start: (pageNum - 1) * pageSize,
|
||||
stop: pageNum * pageSize - 1,
|
||||
})
|
||||
|
||||
const vs = [];
|
||||
for (let member of res) {
|
||||
vs.push({
|
||||
score: member.Score,
|
||||
value: member.Member
|
||||
})
|
||||
}
|
||||
if (resetTableData) {
|
||||
state.values = vs;
|
||||
} else {
|
||||
state.values.push(...vs);
|
||||
}
|
||||
state.pageNum++;
|
||||
state.loadMoreDisable = state.total <= state.values.length
|
||||
}
|
||||
|
||||
const getScanMatch = () => {
|
||||
return state.filterValue ? `*${state.filterValue}*` : '*';
|
||||
}
|
||||
|
||||
const zscanData = async (resetTableData = true, resetCursor = false) => {
|
||||
if (resetCursor) {
|
||||
state.scanCursor = 0;
|
||||
}
|
||||
const res = await redisApi.zscan.request({
|
||||
...getBaseReqParam(),
|
||||
match: getScanMatch(),
|
||||
cursor: state.scanCursor,
|
||||
count: state.pageSize
|
||||
});
|
||||
|
||||
const keys = res.keys;
|
||||
const vs = [];
|
||||
const memCount = keys.length / 2;
|
||||
let nextMemndex = 0;
|
||||
for (let i = 0; i < memCount; i++) {
|
||||
vs.push({ value: keys[nextMemndex++], score: keys[nextMemndex++] });
|
||||
}
|
||||
|
||||
if (resetTableData) {
|
||||
state.values = vs;
|
||||
} else {
|
||||
state.values.push(...vs);
|
||||
}
|
||||
|
||||
state.scanCursor = res.cursor;
|
||||
state.loadMoreDisable = res.cursor == 0
|
||||
};
|
||||
|
||||
const getTotal = () => {
|
||||
redisApi.zcard.request({
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key
|
||||
}).then((res) => {
|
||||
state.total = res;
|
||||
});
|
||||
}
|
||||
|
||||
const showEditDialog = (row: any) => {
|
||||
state.editDialog.dataRow = row;
|
||||
state.editDialog.content = row ? row.value : '';
|
||||
state.editDialog.score = row ? row.score : null;
|
||||
state.editDialog.visible = true;
|
||||
}
|
||||
|
||||
const confirmEditData = async () => {
|
||||
const param = getBaseReqParam();
|
||||
|
||||
// 存在数据行,则说明为修改,则要先删除旧数据后新增
|
||||
const dataRow = state.editDialog.dataRow
|
||||
if (dataRow) {
|
||||
await redisApi.zrem.request({
|
||||
member: state.editDialog.dataRow.value,
|
||||
...param
|
||||
});
|
||||
}
|
||||
|
||||
const score = state.editDialog.score
|
||||
// 获取zset member内容并新增
|
||||
const member = formatViewerRef.value.getContent()
|
||||
await redisApi.zadd.request({
|
||||
score,
|
||||
member,
|
||||
...param
|
||||
});
|
||||
|
||||
ElMessage.success("保存成功");
|
||||
if (dataRow) {
|
||||
state.editDialog.dataRow.value = member;
|
||||
state.editDialog.dataRow.score = score;
|
||||
} else {
|
||||
state.values.unshift({ value: member, score });
|
||||
state.total++;
|
||||
}
|
||||
state.editDialog.visible = false;
|
||||
state.editDialog.dataRow = null;
|
||||
}
|
||||
|
||||
const zrem = async (row: any, index: any) => {
|
||||
await redisApi.zrem.request({
|
||||
...getBaseReqParam(),
|
||||
member: row.value,
|
||||
})
|
||||
ElMessage.success("删除成功");
|
||||
state.values.splice(index, 1)
|
||||
state.total--;
|
||||
}
|
||||
|
||||
const getBaseReqParam = () => {
|
||||
return {
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ initData })
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,199 +0,0 @@
|
||||
<template>
|
||||
<el-dialog class="el-table-z-index-inherit" :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
|
||||
<el-form label-width="85px">
|
||||
<el-form-item prop="key" label="key:">
|
||||
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="timed" label="过期时间:">
|
||||
<el-input v-model.number="key.timed" type="number"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dataType" label="数据类型:">
|
||||
<el-input v-model="key.type" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-button @click="onAddListValue" icon="plus" size="small" plain class="mt10">添加</el-button> -->
|
||||
<div v-if="operationType == 2" class="mt10" style="float: left">
|
||||
<span>len: {{ len }}</span>
|
||||
</div>
|
||||
<el-table :data="value" stripe style="width: 100%">
|
||||
<el-table-column prop="value" label="value" min-width="200">
|
||||
<template #default="scope">
|
||||
<format-input :title="`type:【${key.type}】key:【${key.key}】`" v-model="scope.row.value"
|
||||
:autosize="{ minRows: 2, maxRows: 10 }" size="small"></format-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140">
|
||||
<template #default="scope">
|
||||
<el-button v-if="operationType == 2" type="success" @click="lset(scope.row, scope.$index)"
|
||||
icon="check" size="small" plain></el-button>
|
||||
<!-- <el-button type="danger" @click="set.value.splice(scope.$index, 1)" icon="delete" size="small" plain></el-button> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-row style="margin-top: 20px" type="flex" justify="end">
|
||||
<el-pagination style="text-align: right" :total="len" layout="prev, pager, next, total"
|
||||
@current-change="handlePageChange" v-model:current-page="pageNum" :page-size="pageSize">
|
||||
</el-pagination>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<!-- <template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">确 定</el-button>
|
||||
</div>
|
||||
</template> -->
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import FormatInput from './FormatInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
db: {
|
||||
type: [String],
|
||||
require: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
// 操作类型,1:新增,2:修改
|
||||
operationType: {
|
||||
type: [Number],
|
||||
},
|
||||
listValue: {
|
||||
type: [Array, Object],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'valChange'])
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
operationType: 1,
|
||||
redisId: '',
|
||||
db: '0',
|
||||
key: {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
},
|
||||
value: [{ value: '' }],
|
||||
len: 0,
|
||||
start: 0,
|
||||
stop: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const {
|
||||
dialogVisible,
|
||||
operationType,
|
||||
key,
|
||||
value,
|
||||
len,
|
||||
pageNum,
|
||||
pageSize,
|
||||
} = toRefs(state)
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
setTimeout(() => {
|
||||
state.key = {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
};
|
||||
state.value = [];
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
state.redisId = newValue.redisId;
|
||||
state.db = newValue.db;
|
||||
state.key = newValue.keyInfo;
|
||||
state.operationType = newValue.operationType;
|
||||
// 如果是查看编辑操作,则获取值
|
||||
if (state.dialogVisible && state.operationType == 2) {
|
||||
getListValue();
|
||||
}
|
||||
});
|
||||
|
||||
const getListValue = async () => {
|
||||
const pageNum = state.pageNum;
|
||||
const pageSize = state.pageSize;
|
||||
const res = await redisApi.getListValue.request({
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key.key,
|
||||
start: (pageNum - 1) * pageSize,
|
||||
stop: pageNum * pageSize - 1,
|
||||
});
|
||||
state.len = res.len;
|
||||
state.value = res.list.map((x: any) => {
|
||||
return {
|
||||
value: x,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const lset = async (row: any, rowIndex: number) => {
|
||||
await redisApi.setListValue.request({
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key.key,
|
||||
index: (state.pageNum - 1) * state.pageSize + rowIndex,
|
||||
value: row.value,
|
||||
});
|
||||
ElMessage.success('数据保存成功');
|
||||
};
|
||||
|
||||
// const saveValue = async () => {
|
||||
// notEmpty(state.key.key, 'key不能为空');
|
||||
// isTrue(state.value.length > 0, 'list内容不能为空');
|
||||
// // const sv = { value: state.value.map((x) => x.value), id: state.redisId };
|
||||
// // Object.assign(sv, state.key);
|
||||
// // await redisApi.saveSetValue.request(sv);
|
||||
|
||||
// ElMessage.success('数据保存成功');
|
||||
// cancel();
|
||||
// emit('valChange');
|
||||
// };
|
||||
|
||||
// const onAddListValue = () => {
|
||||
// state.value.unshift({ value: '' });
|
||||
// };
|
||||
|
||||
const handlePageChange = (curPage: number) => {
|
||||
state.pageNum = curPage;
|
||||
getListValue();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<el-dialog class="el-table-z-index-inherit" :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
|
||||
<el-form label-width="85px">
|
||||
<el-form-item prop="key" label="key:">
|
||||
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="timed" label="过期时间:">
|
||||
<el-input v-model.number="key.timed" type="number"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dataType" label="数据类型:">
|
||||
<el-input v-model="key.type" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-button @click="onAddSetValue" icon="plus" size="small" plain class="mt10">添加</el-button>
|
||||
<el-table :data="value" stripe style="width: 100%">
|
||||
<el-table-column prop="value" label="value" min-width="200">
|
||||
<template #default="scope">
|
||||
<format-input :title="`type:【${key.type}】key:【${key.key}】`" v-model="scope.row.value" clearable type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 10 }" size="small"></format-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="scope">
|
||||
<el-button v-auth="'redis:data:del'" type="danger" @click="value.splice(scope.$index, 1)" icon="delete" size="small"
|
||||
plain>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { isTrue, notEmpty } from '@/common/assert';
|
||||
import FormatInput from './FormatInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
db: {
|
||||
type: [String],
|
||||
require: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
// 操作类型,1:新增,2:修改
|
||||
operationType: {
|
||||
type: [Number],
|
||||
},
|
||||
setValue: {
|
||||
type: [Array, Object],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'valChange'])
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
operationType: 1,
|
||||
redisId: '',
|
||||
db: '0',
|
||||
key: {
|
||||
key: '',
|
||||
type: 'set',
|
||||
timed: -1,
|
||||
},
|
||||
value: [{ value: '' }],
|
||||
});
|
||||
|
||||
const {
|
||||
dialogVisible,
|
||||
operationType,
|
||||
key,
|
||||
value,
|
||||
} = toRefs(state)
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
setTimeout(() => {
|
||||
state.key = {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
};
|
||||
state.value = [];
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
state.redisId = newValue.redisId;
|
||||
state.db = newValue.db;
|
||||
state.key = newValue.keyInfo;
|
||||
state.operationType = newValue.operationType;
|
||||
// 如果是查看编辑操作,则获取值
|
||||
if (state.dialogVisible && state.operationType == 2) {
|
||||
getSetValue();
|
||||
}
|
||||
});
|
||||
|
||||
const getSetValue = async () => {
|
||||
const res = await redisApi.getSetValue.request({
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key.key,
|
||||
});
|
||||
state.value = res.map((x: any) => {
|
||||
return {
|
||||
value: x,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const saveValue = async () => {
|
||||
notEmpty(state.key.key, 'key不能为空');
|
||||
isTrue(state.value.length > 0, 'set内容不能为空');
|
||||
const sv = { value: state.value.map((x) => x.value), id: state.redisId, db: state.db };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveSetValue.request(sv);
|
||||
|
||||
ElMessage.success('数据保存成功');
|
||||
cancel();
|
||||
emit('valChange');
|
||||
};
|
||||
|
||||
const onAddSetValue = () => {
|
||||
state.value.unshift({ value: '' });
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
|
||||
<el-form label-width="85px">
|
||||
<el-form-item prop="key" label="key:">
|
||||
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="timed" label="过期时间:">
|
||||
<el-input v-model.number="key.timed" type="number"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dataType" label="数据类型:">
|
||||
<el-input v-model="key.type" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<div id="string-value-text" style="width: 100%">
|
||||
<format-input :title="`type:【${key.type}】key:【${key.key}】`" v-model="string.value" :autosize="{ minRows: 10, maxRows: 20 }"></format-input>
|
||||
<!-- <el-select class="text-type-select" @change="onChangeTextType" v-model="string.type">
|
||||
<el-option key="text" label="text" value="text"> </el-option>
|
||||
<el-option key="json" label="json" value="json"> </el-option>
|
||||
</el-select> -->
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { notEmpty } from '@/common/assert';
|
||||
import FormatInput from './FormatInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
db: {
|
||||
type: [String],
|
||||
require: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
// 操作类型,1:新增,2:修改
|
||||
operationType: {
|
||||
type: [Number],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'valChange'])
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
operationType: 1,
|
||||
redisId: '',
|
||||
db: '0',
|
||||
key: {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
},
|
||||
string: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
dialogVisible,
|
||||
operationType,
|
||||
key,
|
||||
string,
|
||||
} = toRefs(state)
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
setTimeout(() => {
|
||||
state.key = {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
};
|
||||
state.string.value = '';
|
||||
state.string.type = 'text';
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
state.dialogVisible = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.redisId,
|
||||
(val) => {
|
||||
state.redisId = val as any;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.db,
|
||||
(val) => {
|
||||
state.db = val as any;
|
||||
}
|
||||
);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
state.redisId = newValue.redisId;
|
||||
state.db = newValue.db;
|
||||
state.key = newValue.keyInfo;
|
||||
state.operationType = newValue.operationType;
|
||||
// 如果是查看编辑操作,则获取值
|
||||
if (state.dialogVisible && state.operationType == 2) {
|
||||
getStringValue();
|
||||
}
|
||||
});
|
||||
|
||||
const getStringValue = async () => {
|
||||
state.string.value = await redisApi.getStringValue.request({
|
||||
id: state.redisId,
|
||||
db: state.db,
|
||||
key: state.key.key,
|
||||
});
|
||||
};
|
||||
|
||||
const saveValue = async () => {
|
||||
notEmpty(state.key.key, 'key不能为空');
|
||||
|
||||
notEmpty(state.string.value, 'value不能为空');
|
||||
const sv = { value: state.string.value, id: state.redisId, db: state.db };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveStringValue.request(sv);
|
||||
ElMessage.success('数据保存成功');
|
||||
cancel();
|
||||
emit('valChange');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
mayfly_go_web/src/views/ops/redis/ViewerJson.vue
Normal file
56
mayfly_go_web/src/views/ops/redis/ViewerJson.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="text-formated-container">
|
||||
<monaco-editor ref="monacoEditorRef" :canChangeMode="false" v-model="state.modelValue" language="json" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const monacoEditorRef = ref(null) as any
|
||||
|
||||
const state = reactive({
|
||||
modelValue: '',
|
||||
content: null as any,
|
||||
});
|
||||
|
||||
// 因为默认从Text viewer开始,暂时不watch(保存时会触发重新格式化)。
|
||||
// watch(
|
||||
// () => props.content,
|
||||
// (val: any) => {
|
||||
// setContent(val);
|
||||
// }
|
||||
// );
|
||||
|
||||
onMounted(() => {
|
||||
setContent(props.content)
|
||||
})
|
||||
|
||||
const setContent = (val: any) => {
|
||||
state.modelValue = val;
|
||||
setTimeout(() => {
|
||||
monacoEditorRef.value.format();
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const getContent = () => {
|
||||
// 尝试压缩json
|
||||
try {
|
||||
state.content = JSON.stringify(JSON.parse(state.modelValue));
|
||||
return state.content;
|
||||
} catch (e) {
|
||||
return state.modelValue
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ getContent })
|
||||
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
41
mayfly_go_web/src/views/ops/redis/ViewerText.vue
Normal file
41
mayfly_go_web/src/views/ops/redis/ViewerText.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-input type="textarea" v-model="modelValue" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch, toRefs, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
modelValue: '',
|
||||
});
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
} = toRefs(state)
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
(val: any) => {
|
||||
state.modelValue = val;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
state.modelValue = props.content as any;
|
||||
})
|
||||
|
||||
const getContent = () => {
|
||||
return state.modelValue
|
||||
}
|
||||
|
||||
defineExpose({ getContent })
|
||||
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -7,20 +7,41 @@ export const redisApi = {
|
||||
clusterInfo: Api.newGet("/redis/{id}/cluster-info"),
|
||||
saveRedis: Api.newPost("/redis"),
|
||||
delRedis: Api.newDelete("/redis/{id}"),
|
||||
|
||||
keyTtl: Api.newGet("/redis/{id}/{db}/key-ttl"),
|
||||
renameKey: Api.newPost("/redis/{id}/{db}/rename-key"),
|
||||
expireKey: Api.newPost("/redis/{id}/{db}/expire-key"),
|
||||
persistKey: Api.newDelete("/redis/{id}/{db}/persist-key"),
|
||||
|
||||
// 获取权限列表
|
||||
scan: Api.newPost("/redis/{id}/{db}/scan"),
|
||||
getStringValue: Api.newGet("/redis/{id}/{db}/string-value"),
|
||||
saveStringValue: Api.newPost("/redis/{id}/{db}/string-value"),
|
||||
getString: Api.newGet("/redis/{id}/{db}/string-value"),
|
||||
setString: Api.newPost("/redis/{id}/{db}/string-value"),
|
||||
getHashValue: Api.newGet("/redis/{id}/{db}/hash-value"),
|
||||
hscan: Api.newGet("/redis/{id}/{db}/hscan"),
|
||||
hget: Api.newGet("/redis/{id}/{db}/hget"),
|
||||
hset: Api.newPost("/redis/{id}/{db}/hset"),
|
||||
hdel: Api.newDelete("/redis/{id}/{db}/hdel"),
|
||||
saveHashValue: Api.newPost("/redis/{id}/{db}/hash-value"),
|
||||
|
||||
getSetValue: Api.newGet("/redis/{id}/{db}/set-value"),
|
||||
scard: Api.newGet("/redis/{id}/{db}/scard"),
|
||||
sscan: Api.newPost("/redis/{id}/{db}/sscan"),
|
||||
sadd: Api.newPost("/redis/{id}/{db}/sadd"),
|
||||
srem: Api.newPost("/redis/{id}/{db}/srem"),
|
||||
saveSetValue: Api.newPost("/redis/{id}/{db}/set-value"),
|
||||
|
||||
del: Api.newDelete("/redis/{id}/{db}/scan/{cursor}/{count}"),
|
||||
delKey: Api.newDelete("/redis/{id}/{db}/key"),
|
||||
|
||||
lrem: Api.newPost("/redis/{id}/{db}/lrem"),
|
||||
getListValue: Api.newGet("/redis/{id}/{db}/list-value"),
|
||||
saveListValue: Api.newPost("/redis/{id}/{db}/list-value"),
|
||||
setListValue: Api.newPost("/redis/{id}/{db}/list-value/lset"),
|
||||
|
||||
zcard: Api.newGet("/redis/{id}/{db}/zcard"),
|
||||
zscan: Api.newGet("/redis/{id}/{db}/zscan"),
|
||||
zrevrange: Api.newGet("/redis/{id}/{db}/zrevrange"),
|
||||
zadd: Api.newPost("/redis/{id}/{db}/zadd"),
|
||||
zrem: Api.newPost("/redis/{id}/{db}/zrem"),
|
||||
}
|
||||
@@ -14,7 +14,7 @@ func InitCommonRouter(router *gin.RouterGroup) {
|
||||
// 获取公钥
|
||||
common.GET("public-key", func(g *gin.Context) {
|
||||
req.NewCtxWithGin(g).
|
||||
WithNeedToken(false).
|
||||
DontNeedToken().
|
||||
Handle(c.RasPublicKey)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,16 @@ type Redis struct {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type Rename struct {
|
||||
Key string `binding:"required" json:"key"`
|
||||
NewKey string `binding:"required" json:"newKey"`
|
||||
}
|
||||
|
||||
type Expire struct {
|
||||
Key string `binding:"required" json:"key"`
|
||||
Seconds int64 `binding:"required" json:"seconds"`
|
||||
}
|
||||
|
||||
type KeyInfo struct {
|
||||
Key string `binding:"required" json:"key"`
|
||||
Timed int64
|
||||
@@ -50,3 +60,27 @@ type RedisScanForm struct {
|
||||
Match string `json:"match"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type ScanForm struct {
|
||||
Key string `json:"key"`
|
||||
Cursor uint64 `json:"cursor"`
|
||||
Match string `json:"match"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type SmemberOption struct {
|
||||
Key string `json:"key"`
|
||||
Member any `json:"member"`
|
||||
}
|
||||
|
||||
type LRemOption struct {
|
||||
Key string `json:"key"`
|
||||
Count int64 `json:"count"`
|
||||
Member any `json:"member"`
|
||||
}
|
||||
|
||||
type ZAddOption struct {
|
||||
Key string `json:"key"`
|
||||
Score float64 `json:"score"`
|
||||
Member any `json:"member"`
|
||||
}
|
||||
|
||||
79
server/internal/redis/api/hash.go
Normal file
79
server/internal/redis/api/hash.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/redis/api/form"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (r *Redis) Hscan(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
g := rc.GinCtx
|
||||
count := ginx.QueryInt(g, "count", 10)
|
||||
match := g.Query("match")
|
||||
cursor := ginx.QueryInt(g, "cursor", 0)
|
||||
contextTodo := context.TODO()
|
||||
|
||||
cmdable := ri.GetCmdable()
|
||||
keys, nextCursor, err := cmdable.HScan(contextTodo, key, uint64(cursor), match, int64(count)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hcan err: %s")
|
||||
keySize, err := cmdable.HLen(contextTodo, key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hlen err: %s")
|
||||
|
||||
rc.ResData = map[string]interface{}{
|
||||
"keys": keys,
|
||||
"cursor": nextCursor,
|
||||
"keySize": keySize,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) Hdel(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
field := rc.GinCtx.Query("field")
|
||||
|
||||
delRes, err := ri.GetCmdable().HDel(context.TODO(), key, field).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hdel err: %s")
|
||||
rc.ResData = delRes
|
||||
}
|
||||
|
||||
func (r *Redis) Hget(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
field := rc.GinCtx.Query("field")
|
||||
|
||||
res, err := ri.GetCmdable().HGet(context.TODO(), key, field).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hget err: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) Hset(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
hashValue := new(form.HashValue)
|
||||
ginx.BindJsonAndValid(g, hashValue)
|
||||
|
||||
hv := hashValue.Value[0]
|
||||
res, err := r.getRedisIns(rc).GetCmdable().HSet(context.TODO(), hashValue.Key, hv["field"].(string), hv["value"]).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hset失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) SetHashValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
hashValue := new(form.HashValue)
|
||||
ginx.BindJsonAndValid(g, hashValue)
|
||||
|
||||
ri := r.getRedisIns(rc)
|
||||
cmd := ri.GetCmdable()
|
||||
|
||||
key := hashValue.Key
|
||||
contextTodo := context.TODO()
|
||||
for _, v := range hashValue.Value {
|
||||
res := cmd.HSet(contextTodo, key, v["field"].(string), v["value"])
|
||||
biz.ErrIsNilAppendErr(res.Err(), "保存hash值失败: %s")
|
||||
}
|
||||
if hashValue.Timed != 0 && hashValue.Timed != -1 {
|
||||
cmd.Expire(context.TODO(), key, time.Second*time.Duration(hashValue.Timed))
|
||||
}
|
||||
}
|
||||
189
server/internal/redis/api/key.go
Normal file
189
server/internal/redis/api/key.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/redis/api/form"
|
||||
"mayfly-go/internal/redis/api/vo"
|
||||
"mayfly-go/internal/redis/domain/entity"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// scan获取redis的key列表信息
|
||||
func (r *Redis) Scan(rc *req.Ctx) {
|
||||
ri := r.getRedisIns(rc)
|
||||
|
||||
form := &form.RedisScanForm{}
|
||||
ginx.BindJsonAndValid(rc.GinCtx, form)
|
||||
|
||||
cmd := ri.GetCmdable()
|
||||
ctx := context.Background()
|
||||
|
||||
kis := make([]*vo.KeyInfo, 0)
|
||||
var cursorRes map[string]uint64 = make(map[string]uint64)
|
||||
|
||||
mode := ri.Info.Mode
|
||||
if mode == "" || mode == entity.RedisModeStandalone || mode == entity.RedisModeSentinel {
|
||||
redisAddr := ri.Cli.Options().Addr
|
||||
// 汇总所有的查询出来的键值
|
||||
var keys []string
|
||||
// 有通配符或空时使用scan,非模糊匹配直接匹配key
|
||||
if form.Match == "" || strings.ContainsAny(form.Match, "*") {
|
||||
cursorRes[redisAddr] = form.Cursor[redisAddr]
|
||||
for {
|
||||
ks, cursor := ri.Scan(cursorRes[redisAddr], form.Match, form.Count)
|
||||
cursorRes[redisAddr] = cursor
|
||||
if len(ks) > 0 {
|
||||
// 返回了数据则追加总集合中
|
||||
keys = append(keys, ks...)
|
||||
}
|
||||
// 匹配的数量满足用户需求退出
|
||||
if int32(len(keys)) >= int32(form.Count) {
|
||||
break
|
||||
}
|
||||
// 匹配到最后退出
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 精确匹配
|
||||
keys = append(keys, form.Match)
|
||||
}
|
||||
|
||||
var keyInfoSplit []string
|
||||
if len(keys) > 0 {
|
||||
keyInfosLua := `local result = {}
|
||||
-- KEYS[1]为第1个参数,lua数组下标从1开始
|
||||
for i = 1, #KEYS do
|
||||
local ttl = redis.call('ttl', KEYS[i]);
|
||||
local keyType = redis.call('type', KEYS[i]);
|
||||
table.insert(result, string.format("%d,%s", ttl, keyType['ok']));
|
||||
end;
|
||||
return table.concat(result, ".");`
|
||||
// 通过lua获取 ttl,type.ttl2,type2格式,以便下面切割获取ttl和type。避免多次调用ttl和type函数
|
||||
keyInfos, err := cmd.Eval(ctx, keyInfosLua, keys).Result()
|
||||
biz.ErrIsNilAppendErr(err, "执行lua脚本获取key信息失败: %s")
|
||||
keyInfoSplit = strings.Split(keyInfos.(string), ".")
|
||||
}
|
||||
|
||||
for i, k := range keys {
|
||||
ttlType := strings.Split(keyInfoSplit[i], ",")
|
||||
ttl, _ := strconv.Atoi(ttlType[0])
|
||||
// 没有存在该key,则跳过
|
||||
if ttl == -2 {
|
||||
continue
|
||||
}
|
||||
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: int64(ttl)}
|
||||
kis = append(kis, ki)
|
||||
}
|
||||
} else if mode == entity.RedisModeCluster {
|
||||
var keys []string
|
||||
// 有通配符或空时使用scan,非模糊匹配直接匹配key
|
||||
if form.Match == "" || strings.ContainsAny(form.Match, "*") {
|
||||
mu := &sync.Mutex{}
|
||||
// 遍历所有master节点,并执行scan命令,合并keys
|
||||
ri.ClusterCli.ForEachMaster(ctx, func(ctx context.Context, client *redis.Client) error {
|
||||
redisAddr := client.Options().Addr
|
||||
nowCursor := form.Cursor[redisAddr]
|
||||
for {
|
||||
ks, cursor, _ := client.Scan(ctx, nowCursor, form.Match, form.Count).Result()
|
||||
// 遍历节点的内部回调函数使用异步调用,如不加锁会导致集合并发错误
|
||||
mu.Lock()
|
||||
cursorRes[redisAddr] = cursor
|
||||
nowCursor = cursor
|
||||
if len(ks) > 0 {
|
||||
// 返回了数据则追加总集合中
|
||||
keys = append(keys, ks...)
|
||||
}
|
||||
mu.Unlock()
|
||||
// 匹配的数量满足用户需求退出
|
||||
if int32(len(keys)) >= int32(form.Count) {
|
||||
break
|
||||
}
|
||||
// 匹配到最后退出
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
} else {
|
||||
// 精确匹配
|
||||
keys = append(keys, form.Match)
|
||||
}
|
||||
|
||||
// 因为redis集群模式执行lua脚本key必须位于同一slot中,故单机获取的方式不适合
|
||||
// 使用lua获取key的ttl以及类型,减少网络调用
|
||||
keyInfoLua := `local ttl = redis.call('ttl', KEYS[1]);
|
||||
local keyType = redis.call('type', KEYS[1]);
|
||||
return string.format("%d,%s", ttl, keyType['ok'])`
|
||||
for _, k := range keys {
|
||||
keyInfo, err := cmd.Eval(ctx, keyInfoLua, []string{k}).Result()
|
||||
biz.ErrIsNilAppendErr(err, "执行lua脚本获取key信息失败: %s")
|
||||
ttlType := strings.Split(keyInfo.(string), ",")
|
||||
ttl, _ := strconv.Atoi(ttlType[0])
|
||||
// 没有存在该key,则跳过
|
||||
if ttl == -2 {
|
||||
continue
|
||||
}
|
||||
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: int64(ttl)}
|
||||
kis = append(kis, ki)
|
||||
}
|
||||
}
|
||||
|
||||
size, _ := cmd.DBSize(context.TODO()).Result()
|
||||
rc.ResData = &vo.Keys{Cursor: cursorRes, Keys: kis, DbSize: size}
|
||||
}
|
||||
|
||||
func (r *Redis) TtlKey(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
ttl, err := ri.GetCmdable().TTL(context.Background(), key).Result()
|
||||
biz.ErrIsNil(err, "ttl失败: %s")
|
||||
|
||||
if ttl == -1 {
|
||||
rc.ResData = -1
|
||||
} else {
|
||||
rc.ResData = ttl.Seconds()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) DeleteKey(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
rc.ReqParam = fmt.Sprintf("%s -> 删除key: %s", ri.Info.GetLogDesc(), key)
|
||||
ri.GetCmdable().Del(context.Background(), key)
|
||||
}
|
||||
|
||||
func (r *Redis) RenameKey(rc *req.Ctx) {
|
||||
form := &form.Rename{}
|
||||
ginx.BindJsonAndValid(rc.GinCtx, form)
|
||||
|
||||
ri := r.getRedisIns(rc)
|
||||
rc.ReqParam = fmt.Sprintf("%s -> 重命名key[%s] -> [%s]", ri.Info.GetLogDesc(), form.Key, form.NewKey)
|
||||
ri.GetCmdable().Rename(context.Background(), form.Key, form.NewKey)
|
||||
}
|
||||
|
||||
func (r *Redis) ExpireKey(rc *req.Ctx) {
|
||||
form := &form.Expire{}
|
||||
ginx.BindJsonAndValid(rc.GinCtx, form)
|
||||
|
||||
ri := r.getRedisIns(rc)
|
||||
rc.ReqParam = fmt.Sprintf("%s -> 重置key[%s]过期时间为%d", ri.Info.GetLogDesc(), form.Key, form.Seconds)
|
||||
ri.GetCmdable().Expire(context.Background(), form.Key, time.Duration(form.Seconds)*time.Second)
|
||||
}
|
||||
|
||||
// 移除过期时间
|
||||
func (r *Redis) PersistKey(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
rc.ReqParam = fmt.Sprintf("%s -> 移除key[%s]的过期时间", ri.Info.GetLogDesc(), key)
|
||||
ri.GetCmdable().Persist(context.Background(), key)
|
||||
}
|
||||
65
server/internal/redis/api/list.go
Normal file
65
server/internal/redis/api/list.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/redis/api/form"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
)
|
||||
|
||||
func (r *Redis) GetListValue(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
ctx := context.TODO()
|
||||
cmdable := ri.GetCmdable()
|
||||
|
||||
len, err := cmdable.LLen(ctx, key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取list长度失败: %s")
|
||||
|
||||
g := rc.GinCtx
|
||||
start := ginx.QueryInt(g, "start", 0)
|
||||
stop := ginx.QueryInt(g, "stop", 10)
|
||||
res, err := cmdable.LRange(ctx, key, int64(start), int64(stop)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取list值失败: %s")
|
||||
|
||||
rc.ResData = map[string]interface{}{
|
||||
"len": len,
|
||||
"list": res,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) Lrem(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
option := new(form.LRemOption)
|
||||
ginx.BindJsonAndValid(g, option)
|
||||
|
||||
cmd := r.getRedisIns(rc).GetCmdable()
|
||||
res, err := cmd.LRem(context.TODO(), option.Key, int64(option.Count), option.Member).Result()
|
||||
biz.ErrIsNilAppendErr(err, "lrem失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) SaveListValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
listValue := new(form.ListValue)
|
||||
ginx.BindJsonAndValid(g, listValue)
|
||||
|
||||
cmd := r.getRedisIns(rc).GetCmdable()
|
||||
|
||||
key := listValue.Key
|
||||
ctx := context.TODO()
|
||||
for _, v := range listValue.Value {
|
||||
cmd.RPush(ctx, key, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) SetListValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
listSetValue := new(form.ListSetValue)
|
||||
ginx.BindJsonAndValid(g, listSetValue)
|
||||
|
||||
ri := r.getRedisIns(rc)
|
||||
|
||||
_, err := ri.GetCmdable().LSet(context.TODO(), listSetValue.Key, listSetValue.Index, listSetValue.Value).Result()
|
||||
biz.ErrIsNilAppendErr(err, "list set失败: %s")
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/redis/api/form"
|
||||
"mayfly-go/internal/redis/api/vo"
|
||||
"mayfly-go/internal/redis/application"
|
||||
@@ -13,11 +12,9 @@ import (
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
@@ -194,314 +191,20 @@ func (r *Redis) ClusterInfo(rc *req.Ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// scan获取redis的key列表信息
|
||||
func (r *Redis) Scan(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
|
||||
|
||||
form := &form.RedisScanForm{}
|
||||
ginx.BindJsonAndValid(rc.GinCtx, form)
|
||||
|
||||
cmd := ri.GetCmdable()
|
||||
ctx := context.Background()
|
||||
|
||||
kis := make([]*vo.KeyInfo, 0)
|
||||
var cursorRes map[string]uint64 = make(map[string]uint64)
|
||||
|
||||
mode := ri.Info.Mode
|
||||
if mode == "" || mode == entity.RedisModeStandalone || mode == entity.RedisModeSentinel {
|
||||
redisAddr := ri.Cli.Options().Addr
|
||||
// 汇总所有的查询出来的键值
|
||||
var keys []string
|
||||
// 有通配符或空时使用scan,非模糊匹配直接匹配key
|
||||
if form.Match == "" || strings.ContainsAny(form.Match, "*") {
|
||||
cursorRes[redisAddr] = form.Cursor[redisAddr]
|
||||
for {
|
||||
ks, cursor := ri.Scan(cursorRes[redisAddr], form.Match, form.Count)
|
||||
cursorRes[redisAddr] = cursor
|
||||
if len(ks) > 0 {
|
||||
// 返回了数据则追加总集合中
|
||||
keys = append(keys, ks...)
|
||||
}
|
||||
// 匹配的数量满足用户需求退出
|
||||
if int32(len(keys)) >= int32(form.Count) {
|
||||
break
|
||||
}
|
||||
// 匹配到最后退出
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 精确匹配
|
||||
keys = append(keys, form.Match)
|
||||
}
|
||||
|
||||
var keyInfoSplit []string
|
||||
if len(keys) > 0 {
|
||||
keyInfosLua := `local result = {}
|
||||
-- KEYS[1]为第1个参数,lua数组下标从1开始
|
||||
for i = 1, #KEYS do
|
||||
local ttl = redis.call('ttl', KEYS[i]);
|
||||
local keyType = redis.call('type', KEYS[i]);
|
||||
table.insert(result, string.format("%d,%s", ttl, keyType['ok']));
|
||||
end;
|
||||
return table.concat(result, ".");`
|
||||
// 通过lua获取 ttl,type.ttl2,type2格式,以便下面切割获取ttl和type。避免多次调用ttl和type函数
|
||||
keyInfos, err := cmd.Eval(ctx, keyInfosLua, keys).Result()
|
||||
biz.ErrIsNilAppendErr(err, "执行lua脚本获取key信息失败: %s")
|
||||
keyInfoSplit = strings.Split(keyInfos.(string), ".")
|
||||
}
|
||||
|
||||
for i, k := range keys {
|
||||
ttlType := strings.Split(keyInfoSplit[i], ",")
|
||||
ttl, _ := strconv.Atoi(ttlType[0])
|
||||
// 没有存在该key,则跳过
|
||||
if ttl == -2 {
|
||||
continue
|
||||
}
|
||||
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: int64(ttl)}
|
||||
kis = append(kis, ki)
|
||||
}
|
||||
} else if mode == entity.RedisModeCluster {
|
||||
var keys []string
|
||||
// 有通配符或空时使用scan,非模糊匹配直接匹配key
|
||||
if form.Match == "" || strings.ContainsAny(form.Match, "*") {
|
||||
mu := &sync.Mutex{}
|
||||
// 遍历所有master节点,并执行scan命令,合并keys
|
||||
ri.ClusterCli.ForEachMaster(ctx, func(ctx context.Context, client *redis.Client) error {
|
||||
redisAddr := client.Options().Addr
|
||||
nowCursor := form.Cursor[redisAddr]
|
||||
for {
|
||||
ks, cursor, _ := client.Scan(ctx, nowCursor, form.Match, form.Count).Result()
|
||||
// 遍历节点的内部回调函数使用异步调用,如不加锁会导致集合并发错误
|
||||
mu.Lock()
|
||||
cursorRes[redisAddr] = cursor
|
||||
nowCursor = cursor
|
||||
if len(ks) > 0 {
|
||||
// 返回了数据则追加总集合中
|
||||
keys = append(keys, ks...)
|
||||
}
|
||||
mu.Unlock()
|
||||
// 匹配的数量满足用户需求退出
|
||||
if int32(len(keys)) >= int32(form.Count) {
|
||||
break
|
||||
}
|
||||
// 匹配到最后退出
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
} else {
|
||||
// 精确匹配
|
||||
keys = append(keys, form.Match)
|
||||
}
|
||||
|
||||
// 因为redis集群模式执行lua脚本key必须位于同一slot中,故单机获取的方式不适合
|
||||
// 使用lua获取key的ttl以及类型,减少网络调用
|
||||
keyInfoLua := `local ttl = redis.call('ttl', KEYS[1]);
|
||||
local keyType = redis.call('type', KEYS[1]);
|
||||
return string.format("%d,%s", ttl, keyType['ok'])`
|
||||
for _, k := range keys {
|
||||
keyInfo, err := cmd.Eval(ctx, keyInfoLua, []string{k}).Result()
|
||||
biz.ErrIsNilAppendErr(err, "执行lua脚本获取key信息失败: %s")
|
||||
ttlType := strings.Split(keyInfo.(string), ",")
|
||||
ttl, _ := strconv.Atoi(ttlType[0])
|
||||
// 没有存在该key,则跳过
|
||||
if ttl == -2 {
|
||||
continue
|
||||
}
|
||||
ki := &vo.KeyInfo{Key: k, Type: ttlType[1], Ttl: int64(ttl)}
|
||||
kis = append(kis, ki)
|
||||
}
|
||||
}
|
||||
|
||||
size, _ := cmd.DBSize(context.TODO()).Result()
|
||||
rc.ResData = &vo.Keys{Cursor: cursorRes, Keys: kis, DbSize: size}
|
||||
}
|
||||
|
||||
func (r *Redis) DeleteKey(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
key := g.Query("key")
|
||||
// 校验查询参数中的key为必填项,并返回redis实例
|
||||
func (r *Redis) checkKeyAndGetRedisIns(rc *req.Ctx) (*application.RedisInstance, string) {
|
||||
key := rc.GinCtx.Query("key")
|
||||
biz.NotEmpty(key, "key不能为空")
|
||||
return r.getRedisIns(rc), key
|
||||
}
|
||||
|
||||
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
|
||||
func (r *Redis) getRedisIns(rc *req.Ctx) *application.RedisInstance {
|
||||
ri := r.RedisApp.GetRedisInstance(getIdAndDbNum(rc.GinCtx))
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
|
||||
|
||||
rc.ReqParam = fmt.Sprintf("%s -> 删除key: %s", ri.Info.GetLogDesc(), key)
|
||||
ri.GetCmdable().Del(context.Background(), key)
|
||||
return ri
|
||||
}
|
||||
|
||||
func (r *Redis) checkKey(rc *req.Ctx) (*application.RedisInstance, string) {
|
||||
g := rc.GinCtx
|
||||
key := g.Query("key")
|
||||
biz.NotEmpty(key, "key不能为空")
|
||||
|
||||
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
|
||||
|
||||
return ri, key
|
||||
}
|
||||
|
||||
func (r *Redis) GetStringValue(rc *req.Ctx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
str, err := ri.GetCmdable().Get(context.TODO(), key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取字符串值失败: %s")
|
||||
rc.ResData = str
|
||||
}
|
||||
|
||||
func (r *Redis) SetStringValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
keyValue := new(form.StringValue)
|
||||
ginx.BindJsonAndValid(g, keyValue)
|
||||
|
||||
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
|
||||
|
||||
rc.ReqParam = fmt.Sprintf("%s -> %s", ri.Info.GetLogDesc(), utils.ToString(keyValue))
|
||||
|
||||
str, err := ri.GetCmdable().Set(context.TODO(), keyValue.Key, keyValue.Value, time.Second*time.Duration(keyValue.Timed)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "保存字符串值失败: %s")
|
||||
rc.ResData = str
|
||||
}
|
||||
|
||||
func (r *Redis) Hscan(rc *req.Ctx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
g := rc.GinCtx
|
||||
count := ginx.QueryInt(g, "count", 10)
|
||||
match := g.Query("match")
|
||||
cursor := ginx.QueryInt(g, "cursor", 0)
|
||||
contextTodo := context.TODO()
|
||||
|
||||
cmdable := ri.GetCmdable()
|
||||
keys, nextCursor, err := cmdable.HScan(contextTodo, key, uint64(cursor), match, int64(count)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hcan err: %s")
|
||||
keySize, err := cmdable.HLen(contextTodo, key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hlen err: %s")
|
||||
|
||||
rc.ResData = map[string]interface{}{
|
||||
"keys": keys,
|
||||
"cursor": nextCursor,
|
||||
"keySize": keySize,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) Hdel(rc *req.Ctx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
field := rc.GinCtx.Query("field")
|
||||
|
||||
delRes, err := ri.GetCmdable().HDel(context.TODO(), key, field).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hdel err: %s")
|
||||
rc.ResData = delRes
|
||||
}
|
||||
|
||||
func (r *Redis) Hget(rc *req.Ctx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
field := rc.GinCtx.Query("field")
|
||||
|
||||
res, err := ri.GetCmdable().HGet(context.TODO(), key, field).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hget err: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) SetHashValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
hashValue := new(form.HashValue)
|
||||
ginx.BindJsonAndValid(g, hashValue)
|
||||
|
||||
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
|
||||
|
||||
cmd := ri.GetCmdable()
|
||||
key := hashValue.Key
|
||||
contextTodo := context.TODO()
|
||||
for _, v := range hashValue.Value {
|
||||
res := cmd.HSet(contextTodo, key, v["field"].(string), v["value"])
|
||||
biz.ErrIsNilAppendErr(res.Err(), "保存hash值失败: %s")
|
||||
}
|
||||
if hashValue.Timed != 0 && hashValue.Timed != -1 {
|
||||
cmd.Expire(context.TODO(), key, time.Second*time.Duration(hashValue.Timed))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) GetSetValue(rc *req.Ctx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
res, err := ri.GetCmdable().SMembers(context.TODO(), key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取set值失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) SetSetValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
keyvalue := new(form.SetValue)
|
||||
ginx.BindJsonAndValid(g, keyvalue)
|
||||
|
||||
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
|
||||
cmd := ri.GetCmdable()
|
||||
|
||||
key := keyvalue.Key
|
||||
// 简单处理->先删除,后新增
|
||||
cmd.Del(context.TODO(), key)
|
||||
cmd.SAdd(context.TODO(), key, keyvalue.Value...)
|
||||
|
||||
if keyvalue.Timed != -1 {
|
||||
cmd.Expire(context.TODO(), key, time.Second*time.Duration(keyvalue.Timed))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) GetListValue(rc *req.Ctx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
ctx := context.TODO()
|
||||
cmdable := ri.GetCmdable()
|
||||
|
||||
len, err := cmdable.LLen(ctx, key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取list长度失败: %s")
|
||||
|
||||
g := rc.GinCtx
|
||||
start := ginx.QueryInt(g, "start", 0)
|
||||
stop := ginx.QueryInt(g, "stop", 10)
|
||||
res, err := cmdable.LRange(ctx, key, int64(start), int64(stop)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取list值失败: %s")
|
||||
|
||||
rc.ResData = map[string]interface{}{
|
||||
"len": len,
|
||||
"list": res,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) SaveListValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
listValue := new(form.ListValue)
|
||||
ginx.BindJsonAndValid(g, listValue)
|
||||
|
||||
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
|
||||
cmd := ri.GetCmdable()
|
||||
|
||||
key := listValue.Key
|
||||
ctx := context.TODO()
|
||||
for _, v := range listValue.Value {
|
||||
cmd.RPush(ctx, key, v)
|
||||
}
|
||||
|
||||
if listValue.Timed != -1 {
|
||||
cmd.Expire(context.TODO(), key, time.Second*time.Duration(listValue.Timed))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) SetListValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
listSetValue := new(form.ListSetValue)
|
||||
ginx.BindJsonAndValid(g, listSetValue)
|
||||
|
||||
ri := r.RedisApp.GetRedisInstance(uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db"))
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.LoginAccount.Id, ri.Info.TagPath), "%s")
|
||||
|
||||
_, err := ri.GetCmdable().LSet(context.TODO(), listSetValue.Key, listSetValue.Index, listSetValue.Value).Result()
|
||||
biz.ErrIsNilAppendErr(err, "list set失败: %s")
|
||||
// 获取redis id与要操作的库号(统一路径)
|
||||
func getIdAndDbNum(g *gin.Context) (uint64, int) {
|
||||
return uint64(ginx.PathParamInt(g, "id")), ginx.PathParamInt(g, "db")
|
||||
}
|
||||
|
||||
78
server/internal/redis/api/set.go
Normal file
78
server/internal/redis/api/set.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/redis/api/form"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (r *Redis) GetSetValue(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
res, err := ri.GetCmdable().SMembers(context.TODO(), key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取set值失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) SetSetValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
keyvalue := new(form.SetValue)
|
||||
ginx.BindJsonAndValid(g, keyvalue)
|
||||
|
||||
cmd := r.getRedisIns(rc).GetCmdable()
|
||||
|
||||
key := keyvalue.Key
|
||||
// 简单处理->先删除,后新增
|
||||
cmd.Del(context.TODO(), key)
|
||||
cmd.SAdd(context.TODO(), key, keyvalue.Value...)
|
||||
|
||||
if keyvalue.Timed != -1 {
|
||||
cmd.Expire(context.TODO(), key, time.Second*time.Duration(keyvalue.Timed))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) Scard(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
|
||||
total, err := ri.GetCmdable().SCard(context.TODO(), key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "scard失败: %s")
|
||||
rc.ResData = total
|
||||
}
|
||||
|
||||
func (r *Redis) Sscan(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
scan := new(form.ScanForm)
|
||||
ginx.BindJsonAndValid(g, scan)
|
||||
|
||||
cmd := r.getRedisIns(rc).GetCmdable()
|
||||
keys, cursor, err := cmd.SScan(context.TODO(), scan.Key, scan.Cursor, scan.Match, scan.Count).Result()
|
||||
biz.ErrIsNilAppendErr(err, "sscan失败: %s")
|
||||
rc.ResData = map[string]interface{}{
|
||||
"keys": keys,
|
||||
"cursor": cursor,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) Sadd(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
option := new(form.SmemberOption)
|
||||
ginx.BindJsonAndValid(g, option)
|
||||
cmd := r.getRedisIns(rc).GetCmdable()
|
||||
|
||||
res, err := cmd.SAdd(context.TODO(), option.Key, option.Member).Result()
|
||||
biz.ErrIsNilAppendErr(err, "sadd失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) Srem(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
option := new(form.SmemberOption)
|
||||
ginx.BindJsonAndValid(g, option)
|
||||
|
||||
cmd := r.getRedisIns(rc).GetCmdable()
|
||||
res, err := cmd.SRem(context.TODO(), option.Key, option.Member).Result()
|
||||
biz.ErrIsNilAppendErr(err, "srem失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
33
server/internal/redis/api/string.go
Normal file
33
server/internal/redis/api/string.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/redis/api/form"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (r *Redis) GetStringValue(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
str, err := ri.GetCmdable().Get(context.TODO(), key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取字符串值失败: %s")
|
||||
rc.ResData = str
|
||||
}
|
||||
|
||||
func (r *Redis) SetStringValue(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
keyValue := new(form.StringValue)
|
||||
ginx.BindJsonAndValid(g, keyValue)
|
||||
|
||||
ri := r.getRedisIns(rc)
|
||||
cmd := ri.GetCmdable()
|
||||
rc.ReqParam = fmt.Sprintf("%s -> %s", ri.Info.GetLogDesc(), utils.ToString(keyValue))
|
||||
|
||||
str, err := cmd.Set(context.TODO(), keyValue.Key, keyValue.Value, time.Second*time.Duration(keyValue.Timed)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "保存字符串值失败: %s")
|
||||
rc.ResData = str
|
||||
}
|
||||
72
server/internal/redis/api/zset.go
Normal file
72
server/internal/redis/api/zset.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/redis/api/form"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func (r *Redis) ZCard(rc *req.Ctx) {
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
|
||||
total, err := ri.GetCmdable().ZCard(context.TODO(), key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "zcard失败: %s")
|
||||
rc.ResData = total
|
||||
}
|
||||
|
||||
func (r *Redis) ZScan(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
|
||||
cursor := uint64(ginx.QueryInt(g, "cursor", 0))
|
||||
match := ginx.Query(g, "match", "*")
|
||||
count := ginx.QueryInt(g, "count", 50)
|
||||
|
||||
keys, cursor, err := ri.GetCmdable().ZScan(context.TODO(), key, cursor, match, int64(count)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "sscan失败: %s")
|
||||
rc.ResData = map[string]interface{}{
|
||||
"keys": keys,
|
||||
"cursor": cursor,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) ZRevRange(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
ri, key := r.checkKeyAndGetRedisIns(rc)
|
||||
start := ginx.QueryInt(g, "start", 0)
|
||||
stop := ginx.QueryInt(g, "stop", 50)
|
||||
|
||||
res, err := ri.GetCmdable().ZRevRangeWithScores(context.TODO(), key, int64(start), int64(stop)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "ZRevRange失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) ZRem(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
option := new(form.SmemberOption)
|
||||
ginx.BindJsonAndValid(g, option)
|
||||
|
||||
cmd := r.getRedisIns(rc).GetCmdable()
|
||||
res, err := cmd.ZRem(context.TODO(), option.Key, option.Member).Result()
|
||||
biz.ErrIsNilAppendErr(err, "zrem失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) ZAdd(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
option := new(form.ZAddOption)
|
||||
ginx.BindJsonAndValid(g, option)
|
||||
|
||||
cmd := r.getRedisIns(rc).GetCmdable()
|
||||
zm := redis.Z{
|
||||
Score: option.Score,
|
||||
Member: option.Member,
|
||||
}
|
||||
res, err := cmd.ZAdd(context.TODO(), option.Key, zm).Result()
|
||||
biz.ErrIsNilAppendErr(err, "zadd失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
@@ -49,15 +49,47 @@ func InitRedisRouter(router *gin.RouterGroup) {
|
||||
req.NewCtxWithGin(c).Handle(rs.Scan)
|
||||
})
|
||||
|
||||
// 删除key
|
||||
deleteKeyL := req.NewLogInfo("redis-删除key").WithSave(true)
|
||||
deleteKeyP := req.NewPermission("redis:data:del")
|
||||
redis.DELETE(":id/:db/key", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).WithLog(deleteKeyL).WithRequiredPermission(deleteKeyP).Handle(rs.DeleteKey)
|
||||
redis.GET(":id/:db/key-ttl", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).Handle(rs.TtlKey)
|
||||
})
|
||||
|
||||
// 保存数据权限
|
||||
saveDataP := req.NewPermission("redis:data:save")
|
||||
// 删除数据权限
|
||||
deleteDataP := req.NewPermission("redis:data:del")
|
||||
|
||||
// 删除key
|
||||
deleteKeyL := req.NewLogInfo("redis-删除key").WithSave(true)
|
||||
redis.DELETE(":id/:db/key", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithLog(deleteKeyL).
|
||||
WithRequiredPermission(deleteDataP).
|
||||
Handle(rs.DeleteKey)
|
||||
})
|
||||
|
||||
renameKeyL := req.NewLogInfo("redis-重命名key").WithSave(true)
|
||||
redis.POST(":id/:db/rename-key", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithLog(renameKeyL).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.RenameKey)
|
||||
})
|
||||
|
||||
expireKeyL := req.NewLogInfo("redis-设置key过期时间").WithSave(true)
|
||||
redis.POST(":id/:db/expire-key", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithLog(expireKeyL).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.ExpireKey)
|
||||
})
|
||||
|
||||
persistKeyL := req.NewLogInfo("redis-移除key过期时间").WithSave(true)
|
||||
redis.DELETE(":id/:db/persist-key", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithLog(persistKeyL).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.PersistKey)
|
||||
})
|
||||
|
||||
// 获取string类型值
|
||||
redis.GET(":id/:db/string-value", func(c *gin.Context) {
|
||||
@@ -67,7 +99,10 @@ func InitRedisRouter(router *gin.RouterGroup) {
|
||||
// 设置string类型值
|
||||
setStringL := req.NewLogInfo("redis-setString").WithSave(true)
|
||||
redis.POST(":id/:db/string-value", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).WithLog(setStringL).WithRequiredPermission(saveDataP).Handle(rs.SetStringValue)
|
||||
req.NewCtxWithGin(c).
|
||||
WithLog(setStringL).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.SetStringValue)
|
||||
})
|
||||
|
||||
// hscan
|
||||
@@ -79,25 +114,60 @@ func InitRedisRouter(router *gin.RouterGroup) {
|
||||
req.NewCtxWithGin(c).Handle(rs.Hget)
|
||||
})
|
||||
|
||||
hsetL := req.NewLogInfo("redis-hset").WithSave(true)
|
||||
redis.POST(":id/:db/hset", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithLog(hsetL).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.Hset)
|
||||
})
|
||||
|
||||
hdelL := req.NewLogInfo("redis-hdel").WithSave(true)
|
||||
redis.DELETE(":id/:db/hdel", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).WithLog(hdelL).WithRequiredPermission(deleteKeyP).Handle(rs.Hdel)
|
||||
req.NewCtxWithGin(c).
|
||||
WithLog(hdelL).
|
||||
WithRequiredPermission(deleteDataP).
|
||||
Handle(rs.Hdel)
|
||||
})
|
||||
|
||||
// 设置hash类型值
|
||||
setHashValueL := req.NewLogInfo("redis-setHashValue").WithSave(true)
|
||||
redis.POST(":id/:db/hash-value", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).WithLog(setHashValueL).WithRequiredPermission(saveDataP).Handle(rs.SetHashValue)
|
||||
req.NewCtxWithGin(c).
|
||||
WithLog(setHashValueL).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.SetHashValue)
|
||||
})
|
||||
|
||||
// 获取set类型值
|
||||
// set操作
|
||||
redis.GET(":id/:db/set-value", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).Handle(rs.GetSetValue)
|
||||
})
|
||||
|
||||
// 设置set类型值
|
||||
redis.POST(":id/:db/set-value", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).WithRequiredPermission(saveDataP).Handle(rs.SetSetValue)
|
||||
req.NewCtxWithGin(c).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.SetSetValue)
|
||||
})
|
||||
|
||||
redis.GET(":id/:db/scard", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).Handle(rs.Scard)
|
||||
})
|
||||
|
||||
redis.POST(":id/:db/sscan", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).Handle(rs.Sscan)
|
||||
})
|
||||
|
||||
redis.POST(":id/:db/sadd", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.Sadd)
|
||||
})
|
||||
|
||||
redis.POST(":id/:db/srem", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithRequiredPermission(deleteDataP).
|
||||
Handle(rs.Srem)
|
||||
})
|
||||
|
||||
// 获取list类型值
|
||||
@@ -112,5 +182,36 @@ func InitRedisRouter(router *gin.RouterGroup) {
|
||||
redis.POST(":id/:db/list-value/lset", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).Handle(rs.SetListValue)
|
||||
})
|
||||
|
||||
redis.POST(":id/:db/lrem", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithRequiredPermission(deleteDataP).
|
||||
Handle(rs.Lrem)
|
||||
})
|
||||
|
||||
// zset操作
|
||||
redis.GET(":id/:db/zcard", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).Handle(rs.ZCard)
|
||||
})
|
||||
|
||||
redis.GET(":id/:db/zscan", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).Handle(rs.ZScan)
|
||||
})
|
||||
|
||||
redis.GET(":id/:db/zrevrange", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).Handle(rs.ZRevRange)
|
||||
})
|
||||
|
||||
redis.POST(":id/:db/zrem", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithRequiredPermission(deleteDataP).
|
||||
Handle(rs.ZRem)
|
||||
})
|
||||
|
||||
redis.POST(":id/:db/zadd", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).
|
||||
WithRequiredPermission(saveDataP).
|
||||
Handle(rs.ZAdd)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func InitAccountRouter(router *gin.RouterGroup) {
|
||||
loginLog := req.NewLogInfo("用户登录").WithSave(true)
|
||||
account.POST("login", func(g *gin.Context) {
|
||||
req.NewCtxWithGin(g).
|
||||
WithNeedToken(false).
|
||||
DontNeedToken().
|
||||
WithLog(loginLog).
|
||||
Handle(a.Login)
|
||||
})
|
||||
@@ -35,7 +35,7 @@ func InitAccountRouter(router *gin.RouterGroup) {
|
||||
changePwdLog := req.NewLogInfo("用户修改密码").WithSave(true)
|
||||
account.POST("change-pwd", func(g *gin.Context) {
|
||||
req.NewCtxWithGin(g).
|
||||
WithNeedToken(false).
|
||||
DontNeedToken().
|
||||
WithLog(changePwdLog).
|
||||
Handle(a.ChangePassword)
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ func InitCaptchaRouter(router *gin.RouterGroup) {
|
||||
captcha := router.Group("sys/captcha")
|
||||
{
|
||||
captcha.GET("", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).WithNeedToken(false).Handle(api.GenerateCaptcha)
|
||||
req.NewCtxWithGin(c).DontNeedToken().Handle(api.GenerateCaptcha)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func InitSysConfigRouter(router *gin.RouterGroup) {
|
||||
})
|
||||
|
||||
db.GET("/value", func(c *gin.Context) {
|
||||
req.NewCtxWithGin(c).WithNeedToken(false).Handle(r.GetConfigValueByKey)
|
||||
req.NewCtxWithGin(c).DontNeedToken().Handle(r.GetConfigValueByKey)
|
||||
})
|
||||
|
||||
saveConfig := req.NewLogInfo("保存系统配置信息").WithSave(true)
|
||||
|
||||
@@ -4,7 +4,7 @@ import "fmt"
|
||||
|
||||
const (
|
||||
AppName = "mayfly-go"
|
||||
Version = "v1.4.1"
|
||||
Version = "v1.4.2"
|
||||
)
|
||||
|
||||
func GetAppInfo() string {
|
||||
|
||||
@@ -31,6 +31,15 @@ func GetPageParam(g *gin.Context) *model.PageParam {
|
||||
return &model.PageParam{PageNum: QueryInt(g, "pageNum", 1), PageSize: QueryInt(g, "pageSize", 10)}
|
||||
}
|
||||
|
||||
// 获取查询参数,不存在则返回默认值
|
||||
func Query(g *gin.Context, qm string, defaultStr string) string {
|
||||
qv := g.Query(qm)
|
||||
if qv == "" {
|
||||
return defaultStr
|
||||
}
|
||||
return qv
|
||||
}
|
||||
|
||||
// 获取查询参数中指定参数值,并转为int
|
||||
func QueryInt(g *gin.Context, qm string, defaultInt int) int {
|
||||
qv := g.Query(qm)
|
||||
|
||||
@@ -83,7 +83,7 @@ func GetByIdIn(model interface{}, list interface{}, ids []uint64, orderBy ...str
|
||||
global.Db.Model(model).Where("id in (?)", ids).Order(orderByStr).Find(list)
|
||||
}
|
||||
|
||||
// 根据id列表查询
|
||||
// 根据map指定条件查询列表
|
||||
func SelectByMap(model interface{}, list interface{}, where map[string]interface{}, orderBy ...string) {
|
||||
var orderByStr string
|
||||
if orderBy == nil {
|
||||
@@ -94,7 +94,7 @@ func SelectByMap(model interface{}, list interface{}, where map[string]interface
|
||||
global.Db.Model(model).Where(where).Order(orderByStr).Find(list)
|
||||
}
|
||||
|
||||
// 根据id列表查询
|
||||
// 根据model指定条件统计数量
|
||||
func CountBy(model interface{}) int64 {
|
||||
var count int64
|
||||
global.Db.Model(model).Where(model).Count(&count)
|
||||
|
||||
@@ -20,11 +20,6 @@ func NewPermission(code string) *Permission {
|
||||
return &Permission{NeedToken: true, Code: code}
|
||||
}
|
||||
|
||||
func (p *Permission) WithNeedToken(needToken bool) *Permission {
|
||||
p.NeedToken = needToken
|
||||
return p
|
||||
}
|
||||
|
||||
var (
|
||||
permissionCodeRegistry PermissionCodeRegistry
|
||||
)
|
||||
|
||||
@@ -80,8 +80,8 @@ func (r *Ctx) WithRequiredPermission(permission *Permission) *Ctx {
|
||||
return r
|
||||
}
|
||||
|
||||
// 是否需要token
|
||||
func (r *Ctx) WithNeedToken(needToken bool) *Ctx {
|
||||
// 不需要token校验
|
||||
func (r *Ctx) DontNeedToken() *Ctx {
|
||||
r.RequiredPermission = &Permission{NeedToken: false}
|
||||
return r
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user