feat: 机器新增命令过滤配置、首页功能完善(操作记录与快捷操作)

This commit is contained in:
meilin.huang
2024-04-27 01:35:21 +08:00
parent a831614d5a
commit 653953ee76
75 changed files with 2224 additions and 895 deletions

View File

@@ -11,18 +11,17 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^10.9.0",
"asciinema-player": "^3.7.0",
"asciinema-player": "^3.7.1",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
"countup.js": "^2.8.0",
"cropperjs": "^1.6.1",
"echarts": "^5.5.0",
"element-plus": "^2.7.1",
"element-plus": "^2.7.2",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.47.0",
"monaco-editor": "^0.48.0",
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
@@ -34,7 +33,7 @@
"sql-formatter": "^15.0.2",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.4.23",
"vue": "^3.4.25",
"vue-router": "^4.3.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",

View File

@@ -0,0 +1,29 @@
import { defineStore } from 'pinia';
/**
* 自动打开资源
*/
export const useAutoOpenResource = defineStore('autoOpenResource', {
state: () => ({
autoOpenResource: {
machineCodePath: '',
dbCodePath: '',
redisCodePath: '',
mongoCodePath: '',
},
}),
actions: {
setMachineCodePath(codePath: string) {
this.autoOpenResource.machineCodePath = codePath;
},
setDbCodePath(codePath: string) {
this.autoOpenResource.dbCodePath = codePath;
},
setRedisCodePath(codePath: string) {
this.autoOpenResource.redisCodePath = codePath;
},
setMongoCodePath(codePath: string) {
this.autoOpenResource.mongoCodePath = codePath;
},
},
});

View File

@@ -1,137 +1,541 @@
<template>
<div class="home-container">
<div class="home-container personal">
<el-row :gutter="15">
<el-col :sm="6" class="mb15">
<div @click="toPage({ id: 'personal' })" class="home-card-item home-card-first">
<div class="flex-margin flex">
<img :src="userInfo.photo" />
<div class="home-card-first-right ml15">
<div class="flex-margin">
<div class="home-card-first-right-title">{{ `${currentTime}, ${userInfo.username}` }}</div>
</div>
<!-- 个人信息 -->
<el-col :xs="24" :sm="16">
<el-card shadow="hover" header="个人信息">
<div class="personal-user">
<div class="personal-user-left">
<el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
<img :src="userInfo.photo" />
</el-upload>
</div>
<div class="personal-user-right">
<el-row>
<el-col :span="24" class="personal-title mb18"
>{{ currentTime }}{{ userInfo.name }}生活变的再糟糕也不妨碍我变得更好
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">用户名</div>
<div class="personal-item-value">{{ userInfo.username }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">角色</div>
<div class="personal-item-value">{{ roleInfo }}</div>
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录IP</div>
<div class="personal-item-value">{{ userInfo.lastLoginIp }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录时间</div>
<div class="personal-item-value">{{ dateFormat(userInfo.lastLoginTime) }}</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :sm="3" class="mb15" v-for="(v, k) in topCardItemList as any" :key="k">
<div @click="toPage(v)" class="home-card-item home-card-item-box" :style="{ background: v.color }">
<div class="home-card-item-flex">
<div class="home-card-item-title pb3">{{ v.title }}</div>
<div class="home-card-item-title-num pb6" :id="v.id"></div>
<!-- 消息通知 -->
<el-col :xs="24" :sm="8" class="pl15 personal-info">
<el-card shadow="hover">
<template #header>
<span>消息通知</span>
<span @click="showMsgs" class="personal-info-more">更多</span>
</template>
<div class="personal-info-box">
<ul class="personal-info-ul">
<li v-for="(v, k) in state.msgs as any" :key="k" class="personal-info-li">
<a class="personal-info-li-title">{{ `[${getMsgTypeDesc(v.type)}] ${v.msg}` }}</a>
</li>
</ul>
</div>
<i :class="v.icon" :style="{ color: v.iconColor }"></i>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mt20 resource-info">
<el-col :sm="12">
<el-card shadow="hover">
<template #header>
<div class="pointer-icon" @click="toPage('machine')">
<div class="resource-num">
<SvgIcon
class="mb5 mr5"
:size="28"
:name="TagResourceTypeEnum.Machine.extra.icon"
:color="TagResourceTypeEnum.Machine.extra.iconColor"
/>
<span>{{ state.machine.num }}</span>
</div>
</div>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.machine.opLogs" :height="state.resourceOpTableHeight" stripe size="small">
<el-table-column prop="createTime" show-overflow-tooltip width="135">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="400" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column width="30">
<template #default="scope">
<el-link @click="toPage('machine', scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :sm="12">
<el-card shadow="hover">
<template #header>
<div class="pointer-icon" @click="toPage('db')">
<div class="resource-num">
<SvgIcon class="mb5 mr5" :size="28" :name="TagResourceTypeEnum.Db.extra.icon" :color="TagResourceTypeEnum.Db.extra.iconColor" />
<span>{{ state.db.num }}</span>
</div>
</div>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.db.opLogs" :height="state.resourceOpTableHeight" stripe size="small">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column width="30">
<template #default="scope">
<el-link @click="toPage('db', scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mt20 resource-info">
<el-col :sm="12">
<el-card shadow="hover">
<template #header>
<div class="pointer-icon" @click="toPage('redis')">
<div class="resource-num">
<SvgIcon
class="mb5 mr5"
:size="28"
:name="TagResourceTypeEnum.Redis.extra.icon"
:color="TagResourceTypeEnum.Redis.extra.iconColor"
/>
<span>{{ state.redis.num }}</span>
</div>
</div>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.redis.opLogs" :height="state.resourceOpTableHeight" stripe size="small">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column width="30">
<template #default="scope">
<el-link @click="toPage('redis', scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :sm="12">
<el-card shadow="hover">
<template #header>
<div class="pointer-icon" @click="toPage('mongo')">
<div class="resource-num">
<SvgIcon
class="mb5 mr5"
:size="28"
:name="TagResourceTypeEnum.Mongo.extra.icon"
:color="TagResourceTypeEnum.Mongo.extra.iconColor"
/>
<span>{{ state.mongo.num }}</span>
</div>
</div>
</template>
<el-row>
<el-col :sm="24">
<el-table :data="state.mongo.opLogs" :height="state.resourceOpTableHeight" stripe size="small">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
</template>
</el-table-column>
<el-table-column width="30">
<template #default="scope">
<el-link @click="toPage('mongo', scope.row.codePath)" type="primary" icon="Position"></el-link>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<el-dialog width="900px" title="消息" v-model="msgDialog.visible">
<el-table border :data="msgDialog.msgs.list" size="small">
<el-table-column property="type" label="类型" width="60">
<template #default="scope">
{{ getMsgTypeDesc(scope.row.type) }}
</template>
</el-table-column>
<el-table-column property="msg" label="消息"></el-table-column>
<el-table-column property="createTime" label="时间" width="150">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-row type="flex" class="mt5" justify="center">
<el-pagination
small
@current-change="searchMsg"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="msgDialog.msgs.total"
v-model:current-page="msgDialog.query.pageNum"
:page-size="msgDialog.query.pageSize"
/>
</el-row>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted, nextTick, computed } from 'vue';
import { toRefs, reactive, onMounted, computed } from 'vue';
// import * as echarts from 'echarts';
import { CountUp } from 'countup.js';
import { formatAxis } from '@/common/utils/format';
import { indexApi } from './api';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import { personApi } from '../personal/api';
import { dateFormat } from '@/common/utils/date';
import SvgIcon from '@/components/svgIcon/index.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { resourceOpLogApi } from '../ops/tag/api';
import TagCodePath from '../ops/component/TagCodePath.vue';
import { useAutoOpenResource } from '@/store/autoOpenResource';
const router = useRouter();
const { userInfo } = storeToRefs(useUserInfo());
const state = reactive({
topCardItemList: [
{
title: 'Linux机器',
id: 'machineNum',
color: '#F95959',
accountInfo: {
roles: [],
},
msgs: [],
msgDialog: {
visible: false,
query: {
pageSize: 10,
pageNum: 1,
},
{
title: '数据库',
id: 'dbNum',
color: '#8595F4',
msgs: {
list: [],
total: null,
},
{
title: 'redis',
id: 'redisNum',
color: '#1abc9c',
},
{
title: 'Mongo',
id: 'mongoNum',
color: '#FEBB50',
},
],
},
resourceOpTableHeight: 180,
defaultLogSize: 5,
machine: {
num: 0,
opLogs: [],
},
db: {
num: 0,
opLogs: [],
},
redis: {
num: 0,
opLogs: [],
},
mongo: {
num: 0,
opLogs: [],
},
});
const { topCardItemList } = toRefs(state);
const { msgDialog } = toRefs(state);
const roleInfo = computed(() => {
if (state.accountInfo.roles.length == 0) {
return '';
}
return state.accountInfo.roles.map((val: any) => val.roleName).join('、');
});
// 当前时间提示语
const currentTime = computed(() => {
return formatAxis(new Date());
});
// 页面加载时
onMounted(() => {
initData();
getAccountInfo();
getMsgs().then((res) => {
state.msgs = res.list;
});
});
const showMsgs = async () => {
state.msgDialog.query.pageNum = 1;
searchMsg();
state.msgDialog.visible = true;
};
const searchMsg = async () => {
state.msgDialog.msgs = await getMsgs();
};
const getMsgTypeDesc = (type: number) => {
if (type == 1) {
return '登录';
}
if (type == 2) {
return '通知';
}
};
const getAccountInfo = async () => {
state.accountInfo = await personApi.accountInfo.request();
};
const getMsgs = async () => {
return await personApi.getMsgs.request(state.msgDialog.query);
};
// 初始化数字滚动
const initNumCountUp = async () => {
indexApi.machineDashbord.request().then((res: any) => {
nextTick(() => {
new CountUp('machineNum', res.machineNum).start();
const initData = async () => {
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
.then((res: any) => {
state.machine.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.db.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Redis.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.redis.opLogs = res.list;
});
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize }).then((res: any) => {
state.mongo.opLogs = res.list;
});
indexApi.machineDashbord.request().then((res: any) => {
state.machine.num = res.machineNum;
});
indexApi.dbDashbord.request().then((res: any) => {
nextTick(() => {
new CountUp('dbNum', res.dbNum).start();
});
state.db.num = res.dbNum;
});
indexApi.redisDashbord.request().then((res: any) => {
nextTick(() => {
new CountUp('redisNum', res.redisNum).start();
});
state.redis.num = res.redisNum;
});
indexApi.mongoDashbord.request().then((res: any) => {
nextTick(() => {
new CountUp('mongoNum', res.mongoNum).start();
});
state.mongo.num = res.mongoNum;
});
};
const toPage = (item: any) => {
switch (item.id) {
const toPage = (item: any, codePath = '') => {
let path;
switch (item) {
case 'personal': {
router.push('/personal');
break;
}
case 'mongoNum': {
router.push('/mongo/mongo-data-operation');
case 'mongo': {
useAutoOpenResource().setMongoCodePath(codePath);
path = '/mongo/mongo-data-operation';
break;
}
case 'machineNum': {
router.push('/machine/machines-op');
case 'machine': {
useAutoOpenResource().setMachineCodePath(codePath);
path = '/machine/machines-op';
break;
}
case 'dbNum': {
router.push('/dbms/sql-exec');
case 'db': {
useAutoOpenResource().setDbCodePath(codePath);
path = '/dbms/sql-exec';
break;
}
case 'redisNum': {
router.push('/redis/data-operation');
case 'redis': {
useAutoOpenResource().setRedisCodePath(codePath);
path = '/redis/data-operation';
break;
}
}
};
// 页面加载时
onMounted(() => {
initNumCountUp();
// initHomeLaboratory();
// initHomeOvertime();
});
router.push({ path });
};
</script>
<style scoped lang="scss">
@import '@/theme/mixins/index.scss';
.personal {
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 100px;
height: 130px;
border-radius: 3px;
::v-deep(.el-upload) {
height: 100%;
}
.personal-user-left-upload {
img {
width: 100%;
height: 100%;
border-radius: 3px;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
}
.personal-user-right {
flex: 1;
padding: 0 15px;
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: gray;
@include text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
}
}
}
}
.personal-info {
.personal-info-more {
float: right;
color: gray;
font-size: 13px;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
.personal-info-box {
height: 130px;
overflow: hidden;
.personal-info-ul {
list-style: none;
.personal-info-li {
font-size: 13px;
padding-bottom: 10px;
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
color: grey;
text-decoration: none;
}
& a:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
}
}
}
}
.resource-info {
text-align: center;
::v-deep(.el-card__header) {
padding: 2px 20px;
}
.resource-num {
font-weight: 700;
font-size: 2vw;
}
}
.home-container {
overflow-x: hidden;
@@ -182,7 +586,7 @@ onMounted(() => {
}
.home-card-item-title-num {
font-size: 18px;
font-size: 2vw;
}
.home-card-item-tip-num {
@@ -190,124 +594,5 @@ onMounted(() => {
}
}
}
.home-card-first {
background: var(--bg-main-color);
border: 1px solid var(--el-border-color-light, #ebeef5);
display: flex;
align-items: center;
img {
width: 60px;
height: 60px;
border-radius: 100%;
border: 2px solid var(--el-color-primary-light-5);
}
.home-card-first-right {
flex: 1;
display: flex;
flex-direction: column;
.home-card-first-right-msg {
font-size: 13px;
color: gray;
}
}
}
.home-monitor {
height: 200px;
.flex-warp-item {
width: 50%;
height: 100px;
display: flex;
.flex-warp-item-box {
margin: auto;
height: auto;
text-align: center;
}
}
}
.home-warning-card {
height: 292px;
::v-deep(.el-card) {
height: 100%;
}
}
.home-dynamic {
height: 200px;
.home-dynamic-item {
display: flex;
width: 100%;
height: 60px;
overflow: hidden;
&:first-of-type {
.home-dynamic-item-line {
i {
color: orange !important;
}
}
}
.home-dynamic-item-left {
text-align: right;
.home-dynamic-item-left-time1 {
}
.home-dynamic-item-left-time2 {
font-size: 13px;
color: gray;
}
}
.home-dynamic-item-line {
height: 60px;
border-right: 2px dashed #dfdfdf;
margin: 0 20px;
position: relative;
i {
color: var(--el-color-primary);
font-size: 12px;
position: absolute;
top: 1px;
left: -6px;
transform: rotate(46deg);
background: white;
}
}
.home-dynamic-item-right {
flex: 1;
.home-dynamic-item-right-title {
i {
margin-right: 5px;
border: 1px solid #dfdfdf;
width: 20px;
height: 20px;
border-radius: 100%;
padding: 3px 2px 2px;
text-align: center;
color: var(--el-color-primary);
}
}
.home-dynamic-item-right-label {
font-size: 13px;
color: gray;
}
}
}
}
}
</style>

View File

@@ -191,12 +191,13 @@ const showResourceEdit = computed(() => {
return state.form.type != AuthCertTypeEnum.Public.value && !props.resourceEdit;
});
watch(
() => props.authCert,
(val: any) => {
setForm(val);
watch(dialogVisible, (val: any) => {
if (val) {
setForm(props.authCert);
} else {
cancelEdit();
}
);
});
const setForm = (val: any) => {
val = { ...val };
@@ -246,10 +247,11 @@ const getCiphertext = async () => {
const cancelEdit = () => {
dialogVisible.value = false;
emit('cancel');
setTimeout(() => {
acForm.value?.resetFields();
state.form = { ...DefaultForm };
acForm.value?.resetFields();
emit('cancel');
}, 300);
};

View File

@@ -113,9 +113,6 @@ const deleteRow = (idx: any) => {
const cancelEdit = () => {
state.dvisible = false;
setTimeout(() => {
state.form = {};
}, 300);
};
const btnOk = async (authCert: any) => {

View File

@@ -0,0 +1,87 @@
<template>
<div v-if="paths">
<el-row v-for="(path, idx) in paths?.slice(0, 1)" :key="idx">
<span v-for="item in parseTagPath(path)" :key="item.code">
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
class="mr2"
/>
<span> {{ item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
<!-- 展示剩余的标签信息 -->
<el-popover :show-after="300" v-if="paths.length > 1 && idx == 0" placement="bottom" width="500" trigger="hover">
<template #reference>
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
</template>
<el-row v-for="i in paths.slice(1)" :key="i">
<span v-for="item in parseTagPath(i)" :key="item.code">
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
class="mr2"
/>
<span> {{ item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
</el-row>
</el-popover>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import { computed } from 'vue';
const props = defineProps({
path: {
type: [String, Array<string>],
},
});
const paths = computed(() => {
if (Array.isArray(props.path)) {
return props.path;
}
return [props.path];
});
const parseTagPath = (tagPath: string = '') => {
if (!tagPath) {
return [];
}
const res = [] as any;
const codes = tagPath.split('/');
for (let code of codes) {
const typeAndCode = code.split('|');
if (typeAndCode.length == 1) {
const tagCode = typeAndCode[0];
if (!tagCode) {
continue;
}
res.push({
type: TagResourceTypeEnum.Tag.value,
code: typeAndCode[0],
});
continue;
}
res.push({
type: typeAndCode[0],
code: typeAndCode[1],
});
}
res[res.length - 1].isEnd = true;
return res;
};
</script>
<style lang="scss"></style>

View File

@@ -203,8 +203,14 @@ const getNode = (nodeKey: any) => {
return node;
};
const setCurrentKey = (nodeKey: any) => {
treeRef.value.setCurrentKey(nodeKey);
};
defineExpose({
reloadNode,
getNode,
setCurrentKey,
});
</script>

View File

@@ -0,0 +1,153 @@
<template>
<div class="w100" style="border: 1px solid var(--el-border-color)">
<el-input v-model="filterTag" clearable placeholder="输入关键字过滤" size="small" />
<el-scrollbar :style="{ height: props.height }">
<el-tree
v-bind="$attrs"
ref="tagTreeRef"
style="width: 100%"
:data="state.tags"
:default-expanded-keys="checkedTags"
:default-checked-keys="checkedTags"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
:node-key="$props.nodeKey"
:props="{
value: $props.nodeKey,
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
:filter-node-method="filterNode"
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.iconColor"
/>
<span class="font13 ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
</span>
</span>
</template>
</el-tree>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, watch } from 'vue';
import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
const props = defineProps({
height: {
type: [String, Number],
default: 'calc(100vh - 330px)',
},
tagType: {
type: Number,
default: TagResourceTypeEnum.Tag.value,
},
nodeKey: {
type: String,
default: 'codePath',
},
});
const checkedTags = defineModel<Array<any>>('modelValue', {
default: () => [],
});
const tagTreeRef: any = ref(null);
const filterTag = ref('');
const state = reactive({
tags: [],
});
onMounted(() => {
search();
});
const search = async () => {
state.tags = await tagApi.getTagTrees.request({ type: props.tagType });
setTimeout(() => {
const checkedNodes = tagTreeRef.value.getCheckedNodes();
console.log('check nodes: ', checkedNodes);
// 禁用选中节点的所有父节点,不可选中
for (let checkNodeData of checkedNodes) {
disableParentNodes(tagTreeRef.value.getNode(checkNodeData.codePath).parent);
}
}, 200);
};
watch(filterTag, (val) => {
tagTreeRef.value!.filter(val);
});
const filterNode = (value: string, data: any) => {
if (!value) {
return true;
}
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
};
const tagTreeNodeCheck = (data: any) => {
const node = tagTreeRef.value.getNode(data.codePath);
console.log('check node: ', node);
if (node.checked) {
// 如果选中了子节点,则需要将父节点全部取消选中,并禁用父节点
unCheckParentNodes(node.parent);
disableParentNodes(node.parent);
} else {
// 如果取消了选中,则需要根据条件恢复父节点的选中状态
disableParentNodes(node.parent, false);
}
// 更新绑定的值
checkedTags.value = tagTreeRef.value.getCheckedKeys(false);
};
const unCheckParentNodes = (node: any) => {
if (!node) {
return;
}
tagTreeRef.value.setChecked(node, false, false);
unCheckParentNodes(node.parent);
};
/**
* 禁用该节点以及所有父节点
* @param node 节点
* @param disable 是否禁用
*/
const disableParentNodes = (node: any, disable = true) => {
if (!node) {
return;
}
if (!disable) {
// 恢复为非禁用状态时,若同层级存在一个选中状态或者禁用状态,则继续禁用 不恢复非禁用状态。
for (let oneLevelNodes of node.childNodes) {
if (oneLevelNodes.checked || oneLevelNodes.data.disabled) {
return;
}
}
}
node.data.disabled = disable;
disableParentNodes(node.parent, disable);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -6,11 +6,9 @@
@change="changeTag"
:data="tags"
placeholder="请选择关联标签"
:render-after-expand="true"
:default-expanded-keys="[state.selectTags]"
:default-expanded-keys="defaultExpandedKeys"
show-checkbox
node-key="codePath"
:check-strictly="props.checkStrictly"
:props="{
value: 'codePath',
label: 'codePath',
@@ -19,6 +17,7 @@
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" class="mr2" />
<span style="font-size: 13px">
{{ data.code }}
<span style="color: #3c8dbc"></span>
@@ -33,25 +32,22 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import { toRefs, reactive, onMounted, computed } from 'vue';
import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
//定义事件
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
const props = defineProps({
selectTags: {
type: [Array<any>],
type: [Array<any>, Object],
},
tagType: {
type: Number,
default: TagResourceTypeEnum.Tag.value,
},
checkStrictly: {
type: Boolean,
default: false,
},
});
const state = reactive({
@@ -62,6 +58,16 @@ const state = reactive({
const { tags } = toRefs(state);
const defaultExpandedKeys = computed(() => {
if (Array.isArray(state.selectTags)) {
// 如果 state.selectTags 是数组,直接返回
return state.selectTags;
}
// 如果 state.selectTags 不是数组,转换为包含 state.selectTags 的数组
return [state.selectTags];
});
onMounted(async () => {
state.selectTags = props.selectTags;
state.tags = await tagApi.getTagTrees.request({ type: props.tagType });

View File

@@ -171,3 +171,31 @@ export function getTagPathSearchItem(resourceType: number) {
})
);
}
/**
* 根据标签路径获取对应的类型与编号数组
* @param codePath 编号路径 tag1/tag2/1|xxx/11|yyy/
* @returns {1: ['xxx'], 11: ['yyy']}
*/
export function getTagTypeCodeByPath(codePath: string) {
const result = {};
const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
for (let part of parts) {
if (!part) {
continue;
}
let [key, value] = part.split('|'); // 分割数字和值部分
// 如果不存在第二个参数,则说明为标签类型
if (!value) {
value = key;
key = '-1';
}
if (!result[key]) {
result[key] = [];
}
result[key].push(value);
}
return result;
}

View File

@@ -2,7 +2,12 @@
<div class="db-sql-exec">
<Splitpanes class="default-theme">
<Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.DbName.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
<tag-tree
:default-expanded-keys="state.defaultExpendKey"
:resource-type="TagResourceTypeEnum.DbName.value"
:tag-path-node-type="NodeTypeTagPath"
ref="tagTreeRef"
>
<template #prefix="{ data }">
<span v-if="data.type.value == SqlExecNodeType.DbInst">
<el-popover
@@ -167,11 +172,11 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format';
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
import { NodeType, TagTreeNode } from '../component/tag';
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { dbApi } from './api';
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
@@ -183,6 +188,8 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Pane, Splitpanes } from 'splitpanes';
import { useEventListener } from '@vueuse/core';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
@@ -258,7 +265,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
await sleep(100);
return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
return new TagTreeNode(`${x.code}`, x.name, NodeTypeDbInst).withParams(x);
});
})
.withContextMenuItems([ContextmenuItemRefresh]);
@@ -267,6 +274,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
const params = parentNode.params;
const dbs = params.database.split(' ')?.sort();
return dbs.map((x: any) => {
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
.withParams({
@@ -418,6 +426,7 @@ const tagTreeRef: any = ref(null);
const tabs: Map<string, TabInfo> = new Map();
const state = reactive({
defaultExpendKey: [] as any,
/**
* 当前操作的数据库实例
*/
@@ -452,7 +461,11 @@ const serverInfoReqParam = ref({
});
const { execute: getDbServerInfo, isFetching: loadingServerInfo, data: dbServerInfo } = dbApi.getInstanceServerInfo.useApi<any>(serverInfoReqParam);
const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
onMounted(() => {
autoOpenDb(autoOpenResource.value.dbCodePath);
setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度
useEventListener(window, 'resize', setHeight);
@@ -462,6 +475,31 @@ onBeforeUnmount(() => {
dispposeCompletionItemProvider('sql');
});
watch(
() => autoOpenResource.value.dbCodePath,
(codePath: any) => {
autoOpenDb(codePath);
}
);
const autoOpenDb = (codePath: string) => {
if (!codePath) {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const dbCode = typeAndCodes[TagResourceTypeEnum.DbName.value][0];
state.defaultExpendKey = [tagPath, dbCode];
setTimeout(() => {
// 置空
autoOpenResourceStore.setDbCodePath('');
tagTreeRef.value.setCurrentKey(dbCode);
}, 600);
};
/**
* 设置editor高度和数据表高度
*/
@@ -807,7 +845,7 @@ const getNowDbInfo = () => {
}
.db-op {
height: calc(100vh - 108px);
height: calc(100vh - 106px);
}
#data-exec {

View File

@@ -106,7 +106,9 @@ const props = defineProps({
});
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const emit = defineEmits(['cancel', 'val-change']);
const dialogVisible = defineModel<boolean>('visible', { default: false });
const rules = {
tagCodePaths: [
@@ -170,23 +172,19 @@ const defaultForm = {
};
const state = reactive({
dialogVisible: false,
sshTunnelMachineList: [] as any,
form: defaultForm,
submitForm: {} as any,
pwd: '',
});
const { dialogVisible, form, submitForm } = toRefs(state);
const { form, submitForm } = toRefs(state);
const { isFetching: testConnBtnLoading, execute: testConnExec } = machineApi.testConn.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveMachineExec } = machineApi.saveMachine.useApi(submitForm);
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
state.form = { ...defaultForm };
state.form.authCerts = [];
if (!dialogVisible.value) {
return;
}
const machine: any = props.machine;
@@ -194,6 +192,9 @@ watchEffect(() => {
state.form = { ...machine };
state.form.tagCodePaths = machine.tags.map((t: any) => t.codePath);
state.form.authCerts = machine.authCerts || [];
} else {
state.form = { ...defaultForm };
state.form.authCerts = [];
}
});
@@ -250,7 +251,7 @@ const handleChangeProtocol = (val: any) => {
};
const cancel = () => {
emit('update:visible', false);
dialogVisible.value = false;
emit('cancel');
};
</script>

View File

@@ -8,6 +8,7 @@
ref="tagTreeRef"
:resource-type="TagResourceTypeEnum.MachineAuthCert.value"
:tag-path-node-type="NodeTypeTagPath"
:default-expanded-keys="state.defaultExpendKey"
>
<template #prefix="{ data }">
<SvgIcon
@@ -153,13 +154,13 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, reactive, ref, toRefs, watch } from 'vue';
import { defineAsyncComponent, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { useRouter } from 'vue-router';
import { getMachineTerminalSocketUrl, machineApi } from './api';
import { dateFormat } from '@/common/utils/date';
import { hasPerms } from '@/components/auth/auth';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { NodeType, TagTreeNode } from '../component/tag';
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { Pane, Splitpanes } from 'splitpanes';
import { ContextmenuItem } from '@/components/contextmenu/index';
@@ -169,6 +170,8 @@ import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
import ResourceTags from '../component/ResourceTags.vue';
import { MachineProtocolEnum } from './enums';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
// 组件
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
@@ -196,6 +199,7 @@ class MachineNodeType {
}
const state = reactive({
defaultExpendKey: [] as any,
params: {
pageNum: 1,
pageSize: 0,
@@ -252,6 +256,9 @@ const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog
const tagTreeRef: any = ref(null);
const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
let openIds = {};
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
@@ -263,7 +270,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
// 把list 根据name字段排序
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
return res.list.map((x: any) =>
new TagTreeNode(x.id, x.name, NodeTypeMachine)
new TagTreeNode(x.code, x.name, NodeTypeMachine)
.withParams(x)
.withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
.withIcon({
@@ -279,7 +286,7 @@ const NodeTypeMachine = new NodeType(MachineNodeType.Machine)
// 获取授权凭证列表
const authCerts = machine.authCerts;
return authCerts.map((x: any) =>
new TagTreeNode(x.id, x.username, NodeTypeAuthCert)
new TagTreeNode(x.name, x.username, NodeTypeAuthCert)
.withParams({ ...machine, selectAuthCert: x })
.withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
.withIcon({
@@ -323,6 +330,47 @@ const NodeTypeAuthCert = new NodeType(MachineNodeType.AuthCert)
.withOnClick((node: any) => serviceManager(node.params)),
]);
watch(
() => autoOpenResource.value.machineCodePath,
(codePath: any) => {
autoOpenTerminal(codePath);
}
);
watch(
() => state.activeTermName,
(newValue, oldValue) => {
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
}
);
onMounted(() => {
autoOpenTerminal(autoOpenResource.value.machineCodePath);
});
const autoOpenTerminal = (codePath: string) => {
if (!codePath) {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const machineCode = typeAndCodes[TagResourceTypeEnum.Machine.value][0];
state.defaultExpendKey = [tagPath, machineCode];
const authCertName = typeAndCodes[TagResourceTypeEnum.MachineAuthCert.value][0];
setTimeout(() => {
// 置空
autoOpenResourceStore.setMachineCodePath('');
tagTreeRef.value.setCurrentKey(authCertName);
const acNode = tagTreeRef.value.getNode(authCertName);
openTerminal(acNode.data.params);
}, 600);
};
const openTerminal = (machine: any, ex?: boolean) => {
// 授权凭证名
const ac = machine.selectAuthCert.name;
@@ -465,15 +513,6 @@ const onRemoveTab = (targetName: string) => {
}
};
watch(
() => state.activeTermName,
(newValue, oldValue) => {
console.log('oldValue', oldValue);
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
}
);
const terminalStatusChange = (key: string, status: TerminalStatus) => {
state.tabs.get(key).status = status;
};

View File

@@ -58,6 +58,12 @@ export const cronJobApi = {
execList: Api.newGet('/machine-cronjobs/execs'),
};
export const cmdConfApi = {
list: Api.newGet('/machine/security/cmd-confs'),
save: Api.newPost('/machine/security/cmd-confs'),
delete: Api.newDelete('/machine/security/cmd-confs/{id}'),
};
export function getMachineTerminalSocketUrl(authCertName: any) {
return `${config.baseWsUrl}/machines/terminal/${authCertName}?${joinClientParams()}`;
}

View File

@@ -0,0 +1,222 @@
<template>
<div>
<el-table :data="cmdConfs" stripe>
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100px"> </el-table-column>
<el-table-column prop="cmds" label="过滤命令" min-width="320px" show-overflow-tooltip>
<template #default="scope">
<el-tag class="ml2 mt2" v-for="cmd in scope.row.cmds" :key="cmd" type="danger">
{{ cmd }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="codePaths" label="关联机器" min-width="220px" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.tags.map((tag: any) => tag.codePath)" />
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
<el-table-column prop="creator" label="创建者" show-overflow-tooltip width="100px"> </el-table-column>
<el-table-column label="操作" min-wdith="100px">
<template #header>
<el-text tag="b">操作</el-text>
<el-button v-auth="'cmdconf:save'" class="ml5" type="primary" circle size="small" icon="Plus" @click="openFormDialog(false)"> </el-button>
</template>
<template #default="scope">
<el-button v-auth="'cmdconf:save'" @click="openFormDialog(scope.row)" type="primary" link>编辑</el-button>
<el-button v-auth="'cmdconf:del'" @click="deleteCmdConf(scope.row)" type="danger" link>删除</el-button>
</template>
</el-table-column>
</el-table>
<el-drawer title="命令配置" v-model="dialogVisible" :show-close="false" width="600px" :destroy-on-close="true" :close-on-click-modal="false">
<template #header>
<DrawerHeader header="命令配置" :back="cancelEdit" />
</template>
<el-form ref="formRef" :model="state.form" :rules="rules" label-width="auto">
<el-form-item prop="name" label="名称" required>
<el-input v-model="form.name" placeholder="名称"></el-input>
</el-form-item>
<el-form-item prop="cmds" label="过滤命令" required>
<el-row>
<el-tag
class="ml2 mt2"
v-for="tag in form.cmds"
:key="tag"
closable
:disable-transitions="false"
@close="handleCmdClose(tag)"
type="danger"
>
{{ tag }}
</el-tag>
<el-input
v-if="state.inputCmdVisible"
ref="cmdInputRef"
v-model="state.cmdInputValue"
class="mt3"
size="small"
@keyup.enter="handleCmdInputConfirm"
@blur="handleCmdInputConfirm"
placeholder="请输入命令正则表达式"
/>
<el-button v-else class="ml2 mt2" size="small" @click="showCmdInput"> + 新建命令 </el-button>
</el-row>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联机器">
<tag-tree-check height="calc(100vh - 430px)" :tag-type="TagResourceTypeEnum.MachineAuthCert.value" v-model="form.codePaths" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="submiting" @click="cancelEdit"> </el-button>
<el-button v-auth="'cmdconf:save'" type="primary" :loading="submiting" @click="submitForm"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, nextTick } from 'vue';
import TagTreeCheck from '../../component/TagTreeCheck.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { ElMessage, ElMessageBox } from 'element-plus';
import { cmdConfApi } from '../api';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import TagCodePath from '../../component/TagCodePath.vue';
import _ from 'lodash';
const rules = {
tags: [
{
required: true,
message: '请选择关联的机器',
trigger: ['change'],
},
],
cmds: [
{
required: true,
message: '请创建命令',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
message: '请输入名称',
trigger: ['change', 'blur'],
},
],
};
const tagSelectRef: any = ref(null);
const formRef: any = ref(null);
const cmdInputRef: any = ref(null);
const DefaultForm = {
id: 0,
name: '',
codePaths: [],
cmds: [] as any,
remark: '',
};
const state = reactive({
cmdConfs: [],
dialogVisible: false,
form: DefaultForm,
submiting: false,
inputCmdVisible: false,
cmdInputValue: '',
});
const { cmdConfs, dialogVisible, form, submiting } = toRefs(state);
onMounted(async () => {
getCmdConfs();
});
const getCmdConfs = async () => {
state.cmdConfs = await cmdConfApi.list.request();
};
const handleCmdClose = (tag: string) => {
state.form.cmds.splice(state.form.cmds.indexOf(tag), 1);
};
const showCmdInput = () => {
state.inputCmdVisible = true;
nextTick(() => {
cmdInputRef.value!.input!.focus();
});
};
const handleCmdInputConfirm = () => {
if (state.cmdInputValue) {
state.form.cmds.push(state.cmdInputValue);
}
state.inputCmdVisible = false;
state.cmdInputValue = '';
};
const openFormDialog = (data: any) => {
if (!data) {
state.form = { ...DefaultForm };
} else {
state.form = _.cloneDeep(data);
state.form.codePaths = data.tags.map((tag: any) => tag.codePath);
}
state.dialogVisible = true;
};
const deleteCmdConf = async (data: any) => {
await ElMessageBox.confirm(`确定删除该[${data.name}]命令配置?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await cmdConfApi.delete.request({ id: data.id });
ElMessage.success('操作成功');
getCmdConfs();
};
const cancelEdit = () => {
state.dialogVisible = false;
// 取消表单的校验
setTimeout(() => {
state.form = { ...DefaultForm };
formRef.value.resetFields();
}, 200);
};
const submitForm = () => {
formRef.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');
return false;
}
try {
state.submiting = true;
await cmdConfApi.save.request(state.form);
ElMessage.success('操作成功');
cancelEdit();
getCmdConfs();
} finally {
state.submiting = false;
}
});
};
</script>
<style></style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="card">
<el-tabs v-model="activeName" class="demo-tabs" @tab-change="handleTabChange">
<el-tab-pane label="命令配置" :name="CmdConfTab">
<CmdConfList />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
const CmdConfList = defineAsyncComponent(() => import('./CmdConfList.vue'));
const CmdConfTab = 'cmdConf';
const state = reactive({
activeName: CmdConfTab,
cmdConfs: [],
});
const { activeName } = toRefs(state);
onMounted(async () => {
state.activeName = CmdConfTab;
});
const handleTabChange = (tabName: any) => {
if (tabName == CmdConfTab) {
console.log('get cmd confs');
}
console.log(tabName);
};
</script>
<style></style>

View File

@@ -2,7 +2,12 @@
<div class="flex-all-center">
<Splitpanes class="default-theme">
<Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Mongo.value" :tag-path-node-type="NodeTypeTagPath">
<tag-tree
ref="tagTreeRef"
:default-expanded-keys="state.defaultExpendKey"
:resource-type="TagResourceTypeEnum.Mongo.value"
:tag-path-node-type="NodeTypeTagPath"
>
<template #prefix="{ data }">
<span v-if="data.type.value == MongoNodeType.Mongo">
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250">
@@ -168,16 +173,18 @@
<script lang="ts" setup>
import { mongoApi } from './api';
import { computed, defineAsyncComponent, reactive, ref, toRefs } from 'vue';
import { computed, defineAsyncComponent, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { isTrue, notBlank } from '@/common/assert';
import { TagTreeNode, NodeType } from '../component/tag';
import { TagTreeNode, NodeType, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { formatByteSize } from '@/common/utils/format';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { sleep } from '@/common/utils/loading';
import { Splitpanes, Pane } from 'splitpanes';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
@@ -207,7 +214,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
await sleep(100);
return mongoInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeMongo).withParams(x);
return new TagTreeNode(`${x.code}`, x.name, NodeTypeMongo).withParams(x);
});
});
@@ -250,7 +257,10 @@ const NodeTypeColl = new NodeType(MongoNodeType.Coll).withNodeClickFunc((nodeDat
});
const findParamInputRef: any = ref(null);
const tagTreeRef: any = ref(null);
const state = reactive({
defaultExpendKey: [] as any,
tags: [],
mongoList: [] as any,
activeName: '', // 当前操作的tab
@@ -282,10 +292,42 @@ const state = reactive({
const { findDialog, docEditDialog } = toRefs(state);
const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
const nowColl = computed(() => {
return getNowDataTab();
});
watch(
() => autoOpenResource.value.mongoCodePath,
(codePath: any) => {
autoOpenMongo(codePath);
}
);
onMounted(() => {
autoOpenMongo(autoOpenResource.value.mongoCodePath);
});
const autoOpenMongo = (codePath: string) => {
if (!codePath) {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const mongoCode = typeAndCodes[TagResourceTypeEnum.Mongo.value][0];
state.defaultExpendKey = [tagPath, mongoCode];
setTimeout(() => {
// 置空
autoOpenResourceStore.setMongoCodePath('');
tagTreeRef.value.setCurrentKey(mongoCode);
}, 600);
};
const changeCollection = async (id: any, schema: string, collection: string) => {
const label = `${id}:\`${schema}\`.${collection}`;
let dataTab = state.dataTabs[label];

View File

@@ -2,7 +2,12 @@
<div class="redis-data-op flex-all-center">
<Splitpanes class="default-theme">
<Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Redis.value" :tag-path-node-type="NodeTypeTagPath">
<tag-tree
ref="tagTreeRef"
:default-expanded-keys="state.defaultExpendKey"
:resource-type="TagResourceTypeEnum.Redis.value"
:tag-path-node-type="NodeTypeTagPath"
>
<template #prefix="{ data }">
<span v-if="data.type.value == RedisNodeType.Redis">
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250">
@@ -178,11 +183,11 @@
<script lang="ts" setup>
import { redisApi } from './api';
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref } from 'vue';
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { isTrue, notBlank, notNull } from '@/common/assert';
import { copyToClipboard } from '@/common/utils/string';
import { TagTreeNode, NodeType } from '../component/tag';
import { TagTreeNode, NodeType, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { keysToTree, sortByTreeNodes, keysToList } from './utils';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
@@ -190,6 +195,8 @@ import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Splitpanes, Pane } from 'splitpanes';
import { RedisInst } from './redis';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
@@ -230,7 +237,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
await sleep(100);
return redisInfos.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeRedis).withParams(x);
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
});
});
@@ -288,9 +295,11 @@ const treeProps = {
const defaultCount = 250;
const keyTreeRef: any = ref(null);
const tagTreeRef: any = ref(null);
const redisInst: Ref<RedisInst> = ref(new RedisInst());
const state = reactive({
defaultExpendKey: [] as any,
tags: [],
redisList: [] as any,
dbList: [],
@@ -331,7 +340,37 @@ const state = reactive({
const { scanParam, keyTreeData, newKeyDialog } = toRefs(state);
onMounted(async () => {});
const autoOpenResourceStore = useAutoOpenResource();
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
onMounted(async () => {
autoOpenRedis(autoOpenResource.value.redisCodePath);
});
watch(
() => autoOpenResource.value.redisCodePath,
(codePath: any) => {
autoOpenRedis(codePath);
}
);
const autoOpenRedis = (codePath: string) => {
if (!codePath) {
return;
}
const typeAndCodes = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const redisCode = typeAndCodes[TagResourceTypeEnum.Redis.value][0];
state.defaultExpendKey = [tagPath, redisCode];
setTimeout(() => {
// 置空
autoOpenResourceStore.setRedisCodePath('');
tagTreeRef.value.setCurrentKey(redisCode);
}, 600);
};
const scan = async (appendKey = false) => {
isTrue(state.scanParam.id != null, '请先选择redis');

View File

@@ -75,11 +75,7 @@
<el-descriptions-item label="code">{{ currentTag.code }}</el-descriptions-item>
<el-descriptions-item label="路径" :span="2">
<span v-for="item in parseTagPath(currentTag.codePath)" :key="item.code">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon" class="mr2" />
<span> {{ item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
<TagCodePath :path="currentTag.codePath" />
</el-descriptions-item>
<el-descriptions-item label="名称">{{ currentTag.name }}</el-descriptions-item>
@@ -163,6 +159,7 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import EnumValue from '@/common/Enum';
import InstanceList from '../db/InstanceList.vue';
import TagCodePath from '../component/TagCodePath.vue';
interface Tree {
id: number;
@@ -345,38 +342,6 @@ const handleDrop = async (draggingNode: any, dropNode: any) => {
}
};
const parseTagPath = (tagPath: string) => {
if (!tagPath) {
return [];
}
const res = [] as any;
const codes = tagPath.split('/');
for (let code of codes) {
const typeAndCode = code.split('|');
if (typeAndCode.length == 1) {
const tagCode = typeAndCode[0];
if (!tagCode) {
continue;
}
res.push({
type: TagResourceTypeEnum.Tag.value,
code: typeAndCode[0],
});
continue;
}
res.push({
type: typeAndCode[0],
code: typeAndCode[1],
});
}
res[res.length - 1].isEnd = true;
return res;
};
const tabChange = () => {
setNowTabData();
};

View File

@@ -14,11 +14,8 @@
<el-button v-auth="'team:del'" :disabled="selectionData.length < 1" @click="deleteTeam()" type="danger" icon="delete">删除</el-button>
</template>
<template #tagPath="{ data }">
<tag-info :tag-path="data.tagPath" />
<span class="ml5">
{{ data.tagPath }}
</span>
<template #tags="{ data }">
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
</template>
<template #action="{ data }">
@@ -48,45 +45,7 @@
</el-form-item>
<el-form-item prop="tag" label="标签">
<div class="w100" style="border: 1px solid var(--el-border-color)">
<el-input v-model="filterTag" clearable placeholder="输入关键字过滤" size="small" />
<el-scrollbar style="height: calc(100vh - 330px)">
<el-tree
ref="tagTreeRef"
style="width: 100%"
:data="state.tags"
:default-expanded-keys="state.addTeamDialog.form.tags"
:default-checked-keys="state.addTeamDialog.form.tags"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
node-key="id"
:props="{
value: 'id',
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
:filter-node-method="filterNode"
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" />
<span class="font13 ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
</span>
</span>
</template>
</el-tree>
</el-scrollbar>
</div>
<TagTreeCheck v-model="state.addTeamDialog.form.codePaths" :tag-type="0" />
</el-form-item>
</el-form>
<template #footer>
@@ -131,7 +90,7 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, Ref, watch } from 'vue';
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
import { tagApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { notBlank } from '@/common/assert';
@@ -139,20 +98,19 @@ import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import TagTreeCheck from '../component/TagTreeCheck.vue';
import TagCodePath from '../component/TagCodePath.vue';
const teamForm: any = ref(null);
const tagTreeRef: any = ref(null);
const pageTableRef: Ref<any> = ref(null);
const showMemPageTableRef: Ref<any> = ref(null);
const filterTag = ref('');
const searchItems = [SearchItem.input('name', '团队名称')];
const columns = [
TableColumn.new('name', '团队名称'),
TableColumn.new('remark', '备注'),
TableColumn.new('tags', '分配标签').isSlot().setAddWidth(40),
TableColumn.new('creator', '创建者'),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('modifier', '修改者'),
@@ -162,10 +120,9 @@ const columns = [
const state = reactive({
currentEditPermissions: false,
tags: [],
addTeamDialog: {
visible: false,
form: { id: 0, name: '', remark: '', tags: [] },
form: { id: 0, name: '', remark: '', codePaths: [] },
},
query: {
pageNum: 1,
@@ -211,34 +168,13 @@ const search = async () => {
pageTableRef.value.search();
};
watch(filterTag, (val) => {
tagTreeRef.value!.filter(val);
});
const filterNode = (value: string, data: any) => {
if (!value) {
return true;
}
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
};
const showSaveTeamDialog = async (data: any) => {
state.tags = await tagApi.getTagTrees.request(null);
if (data) {
state.addTeamDialog.form.id = data.id;
state.addTeamDialog.form.name = data.name;
state.addTeamDialog.form.remark = data.remark;
state.addTeamDialog.form.tags = await tagApi.getTeamTagIds.request({ teamId: data.id });
setTimeout(() => {
const checkedNodes = tagTreeRef.value.getCheckedNodes();
console.log('check nodes: ', checkedNodes);
// 禁用选中节点的所有父节点,不可选中
for (let checkNodeData of checkedNodes) {
disableParentNodes(tagTreeRef.value.getNode(checkNodeData.id).parent);
}
}, 200);
state.addTeamDialog.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
// state.addTeamDialog.form.tags = await tagApi.getRelateTagIds.request({ relateType: TagTreeRelateTypeEnum.Team.value, relateId: data.id });
}
state.addTeamDialog.visible = true;
@@ -248,7 +184,6 @@ const saveTeam = async () => {
teamForm.value.validate(async (valid: any) => {
if (valid) {
const form = state.addTeamDialog.form;
form.tags = tagTreeRef.value.getCheckedKeys(false);
await tagApi.saveTeam.request(form);
ElMessage.success('保存成功');
search();
@@ -318,48 +253,5 @@ const cancelAddMember = () => {
state.showMemDialog.memForm = {} as any;
state.showMemDialog.addVisible = false;
};
const tagTreeNodeCheck = (data: any) => {
const node = tagTreeRef.value.getNode(data.id);
console.log('check node: ', node);
if (node.checked) {
// 如果选中了子节点,则需要将父节点全部取消选中,并禁用父节点
unCheckParentNodes(node.parent);
disableParentNodes(node.parent);
} else {
// 如果取消了选中,则需要根据条件恢复父节点的选中状态
disableParentNodes(node.parent, false);
}
};
const unCheckParentNodes = (node: any) => {
if (!node) {
return;
}
tagTreeRef.value.setChecked(node, false, false);
unCheckParentNodes(node.parent);
};
/**
* 禁用该节点以及所有父节点
* @param node 节点
* @param disable 是否禁用
*/
const disableParentNodes = (node: any, disable = true) => {
if (!node) {
return;
}
if (!disable) {
// 恢复为非禁用状态时,若同层级存在一个选中状态或者禁用状态,则继续禁用 不恢复非禁用状态。
for (let oneLevelNodes of node.childNodes) {
if (oneLevelNodes.checked || oneLevelNodes.data.disabled) {
return;
}
}
}
node.data.disabled = disable;
disableParentNodes(node.parent, disable);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -9,6 +9,7 @@ export const tagApi = {
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
countTagResource: Api.newGet('/tag-trees/resources/count'),
getRelateTagIds: Api.newGet('/tag-trees/relate/{relateType}/{relateId}'),
getTeams: Api.newGet('/teams'),
saveTeam: Api.newPost('/teams'),
@@ -17,8 +18,6 @@ export const tagApi = {
getTeamMem: Api.newGet('/teams/{teamId}/members'),
saveTeamMem: Api.newPost('/teams/{teamId}/members'),
delTeamMem: Api.newDelete('/teams/{teamId}/members/{accountId}'),
getTeamTagIds: Api.newGet('/teams/{teamId}/tags'),
};
export const resourceAuthCertApi = {
@@ -27,3 +26,7 @@ export const resourceAuthCertApi = {
save: Api.newPost('/auth-certs'),
delete: Api.newDelete('/auth-certs/{id}'),
};
export const resourceOpLogApi = {
getAccountResourceOpLogs: Api.newGet('/resource-op-logs/account'),
};

View File

@@ -14,3 +14,7 @@ export const AuthCertCiphertextTypeEnum = {
PrivateKey: EnumValue.of(2, '秘钥').tagTypeSuccess(),
Public: EnumValue.of(-1, '公共凭证').tagTypeSuccess(),
};
export const TagTreeRelateTypeEnum = {
Team: EnumValue.of(1, '团队'),
};

View File

@@ -1,112 +1,6 @@
<template>
<div class="personal">
<el-row>
<!-- 个人信息 -->
<el-col :xs="24" :sm="16">
<el-card shadow="hover" header="个人信息">
<div class="personal-user">
<div class="personal-user-left">
<el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
<img :src="userInfo.photo" />
</el-upload>
</div>
<div class="personal-user-right">
<el-row>
<el-col :span="24" class="personal-title mb18"
>{{ currentTime }}{{ userInfo.name }}生活变的再糟糕也不妨碍我变得更好
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">用户名</div>
<div class="personal-item-value">{{ userInfo.username }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">角色</div>
<div class="personal-item-value">{{ roleInfo }}</div>
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录IP</div>
<div class="personal-item-value">{{ userInfo.lastLoginIp }}</div>
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录时间</div>
<div class="personal-item-value">{{ dateFormat(userInfo.lastLoginTime) }}</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</div>
</el-card>
</el-col>
<!-- 消息通知 -->
<el-col :xs="24" :sm="8" class="pl15 personal-info">
<el-card shadow="hover">
<template #header>
<span>消息通知</span>
<span @click="showMsgs" class="personal-info-more">更多</span>
</template>
<div class="personal-info-box">
<ul class="personal-info-ul">
<li v-for="(v, k) in msgDialog.msgs.list as any" :key="k" class="personal-info-li">
<a class="personal-info-li-title">{{ `[${getMsgTypeDesc(v.type)}] ${v.msg}` }}</a>
</li>
</ul>
</div>
</el-card>
</el-col>
<el-dialog width="900px" title="消息" v-model="msgDialog.visible">
<el-table border :data="msgDialog.msgs.list" size="small">
<el-table-column property="type" label="类型" width="60">
<template #default="scope">
{{ getMsgTypeDesc(scope.row.type) }}
</template>
</el-table-column>
<el-table-column property="msg" label="消息"></el-table-column>
<el-table-column property="createTime" label="时间" width="150">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-row type="flex" class="mt5" justify="center">
<el-pagination
small
@current-change="getMsgs"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="msgDialog.msgs.total"
v-model:current-page="msgDialog.query.pageNum"
:page-size="msgDialog.query.pageSize"
/>
</el-row>
</el-dialog>
<!-- 营销推荐 -->
<!-- <el-col :span="24">
<el-card shadow="hover" class="mt15" header="营销推荐">
<el-row :gutter="15" class="personal-recommend-row">
<el-col :sm="6" v-for="(v, k) in recommendList" :key="k" class="personal-recommend-col">
<div class="personal-recommend" :style="{ 'background-color': v.bg }">
<i :class="v.icon" :style="{ color: v.iconColor }"></i>
<div class="personal-recommend-auto">
<div>{{ v.title }}</div>
<div class="personal-recommend-msg">{{ v.msg }}</div>
</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col> -->
<!-- 更新信息 -->
<el-col :span="24">
<el-card shadow="hover" class="mt15 personal-edit" header="更新信息">
@@ -142,28 +36,6 @@
</div>
</div>
</span>
<!-- <div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">密保手机</div>
<div class="personal-edit-safe-item-left-value">已绑定手机132****4108</div>
</div>
<div class="personal-edit-safe-item-right">
<el-button type="text">立即修改</el-button>
</div>
</div>
</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">密保问题</div>
<div class="personal-edit-safe-item-left-value">已设置密保问题账号安全大幅度提升</div>
</div>
<div class="personal-edit-safe-item-right">
<el-button type="text">立即设置</el-button>
</div>
</div>
</div> -->
</el-card>
</el-col>
</el-row>
@@ -171,33 +43,16 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, computed, onMounted } from 'vue';
import { toRefs, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { formatAxis } from '@/common/utils/format';
import { personApi } from './api';
import { dateFormat } from '@/common/utils/date';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import config from '@/common/config';
import { joinClientParams } from '@/common/request';
const { userInfo } = storeToRefs(useUserInfo());
const state = reactive({
accountInfo: {
roles: [],
},
msgs: [],
msgDialog: {
visible: false,
query: {
pageSize: 10,
pageNum: 1,
},
msgs: {
list: [],
total: null,
},
},
recommendList: [],
accountForm: {
password: '',
@@ -208,27 +63,10 @@ const state = reactive({
},
});
const { msgDialog, accountForm, authStatus } = toRefs(state);
// 当前时间提示语
const currentTime = computed(() => {
return formatAxis(new Date());
});
const showMsgs = () => {
state.msgDialog.visible = true;
};
const roleInfo = computed(() => {
if (state.accountInfo.roles.length == 0) {
return '';
}
return state.accountInfo.roles.map((val: any) => val.name).join('、');
});
const { accountForm, authStatus } = toRefs(state);
onMounted(async () => {
getAccountInfo();
getMsgs();
state.authStatus = await personApi.authStatus.request();
});
@@ -277,162 +115,11 @@ const unbindOAuth2 = async () => {
ElMessage.success('解绑成功');
state.authStatus = await personApi.authStatus.request();
};
const getMsgs = async () => {
const res = await personApi.getMsgs.request(state.msgDialog.query);
state.msgDialog.msgs = res;
};
const getMsgTypeDesc = (type: number) => {
if (type == 1) {
return '登录';
}
if (type == 2) {
return '通知';
}
};
</script>
<style scoped lang="scss">
@import '../../theme/mixins/index.scss';
.personal {
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 100px;
height: 130px;
border-radius: 3px;
::v-deep(.el-upload) {
height: 100%;
}
.personal-user-left-upload {
img {
width: 100%;
height: 100%;
border-radius: 3px;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
}
.personal-user-right {
flex: 1;
padding: 0 15px;
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: gray;
@include text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
}
}
}
}
.personal-info {
.personal-info-more {
float: right;
color: gray;
font-size: 13px;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
.personal-info-box {
height: 130px;
overflow: hidden;
.personal-info-ul {
list-style: none;
.personal-info-li {
font-size: 13px;
padding-bottom: 10px;
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
color: grey;
text-decoration: none;
}
& a:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
}
}
}
.personal-recommend-row {
.personal-recommend-col {
.personal-recommend {
position: relative;
height: 100px;
color: #ffffff;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
&:hover {
i {
right: 0px !important;
bottom: 0px !important;
transition: all ease 0.3s;
}
}
i {
position: absolute;
right: -10px;
bottom: -10px;
font-size: 70px;
transform: rotate(-30deg);
transition: all ease 0.3s;
}
.personal-recommend-auto {
padding: 15px;
position: absolute;
left: 0;
top: 5%;
.personal-recommend-msg {
font-size: 12px;
margin-top: 10px;
}
}
}
}
}
.personal-edit {
.personal-edit-title {
position: relative;

View File

@@ -128,7 +128,7 @@
</el-col>
</el-row>
</el-form>
e
<template #footer>
<div>
<el-button @click="cancel()"> </el-button>

View File

@@ -28,7 +28,7 @@ require (
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.5.1
github.com/robfig/cron/v3 v3.0.1 //
github.com/sijms/go-ora/v2 v2.8.12
github.com/sijms/go-ora/v2 v2.8.13
github.com/stretchr/testify v1.8.4
github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver v1.15.0 // mongo
@@ -39,7 +39,7 @@ require (
gopkg.in/yaml.v3 v3.0.1
// gorm
gorm.io/driver/mysql v1.5.6
gorm.io/gorm v1.25.9
gorm.io/gorm v1.25.10
)
require (

View File

@@ -20,7 +20,4 @@ const (
ResourceTypeDb int8 = 2
ResourceTypeRedis int8 = 3
ResourceTypeMongo int8 = 4
// 删除机器的事件主题名
DeleteMachineEventTopic = "machine:delete"
)

View File

@@ -12,11 +12,13 @@ import (
"mayfly-go/internal/db/config"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/event"
msgapp "mayfly-go/internal/msg/application"
msgdto "mayfly-go/internal/msg/application/dto"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/global"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
@@ -97,6 +99,8 @@ func (d *Db) ExecSql(rc *req.Ctx) {
biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, dbConn.Info.TagPath[0])
sqlBytes, err := base64.StdEncoding.DecodeString(form.Sql)
biz.ErrIsNilAppendErr(err, "sql解码失败: %s")
// 去除前后空格及换行符

View File

@@ -23,6 +23,7 @@ type InstanceDbNamesForm struct {
Host string `binding:"required" json:"host"`
Port int `json:"port"`
Params string `json:"params"`
Extra string `json:"extra"`
SshTunnelMachineId int `json:"sshTunnelMachineId"`
AuthCert *tagentity.ResourceAuthCert `json:"authCert" binding:"required"` // 资产授权凭证信息
}

View File

@@ -0,0 +1,6 @@
package event
const (
EventTopicDeleteMachine = "machine:delete" // 删除机器的事件主题名
EventTopicResourceOp = "resource:op" // 资源操作主题
)

View File

@@ -44,3 +44,14 @@ type MachineCronJobForm struct {
MachineIds []uint64 `json:"machineIds"`
Remark string `json:"remark"`
}
type MachineCmdConfForm struct {
Id uint64 `json:"id"`
Name string `json:"name"`
Cmds []string `json:"cmds"` // 命令配置
Status int8 `json:"execCmds"` // 状态
Stratege string `json:"stratege"` // 策略,空禁用
Remark string `json:"remark"` // 备注
CodePaths []string `json:"codePaths"`
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/base64"
"fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/event"
"mayfly-go/internal/machine/api/form"
"mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/application"
@@ -15,6 +16,7 @@ import (
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/global"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
@@ -192,7 +194,9 @@ func (m *Machine) WsSSH(g *gin.Context) {
cli, err := m.MachineApp.NewCli(GetMachineAc(rc))
biz.ErrIsNilAppendErr(err, mcm.GetErrorContentRn("获取客户端连接失败: %s"))
defer cli.Close()
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), mcm.GetErrorContentRn("%s"))
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, cli.Info.TagPath[0])
cols := rc.QueryIntDefault("cols", 80)
rows := rc.QueryIntDefault("rows", 32)

View File

@@ -0,0 +1,49 @@
package api
import (
"mayfly-go/internal/machine/api/form"
"mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/application"
"mayfly-go/internal/machine/domain/entity"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
)
type MachineCmdConf struct {
MachineCmdConfApp application.MachineCmdConf `inject:""`
TagTreeRelateApp tagapp.TagTreeRelate `inject:"TagTreeRelateApp"`
}
func (m *MachineCmdConf) MachineCmdConfs(rc *req.Ctx) {
cond := req.BindQuery(rc, new(entity.MachineCmdConf))
var vos []*vo.MachineCmdConfVO
err := m.MachineCmdConfApp.ListByCond(cond, &vos)
biz.ErrIsNil(err)
m.TagTreeRelateApp.FillTagInfo(tagentity.TagRelateTypeMachineCmd, collx.ArrayMap(vos, func(mvo *vo.MachineCmdConfVO) tagentity.IRelateTag {
return mvo
})...)
rc.ResData = vos
}
func (m *MachineCmdConf) Save(rc *req.Ctx) {
cmdForm := new(form.MachineCmdConfForm)
mcj := req.BindJsonAndCopyTo[*entity.MachineCmdConf](rc, cmdForm, new(entity.MachineCmdConf))
rc.ReqParam = cmdForm
err := m.MachineCmdConfApp.SaveCmdConf(rc.MetaCtx, &application.SaveMachineCmdConfParam{
CmdConf: mcj,
CodePaths: cmdForm.CodePaths,
})
biz.ErrIsNil(err)
}
func (m *MachineCmdConf) Delete(rc *req.Ctx) {
m.MachineCmdConfApp.DeleteCmdConf(rc.MetaCtx, uint64(rc.PathParamInt("id")))
}

View File

@@ -2,6 +2,7 @@ package vo
import (
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/model"
"time"
)
@@ -85,3 +86,18 @@ func (s MachineFileInfos) Less(i, j int) bool {
}
return s[i].Name < s[j].Name
}
type MachineCmdConfVO struct {
tagentity.RelateTags // 标签信息
model.Model
Name string `json:"name"`
Cmds model.Slice[string] `json:"cmds"` // 命令配置
Status int8 `json:"execCmds"` // 状态
Stratege string `json:"stratege"` // 策略,空禁用
Remark string `json:"remark"` // 备注
}
func (mcc *MachineCmdConfVO) GetRelateId() uint64 {
return mcc.Id
}

View File

@@ -10,6 +10,7 @@ func InitIoc() {
ioc.Register(new(machineScriptAppImpl), ioc.WithComponentName("MachineScriptApp"))
ioc.Register(new(machineCronJobAppImpl), ioc.WithComponentName("MachineCronJobApp"))
ioc.Register(new(machineTermOpAppImpl), ioc.WithComponentName("MachineTermOpApp"))
ioc.Register(new(machineCmdConfAppImpl), ioc.WithComponentName("MachineCmdConfApp"))
}
func GetMachineApp() Machine {

View File

@@ -3,7 +3,7 @@ package application
import (
"context"
"fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/event"
"mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
@@ -210,7 +210,7 @@ func (m *machineAppImpl) Delete(ctx context.Context, id uint64) error {
mcm.DeleteCli(id)
// 发布机器删除事件
global.EventBus.Publish(ctx, consts.DeleteMachineEventTopic, machine)
global.EventBus.Publish(ctx, event.EventTopicDeleteMachine, machine)
resourceType := tagentity.TagTypeMachine
return m.Tx(ctx,

View File

@@ -0,0 +1,98 @@
package application
import (
"context"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"regexp"
)
type SaveMachineCmdConfParam struct {
CmdConf *entity.MachineCmdConf
CodePaths []string
}
type MachineCmd struct {
CmdRegexp *regexp.Regexp // 命令正则表达式
Stratege string // 策略(拒绝或审批等)
}
type MachineCmdConf interface {
base.App[*entity.MachineCmdConf]
SaveCmdConf(ctx context.Context, cmdConf *SaveMachineCmdConfParam) error
DeleteCmdConf(ctx context.Context, id uint64) error
GetCmdConfsByMachineTags(tagPaths ...string) []*MachineCmd
}
type machineCmdConfAppImpl struct {
base.AppImpl[*entity.MachineCmdConf, repository.MachineCmdConf]
tagTreeRelateApp tagapp.TagTreeRelate `inject:"TagTreeRelateApp"`
}
var _ (MachineCmdConf) = (*machineCmdConfAppImpl)(nil)
// 注入MachineCmdConfRepo
func (m *machineCmdConfAppImpl) InjectMachineCmdConfRepo(repo repository.MachineCmdConf) {
m.Repo = repo
}
func (m *machineCmdConfAppImpl) SaveCmdConf(ctx context.Context, cmdConfParam *SaveMachineCmdConfParam) error {
cmdConf := cmdConfParam.CmdConf
return m.Tx(ctx, func(ctx context.Context) error {
return m.Save(ctx, cmdConf)
}, func(ctx context.Context) error {
return m.tagTreeRelateApp.RelateTag(ctx, tagentity.TagRelateTypeMachineCmd, cmdConf.Id, cmdConfParam.CodePaths...)
})
}
func (m *machineCmdConfAppImpl) DeleteCmdConf(ctx context.Context, id uint64) error {
_, err := m.GetById(new(entity.MachineCmdConf), id)
if err != nil {
return errorx.NewBiz("该命令配置不存在")
}
return m.Tx(ctx, func(ctx context.Context) error {
return m.DeleteById(ctx, id)
}, func(ctx context.Context) error {
return m.tagTreeRelateApp.DeleteByCond(ctx, &tagentity.TagTreeRelate{
RelateType: tagentity.TagRelateTypeMachineCmd,
RelateId: id,
})
})
}
func (m *machineCmdConfAppImpl) GetCmdConfsByMachineTags(tagPaths ...string) []*MachineCmd {
var cmds []*MachineCmd
cmdConfIds, err := m.tagTreeRelateApp.GetRelateIds(tagentity.TagRelateTypeMachineCmd, tagPaths...)
if err != nil {
logx.Errorf("获取命令配置信息失败: %s", err.Error())
return cmds
}
if len(cmdConfIds) == 0 {
return cmds
}
var cmdConfs []*entity.MachineCmdConf
m.GetByIdIn(&cmdConfs, cmdConfIds)
for _, cmdConf := range cmdConfs {
for _, cmd := range cmdConf.Cmds {
if p, err := regexp.Compile(cmd); err != nil {
logx.Errorf("命令配置[%s],正则编译失败", cmd)
} else {
cmds = append(cmds, &MachineCmd{CmdRegexp: p})
}
}
}
return cmds
}

View File

@@ -36,6 +36,8 @@ type MachineTermOp interface {
type machineTermOpAppImpl struct {
base.AppImpl[*entity.MachineTermOp, repository.MachineTermOp]
machineCmdConfApp MachineCmdConf `inject:"MachineCmdConfApp"`
}
// 注入MachineTermOpRepo
@@ -87,12 +89,17 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
LogCmd: cli.Info.EnableRecorder == 1,
}
// createTsParam.CmdFilterFuncs = []mcm.CmdFilterFunc{func(cmd string) error {
// if strings.HasPrefix(cmd, "rm") {
// return errorx.NewBiz("该命令已被禁用...")
// }
// return nil
// }}
cmdConfs := m.machineCmdConfApp.GetCmdConfsByMachineTags(cli.Info.TagPath...)
if len(cmdConfs) > 0 {
createTsParam.CmdFilterFuncs = []mcm.CmdFilterFunc{func(cmd string) error {
for _, cmdConf := range cmdConfs {
if cmdConf.CmdRegexp.Match([]byte(cmd)) {
return errorx.NewBiz("该命令已被禁用...")
}
}
return nil
}}
}
mts, err := mcm.NewTerminalSession(createTsParam)
if err != nil {

View File

@@ -0,0 +1,16 @@
package entity
import (
"mayfly-go/pkg/model"
)
// 机器命令过滤配置
type MachineCmdConf struct {
model.Model
Name string `json:"name"`
Cmds model.Slice[string] `json:"cmds"` // 命令配置
Status int8 `json:"execCmds"` // 状态
Stratege string `json:"stratege"` // 策略,空禁用
Remark string `json:"remark"` // 备注
}

View File

@@ -0,0 +1,10 @@
package repository
import (
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/pkg/base"
)
type MachineCmdConf interface {
base.Repo[*entity.MachineCmdConf]
}

View File

@@ -0,0 +1,15 @@
package persistence
import (
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
"mayfly-go/pkg/base"
)
type machineCmdConfRepoImpl struct {
base.RepoImpl[*entity.MachineCmdConf]
}
func newMachineCmdConfRepo() repository.MachineCmdConf {
return &machineCmdConfRepoImpl{base.RepoImpl[*entity.MachineCmdConf]{M: new(entity.MachineCmdConf)}}
}

View File

@@ -12,4 +12,5 @@ func InitIoc() {
ioc.Register(newMachineCronJobExecRepo(), ioc.WithComponentName("MachineCronJobExecRepo"))
ioc.Register(newMachineCronJobRelateRepo(), ioc.WithComponentName("MachineCronJobRelateRepo"))
ioc.Register(newMachineTermOpRepoImpl(), ioc.WithComponentName("MachineTermOpRepo"))
ioc.Register(newMachineCmdConfRepo(), ioc.WithComponentName("MachineCmdConfRepo"))
}

View File

@@ -3,7 +3,7 @@ package init
import (
"context"
"mayfly-go/initialize"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/event"
"mayfly-go/internal/machine/application"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/infrastructure/persistence"
@@ -28,17 +28,17 @@ func Init() {
application.GetMachineTermOpApp().TimerDeleteTermOp()
global.EventBus.Subscribe(consts.DeleteMachineEventTopic, "machineFile", func(ctx context.Context, event *eventbus.Event) error {
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineFile", func(ctx context.Context, event *eventbus.Event) error {
me := event.Val.(*entity.Machine)
return application.GetMachineFileApp().DeleteByCond(ctx, &entity.MachineFile{MachineId: me.Id})
})
global.EventBus.Subscribe(consts.DeleteMachineEventTopic, "machineScript", func(ctx context.Context, event *eventbus.Event) error {
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineScript", func(ctx context.Context, event *eventbus.Event) error {
me := event.Val.(*entity.Machine)
return application.GetMachineScriptApp().DeleteByCond(ctx, &entity.MachineScript{MachineId: me.Id})
})
global.EventBus.Subscribe(consts.DeleteMachineEventTopic, "machineCronJob", func(ctx context.Context, event *eventbus.Event) error {
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineCronJob", func(ctx context.Context, event *eventbus.Event) error {
me := event.Val.(*entity.Machine)
var jobIds []uint64
application.GetMachineCronJobApp().MachineRelateCronJobs(ctx, me.Id, jobIds)

View File

@@ -0,0 +1,27 @@
package router
import (
"mayfly-go/internal/machine/api"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func InitMachineCmdConfRouter(router *gin.RouterGroup) {
mccs := router.Group("machine/security/cmd-confs")
mcc := new(api.MachineCmdConf)
biz.ErrIsNil(ioc.Inject(mcc))
reqs := [...]*req.Conf{
req.NewGet("", mcc.MachineCmdConfs),
req.NewPost("", mcc.Save).Log(req.NewLogSave("机器命令配置-保存")).RequiredPermissionCode("cmdconf:save"),
req.NewDelete(":id", mcc.Delete).Log(req.NewLogSave("机器命令配置-删除")).RequiredPermissionCode("cmdconf:del"),
}
req.BatchSetGroup(mccs, reqs[:])
}

View File

@@ -7,4 +7,5 @@ func Init(router *gin.RouterGroup) {
InitMachineFileRouter(router)
InitMachineScriptRouter(router)
InitMachineCronJobRouter(router)
InitMachineCmdConfRouter(router)
}

View File

@@ -3,6 +3,7 @@ package api
import (
"context"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/event"
"mayfly-go/internal/mongo/api/form"
"mayfly-go/internal/mongo/api/vo"
"mayfly-go/internal/mongo/application"
@@ -10,6 +11,7 @@ import (
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/global"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
@@ -93,6 +95,9 @@ func (m *Mongo) Databases(rc *req.Ctx) {
func (m *Mongo) Collections(rc *req.Ctx) {
conn, err := m.MongoApp.GetMongoConn(m.GetMongoId(rc))
biz.ErrIsNil(err)
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, conn.Info.TagPath[0])
db := rc.Query("database")
biz.NotEmpty(db, "database不能为空")
ctx := context.TODO()

View File

@@ -1,9 +1,11 @@
package api
import (
"mayfly-go/internal/event"
"mayfly-go/internal/redis/api/form"
"mayfly-go/internal/redis/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/global"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
)
@@ -14,8 +16,11 @@ func (r *Redis) RunCmd(rc *req.Ctx) {
biz.IsTrue(len(cmdReq.Cmd) > 0, "redis命令不能为空")
redisConn := r.getRedisConn(rc)
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.GetLoginAccount().Id, redisConn.Info.TagPath...), "%s")
rc.ReqParam = collx.Kvs("redis", redisConn.Info, "cmd", cmdReq.Cmd)
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, redisConn.Info.TagPath[0])
res, err := r.RedisApp.RunCmd(rc.MetaCtx, redisConn, runCmdParam)
biz.ErrIsNil(err)
rc.ResData = res

View File

@@ -0,0 +1,24 @@
package api
import (
"mayfly-go/internal/tag/application"
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
)
type ResourceOpLog struct {
ResourceOpLogApp application.ResourceOpLog `inject:""`
}
func (r *ResourceOpLog) PageAccountOpLog(rc *req.Ctx) {
cond := new(entity.ResourceOpLog)
cond.ResourceCode = rc.Query("resourceCode")
cond.ResourceType = int8(rc.QueryInt("resourceType"))
cond.CreatorId = rc.GetLoginAccount().Id
var rols []*entity.ResourceOpLog
res, err := r.ResourceOpLogApp.PageQuery(cond, rc.GetPageParam(), &rols)
biz.ErrIsNil(err)
rc.ResData = res
}

View File

@@ -15,7 +15,8 @@ import (
)
type TagTree struct {
TagTreeApp application.TagTree `inject:""`
TagTreeApp application.TagTree `inject:""`
TagTreeRelateApp application.TagTreeRelate `inject:""`
}
func (p *TagTree) GetTagTree(rc *req.Ctx) {
@@ -123,3 +124,8 @@ func (p *TagTree) CountTagResource(rc *req.Ctx) {
"mongo": len(p.TagTreeApp.GetAccountTagCodes(accountId, consts.ResourceTypeMongo, tagPath)),
}
}
// 获取关联的标签id
func (p *TagTree) GetRelateTagIds(rc *req.Ctx) {
rc.ResData = p.TagTreeRelateApp.GetTagPathsByRelate(entity.TagRelateType(rc.PathParamInt("relateType")), uint64(rc.PathParamInt("relateId")))
}

View File

@@ -10,22 +10,29 @@ import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"strings"
"github.com/may-fly/cast"
)
type Team struct {
TeamApp application.Team `inject:""`
TagTreeApp application.TagTree `inject:""`
AccountApp sys_applicaiton.Account `inject:""`
TeamApp application.Team `inject:""`
TagTreeApp application.TagTree `inject:""`
TagTreeRelateApp application.TagTreeRelate `inject:""`
AccountApp sys_applicaiton.Account `inject:""`
}
func (p *Team) GetTeams(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage(rc, new(entity.TeamQuery))
teams := &[]entity.Team{}
res, err := p.TeamApp.GetPageList(queryCond, page, teams)
var teams []*vo.Team
res, err := p.TeamApp.GetPageList(queryCond, page, &teams)
biz.ErrIsNil(err)
p.TagTreeRelateApp.FillTagInfo(entity.TagRelateTypeTeam, collx.ArrayMap(teams, func(mvo *vo.Team) entity.IRelateTag {
return mvo
})...)
rc.ResData = res
}
@@ -89,8 +96,3 @@ func (p *Team) DelTeamMember(rc *req.Ctx) {
p.TeamApp.DeleteMember(rc.MetaCtx, uint64(tid), uint64(aid))
}
// 获取团队关联的标签id
func (p *Team) GetTagIds(rc *req.Ctx) {
rc.ResData = p.TeamApp.ListTagIds(uint64(rc.PathParamInt("id")))
}

View File

@@ -1,6 +1,22 @@
package vo
import "time"
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/model"
"time"
)
type Team struct {
model.Model
entity.RelateTags // 标签信息
Name string `json:"name"` // 名称
Remark string `json:"remark"` // 备注说明
}
func (t *Team) GetRelateId() uint64 {
return t.Id
}
// 团队成员信息
type TeamMember struct {

View File

@@ -8,4 +8,10 @@ func InitIoc() {
ioc.Register(new(tagTreeAppImpl), ioc.WithComponentName("TagTreeApp"))
ioc.Register(new(teamAppImpl), ioc.WithComponentName("TeamApp"))
ioc.Register(new(resourceAuthCertAppImpl), ioc.WithComponentName("ResourceAuthCertApp"))
ioc.Register(new(resourceOpLogAppImpl), ioc.WithComponentName("ResourceOpLogApp"))
ioc.Register(new(tagTreeRelateAppImpl), ioc.WithComponentName("TagTreeRelateApp"))
}
func GetResourceOpLogApp() ResourceOpLog {
return ioc.Get[ResourceOpLog]("ResourceOpLogApp")
}

View File

@@ -0,0 +1,58 @@
package application
import (
"context"
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/utils/collx"
"time"
)
type ResourceOpLog interface {
base.App[*entity.ResourceOpLog]
// AddResourceOpLog 新增资源操作记录
AddResourceOpLog(ctx context.Context, codePath string) error
}
type resourceOpLogAppImpl struct {
base.AppImpl[*entity.ResourceOpLog, repository.ResourceOpLog]
tagTreeApp TagTree `inject:"TagTreeApp"`
}
var _ (ResourceOpLog) = (*resourceOpLogAppImpl)(nil)
// 注入ResourceOpLogRepo
func (rol *resourceOpLogAppImpl) InjectResourceOpLogRepo(resourceOpLogRepo repository.ResourceOpLog) {
rol.Repo = resourceOpLogRepo
}
func (rol *resourceOpLogAppImpl) AddResourceOpLog(ctx context.Context, codePath string) error {
loginAccount := contextx.GetLoginAccount(ctx)
if loginAccount == nil {
return errorx.NewBiz("当前上下文不存在登录信息")
}
var logs []*entity.ResourceOpLog
if err := rol.ListByWheres(collx.Kvs("create_time > ?", time.Now().Add(-5*time.Minute), "creator_id = ?", loginAccount.Id, "code_path = ?", codePath), &logs); err != nil {
return err
}
// 指定时间内多次操作则不记录
if len(logs) > 0 {
return nil
}
tagTree := &entity.TagTree{CodePath: codePath}
if err := rol.tagTreeApp.GetBy(tagTree); err != nil {
return errorx.NewBiz("资源不存在")
}
return rol.Save(ctx, &entity.ResourceOpLog{
ResourceCode: tagTree.Code,
ResourceType: int8(tagTree.Type),
CodePath: tagTree.CodePath,
})
}

View File

@@ -99,7 +99,7 @@ type TagTree interface {
type tagTreeAppImpl struct {
base.AppImpl[*entity.TagTree, repository.TagTree]
tagTreeTeamRepo repository.TagTreeTeam `inject:"TagTreeTeamRepo"`
tagTreeRelateApp TagTreeRelate `inject:"TagTreeRelateApp"`
}
// 注入TagTreeRepo
@@ -450,7 +450,7 @@ func (p *tagTreeAppImpl) ListTagPathByTypeAndCode(resourceType int8, resourceCod
}
func (p *tagTreeAppImpl) ListTagByAccountId(accountId uint64) []string {
return p.tagTreeTeamRepo.SelectTagPathsByAccountId(accountId)
return p.tagTreeRelateApp.GetTagPathsByAccountId(accountId)
}
func (p *tagTreeAppImpl) CanAccess(accountId uint64, tagPath ...string) error {
@@ -486,7 +486,7 @@ func (p *tagTreeAppImpl) FillTagInfo(resourceTagType entity.TagType, resources .
for _, tr := range tagResources {
// 赋值标签信息
resourceCode2Resouce[tr.Code].SetTagInfo(entity.ResourceTag{CodePath: tr.GetTagPath()})
resourceCode2Resouce[tr.Code].SetTagInfo(entity.ResourceTag{TagId: tr.Id, CodePath: tr.GetTagPath()})
}
}
@@ -552,6 +552,8 @@ func (p *tagTreeAppImpl) deleteByIds(ctx context.Context, tagIds []uint64) error
return err
}
// 删除team关联的标签
return p.tagTreeTeamRepo.DeleteByWheres(ctx, collx.M{"tag_id in ?": tagIds})
// 删除与标签有关联信息的记录(如团队关联的标签等)
return p.tagTreeRelateApp.DeleteByWheres(ctx, collx.M{
"tag_id in ?": tagIds,
})
}

View File

@@ -0,0 +1,130 @@
package application
import (
"context"
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/utils/collx"
)
type TagTreeRelate interface {
base.App[*entity.TagTreeRelate]
// RelateTag 关联标签
RelateTag(ctx context.Context, relateType entity.TagRelateType, relateId uint64, tagCodePaths ...string) error
// GetRelateIds 根据标签路径获取对应关联的id
GetRelateIds(relateType entity.TagRelateType, tagPaths ...string) ([]uint64, error)
// GetTagPathsByAccountId 根据账号id获取该账号可操作的标签code路径
GetTagPathsByAccountId(accountId uint64) []string
// GetTagPathsByRelate 根据关联信息获取关联的标签codePaths
GetTagPathsByRelate(relateType entity.TagRelateType, relateId uint64) []string
// FillTagInfo 填充关联的标签信息
FillTagInfo(relateType entity.TagRelateType, relates ...entity.IRelateTag)
}
type tagTreeRelateAppImpl struct {
base.AppImpl[*entity.TagTreeRelate, repository.TagTreeRelate]
tagTreeRelateRepo repository.TagTreeRelate `inject:"TagTreeRelateRepo"`
tagTreeApp TagTree `inject:"TagTreeApp"`
}
var _ (TagTreeRelate) = (*tagTreeRelateAppImpl)(nil)
// 注入TagTreeRelateRepo
func (p *tagTreeRelateAppImpl) InjectTagTreeRelateRepo(tagTreeRelateRepo repository.TagTreeRelate) {
p.Repo = tagTreeRelateRepo
}
func (tr *tagTreeRelateAppImpl) RelateTag(ctx context.Context, relateType entity.TagRelateType, relateId uint64, tagCodePaths ...string) error {
var tags []*entity.TagTree
tr.tagTreeApp.ListByQuery(&entity.TagTreeQuery{CodePaths: tagCodePaths}, &tags)
if len(tags) != len(tagCodePaths) {
return errorx.NewBiz("存在错误标签路径")
}
var oldRelates []*entity.TagTreeRelate
tr.ListByCond(&entity.TagTreeRelate{RelateType: relateType, RelateId: relateId}, &oldRelates)
oldTagIds := collx.ArrayMap[*entity.TagTreeRelate, uint64](oldRelates, func(val *entity.TagTreeRelate) uint64 {
return val.TagId
})
newTagIds := collx.ArrayMap[*entity.TagTree, uint64](tags, func(val *entity.TagTree) uint64 {
return val.Id
})
addTagIds, delTagIds, _ := collx.ArrayCompare(newTagIds, oldTagIds)
if len(addTagIds) > 0 {
trs := make([]*entity.TagTreeRelate, 0)
for _, tagId := range addTagIds {
trs = append(trs, &entity.TagTreeRelate{
TagId: tagId,
RelateType: relateType,
RelateId: relateId,
})
}
if err := tr.BatchInsert(ctx, trs); err != nil {
return err
}
}
if len(delTagIds) > 0 {
if err := tr.DeleteByWheres(ctx, collx.Kvs("relate_type=?", relateType, "relate_id=?", relateId, "tag_id in ?", delTagIds)); err != nil {
return err
}
}
return nil
}
func (tr *tagTreeRelateAppImpl) GetRelateIds(relateType entity.TagRelateType, tagPaths ...string) ([]uint64, error) {
poisibleTagPaths := make([]string, 0)
for _, tagPath := range tagPaths {
// 追加可能关联的标签路径如tagPath = tag1/tag2/1|xxx/需要获取所有关联的自身及父标签tag1/ tag1/tag2/ tag1/tag2/1|xxx
poisibleTagPaths = append(poisibleTagPaths, entity.GetAllCodePath(tagPath)...)
}
return tr.tagTreeRelateRepo.SelectRelateIdsByTagPaths(relateType, poisibleTagPaths...)
}
func (tr *tagTreeRelateAppImpl) GetTagPathsByAccountId(accountId uint64) []string {
return tr.tagTreeRelateRepo.SelectTagPathsByAccountId(accountId)
}
func (tr *tagTreeRelateAppImpl) GetTagPathsByRelate(relateType entity.TagRelateType, relateId uint64) []string {
return tr.tagTreeRelateRepo.SelectTagPathsByRelate(relateType, relateId)
}
func (tr *tagTreeRelateAppImpl) FillTagInfo(relateType entity.TagRelateType, relates ...entity.IRelateTag) {
if len(relates) == 0 {
return
}
// 关联id -> 关联信息
relateIds2Relate := collx.ArrayToMap(relates, func(rt entity.IRelateTag) uint64 {
return rt.GetRelateId()
})
var relateTags []*entity.TagTreeRelate
tr.ListByWheres(collx.Kvs("relate_type=?", relateType, "relate_id in ?", collx.MapKeys(relateIds2Relate)), &relateTags)
tagIds := collx.ArrayMap(relateTags, func(rt *entity.TagTreeRelate) uint64 {
return rt.TagId
})
var tags []*entity.TagTree
tr.tagTreeApp.GetByIdIn(&tags, tagIds)
tagId2Tag := collx.ArrayToMap(tags, func(t *entity.TagTree) uint64 {
return t.Id
})
for _, rt := range relateTags {
// 赋值标签信息
tag := tagId2Tag[rt.TagId]
relateIds2Relate[rt.RelateId].SetTagInfo(entity.ResourceTag{CodePath: tag.CodePath, TagId: tag.Id})
}
}

View File

@@ -10,7 +10,6 @@ import (
"mayfly-go/pkg/gormx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
"gorm.io/gorm"
)
@@ -20,7 +19,7 @@ type SaveTeamParam struct {
Name string `json:"name" binding:"required"` // 名称
Remark string `json:"remark"` // 备注说明
Tags []uint64 `json:"tags"` // 关联标签信息
CodePaths []string `json:"codePaths"` // 关联标签信息
}
type Team interface {
@@ -42,17 +41,14 @@ type Team interface {
IsExistMember(teamId, accounId uint64) bool
//--------------- 关联项目相关接口 ---------------
ListTagIds(teamId uint64) []uint64
DeleteTag(tx context.Context, teamId, tagId uint64) error
}
type teamAppImpl struct {
teamRepo repository.Team `inject:"TeamRepo"`
teamMemberRepo repository.TeamMember `inject:"TeamMemberRepo"`
tagTreeTeamRepo repository.TagTreeTeam `inject:"TagTreeTeamRepo"`
teamRepo repository.Team `inject:"TeamRepo"`
teamMemberRepo repository.TeamMember `inject:"TeamMemberRepo"`
tagTreeRelateApp TagTreeRelate `inject:"TagTreeRelateApp"`
}
func (p *teamAppImpl) GetPageList(condition *entity.TeamQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
@@ -89,35 +85,7 @@ func (p *teamAppImpl) Save(ctx context.Context, saveParam *SaveTeamParam) error
}
// 保存团队关联的标签信息
teamId := team.Id
var addIds, delIds []uint64
if saveParam.Id == 0 {
addIds = saveParam.Tags
} else {
// 将[]uint64转为[]any
oIds := p.ListTagIds(team.Id)
// 比较新旧两合集
addIds, delIds, _ = collx.ArrayCompare(saveParam.Tags, oIds)
}
addTeamTags := make([]*entity.TagTreeTeam, 0)
for _, v := range addIds {
ptt := &entity.TagTreeTeam{TeamId: teamId, TagId: v}
addTeamTags = append(addTeamTags, ptt)
}
if len(addTeamTags) > 0 {
logx.DebugfContext(ctx, "团队[%s]新增关联的标签信息: [%v]", team.Name, addTeamTags)
p.tagTreeTeamRepo.BatchInsert(ctx, addTeamTags)
}
for _, v := range delIds {
p.DeleteTag(ctx, teamId, v)
}
if len(delIds) > 0 {
logx.DebugfContext(ctx, "团队[%s]删除关联的标签信息: [%v]", team.Name, delIds)
}
return nil
return p.tagTreeRelateApp.RelateTag(ctx, entity.TagRelateTypeTeam, team.Id, saveParam.CodePaths...)
}
func (p *teamAppImpl) Delete(ctx context.Context, id uint64) error {
@@ -129,7 +97,7 @@ func (p *teamAppImpl) Delete(ctx context.Context, id uint64) error {
return p.teamMemberRepo.DeleteByCondWithDb(ctx, db, &entity.TeamMember{TeamId: id})
},
func(db *gorm.DB) error {
return p.tagTreeTeamRepo.DeleteByCondWithDb(ctx, db, &entity.TagTreeTeam{TeamId: id})
return p.tagTreeRelateApp.DeleteByCondWithDb(ctx, db, &entity.TagTreeRelate{RelateType: entity.TagRelateTypeTeam, RelateId: id})
},
)
}
@@ -158,17 +126,7 @@ func (p *teamAppImpl) IsExistMember(teamId, accounId uint64) bool {
//--------------- 标签相关接口 ---------------
func (p *teamAppImpl) ListTagIds(teamId uint64) []uint64 {
tags := &[]entity.TagTreeTeam{}
p.tagTreeTeamRepo.ListByCondOrder(&entity.TagTreeTeam{TeamId: teamId}, tags)
ids := make([]uint64, 0)
for _, v := range *tags {
ids = append(ids, v.TagId)
}
return ids
}
// 删除关联项目信息
// 删除关联标签信息
func (p *teamAppImpl) DeleteTag(ctx context.Context, teamId, tagId uint64) error {
return p.tagTreeTeamRepo.DeleteByCond(ctx, &entity.TagTreeTeam{TeamId: teamId, TagId: tagId})
return p.tagTreeRelateApp.DeleteByCond(ctx, &entity.TagTreeRelate{RelateType: entity.TagRelateTypeTeam, RelateId: teamId, TagId: tagId})
}

View File

@@ -0,0 +1,12 @@
package entity
import "mayfly-go/pkg/model"
// 资源操作日志记录
type ResourceOpLog struct {
model.CreateModel
CodePath string `json:"codePath"` // 标签路径
ResourceCode string `json:"resourceCode"` // 资源编号
ResourceType int8 `json:"relateType"` // 资源类型
}

View File

@@ -89,11 +89,13 @@ type ITagResource interface {
// 资源关联的标签信息
type ResourceTag struct {
TagId uint64 `json:"tagId" gorm:"-"`
CodePath string `json:"codePath" gorm:"-"` // 标签路径
}
func (r *ResourceTag) SetTagInfo(rt ResourceTag) {
r.CodePath = rt.CodePath
r.TagId = rt.TagId
}
// 资源标签列表

View File

@@ -0,0 +1,40 @@
package entity
import "mayfly-go/pkg/model"
// 与标签树有关联关系的实体
type TagTreeRelate struct {
model.Model
TagId uint64 `json:"tagId"`
RelateId uint64 `json:"relateId"` // 关联的id
RelateType TagRelateType `json:"relateType"` // 关联的类型
}
type TagRelateType int8
const (
TagRelateTypeTeam TagRelateType = 1 // 关联团队
TagRelateTypeMachineCmd TagRelateType = 2 // 关联机器命令配置
)
// 关联标签信息,如果要实现填充关联标签信息,则结构体需要实现该接口
type IRelateTag interface {
// 获取关联id
GetRelateId() uint64
// 赋值标签路径
SetTagInfo(tag ResourceTag)
}
// 关联的标签信息
type RelateTags struct {
Tags []ResourceTag `json:"tags" gorm:"-"` // 标签路径
}
func (r *RelateTags) SetTagInfo(rt ResourceTag) {
if r.Tags == nil {
r.Tags = make([]ResourceTag, 0)
}
r.Tags = append(r.Tags, rt)
}

View File

@@ -1,11 +0,0 @@
package entity
import "mayfly-go/pkg/model"
// 标签树与团队关联信息
type TagTreeTeam struct {
model.Model
TagId uint64 `json:"tagId"`
TeamId uint64 `json:"teamId"`
}

View File

@@ -0,0 +1,10 @@
package repository
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
)
type ResourceOpLog interface {
base.Repo[*entity.ResourceOpLog]
}

View File

@@ -0,0 +1,19 @@
package repository
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
)
type TagTreeRelate interface {
base.Repo[*entity.TagTreeRelate]
// SelectRelateIdsByTagPaths 根据标签路径查询相关联的id
SelectRelateIdsByTagPaths(relateType entity.TagRelateType, tagPaths ...string) ([]uint64, error)
// SelectTagPathsByAccountId 根据账号id获取该账号可访问操作的标签codePaths该方法调用较频繁故不使用下列方法获取
SelectTagPathsByAccountId(accountId uint64) []string
// SelectTagPathsByRelate 根据关联信息查询对应的关联的标签路径
SelectTagPathsByRelate(relateType entity.TagRelateType, relateId uint64) []string
}

View File

@@ -1,12 +0,0 @@
package repository
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
)
type TagTreeTeam interface {
base.Repo[*entity.TagTreeTeam]
SelectTagPathsByAccountId(accountId uint64) []string
}

View File

@@ -6,8 +6,9 @@ import (
func InitIoc() {
ioc.Register(newTagTreeRepo(), ioc.WithComponentName("TagTreeRepo"))
ioc.Register(newTagTreeTeamRepo(), ioc.WithComponentName("TagTreeTeamRepo"))
ioc.Register(newTeamRepo(), ioc.WithComponentName("TeamRepo"))
ioc.Register(newTeamMemberRepo(), ioc.WithComponentName("TeamMemberRepo"))
ioc.Register(newResourceAuthCertRepoImpl(), ioc.WithComponentName("ResourceAuthCertRepo"))
ioc.Register(newResourceOpLogRepo(), ioc.WithComponentName("ResourceOpLogRepo"))
ioc.Register(newTagTreeRelateRepo(), ioc.WithComponentName("TagTreeRelateRepo"))
}

View File

@@ -0,0 +1,15 @@
package persistence
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
)
type resourceOpLogRepoImpl struct {
base.RepoImpl[*entity.ResourceOpLog]
}
func newResourceOpLogRepo() repository.ResourceOpLog {
return &resourceOpLogRepoImpl{base.RepoImpl[*entity.ResourceOpLog]{M: new(entity.ResourceOpLog)}}
}

View File

@@ -0,0 +1,86 @@
package persistence
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/gormx"
)
type tagTreeRelateRepoImpl struct {
base.RepoImpl[*entity.TagTreeRelate]
}
func newTagTreeRelateRepo() repository.TagTreeRelate {
return &tagTreeRelateRepoImpl{base.RepoImpl[*entity.TagTreeRelate]{M: new(entity.TagTreeRelate)}}
}
// SelectRelateIdsByTagPaths 根据标签路径查询相关联的id
func (tr *tagTreeRelateRepoImpl) SelectRelateIdsByTagPaths(relateType entity.TagRelateType, tagPaths ...string) ([]uint64, error) {
var res []uint64
sql := `
SELECT
t1.relate_id
FROM
t_tag_tree_relate t1
JOIN t_tag_tree t ON
t.id = t1.tag_id
WHERE
t1.relate_type = ?
AND t.code_path in ?
AND t.is_deleted = 0
AND t1.is_deleted = 0
ORDER BY
t.code_path
`
if err := gormx.GetListBySql2Model(sql, &res, relateType, tagPaths); err != nil {
return res, err
}
return res, nil
}
func (tr *tagTreeRelateRepoImpl) SelectTagPathsByAccountId(accountId uint64) []string {
var res []string
sql := `
SELECT
DISTINCT(t.code_path)
FROM
t_tag_tree_relate t1
JOIN t_team_member t2 ON
t1.relate_id = t2.team_id
JOIN t_tag_tree t ON
t.id = t1.tag_id
WHERE
t1.relate_type = ?
AND t2.account_id = ?
AND t1.is_deleted = 0
AND t2.is_deleted = 0
AND t.is_deleted = 0
ORDER BY
t.code_path
`
gormx.GetListBySql2Model(sql, &res, entity.TagRelateTypeTeam, accountId)
return res
}
// SelectTagPathsByRelate 根据关联信息查询对应的关联的标签路径
func (tr *tagTreeRelateRepoImpl) SelectTagPathsByRelate(relateType entity.TagRelateType, relateId uint64) []string {
var res []string
sql := `
SELECT
DISTINCT(t.code_path)
FROM
t_tag_tree_relate t1
JOIN t_tag_tree t ON
t.id = t1.tag_id
WHERE
t1.relate_id = ?
AND t1.relate_type = ?
AND t.is_deleted = 0
AND t1.is_deleted = 0
ORDER BY
t.code_path
`
gormx.GetListBySql2Model(sql, &res, relateId, relateType)
return res
}

View File

@@ -1,39 +0,0 @@
package persistence
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/gormx"
)
type tagTreeTeamRepoImpl struct {
base.RepoImpl[*entity.TagTreeTeam]
}
func newTagTreeTeamRepo() repository.TagTreeTeam {
return &tagTreeTeamRepoImpl{base.RepoImpl[*entity.TagTreeTeam]{M: new(entity.TagTreeTeam)}}
}
func (p *tagTreeTeamRepoImpl) SelectTagPathsByAccountId(accountId uint64) []string {
var res []string
sql := `
SELECT
DISTINCT(t.code_path)
FROM
t_tag_tree_team t1
JOIN t_team_member t2 ON
t1.team_id = t2.team_id
JOIN t_tag_tree t ON
t.id = t1.tag_id
WHERE
t2.account_id = ?
AND t1.is_deleted = 0
AND t2.is_deleted = 0
AND t.is_deleted = 0
ORDER BY
t.code_path
`
gormx.GetListBySql2Model(sql, &res, accountId)
return res
}

View File

@@ -1,10 +1,14 @@
package init
import (
"context"
"mayfly-go/initialize"
"mayfly-go/internal/event"
"mayfly-go/internal/tag/application"
"mayfly-go/internal/tag/infrastructure/persistence"
"mayfly-go/internal/tag/router"
"mayfly-go/pkg/eventbus"
"mayfly-go/pkg/global"
)
func init() {
@@ -13,4 +17,14 @@ func init() {
application.InitIoc()
})
initialize.AddInitRouterFunc(router.Init)
initialize.AddInitFunc(Init)
}
func Init() {
global.EventBus.SubscribeAsync(event.EventTopicResourceOp, "ResourceOpLogApp", func(ctx context.Context, event *eventbus.Event) error {
codePath := event.Val.(string)
return application.GetResourceOpLogApp().AddResourceOpLog(ctx, codePath)
}, false)
}

View File

@@ -0,0 +1,24 @@
package router
import (
"mayfly-go/internal/tag/api"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func InitResourceOpLogRouter(router *gin.RouterGroup) {
m := new(api.ResourceOpLog)
biz.ErrIsNil(ioc.Inject(m))
resourceOpLog := router.Group("/resource-op-logs")
{
reqs := [...]*req.Conf{
req.NewGet("/account", m.PageAccountOpLog),
}
req.BatchSetGroup(resourceOpLog, reqs[:])
}
}

View File

@@ -6,4 +6,5 @@ func Init(router *gin.RouterGroup) {
InitTagTreeRouter(router)
InitTeamRouter(router)
InitResourceAuthCertRouter(router)
InitResourceOpLogRouter(router)
}

View File

@@ -31,6 +31,9 @@ func InitTagTreeRouter(router *gin.RouterGroup) {
req.NewGet("/resources/:rtype/tag-paths", m.TagResources),
req.NewGet("/resources/count", m.CountTagResource),
// 获取关联的标签id列表
req.NewGet("/relate/:relateType/:relateId", m.GetRelateTagIds),
}
req.BatchSetGroup(tagTree, reqs[:])

View File

@@ -29,9 +29,6 @@ func InitTeamRouter(router *gin.RouterGroup) {
req.NewPost("/:id/members", m.SaveTeamMember).Log(req.NewLogSave("团队-新增成员")).RequiredPermissionCode("team:member:save"),
req.NewDelete("/:id/members/:accountId", m.DelTeamMember).Log(req.NewLogSave("团队-删除成员")).RequiredPermissionCode("team:member:del"),
// 获取团队关联的标签id列表
req.NewGet("/:id/tags", m.GetTagIds),
}
req.BatchSetGroup(team, reqs[:])

View File

@@ -89,9 +89,6 @@ func T2022() *gormigrate.Migration {
if err := tx.AutoMigrate(&entity7.TagTree{}); err != nil {
return err
}
if err := tx.AutoMigrate(&entity7.TagTreeTeam{}); err != nil {
return err
}
if err := tx.AutoMigrate(&entity7.Team{}); err != nil {
return err
}

View File

@@ -138,9 +138,18 @@ type Map[K comparable, V any] map[K]V
func (m *Map[K, V]) Scan(value any) error {
return json.Unmarshal(value.([]byte), m)
}
func (m Map[K, V]) Value() (driver.Value, error) {
return json.Marshal(m)
}
type Slice[T int | string | Map[string, any]] []T
func (s *Slice[T]) Scan(value any) error {
return json.Unmarshal(value.([]byte), s)
}
func (s Slice[T]) Value() (driver.Value, error) {
return json.Marshal(s)
}

View File

@@ -503,6 +503,25 @@ CREATE TABLE `t_machine_term_op` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='机器终端操作记录表';
DROP TABLE IF EXISTS `t_machine_cmd_conf`;
CREATE TABLE `t_machine_cmd_conf` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '名称',
`cmds` varchar(500) COLLATE utf8_bin DEFAULT NULL COMMENT '命令配置',
`status` tinyint(4) DEFAULT NULL COMMENT '状态',
`stratege` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '策略',
`remark` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) COLLATE utf8_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(36) COLLATE utf8_bin NOT NULL,
`is_deleted` tinyint(4) DEFAULT '0',
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='机器命令配置';
-- ----------------------------
-- Table structure for t_mongo
-- ----------------------------
@@ -826,7 +845,9 @@ INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `we
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196723, 1709194669, 2, 1, '启停', 'db:transfer:status', 1709196723, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:04', '2024-02-29 16:52:04', 'SmLcpu6c/hGiLN1VT/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196737, 1709194669, 2, 1, '日志', 'db:transfer:log', 1709196737, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:17', '2024-02-29 16:52:17', 'SmLcpu6c/CZhNIbWg/', 0, NULL);
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196755, 1709194669, 2, 1, '运行', 'db:transfer:run', 1709196755, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:36', '2024-02-29 16:52:36', 'SmLcpu6c/b6yHt6V2/', 0, NULL);
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1714032002, 1713875842, '12sSjal1/UnWIUhW0/0tJwC3Gf/', 2, 1, '命令配置-删除', 'cmdconf:del', 1714032002, 'null', 1, 'admin', 1, 'admin', '2024-04-25 16:00:02', '2024-04-25 16:00:02', 0, NULL);
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1714031981, 1713875842, '12sSjal1/UnWIUhW0/tEzIKecl/', 2, 1, '命令配置-保存', 'cmdconf:save', 1714031981, 'null', 1, 'admin', 1, 'admin', '2024-04-25 15:59:41', '2024-04-25 15:59:41', 0, NULL);
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1713875842, 2, '12sSjal1/UnWIUhW0/', 1, 1, '安全配置', 'security', 1713875842, '{"component":"ops/machine/security/SecurityConfList","icon":"Setting","isKeepAlive":true,"routeName":"SecurityConfList"}', 1, 'admin', 1, 'admin', '2024-04-23 20:37:22', '2024-04-23 20:37:22', 0, NULL);
COMMIT;
-- ----------------------------
@@ -913,31 +934,29 @@ BEGIN;
INSERT INTO `t_tag_tree` VALUES (1, -1, 'default', 'default/', '默认', '默认标签', '2022-10-26 20:04:19', 1, 'admin', '2022-10-26 20:04:19', 1, 'admin', 0, NULL);
COMMIT;
-- ----------------------------
-- Table structure for t_tag_tree_team
-- ----------------------------
DROP TABLE IF EXISTS `t_tag_tree_team`;
CREATE TABLE `t_tag_tree_team` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`tag_id` bigint(20) NOT NULL COMMENT '项目树id',
`team_id` bigint(20) NOT NULL COMMENT '团队id',
DROP TABLE IF EXISTS `t_tag_tree_relate`;
CREATE TABLE `t_tag_tree_relate` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tag_id` bigint NOT NULL COMMENT '标签树id',
`relate_id` bigint NOT NULL COMMENT '关联',
`relate_type` tinyint NOT NULL COMMENT '关联类型',
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) NOT NULL,
`creator_id` bigint NOT NULL,
`creator` varchar(36) COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(36) NOT NULL,
`is_deleted` tinyint(8) NOT NULL DEFAULT 0,
`modifier_id` bigint NOT NULL,
`modifier` varchar(36) COLLATE utf8mb4_bin NOT NULL,
`is_deleted` tinyint NOT NULL DEFAULT '0',
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tag_id` (`tag_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='标签树团队关联信息';
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='标签树有关联关系的表';
-- ----------------------------
-- Records of t_tag_tree_team
-- Records of t_tag_tree_relate
-- ----------------------------
BEGIN;
INSERT INTO `t_tag_tree_team` VALUES (1, 1, 1, '2022-10-26 20:04:45', 1, 'admin', '2022-10-26 20:04:45', 1, 'admin', 0, NULL);
INSERT INTO `t_tag_tree_relate` VALUES (1, 1, 1, 1, '2022-10-26 20:04:45', 1, 'admin', '2022-10-26 20:04:45', 1, 'admin', 0, NULL);
COMMIT;
-- ----------------------------
@@ -1019,6 +1038,21 @@ CREATE TABLE `t_resource_auth_cert` (
KEY `idx_name` (`name`) USING BTREE
) COMMENT='资源授权凭证表';
DROP TABLE IF EXISTS `t_resource_op_log`;
CREATE TABLE `t_resource_op_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`code_path` varchar(600) NOT NULL COMMENT '资源标签路径',
`resource_code` varchar(32) NOT NULL COMMENT '资源编号',
`resource_type` tinyint NOT NULL COMMENT '资源类型',
`create_time` datetime NOT NULL,
`creator_id` bigint NOT NULL,
`creator` varchar(36) NOT NULL,
`is_deleted` tinyint NOT NULL DEFAULT '0',
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_resource_code` (`resource_code`) USING BTREE
) ENGINE=InnoDB COMMENT='资源操作记录';
DROP TABLE IF EXISTS `t_flow_procdef`;
-- 工单流程相关表
CREATE TABLE `t_flow_procdef` (

View File

@@ -0,0 +1,84 @@
CREATE TABLE `t_tag_tree_relate` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tag_id` bigint NOT NULL COMMENT '标签树id',
`relate_id` bigint NOT NULL COMMENT '关联',
`relate_type` tinyint NOT NULL COMMENT '关联类型',
`create_time` datetime NOT NULL,
`creator_id` bigint NOT NULL,
`creator` varchar(36) COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint NOT NULL,
`modifier` varchar(36) COLLATE utf8mb4_bin NOT NULL,
`is_deleted` tinyint NOT NULL DEFAULT '0',
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tag_id` (`tag_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='与标签树有关联关系的表';
CREATE TABLE `t_machine_cmd_conf` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '名称',
`cmds` varchar(500) COLLATE utf8_bin DEFAULT NULL COMMENT '命令配置',
`status` tinyint(4) DEFAULT NULL COMMENT '状态',
`stratege` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '策略',
`remark` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) COLLATE utf8_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(36) COLLATE utf8_bin NOT NULL,
`is_deleted` tinyint(4) DEFAULT '0',
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='机器命令配置';
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1714032002, 1713875842, '12sSjal1/UnWIUhW0/0tJwC3Gf/', 2, 1, '命令配置-删除', 'cmdconf:del', 1714032002, 'null', 1, 'admin', 1, 'admin', '2024-04-25 16:00:02', '2024-04-25 16:00:02', 0, NULL);
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1714031981, 1713875842, '12sSjal1/UnWIUhW0/tEzIKecl/', 2, 1, '命令配置-保存', 'cmdconf:save', 1714031981, 'null', 1, 'admin', 1, 'admin', '2024-04-25 15:59:41', '2024-04-25 15:59:41', 0, NULL);
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1713875842, 2, '12sSjal1/UnWIUhW0/', 1, 1, '安全配置', 'security', 1713875842, '{"component":"ops/machine/security/SecurityConfList","icon":"Setting","isKeepAlive":true,"routeName":"SecurityConfList"}', 1, 'admin', 1, 'admin', '2024-04-23 20:37:22', '2024-04-23 20:37:22', 0, NULL);
INSERT
INTO
t_tag_tree_relate (tag_id,
relate_id,
relate_type,
create_time,
creator_id,
creator,
update_time,
modifier_id,
modifier,
is_deleted )
SELECT
tt.tag_id ,
tt.team_id ,
1,
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ),
1,
'admin',
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ),
1,
'admin',
0
FROM
`t_tag_tree_team` tt
WHERE
tt.`is_deleted` = 0;
DROP TABLE t_tag_tree_team;
CREATE TABLE `t_resource_op_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`code_path` varchar(600) NOT NULL COMMENT '资源标签路径',
`resource_code` varchar(32) NOT NULL COMMENT '资源编号',
`resource_type` tinyint NOT NULL COMMENT '资源类型',
`create_time` datetime NOT NULL,
`creator_id` bigint NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`is_deleted` tinyint NOT NULL DEFAULT '0',
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_resource_code` (`resource_code`) USING BTREE
) ENGINE=InnoDB COMMENT='资源操作记录';