feat: 完善数据库信息保存以及项目、redis相关操作

This commit is contained in:
meilin.huang
2021-07-28 18:03:19 +08:00
parent 3ebc3ee14d
commit bda3920c1e
153 changed files with 5527 additions and 1017 deletions

View File

@@ -2,4 +2,4 @@
ENV = 'production'
# 线上环境接口地址
VITE_API_URL = 'http://localhost:8888/api'
VITE_API_URL = 'http://api.mayflygo.1yue.net/api'

View File

@@ -18,6 +18,7 @@
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./config.js"></script>
<script type="module" src="/src/main.ts"></script>
<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script> -->
</body>

View File

@@ -7,9 +7,9 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"core-js": "^3.6.5",
"axios": "^0.21.1",
"codemirror": "^5.61.0",
"core-js": "^3.6.5",
"countup.js": "^2.0.7",
"cropperjs": "^1.5.11",
"echarts": "^5.1.1",

View File

@@ -0,0 +1,3 @@
window.globalConfig = {
"BaseApiUrl": "http://localhost:8888/api"
}

View File

@@ -48,7 +48,7 @@ class Api {
* 操作该权限即请求对应的url
* @param {Object} param 请求该权限的参数
*/
request(param: any): Promise<any> {
request(param: any = null): Promise<any> {
return request.send(this, param);
}

View File

@@ -3,7 +3,7 @@
*/
class AssertError extends Error {
constructor(message: string) {
super(message); // (1)
super(message);
// 错误类名
this.name = "AssertError";
}

View File

@@ -1,5 +1,5 @@
const config = {
baseApiUrl: import.meta.env.VITE_API_URL
baseApiUrl: (window as any).globalConfig.BaseApiUrl
}
export default config

View File

@@ -2,7 +2,7 @@ import request from './request'
export default {
login: (param: any) => request.request('POST', '/sys/accounts/login', param, null),
captcha: () => request.request('GET', '/open/captcha', null, null),
captcha: () => request.request('GET', '/sys/captcha', null, null),
logout: (param: any) => request.request('POST', '/sys/accounts/logout/{token}', param, null),
getMenuRoute: (param: any) => request.request('Get', '/sys/resources/account', param, null)
}

View File

@@ -53,6 +53,14 @@ export default defineComponent({
type: String,
default: null,
},
height: {
type: String,
default: "500px",
},
width: {
type: String,
default: "auto",
},
canChangeMode: {
type: Boolean,
default: false,
@@ -165,15 +173,14 @@ export default defineComponent({
}
);
watch(
() => props.options,
(newValue, oldValue) => {
console.log('options change', newValue);
for (const key in newValue) {
coder.setOption(key, newValue[key]);
}
}
);
// watch(
// () => props.options,
// (newValue, oldValue) => {
// for (const key in newValue) {
// coder.setOption(key, newValue[key]);
// }
// }
// );
const init = () => {
if (props.options) {
@@ -195,6 +202,9 @@ export default defineComponent({
}
});
coder.setSize(props.width, props.height);
// editor.setSize('width','height');
// 修改编辑器的语法配置
setMode(language.value);
@@ -285,6 +295,7 @@ export default defineComponent({
coder.setValue(newVal);
state.content = newVal;
coder.scrollTo(scrollInfo.left, scrollInfo.top);
refresh()
}
};
@@ -292,6 +303,7 @@ export default defineComponent({
...toRefs(state),
textarea,
changeMode,
refresh,
};
},
});

View File

@@ -85,7 +85,7 @@ export default defineComponent({
});
const submit = () => {
dynamicForm.validate((valid: boolean) => {
dynamicForm.value.validate((valid: boolean) => {
if (valid) {
// 提交的表单数据
const subform = { ...state.form };

View File

@@ -8,5 +8,9 @@ export const imports = {
"ResourceList": () => import('@/views/system/resource'),
"RoleList": () => import('@/views/system/role'),
"AccountList": () => import('@/views/system/account'),
"SelectData": () => import('@/views/ops/db'),
"ProjectList": () => import('@/views/ops/project/ProjectList.vue'),
"DbList": () => import('@/views/ops/db/DbList.vue'),
"SqlExec": () => import('@/views/ops/db'),
"RedisList": () => import('@/views/ops/redis'),
"DataOperation": () => import('@/views/ops/redis/DataOperation.vue'),
}

View File

@@ -14,8 +14,8 @@ body,
padding: 0;
width: 100%;
height: 100%;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
font-weight: 500;
font-family: Microsoft YaHei, Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, SimSun, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
@@ -274,7 +274,7 @@ body,
.toolbar {
width: 100%;
padding: 8px;
padding: 6px;
background-color: #ffffff;
overflow: hidden;
line-height: 32px;
@@ -283,4 +283,10 @@ body,
.fl {
float: left;
}
.search-form {
.el-form-item {
margin-bottom: 3px;
}
}

View File

@@ -4,10 +4,10 @@
<div class="left">
<div class="left-item">
<div class="left-item-animation left-item-num">401</div>
<div class="left-item-animation left-item-title">您未被授权没有操作权限</div>
<div class="left-item-animation left-item-title">您未被授权或登录超时没有操作权限</div>
<div class="left-item-animation left-item-msg"></div>
<div class="left-item-animation left-item-btn">
<el-button type="primary" round @click="onSetAuth">重新授权</el-button>
<el-button type="primary" round @click="onSetAuth">重新登录</el-button>
</div>
</div>
</div>

View File

@@ -7,8 +7,8 @@
<img :src="getUserInfos.photo" />
<div class="home-card-first-right ml15">
<div class="flex-margin">
<div class="home-card-first-right-title">{{ currentTime }}admin</div>
<div class="home-card-first-right-msg mt5">超级管理</div>
<div class="home-card-first-right-title">{{ `${currentTime}, ${getUserInfos.username}` }}</div>
<!-- <div class="home-card-first-right-msg mt5">超级管理</div> -->
</div>
</div>
</div>

View File

@@ -1,10 +1,10 @@
<template>
<el-form class="login-content-form">
<el-form-item>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" class="login-content-form">
<el-form-item prop="username">
<el-input type="text" placeholder="请输入用户名" prefix-icon="el-icon-user" v-model="loginForm.username" clearable autocomplete="off">
</el-input>
</el-form-item>
<el-form-item>
<el-form-item prop="password">
<el-input
type="password"
placeholder="请输入密码"
@@ -15,29 +15,36 @@
>
</el-input>
</el-form-item>
<el-form-item>
<el-form-item prop="captcha">
<el-row :gutter="15">
<el-col :span="16">
<el-input
type="text"
maxlength="4"
maxlength="6"
placeholder="请输入验证码"
prefix-icon="el-icon-position"
v-model="loginForm.code"
v-model="loginForm.captcha"
clearable
autocomplete="off"
@keyup.enter="onSignIn"
@keyup.enter="login"
></el-input>
</el-col>
<el-col :span="8">
<div class="login-content-code">
<span class="login-content-code-img">1234</span>
<img
class="login-content-code-img"
@click="getCaptcha"
width="130px"
height="40px"
:src="captchaImage"
style="cursor: pointer"
/>
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-button type="primary" class="login-content-submit" round @click="onSignIn" :loading="loading.signIn">
<el-button type="primary" class="login-content-submit" round @click="login" :loading="loading.signIn">
<span> </span>
</el-button>
</el-form-item>
@@ -45,7 +52,7 @@
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent, computed } from 'vue';
import { onMounted, ref, toRefs, reactive, defineComponent, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { initAllFun, initBackEndControlRoutesFun } from '@/router/index.ts';
@@ -60,22 +67,51 @@ export default defineComponent({
const store = useStore();
const route = useRoute();
const router = useRouter();
const loginFormRef: any = ref(null);
const state = reactive({
captchaImage: '',
loginForm: {
username: 'test',
password: '123456',
code: '1234',
captcha: '',
cid: '',
},
rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
},
loading: {
signIn: false,
},
});
onMounted(() => {
getCaptcha();
});
const getCaptcha = async () => {
let res: any = await openApi.captcha();
state.captchaImage = res.base64Captcha;
state.loginForm.cid = res.cid;
};
// 时间获取
const currentTime = computed(() => {
return formatAxis(new Date());
});
// 校验登录表单并登录
const login = () => {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
onSignIn();
} else {
return false;
}
});
};
// 登录
const onSignIn = async () => {
state.loading.signIn = true;
@@ -87,6 +123,8 @@ export default defineComponent({
setSession('menus', loginRes.menus);
} catch (e) {
state.loading.signIn = false;
state.loginForm.captcha = '';
getCaptcha();
return;
}
// 用户信息模拟数据
@@ -132,9 +170,12 @@ export default defineComponent({
ElMessage.success(`${currentTimeInfo},欢迎回来!`);
}, 300);
};
return {
getCaptcha,
currentTime,
onSignIn,
loginFormRef,
login,
...toRefs(state),
};
},

View File

@@ -0,0 +1,84 @@
<template>
<div>
<el-form class="search-form" label-position="right" :inline="true" label-width="60px" size="small">
<el-form-item prop="project" label="项目" label-width="40px">
<el-select v-model="projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="env" label="环境" label-width="40px">
<el-select style="width: 100px" v-model="envId" placeholder="环境" @change="changeEnv" filterable>
<el-option v-for="item in envs" :key="item.id" :label="item.name" :value="item.id">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.remark }}</span>
</el-option>
</el-select>
</el-form-item>
<slot></slot>
</el-form>
</div>
</template>
<script lang="ts">
import { ref, toRefs, reactive, watch, defineComponent, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { notEmpty } from '@/common/assert';
import { projectApi } from '../project/api';
export default defineComponent({
name: 'ProjectEnvSelect',
props: {
visible: {
type: Boolean,
},
data: {
type: Object,
},
title: {
type: String,
},
machineId: {
type: Number,
},
isCommon: {
type: Boolean,
},
},
setup(props: any, { emit }) {
const state = reactive({
projects: [] as any,
envs: [] as any,
projectId: null,
envId: null,
});
watch(props, (newValue, oldValue) => {});
onMounted(async () => {
state.projects = await projectApi.accountProjects.request(null);
});
const changeProject = async (projectId: any) => {
emit('update:projectId', projectId);
emit('changeProjectEnv', state.projectId, null);
state.envId = null;
state.envs = await projectApi.projectEnvs.request({ projectId });
};
const changeEnv = (envId: any) => {
emit('update:envId', envId);
emit('changeProjectEnv', state.projectId, envId);
};
return {
...toRefs(state),
changeProject,
changeEnv,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,240 @@
<template>
<div>
<el-dialog :title="title" v-model="visible" :show-close="false" :before-close="cancel" width="35%">
<el-form :model="form" ref="dbForm" :rules="rules" label-width="85px" size="small">
<el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="envId" label="环境:" required>
<el-select @change="changeEnv" style="width: 100%" v-model="form.envId" placeholder="请选择环境">
<el-option v-for="item in envs" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="name" label="别名:" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型:" required>
<el-select style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option key="item.id" label="mysql" value="mysql"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-input v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="port" label="port:" required>
<el-input type="number" v-model.trim="form.port" placeholder="请输入端口"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:" required>
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码"
autocomplete="new-password"
></el-input>
</el-form-item>
<el-form-item prop="database" label="数据库名:" required>
<el-input v-model.trim="form.database" placeholder="请输入数据库名"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="btnLoading" @click="btnOk" size="mini"> </el-button>
<el-button @click="cancel()" size="mini"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
import { dbApi } from './api';
import { projectApi } from '../project/api.ts';
import { ElMessage } from 'element-plus';
export default defineComponent({
name: 'DbEdit',
props: {
visible: {
type: Boolean,
},
projects: {
type: Array,
},
db: {
type: [Boolean, Object],
},
title: {
type: String,
},
},
setup(props: any, { emit }) {
const dbForm: any = ref(null);
const state = reactive({
visible: false,
projects: [],
envs: [],
form: {
id: null,
name: null,
port: 3306,
username: null,
password: null,
project: null,
projectId: null,
envId: null,
env: null,
},
btnLoading: false,
rules: {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入别名',
trigger: ['change', 'blur'],
},
],
type: [
{
required: true,
message: '请选择数据库类型',
trigger: ['change', 'blur'],
},
],
host: [
{
required: true,
message: '请输入主机ip',
trigger: ['change', 'blur'],
},
],
port: [
{
required: true,
message: '请输入端口',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['change', 'blur'],
},
],
database: [
{
required: true,
message: '请输入数据库名',
trigger: ['change', 'blur'],
},
],
},
});
watch(props, async (newValue, oldValue) => {
state.visible = newValue.visible;
state.projects = newValue.projects;
if (newValue.db) {
getEnvs(newValue.db.projectId);
state.form = { ...newValue.db };
} else {
state.envs = [];
state.form = { port: 3306 } as any;
}
});
const getEnvs = async (projectId: any) => {
state.envs = await projectApi.projectEnvs.request({ projectId });
};
const changeProject = (projectId: number) => {
for (let p of state.projects as any) {
if (p.id == projectId) {
state.form.project = p.name;
}
}
state.envs = [];
getEnvs(projectId);
};
const changeEnv = (envId: number) => {
for (let p of state.envs as any) {
if (p.id == envId) {
state.form.env = p.name;
}
}
};
const btnOk = async () => {
dbForm.value.validate((valid: boolean) => {
if (valid) {
dbApi.saveDb.request(state.form).then((res: any) => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
dbForm.value.resetFields();
// 重置对象属性为null
state.form = {} as any;
}, 200);
};
return {
...toRefs(state),
dbForm,
changeProject,
changeEnv,
btnOk,
cancel,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,196 @@
<template>
<div class="db-list">
<div class="toolbar">
<el-row>
<el-col>
<el-form class="search-form" label-position="right" :inline="true" label-width="60px" size="small">
<el-form-item prop="project" label="项目">
<el-select v-model="query.projectId" placeholder="请选择项目" filterable clearable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="数据库">
<el-input v-model="query.database" auto-complete="off" clearable></el-input>
</el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="search()">查询</el-button>
</el-form>
</el-col>
</el-row>
<el-row class="mt5">
<el-col>
<el-button v-auth="permissions.saveDb" type="primary" icon="el-icon-plus" size="mini" @click="editDb(true)">添加</el-button>
<el-button
v-auth="permissions.saveDb"
:disabled="chooseId == null"
@click="editDb(false)"
type="primary"
icon="el-icon-edit"
size="mini"
>编辑</el-button
>
<el-button
v-auth="permissions.delDb"
:disabled="chooseId == null"
@click="deleteDb(chooseId)"
type="danger"
icon="el-icon-delete"
size="mini"
>删除</el-button
>
</el-col>
</el-row>
</div>
<el-table :data="datas" border ref="table" @current-change="choose" show-overflow-tooltip>
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="project" label="项目" min-width="100"></el-table-column>
<el-table-column prop="env" label="环境" min-width="100"></el-table-column>
<el-table-column prop="name" label="名称" min-width="200"></el-table-column>
<el-table-column min-width="160" label="host:port">
<template #default="scope">
{{ `${scope.row.host}:${scope.row.port}` }}
</template>
</el-table-column>
<el-table-column prop="type" label="类型" min-width="80"></el-table-column>
<el-table-column prop="database" label="数据库" min-width="120"></el-table-column>
<el-table-column prop="username" label="用户名" min-width="100"></el-table-column>
<el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column>
<el-table-column min-width="160" prop="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-pagination
@current-change="handlePageChange"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="total"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
/>
<db-edit @val-change="valChange" :projects="projects" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
</div>
</template>
<script lang='ts'>
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
import DbEdit from './DbEdit.vue';
import { dbApi } from './api';
import { projectApi } from '../project/api.ts';
export default defineComponent({
name: 'DbList',
components: {
ProjectEnvSelect,
DbEdit,
},
setup() {
const state = reactive({
permissions: {
saveDb: 'db:save',
delDb: 'db:del',
},
projects: [],
chooseId: null,
/**
* 选中的数据
*/
chooseData: null,
/**
* 查询条件
*/
query: {
pageNum: 1,
pageSize: 10,
},
datas: [],
total: 0,
dbEditDialog: {
visible: false,
data: null,
title: '新增数据库',
},
});
onMounted(async () => {
search();
state.projects = (await projectApi.projects.request({ pageNum: 1, pageSize: 100 })).list;
});
const choose = (item: any) => {
if (!item) {
return;
}
state.chooseId = item.id;
state.chooseData = item;
};
const search = async () => {
let res: any = await dbApi.dbs.request(state.query);
state.datas = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const editDb = (isAdd = false) => {
if (isAdd) {
state.dbEditDialog.data = null;
state.dbEditDialog.title = '新增数据库';
} else {
state.dbEditDialog.data = state.chooseData;
state.dbEditDialog.title = '修改数据库';
}
state.dbEditDialog.visible = true;
};
const valChange = () => {
search();
};
const deleteDb = async (id: number) => {
try {
await ElMessageBox.confirm(`确定删除该库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDb.request({ id });
ElMessage.success('删除成功');
state.chooseData = null;
state.chooseId = null;
search();
} catch (err) {}
};
return {
...toRefs(state),
// enums,
search,
choose,
handlePageChange,
editDb,
valChange,
deleteDb,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -1,15 +1,28 @@
<template>
<div>
<div class="toolbar">
<div class="fl">
<el-select size="small" v-model="dbId" placeholder="请选择数据库" @change="changeDb" @clear="clearDb" clearable filterable>
<el-option v-for="item in dbs" :key="item.id" :label="`${item.name} [${dbTypeName(item.type)}]`" :value="item.id"> </el-option>
</el-select>
</div>
<el-row type="flex" justify="space-between">
<el-col :span="24">
<project-env-select @changeProjectEnv="changeProjectEnv" @clear="clearDb">
<template #default>
<el-form-item label="数据库">
<el-select v-model="dbId" placeholder="请选择数据库" @change="changeDb" @clear="clearDb" clearable filterable>
<el-option v-for="item in dbs" :key="item.id" :label="item.database" :value="item.id">
<span style="float: left">{{ item.database }}</span>
<span style="float: right; color: #8492a6; margin-left: 6px; font-size: 13px">{{
`${item.name} [${item.type}]`
}}</span>
</el-option>
</el-select>
</el-form-item>
</template>
</project-env-select>
</el-col>
</el-row>
</div>
<el-container style="height: 50%; border: 1px solid #eee; margin-top: 1px">
<el-aside width="70%" style="background-color: rgb(238, 241, 246)">
<el-container style="border: 1px solid #eee; margin-top: 1px">
<el-aside id="sqlcontent" width="65%" style="background-color: rgb(238, 241, 246)">
<div class="toolbar">
<div class="fl">
<el-button @click="runSql" type="success" icon="el-icon-video-play" size="mini" plain>执行</el-button>
@@ -19,12 +32,12 @@
<el-button @click="saveSql" type="primary" icon="el-icon-document-add" size="mini" plain>保存</el-button>
</div>
</div>
<codemirror class="codesql" ref="cmEditor" language="sql" v-model="sql" :options="cmOptions" />
<codemirror @beforeChange="onBeforeChange" class="codesql" ref="cmEditor" language="sql" v-model="sql" :options="cmOptions" />
</el-aside>
<el-container style="margin-left: 2px">
<el-header style="text-align: left; height: 45px; font-size: 12px; padding: 0px">
<el-select v-model="tableName" placeholder="请选择表" @change="changeTable" clearable filterable style="width: 99%">
<el-select v-model="tableName" placeholder="请选择表" @change="changeTable" filterable style="width: 99%">
<el-option
v-for="item in tableMetadata"
:key="item.tableName"
@@ -35,20 +48,22 @@
</el-select>
</el-header>
<el-main style="padding: 0px; height: 100%; overflow: hidden">
<el-main style="padding: 0px; overflow: hidden">
<el-table :data="columnMetadata" height="100%" size="mini">
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
<el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
</el-table>
</el-main>
</el-container>
</el-container>
<el-table style="margin-top: 1px" :data="selectRes.data" size="mini" max-height="300" stripe border>
<el-table style="margin-top: 1px" :data="execRes.data" size="mini" max-height="300" :empty-text="execRes.emptyResText" stripe border>
<el-table-column
min-width="92"
min-width="100"
:width="flexColumnWidth(item, execRes.data)"
align="center"
v-for="item in selectRes.tableColumn"
v-for="item in execRes.tableColumn"
:key="item"
:prop="item"
:label="item"
@@ -56,20 +71,11 @@
>
</el-table-column>
</el-table>
<!-- <el-pagination
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="data.total"
:current-page.sync="params.pageNum"
:page-size="params.pageSize"
/> -->
</div>
</template>
<script lang="ts">
import { toRefs, reactive, computed, onMounted, defineComponent, ref } from 'vue';
import { toRefs, reactive, computed, defineComponent, ref } from 'vue';
import { dbApi } from './api';
import 'codemirror/theme/ambiance.css';
@@ -79,48 +85,56 @@ import 'codemirror/lib/codemirror.css';
// options
import 'codemirror/theme/base16-light.css';
// require('codemirror/addon/edit/matchbrackets')
import 'codemirror/addon/selection/active-line';
import { codemirror } from '@/components/codemirror';
// import 'codemirror/mode/sql/sql.js';
// import 'codemirror/addon/hint/show-hint.js';
// import 'codemirror/addon/hint/sql-hint.js';
import 'codemirror/addon/hint/show-hint.js';
import 'codemirror/addon/hint/sql-hint.js';
import sqlFormatter from 'sql-formatter';
import { notEmpty } from '@/common/assert';
import { notNull, notEmpty } from '@/common/assert';
import { ElMessage } from 'element-plus';
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
export default defineComponent({
name: 'SelectData',
name: 'SqlExec',
components: {
codemirror,
ProjectEnvSelect,
},
setup() {
const cmEditor: any = ref(null);
const state = reactive({
dbs: [],
tables: [],
dbId: '',
dbId: null,
tableName: '',
tableMetadata: [],
columnMetadata: [],
sql: '',
selectRes: {
sqlTabs: {
tabs: [] as any,
active: '',
index: 1,
},
execRes: {
tableColumn: [],
data: [],
emptyResText: '没有数据',
},
params: {
pageNum: 1,
pageSize: 10,
envId: null,
},
cmOptions: {
tabSize: 4,
mode: 'text/x-sql',
// theme: 'cobalt',
lineNumbers: true,
line: true,
indentWithTabs: true,
smartIndent: true,
// matchBrackets: true,
matchBrackets: true,
theme: 'base16-light',
autofocus: true,
extraKeys: { Tab: 'autocomplete' }, //
@@ -137,14 +151,19 @@ export default defineComponent({
return cmEditor.value.coder;
});
const dbTypeName = (type: any) => {
return 'mysql';
/**
* 项目及环境更改后的回调事件
*/
const changeProjectEnv = (projectId: any, envId: any) => {
state.dbs = [];
state.dbId = null;
clearDb();
if (envId != null) {
state.params.envId = envId;
search();
}
};
onMounted(() => {
search();
});
/**
* 输入字符给提示
*/
@@ -154,29 +173,92 @@ export default defineComponent({
}
};
const onBeforeChange = (instance: any, changeObj: any) => {
var text = changeObj.text[0];
// sql
changeObj.text[0] = text.split(' ')[0];
};
/**
* 执行sql
*/
const runSql = async () => {
notEmpty(state.dbId, '请先选择数据库');
notNull(state.dbId, '请先选择数据库');
//
let selectSql = getSql();
notEmpty(selectSql, '内容不能为空');
const res = await dbApi.selectData.request({
let sql = getSql();
notNull(sql, '内容不能为空');
state.execRes.tableColumn = [];
state.execRes.data = [];
state.execRes.emptyResText = '查询中...';
const res = await dbApi.sqlExec.request({
id: state.dbId,
selectSql: selectSql,
sql: sql,
});
let tableColumn: any;
let data;
if (res.length > 0) {
tableColumn = Object.keys(res[0]);
data = res;
} else {
tableColumn = [];
data = [];
state.execRes.emptyResText = '没有数据';
state.execRes.tableColumn = res.colNames;
state.execRes.data = res.res;
};
const flexColumnWidth = (str: any, tableData: any, flag = 'equal') => {
// str();tableData();
// flag,'max''equal','max'
// flag'max',flag'equal'
str = str + '';
let columnContent = '';
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return;
}
state.selectRes.tableColumn = tableColumn;
state.selectRes.data = data;
if (!str || !str.length || str.length === 0 || str === undefined) {
return;
}
if (flag === 'equal') {
// ()
for (let i = 0; i < tableData.length; i++) {
if (tableData[i][str].length > 0) {
columnContent = tableData[i][str];
break;
}
}
} else {
// ()
let index = 0;
for (let i = 0; i < tableData.length; i++) {
if (tableData[i][str] === null) {
return;
}
const now_temp = tableData[i][str] + '';
const max_temp = tableData[index][str] + '';
if (now_temp.length > max_temp.length) {
index = i;
}
}
columnContent = tableData[index][str];
}
//
let flexWidth = 0;
for (const char of columnContent) {
if ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z')) {
// 8
flexWidth += 8;
} else if (char >= '\u4e00' && char <= '\u9fa5') {
// 15
flexWidth += 15;
} else {
// 8
flexWidth += 8;
}
}
if (flexWidth < 80) {
//
flexWidth = 80;
}
if (flexWidth > 350) {
//
flexWidth = 350;
}
return flexWidth + 'px';
};
/**
@@ -193,7 +275,7 @@ export default defineComponent({
const saveSql = async () => {
notEmpty(state.sql, 'sql内容不能为空');
notEmpty(state.dbId, '请先选择数据库');
notNull(state.dbId, '请先选择数据库');
await dbApi.saveSql.request({ id: state.dbId, sql: state.sql, type: 1 });
ElMessage.success('保存成功');
};
@@ -235,9 +317,10 @@ export default defineComponent({
state.tableName = '';
state.tableMetadata = [];
state.columnMetadata = [];
state.selectRes.data = [];
state.selectRes.tableColumn = [];
state.execRes.data = [];
state.execRes.tableColumn = [];
state.sql = '';
state.cmOptions.hintOptions.tables = [];
};
//
@@ -268,7 +351,7 @@ export default defineComponent({
codemirror.value.replaceSelection(sqlFormatter.format(selectSql));
} else {
/* 将sql内容进行格式后放入编辑器中*/
codemirror.value.setValue(sqlFormatter.format(state.sql));
state.sql = sqlFormatter.format(sqlFormatter.format(state.sql));
}
};
@@ -280,14 +363,16 @@ export default defineComponent({
return {
...toRefs(state),
cmEditor,
dbTypeName,
changeProjectEnv,
inputRead,
changeTable,
runSql,
flexColumnWidth,
saveSql,
changeDb,
clearDb,
formatSql,
onBeforeChange,
};
},
});
@@ -298,4 +383,9 @@ export default defineComponent({
font-size: 10pt;
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
}
#sqlcontent {
.CodeMirror {
height: 300px !important;
}
}
</style>

View File

@@ -3,11 +3,13 @@ import Api from '@/common/Api';
export const dbApi = {
// 获取权限列表
dbs: Api.create("/dbs", 'get'),
saveDb: Api.create("/dbs", 'post'),
deleteDb: Api.create("/dbs/{id}", 'delete'),
tableMetadata: Api.create("/dbs/{id}/t-metadata", 'get'),
columnMetadata: Api.create("/dbs/{id}/c-metadata", 'get'),
// 获取表即列提示
hintTables: Api.create("/dbs/{id}/hint-tables", 'get'),
selectData: Api.create("/dbs/{id}/select", 'get'),
sqlExec: Api.create("/dbs/{id}/exec-sql", 'get'),
// 保存sql
saveSql: Api.create("/dbs/{id}/sql", 'post'),
// 获取保存的sql

View File

@@ -1 +1 @@
export { default } from './SelectData.vue';
export { default } from './SqlExec.vue';

View File

@@ -0,0 +1,182 @@
<template>
<div>
<el-dialog :title="title" v-model="visible" :show-close="false" :before-close="cancel" width="35%">
<el-form :model="form" ref="machineForm" :rules="rules" label-width="85px" size="small">
<!-- <el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="envId" label="环境:" required>
<el-select @change="changeEnv" style="width: 100%" v-model="form.envId" placeholder="请选择环境">
<el-option v-for="item in envs" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item> -->
<el-form-item prop="name" label="名称:" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="ip" label="ip:" required>
<el-input v-model.trim="form.ip" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="port" label="port:" required>
<el-input type="number" v-model.trim="form.port" placeholder="请输入端口"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:" required>
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码"
autocomplete="new-password"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="btnLoading" @click="btnOk" size="mini"> </el-button>
<el-button @click="cancel()" size="mini"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, onMounted, defineComponent, ref } from 'vue';
import { machineApi } from './api';
import { projectApi } from '../project/api.ts';
import { ElMessage } from 'element-plus';
export default defineComponent({
name: 'MachineEdit',
props: {
visible: {
type: Boolean,
},
machine: {
type: [Boolean, Object],
},
title: {
type: String,
},
},
setup(props: any, { emit }) {
const machineForm: any = ref(null);
const state = reactive({
visible: false,
form: {
id: null,
name: null,
port: 22,
username: null,
password: null,
},
btnLoading: false,
rules: {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入别名',
trigger: ['change', 'blur'],
},
],
ip: [
{
required: true,
message: '请输入主机ip',
trigger: ['change', 'blur'],
},
],
port: [
{
required: true,
message: '请输入端口',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['change', 'blur'],
},
],
},
});
watch(props, async (newValue, oldValue) => {
state.visible = newValue.visible;
if (newValue.machine) {
state.form = { ...newValue.machine };
} else {
state.form = { port: 22 } as any;
}
});
const btnOk = async () => {
machineForm.value.validate((valid: boolean) => {
if (valid) {
machineApi.saveMachine.request(state.form).then((res: any) => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
machineForm.value.resetFields();
// 重置对象属性为null
state.form = {} as any;
}, 200);
};
return {
...toRefs(state),
machineForm,
btnOk,
cancel,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -42,16 +42,19 @@
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width></el-table-column>
<el-table-column prop="ip" label="IP" width></el-table-column>
<el-table-column prop="port" label="端口" :min-width="40"></el-table-column>
<el-table-column prop="username" label="用户名" :min-width="40"></el-table-column>
<el-table-column prop="createTime" label="创建时间" :min-width="100">
<el-table-column prop="ip" label="ip:port" min-width="160">
<template #default="scope">
{{ `${scope.row.ip}:${scope.row.port}` }}
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" :min-width="45"></el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="160">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建者" :min-width="50"></el-table-column>
<el-table-column prop="updateTime" label="更新时间" :min-width="100">
<el-table-column prop="creator" label="创建者" min-width="50"></el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="160">
<template #default="scope">
{{ $filters.dateFormat(scope.row.updateTime) }}
</template>
@@ -59,7 +62,7 @@
<el-table-column prop="modifier" label="修改者" :min-width="50"></el-table-column>
<el-table-column label="操作" min-width="200px">
<template #default="scope">
<el-button type="primary" @click="monitor(scope.row.id)" icom="el-icon-tickets" size="mini" plain>监控</el-button>
<!-- <el-button type="primary" @click="monitor(scope.row.id)" icom="el-icon-tickets" size="mini" plain>监控</el-button> -->
<el-button type="success" @click="serviceManager(scope.row)" size="mini" plain>脚本管理</el-button>
<el-button v-auth="'machine:terminal'" type="success" @click="showTerminal(scope.row)" size="mini" plain>终端</el-button>
</template>
@@ -75,6 +78,12 @@
:page-size="params.pageSize"
/>
<machine-edit
:title="machineEditDialog.title"
v-model:visible="machineEditDialog.visible"
v-model:machine="machineEditDialog.data"
></machine-edit>
<!-- <el-dialog @close="closeMonitor" title="监控信息" v-model="monitorDialog.visible" width="60%">
<monitor ref="monitorDialogRef" :machineId="monitorDialog.machineId" />
</el-dialog> -->
@@ -82,40 +91,32 @@
<service-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
<file-manage :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
<dynamic-form-dialog
v-model:visible="formDialog.visible"
:title="formDialog.title"
:formInfo="formDialog.formInfo"
v-model:formData="formDialog.formData"
@submitSuccess="submitSuccess"
></dynamic-form-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import { DynamicFormDialog } from '@/components/dynamic-form';
// import Monitor from './Monitor.vue';
import { machineApi } from './api';
import SshTerminal from './SshTerminal.vue';
import ServiceManage from './ServiceManage.vue';
import FileManage from './FileManage.vue';
import MachineEdit from './MachineEdit.vue';
export default defineComponent({
name: 'MachineList',
components: {
// Monitor,
SshTerminal,
ServiceManage,
FileManage,
DynamicFormDialog,
MachineEdit,
},
setup() {
const router = useRouter();
// const monitorDialogRef = ref();
const state = reactive({
params: {
pageNum: 1,
@@ -149,86 +150,10 @@ export default defineComponent({
visible: false,
machineId: 0,
},
formDialog: {
machineEditDialog: {
visible: false,
title: '',
formInfo: {
createApi: machineApi.save,
updateApi: machineApi.save,
formRows: [
[
{
type: 'input',
label: '名称:',
name: 'name',
placeholder: '请输入名称',
rules: [
{
required: true,
message: '请输入名称',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: 'ip',
name: 'ip',
placeholder: '请输入ip',
rules: [
{
required: true,
message: '请输入ip',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '端口号:',
name: 'port',
placeholder: '请输入端口号',
inputType: 'number',
rules: [
{
required: true,
message: '请输入ip',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '用户名:',
name: 'username',
placeholder: '请输入用户名',
rules: [
{
required: true,
message: '请输入用户名',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '密码:',
name: 'password',
placeholder: '请输入密码',
inputType: 'password',
},
],
],
},
formData: { port: 22 },
data: null,
title: '新增机器',
},
});
@@ -261,7 +186,6 @@ export default defineComponent({
// };
const showTerminal = (row: any) => {
// router.push(`/machine/${row.id}/terminal?id=${row.id}&name=${row.name}&time=${new Date().getTime()}`);
const { href } = router.resolve({
path: `/machine/terminal`,
query: {
@@ -275,21 +199,30 @@ export default defineComponent({
const openFormDialog = (redis: any) => {
let dialogTitle;
if (redis) {
state.formDialog.formData = state.currentData as any;
state.machineEditDialog.data = state.currentData as any;
dialogTitle = '编辑机器';
} else {
state.formDialog.formData = { port: 22 };
state.machineEditDialog.data = { port: 22 } as any;
dialogTitle = '添加机器';
}
state.formDialog.title = dialogTitle;
state.formDialog.visible = true;
state.machineEditDialog.title = dialogTitle;
state.machineEditDialog.visible = true;
};
const deleteMachine = async (id: number) => {
await machineApi.del.request({ id });
ElMessage.success('操作成功');
search();
try {
await ElMessageBox.confirm(`确定删除该机器信息? 该操作将同时删除脚本及文件配置信息`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await machineApi.del.request({ id });
ElMessage.success('操作成功');
state.currentId = null;
state.currentData = null;
search();
} catch (err) {}
};
const serviceManager = (row: any) => {

View File

@@ -24,6 +24,10 @@
</el-select>
</el-form-item>
<el-form-item prop="params" label="参数">
<el-input v-model.trim="form.params" placeholder="参数数组json若无可不填"></el-input>
</el-form-item>
<el-form-item prop="script" label="内容" id="content">
<codemirror ref="cmEditor" v-model="form.script" language="shell" />
</el-form-item>
@@ -31,7 +35,15 @@
<template #footer>
<div class="dialog-footer">
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" size="mini" :disabled="submitDisabled"> </el-button>
<el-button
v-auth="'machine:script:save'"
type="primary"
:loading="btnLoading"
@click="btnOk"
size="mini"
:disabled="submitDisabled"
> </el-button
>
<el-button @click="cancel()" :disabled="submitDisabled" size="mini"> </el-button>
</div>
</template>
@@ -83,6 +95,7 @@ export default defineComponent({
machineId: 0,
description: '',
script: '',
params: null,
type: null,
},
btnLoading: false,

View File

@@ -36,7 +36,7 @@
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" :min-width="50"> </el-table-column>
<el-table-column prop="name" label="名称" :min-width="70"> </el-table-column>
<el-table-column prop="description" label="描述" :min-width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="name" label="类型" :min-width="50">
<template #default="scope">
@@ -64,6 +64,19 @@
</el-table>
</el-dialog>
<el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px">
<el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="70px" size="mini">
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem" :key="item.name" :prop="item.model" :label="item.name" required>
<el-input v-model="scriptParamsDialog.params[item.model]" :placeholder="item.placeholder" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="hasParamsRun(currentData)" size="mini"> </el-button>
</span>
</template>
</el-dialog>
<el-dialog title="执行结果" v-model="resultDialog.visible" width="40%">
<div style="white-space: pre-line; padding: 10px; color: #000000">
{{ resultDialog.result }}
@@ -94,7 +107,7 @@
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent } from 'vue';
import { ref, toRefs, reactive, watch, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import SshTerminal from './SshTerminal.vue';
import { machineApi } from './api';
@@ -113,6 +126,7 @@ export default defineComponent({
title: { type: String },
},
setup(props: any, context) {
const paramsForm: any = ref(null);
const state = reactive({
visible: false,
type: 0,
@@ -125,6 +139,11 @@ export default defineComponent({
machineId: 9999999,
},
scriptTable: [],
scriptParamsDialog: {
visible: false,
params: {},
paramsFormItem: [],
},
resultDialog: {
visible: false,
result: '',
@@ -152,13 +171,43 @@ export default defineComponent({
};
const runScript = async (script: any) => {
// 如果存在参数,则弹窗输入参数后执行
if (script.params) {
state.scriptParamsDialog.paramsFormItem = JSON.parse(script.params);
state.scriptParamsDialog.visible = true;
return;
}
run(script);
};
// 有参数的脚本执行函数
const hasParamsRun = async (script: any) => {
// 如果脚本参数弹窗显示,则校验参数表单数据通过后执行
if (state.scriptParamsDialog.visible) {
paramsForm.value.validate((valid: any) => {
if (valid) {
run(script);
state.scriptParamsDialog.params = {};
state.scriptParamsDialog.visible = false;
paramsForm.value.resetFields();
} else {
return false;
}
});
}
};
const run = async (script: any) => {
const noResult = script.type == enums.scriptTypeEnum['NO_RESULT'].value;
// 如果脚本类型为有结果类型,则显示结果信息
if (script.type == enums.scriptTypeEnum['RESULT'].value || noResult) {
const res = await machineApi.runScript.request({
machineId: props.machineId,
scriptId: script.id,
params: state.scriptParamsDialog.params,
});
if (noResult) {
ElMessage.success('执行完成');
return;
@@ -241,9 +290,11 @@ export default defineComponent({
return {
...toRefs(state),
paramsForm,
enums,
getScripts,
runScript,
hasParamsRun,
closeTermnial,
choose,
editScript,

View File

@@ -6,9 +6,9 @@ export const machineApi = {
info: Api.create("/machines/{id}/sysinfo", 'get'),
top: Api.create("/machines/{id}/top", 'get'),
// 保存按钮
save: Api.create("/machines", 'post'),
saveMachine: Api.create("/machines", 'post'),
// 删除机器
del: Api.create("/devops/machines/{id}", 'delete'),
del: Api.create("/machines/delete/{id}", 'delete'),
scripts: Api.create("/machines/{machineId}/scripts", 'get'),
runScript: Api.create("/machines/{machineId}/scripts/{scriptId}/run", 'get'),
saveScript: Api.create("/machines/{machineId}/scripts", 'post'),

View File

@@ -0,0 +1,413 @@
<template>
<div class="project-list">
<div class="toolbar">
<el-button @click="showAddProjectDialog" v-auth="permissions.saveProject" type="primary" icon="el-icon-plus" size="mini">添加</el-button>
<el-button
@click="showAddProjectDialog(chooseData)"
v-auth="permissions.saveProject"
:disabled="chooseId == null"
type="primary"
icon="el-icon-edit"
size="mini"
>编辑</el-button
>
<el-button @click="showMembers(chooseData)" :disabled="chooseId == null" type="success" icon="el-icon-setting" size="mini"
>成员管理</el-button
>
<el-button @click="showEnv(chooseData)" :disabled="chooseId == null" type="info" icon="el-icon-setting" size="mini">环境管理</el-button>
<el-button v-auth="'role:del'" :disabled="chooseId == null" type="danger" icon="el-icon-delete" size="mini">删除</el-button>
<div style="float: right">
<el-input
class="mr2"
placeholder="请输入项目名!"
size="small"
style="width: 140px"
v-model="query.name"
@clear="search"
clearable
></el-input>
<el-button @click="search" type="success" icon="el-icon-search" size="mini"></el-button>
</div>
</div>
<el-table :data="projects" @current-change="choose" border ref="table" style="width: 100%">
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="项目名"></el-table-column>
<el-table-column prop="remark" label="描述" min-width="180px" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建者"> </el-table-column>
<!-- <el-table-column label="查看更多" min-width="80px">
<template #default="scope">
<el-link @click.prevent="showMembers(scope.row)" type="success">成员</el-link>
<el-link class="ml5" @click.prevent="showEnv(scope.row)" type="info">环境</el-link>
</template>
</el-table-column> -->
</el-table>
<el-pagination
@current-change="handlePageChange"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="total"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
/>
<el-dialog width="400px" title="项目编辑" :before-close="cancelAddProject" v-model="addProjectDialog.visible">
<el-form :model="addProjectDialog.form" size="small" label-width="70px">
<el-form-item label="项目名:" required>
<el-input :disabled="addProjectDialog.form.id" v-model="addProjectDialog.form.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="描述:">
<el-input v-model="addProjectDialog.form.remark" auto-complete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="addProject" type="primary" size="small"> </el-button>
<el-button @click="cancelAddProject()" size="small"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog width="500px" :title="showEnvDialog.title" v-model="showEnvDialog.visible">
<div class="toolbar">
<el-button @click="showAddEnvDialog" v-auth="permissions.saveMember" type="primary" icon="el-icon-plus" size="mini">添加</el-button>
<!-- <el-button v-auth="'role:update'" :disabled="chooseId == null" type="danger" icon="el-icon-delete" size="mini">删除</el-button> -->
</div>
<el-table border :data="showEnvDialog.envs" size="small">
<el-table-column property="name" label="环境名" width="125"></el-table-column>
<el-table-column property="remark" label="描述" width="125"></el-table-column>
<el-table-column property="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-dialog width="400px" title="添加环境" :before-close="cancelAddEnv" v-model="showEnvDialog.addVisible">
<el-form :model="showEnvDialog.envForm" size="small" label-width="70px">
<el-form-item label="环境名:" required>
<el-input v-model="showEnvDialog.envForm.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="描述:">
<el-input v-model="showEnvDialog.envForm.remark" auto-complete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button v-auth="permissions.saveEnv" @click="addEnv" type="primary" :loading="btnLoading" size="small"> </el-button>
<el-button @click="cancelAddEnv()" size="small"> </el-button>
</div>
</template>
</el-dialog>
</el-dialog>
<el-dialog width="500px" :title="showMemDialog.title" v-model="showMemDialog.visible">
<div class="toolbar">
<el-button v-auth="permissions.saveMember" @click="showAddMemberDialog()" type="primary" icon="el-icon-plus" size="mini"
>添加</el-button
>
<el-button
v-auth="permissions.delMember"
@click="deleteMember"
:disabled="showMemDialog.chooseId == null"
type="danger"
icon="el-icon-delete"
size="mini"
>移除</el-button
>
</div>
<el-table @current-change="chooseMember" border :data="showMemDialog.members.list" size="small">
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="showMemDialog.chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column property="username" label="账号" width="125"></el-table-column>
<el-table-column property="createTime" label="加入时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column property="creator" label="分配者" width="125"></el-table-column>
</el-table>
<el-pagination
@current-change="setMemebers"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="showMemDialog.members.total"
v-model:current-page="showMemDialog.query.pageNum"
:page-size="showMemDialog.query.pageSize"
/>
<el-dialog width="400px" title="添加成员" :before-close="cancelAddMember" v-model="showMemDialog.addVisible">
<el-form :model="showMemDialog.memForm" size="small" label-width="70px">
<el-form-item label="账号:">
<el-select style="width: 100%" remote :remote-method="getAccount" v-model="showMemDialog.memForm.accountId" filterable placeholder="请选择">
<el-option v-for="item in showMemDialog.accounts" :key="item.id" :label="item.username" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="描述:">
<el-input v-model="showEnvDialog.envForm.remark" auto-complete="off"></el-input>
</el-form-item> -->
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button v-auth="permissions.saveMember" @click="addMember" type="primary" :loading="btnLoading" size="small"
> </el-button
>
<el-button @click="cancelAddMember()" size="small"> </el-button>
</div>
</template>
</el-dialog>
</el-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import { projectApi } from './api';
import { accountApi } from '../../system/api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { notEmpty, notNull } from '@/common/assert';
import { auth } from '../../../common/utils/authFunction';
export default defineComponent({
name: 'ProjectList',
components: {},
setup() {
const state = reactive({
permissions: {
saveProject: 'project:save',
saveMember: 'project:member:add',
delMember: 'project:member:del',
saveEnv: 'project:env:add',
},
query: {
pageNum: 1,
pageSize: 10,
name: null,
},
total: 0,
projects: [],
btnLoading: false,
chooseId: null as any,
chooseData: null as any,
addProjectDialog: {
title: '新增项目',
visible: false,
form: { name: '', remark: '' },
},
showEnvDialog: {
visible: false,
envs: [],
title: '',
addVisible: false,
envForm: {
name: '',
remark: '',
projectId: 0,
},
},
showMemDialog: {
visible: false,
chooseId: null,
chooseData: null,
query: {
pageSize: 8,
pageNum: 1,
projectId: null,
},
members: {
list: [],
total: null,
},
title: '',
addVisible: false,
memForm: {},
accounts: [],
},
});
onMounted(() => {
search();
});
const search = async () => {
let res = await projectApi.projects.request(state.query);
state.projects = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const showAddProjectDialog = (data: any) => {
if (data) {
state.addProjectDialog.form = data;
} else {
state.addProjectDialog.form = {} as any;
}
state.addProjectDialog.visible = true;
};
const cancelAddProject = () => {
state.addProjectDialog.visible = false;
state.addProjectDialog.form = {} as any;
};
const addProject = async () => {
const form = state.addProjectDialog.form as any;
notEmpty(form.name, '项目名不能为空');
notEmpty(form.remark, '项目描述不能为空');
await projectApi.saveProject.request(form);
ElMessage.success('保存成功');
search();
cancelAddProject();
};
const choose = (item: any) => {
if (!item) {
return;
}
state.chooseId = item.id;
state.chooseData = item;
};
const showMembers = async (project: any) => {
state.showMemDialog.query.projectId = project.id;
await setMemebers();
state.showMemDialog.title = `${project.name}的成员信息`;
state.showMemDialog.visible = true;
};
/**
* 选中成员
*/
const chooseMember = (item: any) => {
if (!item) {
return;
}
state.showMemDialog.chooseData = item;
state.showMemDialog.chooseId = item.id;
};
const deleteMember = async () => {
notNull(state.showMemDialog.chooseData, '请选选择成员');
await projectApi.deleteProjectMem.request(state.showMemDialog.chooseData);
ElMessage.success('移除成功');
// 重新赋值成员列表
setMemebers();
};
/**
* 设置成员列表信息
*/
const setMemebers = async () => {
const res = await projectApi.projectMems.request(state.showMemDialog.query);
state.showMemDialog.members.list = res.list;
state.showMemDialog.members.total = res.total;
};
const showEnv = async (project: any) => {
state.showEnvDialog.envs = await projectApi.projectEnvs.request({ projectId: project.id });
state.showEnvDialog.title = `${project.name}的环境信息`;
state.showEnvDialog.visible = true;
};
const showAddMemberDialog = () => {
state.showMemDialog.addVisible = true;
};
const addMember = async () => {
const memForm = state.showMemDialog.memForm as any;
memForm.projectId = state.chooseData.id;
notEmpty(memForm.accountId, '请先选择账号');
await projectApi.saveProjectMem.request(memForm);
ElMessage.success('保存成功');
setMemebers();
cancelAddMember();
};
const cancelAddMember = () => {
state.showMemDialog.memForm = {};
state.showMemDialog.addVisible = false;
state.showMemDialog.chooseData = null;
state.showMemDialog.chooseId = null;
};
const getAccount = (username: any) => {
accountApi.list.request({ username }).then((res) => {
state.showMemDialog.accounts = res.list;
});
};
const showAddEnvDialog = () => {
state.showEnvDialog.addVisible = true;
};
const addEnv = async () => {
const envForm = state.showEnvDialog.envForm;
envForm.projectId = state.chooseData.id;
await projectApi.saveProjectEnv.request(envForm);
ElMessage.success('保存成功');
state.showEnvDialog.envs = await projectApi.projectEnvs.request({ projectId: envForm.projectId });
cancelAddEnv();
};
const cancelAddEnv = () => {
state.showEnvDialog.envForm = {} as any;
state.showEnvDialog.addVisible = false;
};
const roleEditChange = (data: any) => {
ElMessage.success('修改成功!');
search();
};
return {
...toRefs(state),
search,
handlePageChange,
choose,
showAddProjectDialog,
addProject,
cancelAddProject,
showMembers,
setMemebers,
showEnv,
showAddMemberDialog,
addMember,
chooseMember,
deleteMember,
cancelAddMember,
showAddEnvDialog,
addEnv,
cancelAddEnv,
getAccount,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,15 @@
import Api from '@/common/Api';
export const projectApi = {
// 获取账号可访问的项目列表
accountProjects: Api.create("/accounts/projects", 'get'),
projects: Api.create("/projects", 'get'),
saveProject: Api.create("/projects", 'post'),
// 获取项目下的环境信息
projectEnvs: Api.create("/projects/{projectId}/envs", 'get'),
saveProjectEnv: Api.create("/projects/{projectId}/envs", 'post'),
// 获取项目下的成员信息
projectMems: Api.create("/projects/{projectId}/members", 'get'),
saveProjectMem: Api.create("/projects/{projectId}/members", 'post'),
deleteProjectMem: Api.create("/projects/{projectId}/members/{accountId}", 'delete'),
}

View File

@@ -0,0 +1,281 @@
<template>
<div>
<div class="toolbar">
<div style="float: left">
<el-row type="flex" justify="space-between">
<el-col :span="24">
<project-env-select @changeProjectEnv="changeProjectEnv" @clear="clearRedis">
<template #default>
<el-form-item label="redis" label-width="40px">
<el-select v-model="scanParam.id" placeholder="请选择redis" @change="changeRedis" @clear="clearRedis" clearable>
<el-option v-for="item in redisList" :key="item.id" :label="item.host" :value="item.id">
<span style="float: left">{{ item.host }}</span>
<span style="float: right; color: #8492a6; margin-left: 6px; font-size: 13px">{{
`库: [${item.db}]`
}}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="key" label-width="40px">
<el-input
placeholder="支持*模糊key"
style="width: 180px"
v-model="scanParam.match"
size="mini"
@clear="clear()"
clearable
></el-input>
</el-form-item>
<el-form-item label-width="40px">
<el-input placeholder="count" style="width: 62px" v-model="scanParam.count" size="mini"></el-input>
</el-form-item>
<el-button @click="searchKey()" type="success" icon="el-icon-search" size="mini" plain></el-button>
<el-button @click="scan()" icon="el-icon-bottom" size="mini" plain>scan</el-button>
<el-button type="primary" icon="el-icon-plus" size="mini" @click="save(false)" plain></el-button>
</template>
</project-env-select>
</el-col>
</el-row>
</div>
<div style="float: right">
<!-- <el-button @click="scan()" icon="el-icon-refresh" size="small" plain>刷新</el-button> -->
<span>keys: {{ dbsize }}</span>
</div>
</div>
<el-table v-loading="loading" :data="keys" border 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"> </el-table-column>
<el-table-column prop="ttl" label="ttl(过期时间)" width="120">
<template #default="scope">
{{ ttlConveter(scope.row.ttl) }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="getValue(scope.row)" type="success" icon="el-icon-search" size="mini" plain>查看</el-button>
<el-button @click="del(scope.row.key)" type="danger" size="mini" icon="el-icon-delete" plain>删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="text-align: center; margin-top: 10px"></div>
<value-dialog v-model:visible="valueDialog.visible" :keyValue="valueDialog.value" />
</div>
</template>
<script lang="ts">
import ValueDialog from './ValueDialog.vue';
import { redisApi } from './api';
import { toRefs, reactive, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
import { isTrue, notNull } from '@/common/assert';
export default defineComponent({
name: 'DataOperation',
components: {
ValueDialog,
ProjectEnvSelect,
},
setup() {
const state = reactive({
loading: false,
cluster: 0,
redisList: [],
query: {
envId: 0,
},
// redis: {
// id: 0,
// info: '',
// conf: '',
// },
scanParam: {
id: null,
cluster: 0,
match: null,
count: 10,
cursor: 0,
prevCursor: null,
},
valueDialog: {
visible: false,
value: {},
},
keys: [],
dbsize: 0,
});
const searchRedis = async () => {
notNull(state.query.envId, '请先选择项目环境');
const res = await redisApi.redisList.request(state.query);
state.redisList = res.list;
};
const changeProjectEnv = (projectId: any, envId: any) => {
clearRedis();
if (envId != null) {
state.query.envId = envId;
searchRedis();
}
};
const changeRedis = (redisId: any) => {
resetScanParam();
state.keys = [];
state.dbsize = 0;
searchKey();
};
const scan = () => {
isTrue(state.scanParam.id != null, '请先选择redis');
isTrue(state.scanParam.count < 2001, 'count不能超过2000');
state.loading = true;
state.scanParam.cluster = state.cluster == 0 ? 0 : 1;
redisApi.scan.request(state.scanParam).then((res) => {
state.keys = res.keys;
state.dbsize = res.dbSize;
state.scanParam.cursor = res.cursor;
state.loading = false;
});
};
const searchKey = () => {
state.scanParam.cursor = 0;
scan();
};
const clearRedis = () => {
state.redisList = [];
state.scanParam.id = null;
resetScanParam();
state.keys = [];
state.dbsize = 0;
};
const clear = () => {
resetScanParam();
if (state.scanParam.id) {
scan();
}
};
const resetScanParam = () => {
state.scanParam.match = null;
state.scanParam.cursor = 0;
state.scanParam.count = 10;
};
const getValue = async (row: any) => {
let api: any;
switch (row.type) {
case 'string':
api = redisApi.getStringValue;
break;
case 'hash':
api = redisApi.getHashValue;
break;
case 'set':
api = redisApi.getSetValue;
break;
default:
api = redisApi.getStringValue;
break;
}
const id = state.cluster == 0 ? state.scanParam.id : state.cluster;
const res = await api.request({
cluster: state.cluster,
key: row.key,
id,
});
let timed = row.ttl == 18446744073709552000 ? 0 : row.ttl;
state.valueDialog.value = { id: state.scanParam.id, key: row.key, value: res, timed: timed, type: row.type };
state.valueDialog.visible = true;
};
// closeValueDialog() {
// this.valueDialog.visible = false
// this.valueDialog.value = {}
// }
const update = (key: string) => {};
const del = (key: string) => {
ElMessageBox.confirm(`此操作将删除对应的key , 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
let id = state.cluster == 0 ? state.scanParam.id : state.cluster;
redisApi.delKey
.request({
cluster: state.cluster,
key,
id,
})
.then((res) => {
ElMessage.success('删除成功!');
scan();
});
})
.catch((err) => {});
};
const ttlConveter = (ttl: any) => {
if (ttl == 18446744073709552000) {
return '永久';
}
if (!ttl) {
ttl = 0;
}
let second = parseInt(ttl); // 秒
let min = 0; // 分
let hour = 0; // 小时
let day = 0;
if (second > 60) {
min = parseInt(second / 60 + '');
second = second % 60;
if (min > 60) {
hour = parseInt(min / 60 + '');
min = min % 60;
if (hour > 24) {
day = parseInt(hour / 24 + '');
hour = hour % 24;
}
}
}
let result = '' + second + 's';
if (min > 0) {
result = '' + min + 'm:' + result;
}
if (hour > 0) {
result = '' + hour + 'h:' + result;
}
if (day > 0) {
result = '' + day + 'd:' + result;
}
return result;
};
return {
...toRefs(state),
changeProjectEnv,
changeRedis,
clearRedis,
searchKey,
scan,
clear,
getValue,
del,
ttlConveter,
};
},
});
</script>
<style>
</style>

View File

@@ -0,0 +1,200 @@
<template>
<div>
<el-dialog :title="title" v-model="visible" :show-close="true" width="35%" @close="close()">
<el-collapse>
<el-collapse-item title="Server(Redis服务器的一般信息)" name="server">
<div class="row">
<span class="title">redis_version(版本):</span>
<span class="value">{{ info.Server.redis_version }}</span>
</div>
<div class="row">
<span class="title">tcp_port(端口):</span>
<span class="value">{{ info.Server.tcp_port }}</span>
</div>
<div class="row">
<span class="title">redis_mode(模式):</span>
<span class="value">{{ info.Server.redis_mode }}</span>
</div>
<div class="row">
<span class="title">os(宿主操作系统):</span>
<span class="value">{{ info.Server.os }}</span>
</div>
<div class="row">
<span class="title">uptime_in_days(运行天数):</span>
<span class="value">{{ info.Server.uptime_in_days }}</span>
</div>
<div class="row">
<span class="title">executable(可执行文件路径):</span>
<span class="value">{{ info.Server.executable }}</span>
</div>
<div class="row">
<span class="title">config_file(配置文件路径):</span>
<span class="value">{{ info.Server.config_file }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Clients(客户端连接)" name="client">
<div class="row">
<span class="title">connected_clients(已连接客户端数):</span>
<span class="value">{{ info.Clients.connected_clients }}</span>
</div>
<div class="row">
<span class="title">blocked_clients(正在等待阻塞命令客户端数):</span>
<span class="value">{{ info.Clients.blocked_clients }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Keyspace(key信息)" name="keyspace">
<div class="row" v-for="(value, key) in info.Keyspace" :key="key">
<span class="title">{{ key }}: </span>
<span class="value">{{ value }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Stats(统计)" name="state">
<div class="row">
<span class="title">total_commands_processed(总处理命令数):</span>
<span class="value">{{ info.Stats.total_commands_processed }}</span>
</div>
<div class="row">
<span class="title">instantaneous_ops_per_sec(当前qps):</span>
<span class="value">{{ info.Stats.instantaneous_ops_per_sec }}</span>
</div>
<div class="row">
<span class="title">total_net_input_bytes(网络入口流量字节数):</span>
<span class="value">{{ info.Stats.total_net_input_bytes }}</span>
</div>
<div class="row">
<span class="title">total_net_output_bytes(网络出口流量字节数):</span>
<span class="value">{{ info.Stats.total_net_output_bytes }}</span>
</div>
<div class="row">
<span class="title">expired_keys(过期key的总数量):</span>
<span class="value">{{ info.Stats.expired_keys }}</span>
</div>
<div class="row">
<span class="title">instantaneous_ops_per_sec(当前qps):</span>
<span class="value">{{ info.Stats.instantaneous_ops_per_sec }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Persistence(持久化)" name="persistence">
<div class="row">
<span class="title">aof_enabled(是否启用aof):</span>
<span class="value">{{ info.Persistence.aof_enabled }}</span>
</div>
<div class="row">
<span class="title">loading(是否正在载入持久化文件):</span>
<span class="value">{{ info.Persistence.loading }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Cluster(集群)" name="cluster">
<div class="row">
<span class="title">cluster_enabled(是否启用集群模式):</span>
<span class="value">{{ info.Cluster.cluster_enabled }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Memory(内存消耗相关信息)" name="memory">
<div class="row">
<span class="title">used_memory(分配内存总量):</span>
<span class="value">{{ info.Memory.used_memory_human }}</span>
</div>
<div class="row">
<span class="title">maxmemory(最大内存配置):</span>
<span class="value">{{ info.Memory.maxmemory }}</span>
</div>
<div class="row">
<span class="title">used_memory_rss(已分配的内存总量操作系统角度):</span>
<span class="value">{{ info.Memory.used_memory_rss_human }}</span>
</div>
<div class="row">
<span class="title">mem_fragmentation_ratio(used_memory_rss和used_memory 之间的比率):</span>
<span class="value">{{ info.Memory.mem_fragmentation_ratio }}</span>
</div>
<div class="row">
<span class="title">used_memory_peak(内存消耗峰值):</span>
<span class="value">{{ info.Memory.used_memory_peak_human }}</span>
</div>
<div class="row">
<span class="title">total_system_memory(主机总内存):</span>
<span class="value">{{ info.Memory.total_system_memory_human }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="CPU" name="cpu">
<div class="row">
<span class="title">used_cpu_sys(由Redis服务器消耗的系统CPU):</span>
<span class="value">{{ info.CPU.used_cpu_sys }}</span>
</div>
<div class="row">
<span class="title">used_cpu_user(由Redis服务器消耗的用户CPU):</span>
<span class="value">{{ info.CPU.used_cpu_user }}</span>
</div>
<div class="row">
<span class="title">used_cpu_sys_children(由后台进程消耗的系统CPU):</span>
<span class="value">{{ info.CPU.used_cpu_sys_children }}</span>
</div>
<div class="row">
<span class="title">used_cpu_user_children(由后台进程消耗的用户CPU):</span>
<span class="value">{{ info.CPU.used_cpu_user_children }}</span>
</div>
</el-collapse-item>
</el-collapse>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, watch, toRefs } from 'vue';
export default defineComponent({
name: 'Info',
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
info: {
type: [Boolean, Object],
},
},
setup(props: any, { emit }) {
const state = reactive({
visible: false,
});
watch(
() => props.visible,
(val) => {
state.visible = val;
}
);
const close = () => {
emit('update:visible', false);
emit('close');
};
return {
...toRefs(state),
close,
};
},
});
</script>
<style>
.row .title {
font-size: 12px;
color: #8492a6;
margin-right: 6px;
}
.row .value {
font-size: 12px;
color: black;
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<div>
<el-dialog :title="title" v-model="visible" :show-close="false" :before-close="cancel" width="35%">
<el-form :model="form" ref="redisForm" :rules="rules" label-width="85px" size="small">
<el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="envId" label="环境:" required>
<el-select @change="changeEnv" style="width: 100%" v-model="form.envId" placeholder="请选择环境">
<el-option v-for="item in envs" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-input v-model.trim="form.host" placeholder="请输入host:port" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input
type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码"
autocomplete="new-password"
></el-input>
</el-form-item>
<el-form-item prop="db" label="库号:" required>
<el-input v-model.trim="form.db" placeholder="请输入库号"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="btnLoading" @click="btnOk" size="mini"> </el-button>
<el-button @click="cancel()" size="mini"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
import { redisApi } from './api';
import { projectApi } from '../project/api.ts';
import { ElMessage } from 'element-plus';
export default defineComponent({
name: 'RedisEdit',
props: {
visible: {
type: Boolean,
},
projects: {
type: Array,
},
redis: {
type: [Boolean, Object],
},
title: {
type: String,
},
},
setup(props: any, { emit }) {
const redisForm: any = ref(null);
const state = reactive({
visible: false,
projects: [],
envs: [],
form: {
id: null,
name: null,
host: null,
password: null,
project: null,
projectId: null,
envId: null,
env: null,
},
btnLoading: false,
rules: {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
host: [
{
required: true,
message: '请输入主机ip:port',
trigger: ['change', 'blur'],
},
],
db: [
{
required: true,
message: '请输入库号',
trigger: ['change', 'blur'],
},
],
},
});
watch(props, async (newValue, oldValue) => {
state.visible = newValue.visible;
state.projects = newValue.projects;
if (newValue.redis) {
getEnvs(newValue.redis.projectId);
state.form = { ...newValue.redis };
} else {
state.envs = [];
state.form = { db: 0 } as any;
}
});
const getEnvs = async (projectId: any) => {
state.envs = await projectApi.projectEnvs.request({ projectId });
};
const changeProject = (projectId: number) => {
for (let p of state.projects as any) {
if (p.id == projectId) {
state.form.project = p.name;
}
}
state.envs = [];
getEnvs(projectId);
};
const changeEnv = (envId: number) => {
for (let p of state.envs as any) {
if (p.id == envId) {
state.form.env = p.name;
}
}
};
const btnOk = async () => {
redisForm.value.validate((valid: boolean) => {
if (valid) {
redisApi.saveRedis.request(state.form).then((res: any) => {
ElMessage.success('保存成功');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
redisForm.value.resetFields();
// 重置对象属性为null
state.form = {} as any;
}, 200);
};
return {
...toRefs(state),
redisForm,
changeProject,
changeEnv,
btnOk,
cancel,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,369 @@
<template>
<div>
<div class="toolbar">
<el-button type="primary" icon="el-icon-plus" size="mini" @click="editRedis(true)" plain>添加</el-button>
<el-button type="primary" icon="el-icon-edit" :disabled="currentId == null" size="mini" @click="editRedis(false)" plain>编辑</el-button>
<el-button type="danger" icon="el-icon-delete" :disabled="currentId == null" size="mini" @click="deleteRedis" plain>删除</el-button>
<div style="float: right">
<!-- <el-input placeholder="host" size="mini" style="width: 140px" v-model="query.host" @clear="search" plain clearable></el-input>
<el-select v-model="params.clusterId" size="mini" clearable placeholder="集群选择">
<el-option v-for="item in clusters" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select> -->
<el-select v-model="query.projectId" placeholder="请选择项目" filterable clearable size="small">
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select>
<el-button class="ml5" @click="search" type="success" icon="el-icon-search" size="mini"></el-button>
</div>
</div>
<el-table :data="redisTable" stripe style="width: 100%" @current-change="choose">
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="currentId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="project" label="项目" width></el-table-column>
<el-table-column prop="env" label="环境" width></el-table-column>
<el-table-column prop="host" label="host:port" width></el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建人"></el-table-column>
<el-table-column label="操作" width>
<template #default="scope">
<el-button type="primary" @click="info(scope.row)" icon="el-icon-tickets" size="mini" plain>info</el-button>
<!-- <el-button type="success" @click="manage(scope.row)" :ref="scope.row" size="mini" plain>数据管理</el-button> -->
</template>
</el-table-column>
</el-table>
<el-pagination
@current-change="handlePageChange"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="total"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
/>
<info v-model:visible="infoDialog.visible" :title="infoDialog.title" :info="infoDialog.info"></info>
<redis-edit
@val-change="valChange"
:projects="projects"
:title="redisEditDialog.title"
v-model:visible="redisEditDialog.visible"
v-model:redis="redisEditDialog.data"
></redis-edit>
</div>
</template>
<script lang="ts">
import Info from './Info.vue';
import { redisApi } from './api';
import { toRefs, reactive, defineComponent, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { projectApi } from '../project/api.ts';
import RedisEdit from './RedisEdit.vue';
export default defineComponent({
name: 'RedisList',
components: {
Info,
RedisEdit,
},
setup() {
const state = reactive({
projects: [],
redisTable: [],
total: 0,
currentId: null,
currentData: null,
query: {
pageNum: 1,
pageSize: 10,
prjectId: null,
clusterId: null,
},
redisInfo: {
url: '',
},
clusters: [
{
id: 0,
name: '单机',
},
],
infoDialog: {
title: '',
visible: false,
info: {
Server: {},
Keyspace: {},
Clients: {},
CPU: {},
Memory: {},
},
},
redisEditDialog: {
visible: false,
data: null,
title: '新增redis',
},
});
onMounted(async () => {
search();
state.projects = (await projectApi.projects.request({ pageNum: 1, pageSize: 100 })).list;
});
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const choose = (item: any) => {
if (!item) {
return;
}
state.currentId = item.id;
state.currentData = item;
};
// connect() {
// Req.post('/open/redis/connect', this.form, res => {
// this.redisInfo = res
// })
// }
const deleteRedis = async () => {
try {
await ElMessageBox.confirm(`确定删除该redis?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await redisApi.delRedis.request({ id: state.currentId });
ElMessage.success('删除成功');
state.currentData = null;
state.currentId = null;
search();
} catch (err) {}
};
const info = (redis: any) => {
redisApi.redisInfo.request({ id: redis.id }).then((res: any) => {
state.infoDialog.info = res;
state.infoDialog.title = `'${redis.host}' info`;
state.infoDialog.visible = true;
});
};
const search = async () => {
const res = await redisApi.redisList.request(state.query);
state.redisTable = res.list;
state.total = res.total;
};
const editRedis = (isAdd = false) => {
if (isAdd) {
state.redisEditDialog.data = null;
state.redisEditDialog.title = '新增redis';
} else {
state.redisEditDialog.data = state.currentData;
state.redisEditDialog.title = '修改redis';
}
state.redisEditDialog.visible = true;
};
const valChange = () => {
search();
};
return {
...toRefs(state),
search,
handlePageChange,
choose,
info,
deleteRedis,
editRedis,
valChange,
};
},
});
// @Component({
// name: 'RedisList',
// components: {
// Info,
// DynamicFormDialog
// }
// })
// export default class RedisList extends Vue {
// validatePort = (rule: any, value: any, callback: any) => {
// if (value > 65535 || value < 1) {
// callback(new Error('端口号错误'))
// }
// callback()
// }
// redisTable = []
// permission = redisPermission
// keyPermission = redisKeyPermission
// currentId = null
// currentData: any = null
// params = {
// host: null,
// clusterId: null
// }
// redisInfo = {
// url: ''
// }
// clusters = [
// {
// id: 0,
// name: '单机'
// }
// ]
// infoDialog = {
// title: '',
// visible: false,
// info: {
// Server: {},
// Keyspace: {},
// Clients: {},
// CPU: {},
// Memory: {}
// }
// }
// formDialog = {
// visible: false,
// title: '',
// formInfo: {
// createApi: redisApi.save,
// updateApi: redisApi.update,
// formRows: [
// [
// {
// type: 'input',
// label: '主机:',
// name: 'host',
// placeholder: '请输入节点ip',
// rules: [
// {
// required: true,
// message: '请输入节点ip',
// trigger: ['blur', 'change']
// }
// ]
// }
// ],
// [
// {
// type: 'input',
// label: '端口号:',
// name: 'port',
// placeholder: '请输入节点端口号',
// inputType: 'number',
// rules: [
// {
// required: true,
// message: '请输入节点端口号',
// trigger: ['blur', 'change']
// }
// ]
// }
// ],
// [
// {
// type: 'input',
// label: '密码:',
// name: 'pwd',
// placeholder: '请输入节点密码',
// inputType: 'password'
// }
// ],
// [
// {
// type: 'input',
// label: '描述:',
// name: 'description',
// placeholder: '请输入节点描述',
// inputType: 'textarea'
// }
// ]
// ]
// },
// formData: { port: 6379 }
// }
// mounted() {
// this.search()
// }
// choose(item: any) {
// if (!item) {
// return
// }
// this.currentId = item.id
// this.currentData = item
// }
// // connect() {
// // Req.post('/open/redis/connect', this.form, res => {
// // this.redisInfo = res
// // })
// // }
// async deleteNode() {
// await redisApi.del.request({ id: this.currentId })
// this.$message.success('删除成功')
// this.search()
// }
// manage(row: any) {
// this.$router.push(`/redis_operation/${row.clusterId}/${row.id}`)
// }
// info(redis: any) {
// redisApi.info.request({ id: redis.id }).then(res => {
// this.infoDialog.info = res
// this.infoDialog.title = `'${redis.host}' info`
// this.infoDialog.visible = true
// })
// }
// search() {
// redisApi.list.request(this.params).then(res => {
// this.redisTable = res
// })
// }
// openFormDialog(redis: any) {
// let dialogTitle
// if (redis) {
// this.formDialog.formData = this.currentData
// dialogTitle = '编辑redis节点'
// } else {
// this.formDialog.formData = { port: 6379 }
// dialogTitle = '添加redis节点'
// }
// this.formDialog.title = dialogTitle
// this.formDialog.visible = true
// }
// submitSuccess() {
// this.currentId = null
// this.currentData = null
// this.search()
// }
// }
</script>
<style>
</style>

View File

@@ -0,0 +1,76 @@
<template>
<el-dialog :title="keyValue.key" v-model="visible" :before-close="cancel" :show-close="false" width="750px">
<el-form>
<el-form-item>
<el-input v-model="keyValue.value" type="textarea" :autosize="{ minRows: 10, maxRows: 20 }" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="saveValue" type="primary" size="mini"> </el-button>
<el-button @click="cancel()" size="mini"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { isTrue } from '@/common/assert';
export default defineComponent({
name: 'ValueDialog',
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
keyValue: {
type: [String, Object],
},
},
setup(props: any, { emit }) {
const state = reactive({
visible: false,
keyValue: {} as any,
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
watch(
() => props.visible,
(val) => {
state.visible = val;
}
);
watch(
() => props.keyValue,
(val) => {
state.keyValue = val;
if (state.keyValue.type != 'string') {
state.keyValue.value = JSON.stringify(val.value, undefined, 2)
}
// state.keyValue.value = JSON.stringify(val.value, undefined, 2)
}
);
const saveValue = async () => {
isTrue(state.keyValue.type == 'string', "暂不支持除string外其他类型修改")
await redisApi.saveStringValue.request(state.keyValue);
ElMessage.success('保存成功');
cancel();
};
return {
...toRefs(state),
saveValue,
cancel,
};
},
});
</script>

View File

@@ -0,0 +1,17 @@
import Api from '@/common/Api';
export const redisApi = {
redisList : Api.create("/redis", 'get'),
redisInfo: Api.create("/redis/{id}/info", 'get'),
saveRedis: Api.create("/redis", 'post'),
delRedis: Api.create("/redis/{id}", 'delete'),
// 获取权限列表
scan: Api.create("/redis/{id}/scan/{cursor}/{count}", 'get'),
getStringValue: Api.create("/redis/{id}/string-value", 'get'),
saveStringValue: Api.create("/redis/{id}/string-value", 'post'),
getHashValue: Api.create("/redis/{id}/hash-value", 'get'),
getSetValue: Api.create("/redis/{id}/set-value", 'get'),
saveHashValue: Api.create("/redis/{id}/hash-value", 'post'),
del: Api.create("/redis/{id}/scan/{cursor}/{count}", 'delete'),
delKey: Api.create("/redis/{id}/key", 'delete'),
}

View File

@@ -0,0 +1 @@
export { default } from './RedisList.vue';

View File

@@ -3,14 +3,14 @@
<el-dialog :title="title" v-model="visible" :show-close="false" width="35%">
<el-form :model="form" ref="accountForm" :rules="rules" label-width="85px" size="small">
<el-form-item prop="username" label="用户名:" required>
<el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入用户名" auto-complete="off"></el-input>
<el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入账号用户名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:" required>
<!-- <el-form-item prop="password" label="密码:" required>
<el-input type="password" v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password"></el-input>
</el-form-item>
<el-form-item v-if="!edit" label="确认密码:" required>
<el-input type="password" v-model.trim="form.repassword" placeholder="请输入确认密码" autocomplete="new-password"></el-input>
</el-form-item>
</el-form-item> -->
</el-form>
<template #footer>
@@ -61,13 +61,13 @@ export default defineComponent({
trigger: ['change', 'blur'],
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['change', 'blur'],
},
],
// password: [
// {
// required: true,
// message: '请输入密码',
// trigger: ['change', 'blur'],
// },
// ],
},
});

View File

@@ -19,6 +19,7 @@
>
<div style="float: right">
<el-input
class="mr2"
placeholder="请输入账号名"
size="small"
style="width: 140px"
@@ -45,8 +46,13 @@
<el-tag v-if="scope.row.status == -1" type="danger" size="mini">禁用</el-tag>
</template>
</el-table-column>
<el-table-column min-width="160" prop="lastLoginTime" label="最后登录时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.lastLoginTime) }}
</template>
</el-table-column>
<!-- <el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column> -->
<el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column>
<el-table-column min-width="160" prop="createTime" label="创建时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
@@ -58,8 +64,8 @@
{{ $filters.dateFormat(scope.row.updateTime) }}
</template>
</el-table-column> -->
<el-table-column min-width="160" prop="lastLoginTime" label="最后登录时间"></el-table-column>
<el-table-column min-width="120" prop="remark" label="备注" show-overflow-tooltip></el-table-column>
<!-- <el-table-column min-width="120" prop="remark" label="备注" show-overflow-tooltip></el-table-column> -->
<el-table-column label="查看更多" min-width="150">
<template #default="scope">
<el-link @click.prevent="showRoles(scope.row)" type="success">角色</el-link>
@@ -70,14 +76,21 @@
<el-table-column label="操作" min-width="200px">
<template #default="scope">
<el-button v-auth="'account:changeStatus'" v-if="scope.row.status == 1" type="danger" icom="el-icon-tickets" size="mini" plain
<el-button
v-auth="'account:changeStatus'"
@click="changeStatus(scope.row)"
v-if="scope.row.status == 1"
type="danger"
icom="el-icon-tickets"
size="mini"
plain
>禁用</el-button
>
<el-button
v-auth="'account:changeStatus'"
v-if="scope.row.status == -1"
type="success"
@click="serviceManager(scope.row)"
@click="changeStatus(scope.row)"
size="mini"
plain
>启用</el-button
@@ -136,7 +149,7 @@ import RoleEdit from './RoleEdit.vue';
import AccountEdit from './AccountEdit.vue';
import enums from '../enums';
import { accountApi } from '../api';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
export default defineComponent({
name: 'AccountList',
components: {
@@ -223,13 +236,13 @@ export default defineComponent({
const changeStatus = async (row: any) => {
let id = row.id;
let status = row.status ? 1 : -1;
// await accountApi.changeStatus.request({
// id,
// status,
// });
// ElMessage.success('操作成功');
// search();
let status = row.status == -1 ? 1 : -1;
await accountApi.changeStatus.request({
id,
status,
});
ElMessage.success('操作成功');
search();
};
const handlePageChange = (curPage: number) => {
@@ -267,12 +280,17 @@ export default defineComponent({
const deleteAccount = async () => {
try {
await ElMessageBox.confirm(`确定删除该账号?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await accountApi.del.request({ id: state.chooseId });
ElMessage.success('删除成功');
state.chooseData = null;
state.chooseId = null;
search();
} catch (error) {
ElMessage.error('刪除失败');
}
} catch (err) {}
};
return {

View File

@@ -13,8 +13,9 @@
</div>
</div>
<el-table :data="allRole" border ref="roleTable" @select="select" style="width: 100%">
<el-table-column type="selection" width="40"></el-table-column>
<el-table-column :selectable="selectable" type="selection" width="40"></el-table-column>
<el-table-column prop="name" label="角色名称"></el-table-column>
<el-table-column prop="code" label="角色code"></el-table-column>
<el-table-column prop="remark" label="角色描述">
<template #default="scope">
{{ scope.row.remark ? scope.row.remark : '暂无描述' }}
@@ -92,6 +93,11 @@ export default defineComponent({
search();
};
const selectable = (row: any) => {
// 角色code不以COMMON开头才可勾选
return row.code.indexOf('COMMON') != 0;
};
const select = (val: any, row: any) => {
let roles = state.roles;
// 如果账号的角色id存在则为取消该角色(删除角色id列表中的该记录id),否则为新增角色
@@ -164,6 +170,7 @@ export default defineComponent({
roleTable,
search,
handlePageChange,
selectable,
select,
btnOk,
cancel,

View File

@@ -25,7 +25,7 @@ export const accountApi = {
save: Api.create("/sys/accounts", 'post'),
update: Api.create("/sys/accounts/{id}", 'put'),
del: Api.create("/sys/accounts/{id}", 'delete'),
changeStatus: Api.create("/sys/accounts/{id}/{status}", 'put'),
changeStatus: Api.create("/sys/accounts/change-status/{id}/{status}", 'put'),
roleIds: Api.create("/sys/accounts/{id}/roleIds", 'get'),
roles: Api.create("/sys/accounts/{id}/roles", 'get'),
resources: Api.create("/sys/accounts/{id}/resources", 'get'),

View File

@@ -13,7 +13,7 @@
</el-form-item>
<el-form-item prop="code" label="path|code">
<el-input v-model.trim="form.code" placeholder="菜单为路由path"></el-input>
<el-input v-model.trim="form.code" placeholder="菜单不带/自动拼接父路径"></el-input>
</el-form-item>
<el-form-item label="序号" prop="weight" required>

View File

@@ -5,6 +5,9 @@
<el-form-item label="角色名称:" required>
<el-input v-model="form.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="角色code:" required>
<el-input :disabled="form.id" v-model="form.code" placeholder="COMMON开头则为所有账号共有角色" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="角色描述:">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入角色描述"></el-input>
</el-form-item>

View File

@@ -44,6 +44,7 @@
</template>
</el-table-column>
<el-table-column prop="name" label="角色名称"></el-table-column>
<el-table-column prop="code" label="角色code"></el-table-column>
<el-table-column prop="remark" label="描述" min-width="180px" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">