10 Commits

Author SHA1 Message Date
meilin.huang
2deb3109c2 feat: dbms表数据新增表单视图 2024-07-19 17:06:11 +08:00
meilin.huang
a80221a950 fix: 数据库实例删除等问题修复 2024-07-05 13:14:31 +08:00
meilin.huang
10630847df fix: 工单流程信息展示问题修复 2024-06-24 17:17:57 +08:00
meilin.huang
f43851698e fix: 资源关联多标签删除、数据库实例删除等问题修复与数据库等名称过滤优化 2024-06-07 12:31:40 +08:00
Coder慌
73884bb693 !122 fix: mysql导出修复
Merge pull request !122 from zongyangleo/dev_1.8.6_fix
2024-06-05 04:17:03 +00:00
zongyangleo
1b5bb1de8b fix: mysql导出修复 2024-06-01 13:35:31 +08:00
meilin.huang
4814793546 fix: 修复数据库表数据横向滚动后切换tab导致表头错位&数据取消居中显示 2024-05-31 12:12:40 +08:00
meilin.huang
d85bbff270 release v1.8.6 2024-05-23 17:18:22 +08:00
meilin.huang
bb1522f4dc refactor: 数据库管理迁移至数据库实例-库管理、机器管理-文件支持用户和组信息查看 2024-05-21 12:34:26 +08:00
zongyangleo
a7632fbf58 !121 fix: rdp ssh
* fix: rdp ssh
2024-05-21 04:06:13 +00:00
72 changed files with 980 additions and 893 deletions

View File

@@ -22,7 +22,7 @@
### 介绍 ### 介绍
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle sqlserver 达梦 高斯 sqlite数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台** web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle sqlserver 达梦 高斯 sqlite数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
### 开发语言与主要框架 ### 开发语言与主要框架

View File

@@ -10,20 +10,20 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.11.0",
"asciinema-player": "^3.7.1", "asciinema-player": "^3.8.0",
"axios": "^1.6.2", "axios": "^1.6.2",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"cropperjs": "^1.6.1", "cropperjs": "^1.6.1",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"echarts": "^5.5.0", "echarts": "^5.5.1",
"element-plus": "^2.7.3", "element-plus": "^2.7.7",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"monaco-editor": "^0.48.0", "monaco-editor": "^0.50.0",
"monaco-sql-languages": "^0.11.0", "monaco-sql-languages": "^0.12.2",
"monaco-themes": "^0.4.4", "monaco-themes": "^0.4.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
@@ -34,8 +34,8 @@
"sql-formatter": "^15.0.2", "sql-formatter": "^15.0.2",
"trzsz": "^1.1.5", "trzsz": "^1.1.5",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.4.27", "vue": "^3.4.32",
"vue-router": "^4.3.2", "vue-router": "^4.4.0",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0", "xterm-addon-search": "^0.13.0",
@@ -48,16 +48,16 @@
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4", "@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.5",
"@vue/compiler-sfc": "^3.4.27", "@vue/compiler-sfc": "^3.4.32",
"code-inspector-plugin": "^0.4.5", "code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8.35.0", "eslint": "^8.35.0",
"eslint-plugin-vue": "^9.25.0", "eslint-plugin-vue": "^9.25.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"sass": "^1.77.1", "sass": "^1.77.8",
"typescript": "^5.4.5", "typescript": "^5.5.3",
"vite": "^5.2.11", "vite": "^5.3.4",
"vue-eslint-parser": "^9.4.2" "vue-eslint-parser": "^9.4.2"
}, },
"browserslist": [ "browserslist": [

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`, baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本 // 系统版本
version: 'v1.8.5', version: 'v1.8.8',
}; };
export default config; export default config;

View File

@@ -9,7 +9,19 @@ export function getValueByPath(obj: any, path: string) {
const keys = path.split('.'); const keys = path.split('.');
let result = obj; let result = obj;
for (let key of keys) { for (let key of keys) {
if (!result || typeof result !== 'object') { if (!result) {
return undefined;
}
// 如果是字符串则尝试使用json解析
if (typeof result == 'string') {
try {
result = JSON.parse(result);
} catch (e) {
console.error(e);
return undefined;
}
}
if (typeof result !== 'object') {
return undefined; return undefined;
} }
@@ -23,7 +35,18 @@ export function getValueByPath(obj: any, path: string) {
} }
const index = parseInt(matchIndex[1]); const index = parseInt(matchIndex[1]);
result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
let arrValue = result[arrayKey];
if (typeof arrValue == 'string') {
try {
arrValue = JSON.parse(arrValue);
} catch (e) {
result = undefined;
break;
}
}
result = Array.isArray(arrValue) ? arrValue[index] : undefined;
} else { } else {
result = result[key]; result = result[key];
} }

View File

@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
return width; return width;
} }
/**
* 获取内容所需要占用的宽度
*/
export function getContentWidth(content: any): number {
if (!content) {
return 50;
}
// 以下分配的单位长度可根据实际需求进行调整
let flexWidth = 0;
for (const char of content) {
if (flexWidth > 500) {
break;
}
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
// 小写字母、数字字符
flexWidth += 9.3;
continue;
}
if (char >= 'A' && char <= 'Z') {
flexWidth += 9;
continue;
}
if (char >= '\u4e00' && char <= '\u9fa5') {
// 如果是中文字符为字符分配16个单位宽度
flexWidth += 20;
} else {
// 其他种类字符
flexWidth += 8;
}
}
// if (flexWidth > 450) {
// // 设置最大宽度
// flexWidth = 450;
// }
return flexWidth;
}
/** /**
* *
* @returns uuid * @returns uuid
@@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
clipboard.destroy(); clipboard.destroy();
}); });
} }
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
keyword = keyword?.toLowerCase();
return fields.filter((field) => {
for (let valueExtractFunc of valueExtractFuncs) {
const value = valueExtractFunc(field)?.toLowerCase();
if (isPrefixSubsequence(keyword, value)) {
return true;
}
}
return false;
});
}
/**
* 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> trueprefix=uname2 -> false
* @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
* @param targetTemplate 目标模板
* @returns 是否匹配
*/
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
let i = 0; // 指向prefix的索引
let j = 0; // 指向targetTemplate的索引
while (i < prefix.length && j < targetTemplate.length) {
if (prefix[i] === targetTemplate[j]) {
// 字符匹配,两个指针都向前移动
i++;
}
j++; // 目标字符串指针始终向前移动
}
// 如果prefix的所有字符都被找到返回true
return i === prefix.length;
}

View File

@@ -67,7 +67,7 @@ const state = reactive({
search: null as any, search: null as any,
weblinks: null as any, weblinks: null as any,
}, },
status: TerminalStatus.NoConnected, status: -11,
}); });
onMounted(() => { onMounted(() => {
@@ -96,6 +96,7 @@ onBeforeUnmount(() => {
}); });
function init() { function init() {
state.status = TerminalStatus.NoConnected;
if (term) { if (term) {
console.log('重新连接...'); console.log('重新连接...');
close(); close();
@@ -105,7 +106,7 @@ function init() {
}); });
} }
function initTerm() { async function initTerm() {
term = new Terminal({ term = new Terminal({
fontSize: themeConfig.value.terminalFontSize || 15, fontSize: themeConfig.value.terminalFontSize || 15,
fontWeight: themeConfig.value.terminalFontWeight || 'normal', fontWeight: themeConfig.value.terminalFontWeight || 'normal',
@@ -155,6 +156,7 @@ function initSocket() {
state.status = TerminalStatus.Connected; state.status = TerminalStatus.Connected;
focus(); focus();
fitTerminal();
// 如果有初始要执行的命令,则发送执行命令 // 如果有初始要执行的命令,则发送执行命令
if (props.cmd) { if (props.cmd) {
@@ -209,7 +211,6 @@ function loadAddon() {
// tell trzsz the terminal columns has been changed // tell trzsz the terminal columns has been changed
trzsz.setTerminalColumns(size.cols); trzsz.setTerminalColumns(size.cols);
}); });
window.addEventListener('resize', () => state.addon.fit.fit());
// enable drag files or directories to upload // enable drag files or directories to upload
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault()); terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
terminalRef.value.addEventListener('drop', (event: any) => { terminalRef.value.addEventListener('drop', (event: any) => {

View File

@@ -1,6 +1,15 @@
import EnumValue from '@/common/Enum';
export enum TerminalStatus { export enum TerminalStatus {
Error = -1, Error = -1,
NoConnected = 0, NoConnected = 0,
Connected = 1, Connected = 1,
Disconnected = 2, Disconnected = 2,
} }
export const TerminalStatusEnum = {
Error: EnumValue.of(TerminalStatus.Error, '连接出错').setExtra({ iconColor: 'var(--el-color-error)' }),
NoConnected: EnumValue.of(TerminalStatus.NoConnected, '未连接').setExtra({ iconColor: 'var(--el-color-primary)' }),
Connected: EnumValue.of(TerminalStatus.Connected, '连接成功').setExtra({ iconColor: 'var(--el-color-success)' }),
Disconnected: EnumValue.of(TerminalStatus.Disconnected, '连接失败').setExtra({ iconColor: 'var(--el-color-error)' }),
};

View File

@@ -1,16 +1,13 @@
<template> <template>
<div> <div>
<el-descriptions :column="3" border> <el-descriptions :column="3" border>
<el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item> <el-descriptions-item :span="3" label="标签"><TagCodePath :path="db.codePaths" /></el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
<el-descriptions-item :span="1" label="名称">{{ db?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item> <el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型"> <el-descriptions-item :span="1" label="类型">
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }} <SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
<el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item> <el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
<el-descriptions-item label="表"> <el-descriptions-item label="表">
@@ -33,7 +30,9 @@ import { dbApi } from '@/views/ops/db/api';
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums'; import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { getDbDialect } from '@/views/ops/db/dialect'; import { getDbDialect } from '@/views/ops/db/dialect';
import ResourceTags from '@/views/ops/component/ResourceTags.vue'; import { tagApi } from '@/views/ops/tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
const props = defineProps({ const props = defineProps({
// 业务key // 业务key
@@ -74,6 +73,10 @@ const getDbSqlExec = async (bizKey: string) => {
state.sqlExec = res.list?.[0]; state.sqlExec = res.list?.[0];
const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId }); const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
state.db = dbRes.list?.[0]; state.db = dbRes.list?.[0];
tagApi.listByQuery.request({ type: TagResourceTypeEnum.DbName.value, codes: state.db.code }).then((res) => {
state.db.codePaths = res.map((item: any) => item.codePath);
});
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,11 +1,10 @@
<template> <template>
<div> <div>
<el-descriptions :column="3" border> <el-descriptions :column="3" border>
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item> <el-descriptions-item :span="3" label="标签"><TagCodePath :path="redis.codePaths" /></el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ redis?.id }}</el-descriptions-item>
<el-descriptions-item :span="1" label="用户名">{{ redis?.username }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="redis.tags" /></el-descriptions-item> <el-descriptions-item :span="2" label="编号">{{ redis?.code }}</el-descriptions-item>
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item> <el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item> <el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
@@ -22,8 +21,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, watch, onMounted } from 'vue'; import { toRefs, reactive, watch, onMounted } from 'vue';
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
import { redisApi } from '@/views/ops/redis/api'; import { redisApi } from '@/views/ops/redis/api';
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
import { tagApi } from '@/views/ops/tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({ const props = defineProps({
// 业务表单 // 业务表单
@@ -75,6 +76,10 @@ const parseRunCmdForm = async (bizForm: string) => {
return; return;
} }
state.redis = res.list?.[0]; state.redis = res.list?.[0];
tagApi.listByQuery.request({ type: TagResourceTypeEnum.Redis.value, codes: state.redis.code }).then((res) => {
state.redis.codePaths = res.map((item: any) => item.codePath);
});
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -18,7 +18,7 @@
:default-expanded-keys="props.defaultExpandedKeys" :default-expanded-keys="props.defaultExpandedKeys"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''"> <span :id="node.key" @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
<span v-if="data.type.value == TagTreeNode.TagPath"> <span v-if="data.type.value == TagTreeNode.TagPath">
<tag-info :tag-path="data.label" /> <tag-info :tag-path="data.label" />
</span> </span>
@@ -48,11 +48,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive, ref, watch, toRefs } from 'vue'; import { onMounted, reactive, ref, watch, toRefs, nextTick } from 'vue';
import { NodeType, TagTreeNode } from './tag'; import { NodeType, TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue'; import TagInfo from './TagInfo.vue';
import { Contextmenu } from '@/components/contextmenu'; import { Contextmenu } from '@/components/contextmenu';
import { tagApi } from '../tag/api'; import { tagApi } from '../tag/api';
import { isPrefixSubsequence } from '@/common/utils/string';
const props = defineProps({ const props = defineProps({
resourceType: { resourceType: {
@@ -105,8 +106,7 @@ watch(filterText, (val) => {
}); });
const filterNode = (value: string, data: any) => { const filterNode = (value: string, data: any) => {
if (!value) return true; return !value || isPrefixSubsequence(value, data.label);
return data.label.includes(value);
}; };
/** /**
@@ -126,7 +126,7 @@ const loadTags = async () => {
* @param { Object } node * @param { Object } node
* @param { Object } resolve * @param { Object } resolve
*/ */
const loadNode = async (node: any, resolve: any) => { const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
if (typeof resolve !== 'function') { if (typeof resolve !== 'function') {
return; return;
} }
@@ -141,6 +141,8 @@ const loadNode = async (node: any, resolve: any) => {
} }
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
// 调用 reject 以保持节点状态,并允许远程加载继续。
return reject();
} }
return resolve(nodes); return resolve(nodes);
}; };
@@ -207,6 +209,17 @@ const getNode = (nodeKey: any) => {
const setCurrentKey = (nodeKey: any) => { const setCurrentKey = (nodeKey: any) => {
treeRef.value.setCurrentKey(nodeKey); treeRef.value.setCurrentKey(nodeKey);
// 通过Id获取到对应的dom元素
const node = document.getElementById(nodeKey);
if (node) {
setTimeout(() => {
nextTick(() => {
// 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
node.scrollIntoView({ block: 'center' });
});
}, 100);
}
}; };
defineExpose({ defineExpose({

View File

@@ -7,7 +7,7 @@
v-bind="$attrs" v-bind="$attrs"
ref="tagTreeRef" ref="tagTreeRef"
:data="state.tags" :data="state.tags"
:default-expanded-keys="checkedTags" :default-expanded-keys="state.defaultExpandedKeys"
:default-checked-keys="checkedTags" :default-checked-keys="checkedTags"
multiple multiple
:render-after-expand="true" :render-after-expand="true"
@@ -50,6 +50,7 @@ import { ref, reactive, onMounted } from 'vue';
import { tagApi } from '../tag/api'; import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum'; import EnumValue from '@/common/Enum';
import { isPrefixSubsequence } from '@/common/utils/string';
const props = defineProps({ const props = defineProps({
height: { height: {
@@ -74,10 +75,12 @@ const tagTreeRef: any = ref(null);
const filterTag = ref(''); const filterTag = ref('');
const state = reactive({ const state = reactive({
defaultExpandedKeys: [] as any,
tags: [], tags: [],
}); });
onMounted(() => { onMounted(() => {
state.defaultExpandedKeys = checkedTags.value;
search(); search();
}); });
@@ -100,10 +103,7 @@ const search = async () => {
}; };
const filterNode = (value: string, data: any) => { const filterNode = (value: string, data: any) => {
if (!value) { return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
return true;
}
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
}; };
const onFilterValChanged = (val: string) => { const onFilterValChanged = (val: string) => {

View File

@@ -1,25 +1,40 @@
<template> <template>
<div class="db-list"> <div class="db-list">
<el-drawer
:title="title"
v-model="dialogVisible"
@open="search"
:before-close="cancel"
:destroy-on-close="true"
:close-on-click-modal="true"
size="60%"
>
<template #header>
<DrawerHeader :header="title" :back="cancel">
<template #extra>
<div class="mr20">
<span>{{ $props.instance?.tags?.[0]?.codePath }}</span>
<el-divider direction="vertical" border-style="dashed" />
<SvgIcon :name="getDbDialect($props.instance?.type).getInfo()?.icon" :size="20" />
<el-divider direction="vertical" border-style="dashed" />
<span>{{ $props.instance?.host }}:{{ $props.instance?.port }}</span>
</div>
</template>
</DrawerHeader>
</template>
<page-table <page-table
ref="pageTableRef" ref="pageTableRef"
:page-api="dbApi.dbs" :page-api="dbApi.dbs"
:before-query-fn="checkRouteTagPath"
:search-items="searchItems"
v-model:query-form="query" v-model:query-form="query"
:columns="columns" :columns="columns"
lazy lazy
show-selection
v-model:selection-data="state.selectionData"
> >
<template #instanceSelect> <template #tableHeader>
<el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable> <el-button v-auth="perms.saveDb" type="primary" circle icon="Plus" @click="editDb(null)"> </el-button>
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id"> <el-button v-auth="perms.delDb" :disabled="state.selectionData.length < 1" @click="deleteDb" type="danger" circle icon="delete"></el-button>
{{ item.name }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.type }} / {{ item.host }}:{{ item.port }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.username }}
</el-option>
</el-select>
</template> </template>
<template #type="{ data }"> <template #type="{ data }">
@@ -28,10 +43,6 @@
</el-tooltip> </el-tooltip>
</template> </template>
<template #host="{ data }">
{{ `${data.host}:${data.port}` }}
</template>
<template #database="{ data }"> <template #database="{ data }">
<el-popover placement="bottom" :width="200" trigger="click"> <el-popover placement="bottom" :width="200" trigger="click">
<template #reference> <template #reference>
@@ -52,7 +63,12 @@
</template> </template>
<template #action="{ data }"> <template #action="{ data }">
<el-button v-auth="perms.saveDb" @click="editDb(data)" type="primary" link>编辑</el-button>
<el-divider direction="vertical" border-style="dashed" />
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button> <el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-dropdown @command="handleMoreActionCommand"> <el-dropdown @command="handleMoreActionCommand">
@@ -64,9 +80,11 @@
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item> <el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"> <el-dropdown-item
:command="{ type: 'backupDb', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
备份任务 备份任务
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
@@ -86,8 +104,9 @@
</el-dropdown> </el-dropdown>
</template> </template>
</page-table> </page-table>
</el-drawer>
<el-dialog width="750px" :title="`${db} 数据库导出`" v-model="exportDialog.visible"> <el-dialog width="750px" :title="`${exportDialog.db} 数据库导出`" v-model="exportDialog.visible">
<el-row justify="space-between"> <el-row justify="space-between">
<el-col :span="9"> <el-col :span="9">
<el-form-item label="导出内容: "> <el-form-item label="导出内容: ">
@@ -168,54 +187,29 @@
<db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" /> <db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
</el-dialog> </el-dialog>
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog"> <db-edit
<el-descriptions title="详情" :column="3" border> @confirm="confirmEditDb"
<el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item> @cancel="cancelEditDb"
<el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item> :title="dbEditDialog.title"
v-model:visible="dbEditDialog.visible"
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item> :instance="props.instance"
<el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item> v-model:db="dbEditDialog.data"
></db-edit>
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="授权凭证">{{ infoDialog.instance.authCertName }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data?.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ formatDate(infoDialog.data?.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<db-edit @val-change="search()" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue'; import { computed, defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
import { dbApi } from './api'; import { dbApi } from './api';
import config from '@/common/config'; import config from '@/common/config';
import { joinClientParams } from '@/common/request'; import { joinClientParams } from '@/common/request';
import { isTrue } from '@/common/assert'; import { isTrue } from '@/common/assert';
import { formatDate } from '@/common/utils/format';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
import DbSqlExecLog from './DbSqlExecLog.vue'; import DbSqlExecLog from './DbSqlExecLog.vue';
import { DbType } from './dialect'; import { DbType } from './dialect';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { useRoute } from 'vue-router';
import { getDbDialect } from './dialect/index'; import { getDbDialect } from './dialect/index';
import { getTagPathSearchItem } from '../component/tag';
import { SearchItem } from '@/components/SearchForm';
import DbBackupList from './DbBackupList.vue'; import DbBackupList from './DbBackupList.vue';
import DbBackupHistoryList from './DbBackupHistoryList.vue'; import DbBackupHistoryList from './DbBackupHistoryList.vue';
import DbRestoreList from './DbRestoreList.vue'; import DbRestoreList from './DbRestoreList.vue';
@@ -223,44 +217,47 @@ import ResourceTags from '../component/ResourceTags.vue';
import { sleep } from '@/common/utils/loading'; import { sleep } from '@/common/utils/loading';
import { DbGetDbNamesMode } from './enums'; import { DbGetDbNamesMode } from './enums';
import { DbInst } from './db'; import { DbInst } from './db';
import { ElMessage, ElMessageBox } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue')); const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
const searchItems = [ const props = defineProps({
getTagPathSearchItem(TagResourceTypeEnum.DbName.value), instance: {
SearchItem.slot('instanceId', '实例', 'instanceSelect'), type: [Object],
SearchItem.input('code', '编号'), required: true,
]; },
title: {
type: String,
},
});
const dialogVisible = defineModel<boolean>('visible');
const emit = defineEmits(['cancel']);
const columns = ref([ const columns = ref([
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
TableColumn.new('name', '名称'), TableColumn.new('name', '名称'),
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
TableColumn.new('instanceName', '实例名'),
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
TableColumn.new('authCertName', '授权凭证'), TableColumn.new('authCertName', '授权凭证'),
TableColumn.new('getDatabaseMode', '获库方式').typeTag(DbGetDbNamesMode), TableColumn.new('getDatabaseMode', '获库方式').typeTag(DbGetDbNamesMode),
TableColumn.new('database', '库').isSlot().setMinWidth(80), TableColumn.new('database', '库').isSlot().setMinWidth(80),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('code', '编号'), TableColumn.new('code', '编号'),
TableColumn.new('action', '操作').isSlot().setMinWidth(210).fixedRight().alignCenter(),
]); ]);
const perms = { const perms = {
base: 'db',
saveDb: 'db:save',
delDb: 'db:del',
backupDb: 'db:backup', backupDb: 'db:backup',
restoreDb: 'db:restore', restoreDb: 'db:restore',
}; };
// 该用户拥有的的操作列按钮权限
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
const actionBtns = hasPerms(Object.values(perms)); const actionBtns = hasPerms(Object.values(perms));
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
const route = useRoute();
const pageTableRef: Ref<any> = ref(null); const pageTableRef: Ref<any> = ref(null);
const state = reactive({ const state = reactive({
row: {} as any,
dbId: 0,
db: '',
loadingDbNames: false, loadingDbNames: false,
currentDbNames: [], currentDbNames: [],
dbNameSearch: '', dbNameSearch: '',
@@ -268,29 +265,20 @@ const state = reactive({
/** /**
* 选中的数据 * 选中的数据
*/ */
selectionData: [], selectionData: [] as any,
/** /**
* 查询条件 * 查询条件
*/ */
query: { query: {
tagPath: '', instanceId: 0,
instanceId: null,
pageNum: 1, pageNum: 1,
pageSize: 0, pageSize: 0,
}, },
infoDialog: {
visible: false,
data: null as any,
instance: null as any,
query: {
instanceId: 0,
},
},
// sql执行记录弹框 // sql执行记录弹框
sqlExecLogDialog: { sqlExecLogDialog: {
title: '', title: '',
visible: false, visible: false,
dbs: [], dbs: [] as any,
dbId: 0, dbId: 0,
}, },
// 数据库备份弹框 // 数据库备份弹框
@@ -321,6 +309,7 @@ const state = reactive({
exportDialog: { exportDialog: {
visible: false, visible: false,
dbId: 0, dbId: 0,
db: '',
type: 3, type: 3,
data: [] as any, data: [] as any,
value: [], value: [],
@@ -339,14 +328,12 @@ const state = reactive({
}, },
}); });
const { db, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state); const { query, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
onMounted(async () => { const search = async () => {
if (Object.keys(actionBtns).length > 0) { state.query.instanceId = props.instance?.id;
columns.value.push(actionColumn); pageTableRef.value.search();
} };
search();
});
const getDbNames = async (db: any) => { const getDbNames = async (db: any) => {
try { try {
@@ -372,42 +359,46 @@ const filterDbs = computed(() => {
}); });
}); });
const checkRouteTagPath = (query: any) => { const editDb = (data: any) => {
if (route.query.tagPath) {
query.tagPath = route.query.tagPath as string;
}
return query;
};
const search = async (tagPath: string = '') => {
if (tagPath) {
state.query.tagPath = tagPath;
}
pageTableRef.value.search();
};
const showInfo = async (info: any) => {
state.infoDialog.data = info;
state.infoDialog.query.instanceId = info.instanceId;
const res = await dbApi.getInstance.request(state.infoDialog.query);
state.infoDialog.instance = res;
state.infoDialog.visible = true;
};
const onBeforeCloseInfoDialog = () => {
state.infoDialog.visible = false;
state.infoDialog.data = null;
state.infoDialog.instance = null;
};
const getInstances = async (instanceName = '') => {
if (!instanceName) {
state.instances = [];
return;
}
const data = await dbApi.instances.request({ name: instanceName });
if (data) { if (data) {
state.instances = data.list; state.dbEditDialog.data = { ...data };
} else {
state.dbEditDialog.data = {
instanceId: props.instance.id,
};
}
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
state.dbEditDialog.visible = true;
};
const confirmEditDb = async (db: any) => {
db.instanceId = props.instance.id;
await dbApi.saveDb.request(db);
ElMessage.success('保存成功');
search();
cancelEditDb();
};
const cancelEditDb = () => {
state.dbEditDialog.visible = false;
state.dbEditDialog.data = {};
};
const deleteDb = async () => {
try {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
for (let db of state.selectionData) {
await dbApi.deleteDb.request({ id: db.id });
}
ElMessage.success('删除成功');
} catch (err) {
//
} finally {
search();
} }
}; };
@@ -415,10 +406,6 @@ const handleMoreActionCommand = (commond: any) => {
const data = commond.data; const data = commond.data;
const type = commond.type; const type = commond.type;
switch (type) { switch (type) {
case 'detail': {
showInfo(data);
return;
}
case 'dumpDb': { case 'dumpDb': {
onDumpDbs(data); onDumpDbs(data);
return; return;
@@ -441,7 +428,9 @@ const handleMoreActionCommand = (commond: any) => {
const onShowSqlExec = async (row: any) => { const onShowSqlExec = async (row: any) => {
state.sqlExecLogDialog.title = `${row.name}`; state.sqlExecLogDialog.title = `${row.name}`;
state.sqlExecLogDialog.dbId = row.id; state.sqlExecLogDialog.dbId = row.id;
state.sqlExecLogDialog.dbs = row.database.split(' '); DbInst.getDbNames(row).then((res) => {
state.sqlExecLogDialog.dbs = res;
});
state.sqlExecLogDialog.visible = true; state.sqlExecLogDialog.visible = true;
}; };
@@ -454,26 +443,32 @@ const onBeforeCloseSqlExecDialog = () => {
const onShowDbBackupDialog = async (row: any) => { const onShowDbBackupDialog = async (row: any) => {
state.dbBackupDialog.title = `${row.name}`; state.dbBackupDialog.title = `${row.name}`;
state.dbBackupDialog.dbId = row.id; state.dbBackupDialog.dbId = row.id;
state.dbBackupDialog.dbs = row.database.split(' '); DbInst.getDbNames(row).then((res) => {
state.sqlExecLogDialog.dbs = res;
});
state.dbBackupDialog.visible = true; state.dbBackupDialog.visible = true;
}; };
const onShowDbBackupHistoryDialog = async (row: any) => { const onShowDbBackupHistoryDialog = async (row: any) => {
state.dbBackupHistoryDialog.title = `${row.name}`; state.dbBackupHistoryDialog.title = `${row.name}`;
state.dbBackupHistoryDialog.dbId = row.id; state.dbBackupHistoryDialog.dbId = row.id;
state.dbBackupHistoryDialog.dbs = row.database.split(' '); DbInst.getDbNames(row).then((res) => {
state.sqlExecLogDialog.dbs = res;
});
state.dbBackupHistoryDialog.visible = true; state.dbBackupHistoryDialog.visible = true;
}; };
const onShowDbRestoreDialog = async (row: any) => { const onShowDbRestoreDialog = async (row: any) => {
state.dbRestoreDialog.title = `${row.name}`; state.dbRestoreDialog.title = `${row.name}`;
state.dbRestoreDialog.dbId = row.id; state.dbRestoreDialog.dbId = row.id;
state.dbRestoreDialog.dbs = row.database.split(' '); DbInst.getDbNames(row).then((res) => {
state.sqlExecLogDialog.dbs = res;
});
state.dbRestoreDialog.visible = true; state.dbRestoreDialog.visible = true;
}; };
const onDumpDbs = async (row: any) => { const onDumpDbs = async (row: any) => {
const dbs = row.database.split(' '); const dbs = await DbInst.getDbNames(row);
const data = []; const data = [];
for (let name of dbs) { for (let name of dbs) {
data.push({ data.push({
@@ -481,6 +476,7 @@ const onDumpDbs = async (row: any) => {
label: name, label: name,
}); });
} }
state.exportDialog.db = row.name;
state.exportDialog.value = []; state.exportDialog.value = [];
state.exportDialog.data = data; state.exportDialog.data = data;
state.exportDialog.dbId = row.id; state.exportDialog.dbId = row.id;
@@ -524,7 +520,10 @@ const supportAction = (action: string, dbType: string): boolean => {
return actions.includes(action); return actions.includes(action);
}; };
defineExpose({ search }); const cancel = () => {
dialogVisible.value = false;
emit('cancel');
};
</script> </script>
<style lang="scss"> <style lang="scss">
.db-list { .db-list {

View File

@@ -1,190 +0,0 @@
<template>
<div>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-table :data="state.dbs" stripe>
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100"> </el-table-column>
<el-table-column prop="authCertName" label="授权凭证" min-width="120" show-overflow-tooltip> </el-table-column>
<el-table-column prop="getDatabaseMode" label="获库方式" min-width="80">
<template #default="scope">
<EnumTag :enums="DbGetDbNamesMode" :value="scope.row.getDatabaseMode" />
</template>
</el-table-column>
<el-table-column prop="database" label="库" min-width="80">
<template #default="scope">
<el-popover placement="bottom" :width="200" trigger="click">
<template #reference>
<el-button @click="getDbNames(scope.row)" type="primary" link>查看库</el-button>
</template>
<el-table :data="filterDbs" size="small" v-loading="state.loadingDbNames">
<el-table-column prop="dbName" label="数据库">
<template #header>
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="120"> </el-table-column>
<el-table-column prop="code" label="编号" show-overflow-tooltip min-width="120"> </el-table-column>
<el-table-column min-wdith="120px">
<template #header>
操作
<el-button v-auth="perms.saveDb" type="primary" circle size="small" icon="Plus" @click="editDb(null)"> </el-button>
</template>
<template #default="scope">
<el-button v-auth="perms.saveDb" @click="editDb(scope.row)" type="primary" icon="edit" link></el-button>
<el-button class="ml1" v-auth="perms.delDb" type="danger" @click="deleteDb(scope.row)" icon="delete" link></el-button>
</template>
</el-table-column>
</el-table>
<db-edit
@confirm="confirmEditDb"
@cancel="cancelEditDb"
:title="dbEditDialog.title"
v-model:visible="dbEditDialog.visible"
:instance="props.instance"
v-model:db="dbEditDialog.data"
></db-edit>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, toRefs, watchEffect } from 'vue';
import { dbApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import DbEdit from './DbEdit.vue';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { DbGetDbNamesMode } from './enums';
import { DbInst } from './db';
const props = defineProps({
visible: {
type: Boolean,
},
instance: {
type: [Object],
required: true,
},
title: {
type: String,
},
});
const perms = {
base: 'db',
saveDb: 'db:save',
delDb: 'db:del',
};
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const state = reactive({
dialogVisible: false,
dbs: [] as any,
loadingDbNames: false,
currentDbNames: [], // 当前数据库名
dbNameSearch: '',
dbEditDialog: {
visible: false,
data: null as any,
title: '新增数据库',
},
});
const { dialogVisible, dbEditDialog } = toRefs(state);
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
getDbs();
});
const getDbNames = async (db: any) => {
try {
state.loadingDbNames = true;
state.currentDbNames = await DbInst.getDbNames(db);
} finally {
state.loadingDbNames = false;
}
};
const filterDbs = computed(() => {
const dbNames = state.currentDbNames;
if (!dbNames) {
return [];
}
const dbNameObjs = dbNames.map((x) => {
return {
dbName: x,
};
});
return dbNameObjs.filter((db: any) => {
return db.dbName.includes(state.dbNameSearch);
});
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
const getDbs = () => {
dbApi.dbs.request({ pageSize: 200, instanceId: props.instance.id }).then((res: any) => {
state.dbs = res.list || [];
});
};
const editDb = (data: any) => {
if (data) {
state.dbEditDialog.data = { ...data };
} else {
state.dbEditDialog.data = {
instanceId: props.instance.id,
};
}
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
state.dbEditDialog.visible = true;
};
const deleteDb = async (db: any) => {
try {
await ElMessageBox.confirm(`确定删除【${db.name}】库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDb.request({ id: db.id });
ElMessage.success('删除成功');
getDbs();
} catch (err) {
//
}
};
const confirmEditDb = async (db: any) => {
db.instanceId = props.instance.id;
await dbApi.saveDb.request(db);
ElMessage.success('保存成功');
getDbs();
cancelEditDb();
};
const cancelEditDb = () => {
state.dbEditDialog.visible = false;
state.dbEditDialog.data = {};
};
</script>
<style lang="scss"></style>

View File

@@ -35,7 +35,7 @@
<template #action="{ data }"> <template #action="{ data }">
<el-button @click="showInfo(data)" link>详情</el-button> <el-button @click="showInfo(data)" link>详情</el-button>
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button> <el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>配置</el-button> <el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>管理</el-button>
</template> </template>
</page-table> </page-table>
@@ -68,7 +68,7 @@
v-model:data="instanceEditDialog.data" v-model:data="instanceEditDialog.data"
></instance-edit> ></instance-edit>
<instance-db-conf :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" /> <DbList :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
</div> </div>
</template> </template>
@@ -89,7 +89,7 @@ import { getTagPathSearchItem } from '../component/tag';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue')); const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
const InstanceDbConf = defineAsyncComponent(() => import('./InstanceDbConf.vue')); const DbList = defineAsyncComponent(() => import('./DbList.vue'));
const props = defineProps({ const props = defineProps({
lazy: { lazy: {
@@ -215,7 +215,7 @@ const deleteInstance = async () => {
const editDb = (data: any) => { const editDb = (data: any) => {
state.dbEditDialog.instance = data; state.dbEditDialog.instance = data;
state.dbEditDialog.title = `配置 "${data.name}" 数据库`; state.dbEditDialog.title = `管理 "${data.name}" 数据库`;
state.dbEditDialog.visible = true; state.dbEditDialog.visible = true;
}; };

View File

@@ -58,16 +58,61 @@
<el-row> <el-row>
<el-col :span="24" v-if="state.db"> <el-col :span="24" v-if="state.db">
<el-descriptions :column="4" size="small" border> <el-descriptions :column="4" size="small" border>
<el-descriptions-item label-align="right" label="操作" <el-descriptions-item label-align="right" label="操作">
><el-button <el-button
:disabled="!state.db || !nowDbInst.id" :disabled="!state.db || !nowDbInst.id"
type="primary" type="primary"
icon="Search" icon="Search"
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases }, state.db)" link
size="small" @click="
>新建查询</el-button addQueryTab(
></el-descriptions-item { id: nowDbInst.id, dbs: nowDbInst.databases, nodeKey: getSqlMenuNodeKey(nowDbInst.id, state.db) },
state.db
)
"
title="新建查询"
> >
</el-button>
<template v-if="!dbConfig.locationTreeNode">
<el-divider direction="vertical" border-style="dashed" />
<el-button @click="locationNowTreeNode(null)" title="定位至左侧树的指定位置" icon="Location" link></el-button>
</template>
<el-divider direction="vertical" border-style="dashed" />
<!-- 数据库展示配置 -->
<el-popover
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
placement="bottom"
width="auto"
title="数据库展示配置"
trigger="click"
>
<el-row>
<el-checkbox
v-model="dbConfig.showColumnComment"
label="显示字段备注"
:true-value="1"
:false-value="0"
size="small"
/>
</el-row>
<el-row>
<el-checkbox
v-model="dbConfig.locationTreeNode"
label="自动定位树节点"
:true-value="1"
:false-value="0"
size="small"
/>
</el-row>
<template #reference>
<el-link type="primary" icon="setting" :underline="false"></el-link>
</template>
</el-popover>
</el-descriptions-item>
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item> <el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
@@ -103,7 +148,9 @@
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key"> <el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label> <template #label>
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250"> <el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
<template #reference> {{ dt.label }} </template> <template #reference>
<span class="font12">{{ dt.label }}</span>
</template>
<template #default> <template #default>
<el-descriptions :column="1" size="small"> <el-descriptions :column="1" size="small">
<el-descriptions-item label="tagPath"> <el-descriptions-item label="tagPath">
@@ -130,6 +177,7 @@
:db-name="dt.db" :db-name="dt.db"
:table-name="dt.params.table" :table-name="dt.params.table"
:table-height="state.dataTabsTableHeight" :table-height="state.dataTabsTableHeight"
:ref="(el: any) => (dt.componentRef = el)"
></db-table-data-op> ></db-table-data-op>
<db-sql-editor <db-sql-editor
@@ -138,6 +186,7 @@
:db-name="dt.db" :db-name="dt.db"
:sql-name="dt.params.sqlName" :sql-name="dt.params.sqlName"
@save-sql-success="reloadSqls" @save-sql-success="reloadSqls"
:ref="(el: any) => (dt.componentRef = el)"
> >
</db-sql-editor> </db-sql-editor>
@@ -173,7 +222,7 @@
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue'; import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus'; import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db'; import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag'; import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue'; import TagTree from '../component/TagTree.vue';
import { dbApi } from './api'; import { dbApi } from './api';
@@ -184,7 +233,7 @@ import { getDbDialect, schemaDbTypes } from './dialect/index';
import { sleep } from '@/common/utils/loading'; import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Pane, Splitpanes } from 'splitpanes'; import { Pane, Splitpanes } from 'splitpanes';
import { useEventListener } from '@vueuse/core'; import { useEventListener, useStorage } from '@vueuse/core';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox'; import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
import { useAutoOpenResource } from '@/store/autoOpenResource'; import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@@ -456,6 +505,8 @@ const state = reactive({
const { nowDbInst, tableCreateDialog } = toRefs(state); const { nowDbInst, tableCreateDialog } = toRefs(state);
const dbConfig = useStorage('dbConfig', DbThemeConfig);
const serverInfoReqParam = ref({ const serverInfoReqParam = ref({
instanceId: 0, instanceId: 0,
}); });
@@ -530,7 +581,7 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
} }
changeDb(db, dbName); changeDb(db, dbName);
const key = `${db.id}:\`${dbName}\`.${tableName}`; const key = `tableData:${db.id}.${dbName}.${tableName}`;
let tab = state.tabs.get(key); let tab = state.tabs.get(key);
state.activeName = key; state.activeName = key;
// 如果存在该表tab则直接返回 // 如果存在该表tab则直接返回
@@ -565,7 +616,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
// 存在sql模板名则该模板名只允许一个tab // 存在sql模板名则该模板名只允许一个tab
if (sqlName) { if (sqlName) {
label = `查询-${sqlName}`; label = `查询-${sqlName}`;
key = `查询:${dbId}:${dbName}.${sqlName}`; key = `query:${dbId}.${dbName}.${sqlName}`;
} else { } else {
let count = 1; let count = 1;
state.tabs.forEach((v) => { state.tabs.forEach((v) => {
@@ -574,7 +625,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
} }
}); });
label = `新查询-${count}`; label = `新查询-${count}`;
key = `新查询${count}:${dbId}:${dbName}`; key = `query:${count}.${dbId}.${dbName}`;
} }
state.activeName = key; state.activeName = key;
let tab = state.tabs.get(key); let tab = state.tabs.get(key);
@@ -611,7 +662,7 @@ const addTablesOpTab = async (db: any) => {
changeDb(db, dbName); changeDb(db, dbName);
const dbId = db.id; const dbId = db.id;
let key = `表操作:${dbId}:${dbName}.tablesOp`; let key = `tablesOp:${dbId}.${dbName}`;
state.activeName = key; state.activeName = key;
let tab = state.tabs.get(key); let tab = state.tabs.get(key);
@@ -642,15 +693,22 @@ const onRemoveTab = (targetName: string) => {
if (tabName !== targetName) { if (tabName !== targetName) {
continue; continue;
} }
state.tabs.delete(targetName);
if (activeName != targetName) {
break;
}
// 如果删除的tab是当前激活的tab则切换到前一个或后一个tab
const nextTab = tabNames[i + 1] || tabNames[i - 1]; const nextTab = tabNames[i + 1] || tabNames[i - 1];
if (nextTab) { if (nextTab) {
activeName = nextTab; activeName = nextTab;
} else { } else {
activeName = ''; activeName = '';
} }
state.tabs.delete(targetName);
state.activeName = activeName; state.activeName = activeName;
onTabChange(); onTabChange();
break;
} }
}; };
@@ -670,6 +728,21 @@ const onTabChange = () => {
registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type); registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
} }
// 激活当前tab需要调用DbTableData组件的active否则表头与数据会出现错位暂不知为啥先这样处理
nowTab?.componentRef?.active();
if (dbConfig.value.locationTreeNode) {
locationNowTreeNode(nowTab);
}
};
/**
* 定位至当前树节点
*/
const locationNowTreeNode = (nowTab: any = null) => {
if (!nowTab) {
nowTab = state.tabs.get(state.activeName);
}
tagTreeRef.value.setCurrentKey(nowTab?.treeNodeKey); tagTreeRef.value.setCurrentKey(nowTab?.treeNodeKey);
}; };
@@ -854,7 +927,7 @@ const getNowDbInfo = () => {
margin: 0 0 5px; margin: 0 0 5px;
.el-tabs__item { .el-tabs__item {
padding: 0 10px; padding: 0 5px;
} }
} }

View File

@@ -23,8 +23,11 @@ export const dbApi = {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log(param.sql); console.log(param.sql);
} }
// 非base64编码sql则进行base64编码refreshToken时会重复调用该方法故简单判断下
if (!Base64.isValid(param.sql)) {
param.sql = Base64.encode(param.sql); param.sql = Base64.encode(param.sql);
} }
}
return param; return param;
}), }),
// 保存sql // 保存sql

View File

@@ -52,7 +52,7 @@
<Pane :size="100 - state.editorSize"> <Pane :size="100 - state.editorSize">
<div class="mt5 sql-exec-res h100"> <div class="mt5 sql-exec-res h100">
<el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" v-model="state.activeTab"> <el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" @tab-change="active" v-model="state.activeTab">
<el-tab-pane class="h100" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id"> <el-tab-pane class="h100" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
<template #label> <template #label>
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300"> <el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
@@ -700,6 +700,19 @@ const initMonacoEditor = () => {
}, },
}); });
}; };
const active = () => {
const resTab = state.execResTabs[state.activeTab - 1];
if (!resTab || !resTab.dbTableRef) {
return;
}
resTab.dbTableRef?.active();
};
defineExpose({
active,
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -25,7 +25,7 @@
:clearable="false" :clearable="false"
type="Date" type="Date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="选择日期" :placeholder="`选择日期-${placeholder}`"
/> />
<el-date-picker <el-date-picker
@@ -41,7 +41,7 @@
:clearable="false" :clearable="false"
type="datetime" type="datetime"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期时间" :placeholder="`选择日期时间-${placeholder}`"
/> />
<el-time-picker <el-time-picker
@@ -56,7 +56,7 @@
v-model="itemValue" v-model="itemValue"
:clearable="false" :clearable="false"
value-format="HH:mm:ss" value-format="HH:mm:ss"
placeholder="选择时间" :placeholder="`选择时间-${placeholder}`"
/> />
</template> </template>
@@ -152,6 +152,10 @@ const getEditorLangByValue = (value: any) => {
<style lang="scss"> <style lang="scss">
.string-input-container { .string-input-container {
position: relative; position: relative;
.el-input__wrapper {
padding: 1px 3px;
}
} }
.string-input-container-show-icon { .string-input-container-show-icon {
.el-input__inner { .el-input__inner {
@@ -174,6 +178,10 @@ const getEditorLangByValue = (value: any) => {
.el-input__prefix { .el-input__prefix {
display: none; display: none;
} }
.el-input__wrapper {
padding: 1px 3px;
}
} }
.edit-time-picker-popper { .edit-time-picker-popper {

View File

@@ -15,6 +15,7 @@
fixed fixed
class="table" class="table"
:row-event-handlers="rowEventHandlers" :row-event-handlers="rowEventHandlers"
@scroll="onTableScroll"
> >
<template #header="{ columns }"> <template #header="{ columns }">
<div v-for="(column, i) in columns" :key="i"> <div v-for="(column, i) in columns" :key="i">
@@ -59,9 +60,7 @@
</div> </div>
<div v-else class="header-column-title"> <div v-else class="header-column-title">
<b class="el-text"> <b class="el-text"> {{ column.title }} </b>
{{ column.title }}
</b>
</div> </div>
<!-- 字段列右部分内容 --> <!-- 字段列右部分内容 -->
@@ -96,7 +95,7 @@
/> />
</div> </div>
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active' : ''"> <div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active ml2 mr2' : 'ml2 mr2'">
<span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span> <span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
<span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated"> <span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated">
@@ -121,7 +120,7 @@
<template #empty> <template #empty>
<div style="text-align: center"> <div style="text-align: center">
<el-empty class="h100" :description="props.emptyText" :image-size="100" /> <el-empty :description="props.emptyText" :image-size="100" />
</div> </div>
</template> </template>
</el-table-v2> </el-table-v2>
@@ -157,7 +156,7 @@
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue'; import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElInput, ElMessage } from 'element-plus'; import { ElInput, ElMessage } from 'element-plus';
import { copyToClipboard } from '@/common/utils/string'; import { copyToClipboard } from '@/common/utils/string';
import { DbInst } from '@/views/ops/db/db'; import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { exportCsv, exportFile } from '@/common/utils/export'; import { exportCsv, exportFile } from '@/common/utils/export';
@@ -259,12 +258,10 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
return state.table == ''; return state.table == '';
}); });
const cmDataEdit = new ContextmenuItem('editData', '编辑行') const cmFormView = new ContextmenuItem('formView', '表单视图').withIcon('Document').withOnClick(() => onEditRowData());
.withIcon('edit') // .withHideFunc(() => {
.withOnClick(() => onEditRowData()) // return state.table == '';
.withHideFunc(() => { // });
return state.table == '';
});
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL') const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
.withIcon('tickets') .withIcon('tickets')
@@ -364,7 +361,7 @@ const state = reactive({
const { tableHeight, datas } = toRefs(state); const { tableHeight, datas } = toRefs(state);
const dbConfig = useStorage('dbConfig', { showColumnComment: false }); const dbConfig = useStorage('dbConfig', DbThemeConfig);
/** /**
* 行号字段列 * 行号字段列
@@ -486,7 +483,7 @@ const setTableColumns = (columns: any) => {
dataKey: columnName, dataKey: columnName,
width: DbInst.flexColumnWidth(columnName, state.datas), width: DbInst.flexColumnWidth(columnName, state.datas),
title: columnName, title: columnName,
align: 'center', align: x.dataType == DataType.Number ? 'right' : 'left',
headerClass: 'table-column', headerClass: 'table-column',
class: 'table-column', class: 'table-column',
sortable: true, sortable: true,
@@ -596,7 +593,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
const { clientX, clientY } = event; const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX; state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY; state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql]; state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
contextmenuRef.value.openContextmenu({ column, rowData: data }); contextmenuRef.value.openContextmenu({ column, rowData: data });
}; };
@@ -628,12 +625,12 @@ const onDeleteData = async () => {
const onEditRowData = () => { const onEditRowData = () => {
const selectionDatas = Array.from(selectionRowsMap.values()); const selectionDatas = Array.from(selectionRowsMap.values());
if (selectionDatas.length > 1) { if (selectionDatas.length > 1) {
ElMessage.warning('只能编辑一行数据'); ElMessage.warning('只能选择一行数据');
return; return;
} }
const data = selectionDatas[0]; const data = selectionDatas[0];
state.tableDataFormDialog.data = { ...data }; state.tableDataFormDialog.data = { ...data };
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`; state.tableDataFormDialog.title = state.table ? `'${props.table}'表单数据` : '表单视图';
state.tableDataFormDialog.visible = true; state.tableDataFormDialog.visible = true;
}; };
@@ -649,7 +646,7 @@ const onGenerateJson = async () => {
// 按列字段重新排序对象key // 按列字段重新排序对象key
const jsonObj = []; const jsonObj = [];
for (let selectionData of selectionDatas) { for (let selectionData of selectionDatas) {
let obj = {}; let obj: any = {};
for (let column of state.columns) { for (let column of state.columns) {
if (column.show) { if (column.show) {
obj[column.title] = selectionData[column.dataKey]; obj[column.title] = selectionData[column.dataKey];
@@ -753,7 +750,7 @@ const submitUpdateFields = async () => {
for (let updateRow of cellUpdateMap.values()) { for (let updateRow of cellUpdateMap.values()) {
const rowData = { ...updateRow.rowData }; const rowData = { ...updateRow.rowData };
let updateColumnValue = {}; let updateColumnValue: any = {};
for (let k of updateRow.columnsMap.keys()) { for (let k of updateRow.columnsMap.keys()) {
const v = updateRow.columnsMap.get(k); const v = updateRow.columnsMap.get(k);
@@ -841,11 +838,23 @@ const triggerRefresh = () => {
} }
}; };
const scrollLeftValue = ref(0);
const onTableScroll = (param: any) => {
scrollLeftValue.value = param.scrollLeft;
};
/**
* 激活表格,恢复滚动位置,否则会造成表头与数据单元格错位(暂不知为啥,先这样解决)
*/
const active = () => {
setTimeout(() => tableRef.value.scrollToLeft(scrollLeftValue.value));
};
const getNowDbInst = () => { const getNowDbInst = () => {
return DbInst.getInst(state.dbId); return DbInst.getInst(state.dbId);
}; };
defineExpose({ defineExpose({
active,
submitUpdateFields, submitUpdateFields,
cancelUpdateFields, cancelUpdateFields,
}); });

View File

@@ -6,10 +6,10 @@
:key="column.columnName" :key="column.columnName"
class="w100 mb5" class="w100 mb5"
:prop="column.columnName" :prop="column.columnName"
:required="!column.nullable && !column.isPrimaryKey && !column.isIdentity" :required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.isIdentity"
> >
<template #label> <template #label>
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`"> <span class="pointer" :title="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType">
{{ column.columnName }} {{ column.columnName }}
</span> </span>
</template> </template>
@@ -17,13 +17,13 @@
<ColumnFormItem <ColumnFormItem
v-model="modelValue[`${column.columnName}`]" v-model="modelValue[`${column.columnName}`]"
:data-type="dbInst.getDialect().getDataType(column.dataType)" :data-type="dbInst.getDialect().getDataType(column.dataType)"
:placeholder="`${column.columnType} ${column.columnComment}`" :placeholder="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType"
:column-name="column.columnName" :column-name="column.columnName"
:disabled="column.isIdentity" :disabled="column.isIdentity"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer v-if="props.tableName">
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="closeDialog">取消</el-button> <el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirm">确定</el-button> <el-button type="primary" @click="confirm">确定</el-button>
@@ -99,7 +99,7 @@ const confirm = async () => {
let sql = ''; let sql = '';
if (oldValue) { if (oldValue) {
const updateColumnValue = {}; const updateColumnValue: any = {};
Object.keys(oldValue).forEach((key) => { Object.keys(oldValue).forEach((key) => {
// 如果新旧值不相等,则为需要更新的字段 // 如果新旧值不相等,则为需要更新的字段
if (oldValue[key] !== modelValue.value[key]) { if (oldValue[key] !== modelValue.value[key]) {

View File

@@ -50,22 +50,6 @@
</el-tooltip> </el-tooltip>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<!-- 表数据展示配置 -->
<el-popover
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
placement="bottom"
width="auto"
title="展示配置"
trigger="click"
>
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-value="true" :false-value="false" size="small" />
<template #reference>
<el-link type="primary" icon="setting" :underline="false"></el-link>
</template>
</el-popover>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top"> <el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="font12">提交</el-link> <el-link @click="submitUpdateFields()" type="success" :underline="false" class="font12">提交</el-link>
</el-tooltip> </el-tooltip>
@@ -258,8 +242,8 @@ import { DbInst } from '@/views/ops/db/db';
import DbTableData from './DbTableData.vue'; import DbTableData from './DbTableData.vue';
import { DbDialect } from '@/views/ops/db/dialect'; import { DbDialect } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { useEventListener, useStorage } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
import { copyToClipboard } from '@/common/utils/string'; import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
import DbTableDataForm from './DbTableDataForm.vue'; import DbTableDataForm from './DbTableDataForm.vue';
const props = defineProps({ const props = defineProps({
@@ -288,8 +272,6 @@ const condDialogInputRef: Ref = ref(null);
const defaultPageSize = DbInst.DefaultLimit; const defaultPageSize = DbInst.DefaultLimit;
const dbConfig = useStorage('dbConfig', { showColumnComment: false });
const state = reactive({ const state = reactive({
datas: [], datas: [],
sql: '', // 当前数据tab执行的sql sql: '', // 当前数据tab执行的sql
@@ -476,10 +458,7 @@ const getColumnTips = (queryString: string, callback: any) => {
let res = []; let res = [];
if (columnNameSearch) { if (columnNameSearch) {
columnNameSearch = columnNameSearch.toLowerCase(); res = fuzzyMatchField(columnNameSearch, columns, (x: any) => x.columnName);
res = columns.filter((data: any) => {
return data.columnName.toLowerCase().includes(columnNameSearch);
});
} }
completeCond = condition.value; completeCond = condition.value;
@@ -534,10 +513,12 @@ const filterColumns = (searchKey: string) => {
if (!searchKey) { if (!searchKey) {
return columns; return columns;
} }
searchKey = searchKey.toLowerCase(); return fuzzyMatchField(
return columns.filter((data: any) => { searchKey,
return data.columnName.toLowerCase().includes(searchKey) || data.columnComment.toLowerCase().includes(searchKey); columns,
}); (x: any) => x.columnName,
(x: any) => x.columnComment
);
}; };
/** /**
@@ -622,6 +603,10 @@ const onShowAddDataDialog = async () => {
state.addDataDialog.title = `添加'${props.tableName}'表数据`; state.addDataDialog.title = `添加'${props.tableName}'表数据`;
state.addDataDialog.visible = true; state.addDataDialog.visible = true;
}; };
defineExpose({
active: () => dbTableRef.value.active(),
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -131,6 +131,7 @@ import { compatibleMysql, editDbTypes, getDbDialect } from '../../dialect/index'
import { DbInst } from '../../db'; import { DbInst } from '../../db';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { format as sqlFormatter } from 'sql-formatter'; import { format as sqlFormatter } from 'sql-formatter';
import { fuzzyMatchField } from '@/common/utils/string';
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue')); const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
@@ -219,17 +220,11 @@ const filterTableInfos = computed(() => {
if (!tableNameSearch && !tableCommentSearch) { if (!tableNameSearch && !tableCommentSearch) {
return tables; return tables;
} }
return tables.filter((data: any) => {
let tnMatch = true;
let tcMatch = true;
if (tableNameSearch) { if (tableNameSearch) {
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase()); return fuzzyMatchField(tableNameSearch, tables, (table: any) => table.tableName);
} }
if (tableCommentSearch) { return fuzzyMatchField(tableCommentSearch, tables, (table: any) => table.tableComment);
tcMatch = data.tableComment.includes(tableCommentSearch);
}
return tnMatch && tcMatch;
});
}); });
const getTables = async () => { const getTables = async () => {

View File

@@ -450,8 +450,8 @@ export class DbInst {
return; return;
} }
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符 // 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符、排序图标等
const columnWidth: number = getTextWidth(prop + 'abc') + 23; const columnWidth: number = getTextWidth(prop + 'abc') + 10;
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量); // prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) { if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return columnWidth; return columnWidth;
@@ -471,7 +471,7 @@ export class DbInst {
maxWidthText = nowText; maxWidthText = nowText;
} }
} }
const contentWidth: number = getTextWidth(maxWidthText) + 15; const contentWidth: number = getTextWidth(maxWidthText) + 3;
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth; const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
return flexWidth > 500 ? 500 : flexWidth; return flexWidth > 500 ? 500 : flexWidth;
}; };
@@ -601,6 +601,11 @@ export class TabInfo {
*/ */
params: any; params: any;
/**
* 组件ref
*/
componentRef: any;
getNowDbInst() { getNowDbInst() {
return DbInst.getInst(this.dbId); return DbInst.getInst(this.dbId);
} }
@@ -837,3 +842,18 @@ function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string)
return tables.length > 0 ? tables[0] : undefined; return tables.length > 0 ? tables[0] : undefined;
} }
} }
/**
* 数据库主题配置
*/
export const DbThemeConfig = {
/**
* 表数据表头是否显示备注
*/
showColumnComment: true,
/**
* 是否自动定位至树节点
*/
locationTreeNode: true,
};

View File

@@ -69,11 +69,11 @@ export enum DataType {
} }
/** 列数据类型角标 */ /** 列数据类型角标 */
export const ColumnTypeSubscript = { export const ColumnTypeSubscript: any = {
/** 字符串 */ /** 字符串 */
string: 'abc', string: 'ab',
/** 数字 */ /** 数字 */
number: '123', number: '12',
/** 日期 */ /** 日期 */
date: 'icon-clock', date: 'icon-clock',
/** 时间 */ /** 时间 */

View File

@@ -34,20 +34,15 @@
<Pane> <Pane>
<div class="machine-terminal-tabs card pd5"> <div class="machine-terminal-tabs card pd5">
<el-tabs <el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTermName" class="h100">
v-if="state.tabs.size > 0"
type="card"
@tab-remove="onRemoveTab"
@tab-change="onTabChange"
style="width: 100%"
v-model="state.activeTermName"
class="h100"
>
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key"> <el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label> <template #label>
<el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?"> <el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?">
<template #reference> <template #reference>
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'" <el-icon
class="mr5"
:color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
:title="dt.status == TerminalStatusEnum.Connected.value ? '' : '点击重连'"
><Connection /> ><Connection />
</el-icon> </el-icon>
</template> </template>
@@ -62,7 +57,7 @@
<el-descriptions :column="1" size="small"> <el-descriptions :column="1" size="small">
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item> <el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item> <el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item> <el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item> <el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
</el-descriptions> </el-descriptions>
</template> </template>
@@ -165,13 +160,14 @@ import TagTree from '../component/TagTree.vue';
import { Pane, Splitpanes } from 'splitpanes'; import { Pane, Splitpanes } from 'splitpanes';
import { ContextmenuItem } from '@/components/contextmenu/index'; import { ContextmenuItem } from '@/components/contextmenu/index';
import TerminalBody from '@/components/terminal/TerminalBody.vue'; import TerminalBody from '@/components/terminal/TerminalBody.vue';
import { TerminalStatus } from '@/components/terminal/common'; import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue'; import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
import MachineFile from '@/views/ops/machine/file/MachineFile.vue'; import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
import ResourceTags from '../component/ResourceTags.vue'; import ResourceTags from '../component/ResourceTags.vue';
import { MachineProtocolEnum } from './enums'; import { MachineProtocolEnum } from './enums';
import { useAutoOpenResource } from '@/store/autoOpenResource'; import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import EnumValue from '@/common/Enum';
// 组件 // 组件
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue')); const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
@@ -340,8 +336,13 @@ watch(
watch( watch(
() => state.activeTermName, () => state.activeTermName,
(newValue, oldValue) => { (newValue, oldValue) => {
fitTerminal();
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur(); oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus(); terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
const nowTab = state.tabs.get(state.activeTermName);
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
} }
); );
@@ -496,20 +497,27 @@ const onRemoveTab = (targetName: string) => {
if (tabName !== targetName) { if (tabName !== targetName) {
continue; continue;
} }
state.tabs.delete(targetName);
let info = state.tabs.get(targetName);
if (info) {
terminalRefs[info.key]?.close();
}
if (activeTermName != targetName) {
break;
}
// 如果删除的tab是当前激活的tab则切换到前一个或后一个tab
const nextTab = tabNames[i + 1] || tabNames[i - 1]; const nextTab = tabNames[i + 1] || tabNames[i - 1];
if (nextTab) { if (nextTab) {
activeTermName = nextTab; activeTermName = nextTab;
} else { } else {
activeTermName = ''; activeTermName = '';
} }
let info = state.tabs.get(targetName);
if (info) {
terminalRefs[info.key]?.close();
}
state.tabs.delete(targetName);
state.activeTermName = activeTermName; state.activeTermName = activeTermName;
onTabChange(); break;
} }
}; };
@@ -535,21 +543,13 @@ const onResizeTagTree = () => {
fitTerminal(); fitTerminal();
}; };
const onTabChange = () => {
fitTerminal();
const nowTab = state.tabs.get(state.activeTermName);
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
};
const fitTerminal = () => { const fitTerminal = () => {
setTimeout(() => { setTimeout(() => {
let info = state.tabs.get(state.activeTermName); let info = state.tabs.get(state.activeTermName);
if (info) { if (info) {
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal(); terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
terminalRefs[info.key]?.focus && terminalRefs[info.key]?.focus();
} }
}, 100); });
}; };
const handleReconnect = (tab: any, force = false) => { const handleReconnect = (tab: any, force = false) => {

View File

@@ -12,6 +12,8 @@ export const machineApi = {
process: Api.newGet('/machines/{id}/process'), process: Api.newGet('/machines/{id}/process'),
// 终止进程 // 终止进程
killProcess: Api.newDelete('/machines/{id}/process'), killProcess: Api.newDelete('/machines/{id}/process'),
users: Api.newGet('/machines/{id}/users'),
groups: Api.newGet('/machines/{id}/groups'),
testConn: Api.newPost('/machines/test-conn'), testConn: Api.newPost('/machines/test-conn'),
// 保存按钮 // 保存按钮
saveMachine: Api.newPost('/machines'), saveMachine: Api.newPost('/machines'),

View File

@@ -3,6 +3,7 @@
<el-dialog <el-dialog
:title="title" :title="title"
v-model="dialogVisible" v-model="dialogVisible"
@open="search()"
:close-on-click-modal="false" :close-on-click-modal="false"
:before-close="cancel" :before-close="cancel"
:show-close="true" :show-close="true"
@@ -27,7 +28,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, ref, toRefs, reactive, Ref } from 'vue'; import { ref, toRefs, reactive, Ref } from 'vue';
import { cronJobApi } from '../api'; import { cronJobApi } from '../api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
@@ -47,8 +48,6 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['update:visible', 'update:data', 'cancel']);
const searchItems = [SearchItem.input('machineCode', '机器编号'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)]; const searchItems = [SearchItem.input('machineCode', '机器编号'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
const columns = ref([ const columns = ref([
@@ -65,7 +64,7 @@ const state = reactive({
tags: [] as any, tags: [] as any,
params: { params: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 8,
cronJobId: 0, cronJobId: 0,
status: null, status: null,
machineCode: '', machineCode: '',
@@ -78,24 +77,17 @@ const state = reactive({
machines: [], machines: [],
}); });
const { dialogVisible, params } = toRefs(state); const { params } = toRefs(state);
watch(props, async (newValue: any) => { const dialogVisible = defineModel<boolean>('visible');
state.dialogVisible = newValue.visible;
if (!newValue.visible) {
return;
}
state.params.cronJobId = props.data?.id;
setTimeout(() => search(), 300);
});
const search = async () => { const search = async () => {
state.params.cronJobId = props.data?.id;
pageTableRef.value.search(); pageTableRef.value.search();
}; };
const cancel = () => { const cancel = () => {
emit('update:visible', false); dialogVisible.value = false;
setTimeout(() => { setTimeout(() => {
initData(); initData();
}, 500); }, 500);

View File

@@ -18,12 +18,12 @@
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip> <el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input> <el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" min-wdith="120px"> <el-table-column label="操作" min-width="130">
<template #default="scope"> <template #default="scope">
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button> <el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
<el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button> <el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>

View File

@@ -22,7 +22,7 @@
> >
<el-table-column type="selection" width="30" /> <el-table-column type="selection" width="30" />
<el-table-column prop="name" label="名称"> <el-table-column prop="name" label="名称" min-width="380">
<template #header> <template #header>
<div class="machine-file-table-header"> <div class="machine-file-table-header">
<div> <div>
@@ -171,7 +171,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="size" label="大小" width="100" sortable> <el-table-column prop="size" label="大小" min-width="90" sortable>
<template #default="scope"> <template #default="scope">
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span> <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span>
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span> <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span>
@@ -182,7 +182,11 @@
</el-table-column> </el-table-column>
<el-table-column prop="mode" label="属性" width="110"> </el-table-column> <el-table-column prop="mode" label="属性" width="110"> </el-table-column>
<el-table-column prop="modTime" label="修改时间" width="165" sortable> </el-table-column> <el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="username" label="用户" min-width="55" show-overflow-tooltip>
</el-table-column>
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="groupname" label="组" min-width="55" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="modTime" label="修改时间" width="160" sortable> </el-table-column>
<el-table-column width="100"> <el-table-column width="100">
<template #header> <template #header>
@@ -288,6 +292,8 @@ import MachineFileContent from './MachineFileContent.vue';
import { getToken } from '@/common/utils/storage'; import { getToken } from '@/common/utils/storage';
import { convertToBytes, formatByteSize } from '@/common/utils/format'; import { convertToBytes, formatByteSize } from '@/common/utils/format';
import { getMachineConfig } from '@/common/sysconfig'; import { getMachineConfig } from '@/common/sysconfig';
import { MachineProtocolEnum } from '../enums';
import { fuzzyMatchField } from '@/common/utils/string';
const props = defineProps({ const props = defineProps({
machineId: { type: Number }, machineId: { type: Number },
@@ -303,6 +309,9 @@ const folderUploadRef: any = ref();
const folderType = 'd'; const folderType = 'd';
const userMap = new Map<number, any>();
const groupMap = new Map<number, any>();
// 路径分隔符 // 路径分隔符
const pathSep = '/'; const pathSep = '/';
@@ -343,13 +352,27 @@ const { basePath, nowPath, loading, fileNameFilter, progressNum, uploadProgressS
onMounted(async () => { onMounted(async () => {
state.basePath = props.path; state.basePath = props.path;
const machineId = props.machineId;
if (props.protocol == MachineProtocolEnum.Ssh.value) {
machineApi.users.request({ id: machineId }).then((res: any) => {
for (let user of res) {
userMap.set(user.uid, user);
}
});
machineApi.groups.request({ id: machineId }).then((res: any) => {
for (let group of res) {
groupMap.set(group.gid, group);
}
});
}
setFiles(props.path); setFiles(props.path);
state.machineConfig = await getMachineConfig(); state.machineConfig = await getMachineConfig();
}); });
const filterFiles = computed(() => const filterFiles = computed(() => fuzzyMatchField(state.fileNameFilter, state.files, (file: any) => file.name));
state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase()))
);
const filePathNav = computed(() => { const filePathNav = computed(() => {
let basePath = state.basePath; let basePath = state.basePath;
@@ -517,6 +540,11 @@ const lsFile = async (path: string) => {
path, path,
}); });
for (const file of res) { for (const file of res) {
if (props.protocol == MachineProtocolEnum.Ssh.value) {
file.username = userMap.get(file.uid)?.uname || file.uid;
file.groupname = groupMap.get(file.gid)?.gname || file.gid;
}
const type = file.type; const type = file.type;
if (type == folderType) { if (type == folderType) {
file.isFolder = true; file.isFolder = true;

View File

@@ -11,7 +11,7 @@
</el-table-column> </el-table-column>
<el-table-column prop="codePaths" label="关联机器" min-width="250px" show-overflow-tooltip> <el-table-column prop="codePaths" label="关联机器" min-width="250px" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
<TagCodePath :path="scope.row.tags.map((tag: any) => tag.codePath)" /> <TagCodePath :path="scope.row.tags?.map((tag: any) => tag.codePath)" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column> <el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
@@ -173,7 +173,7 @@ const openFormDialog = (data: any) => {
state.form = { ...DefaultForm }; state.form = { ...DefaultForm };
} else { } else {
state.form = _.cloneDeep(data); state.form = _.cloneDeep(data);
state.form.codePaths = data.tags.map((tag: any) => tag.codePath); state.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
} }
state.dialogVisible = true; state.dialogVisible = true;
}; };

View File

@@ -156,6 +156,7 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumTag from '@/components/enumtag/EnumTag.vue'; import EnumTag from '@/components/enumtag/EnumTag.vue';
import EnumValue from '@/common/Enum'; import EnumValue from '@/common/Enum';
import TagCodePath from '../component/TagCodePath.vue'; import TagCodePath from '../component/TagCodePath.vue';
import { isPrefixSubsequence } from '@/common/utils/string';
const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue')); const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue'));
const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue')); const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue'));
@@ -371,8 +372,7 @@ const setNowTabData = () => {
}; };
const filterNode = (value: string, data: Tree) => { const filterNode = (value: string, data: Tree) => {
if (!value) return true; return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
}; };
const search = async () => { const search = async () => {

View File

@@ -100,7 +100,9 @@ const { dvisible, params, form } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveConfigExec } = configApi.save.useApi(form); const { isFetching: saveBtnLoading, execute: saveConfigExec } = configApi.save.useApi(form);
watchEffect(() => { watch(
() => props.visible,
() => {
state.dvisible = props.visible; state.dvisible = props.visible;
if (!state.dvisible) { if (!state.dvisible) {
return; return;
@@ -124,7 +126,8 @@ watchEffect(() => {
} else { } else {
state.permissionAccount = []; state.permissionAccount = [];
} }
}); }
);
const cancel = () => { const cancel = () => {
// 更新父组件visible prop对应的值为false // 更新父组件visible prop对应的值为false

View File

@@ -123,6 +123,7 @@ import { formatDate } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue'; import EnumTag from '@/components/enumtag/EnumTag.vue';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { Splitpanes, Pane } from 'splitpanes'; import { Splitpanes, Pane } from 'splitpanes';
import { isPrefixSubsequence } from '@/common/utils/string';
const menuTypeValue = ResourceTypeEnum.Menu.value; const menuTypeValue = ResourceTypeEnum.Menu.value;
const permissionTypeValue = ResourceTypeEnum.Permission.value; const permissionTypeValue = ResourceTypeEnum.Permission.value;
@@ -209,10 +210,7 @@ watch(filterResource, (val) => {
}); });
const filterNode = (value: string, data: any) => { const filterNode = (value: string, data: any) => {
if (!value) { return !value || isPrefixSubsequence(value, data.name);
return true;
}
return data.name.includes(value);
}; };
const search = async () => { const search = async () => {

View File

@@ -3,9 +3,8 @@ module mayfly-go
go 1.22 go 1.22
require ( require (
gitee.com/chunanyong/dm v1.8.14 gitee.com/chunanyong/dm v1.8.15
gitee.com/liuzongyang/libpq v1.0.9 gitee.com/liuzongyang/libpq v1.0.9
github.com/buger/jsonparser v1.1.1
github.com/emirpasic/gods v1.18.1 github.com/emirpasic/gods v1.18.1
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
@@ -17,29 +16,30 @@ require (
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.3
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9 github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
github.com/may-fly/cast v1.6.1 github.com/may-fly/cast v1.6.1
github.com/microsoft/go-mssqldb v1.7.1 github.com/microsoft/go-mssqldb v1.7.2
github.com/mojocn/base64Captcha v1.3.6 // github.com/mojocn/base64Captcha v1.3.6 //
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6 github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.5.1 github.com/redis/go-redis/v9 v9.5.3
github.com/robfig/cron/v3 v3.0.1 // github.com/robfig/cron/v3 v3.0.1 //
github.com/sijms/go-ora/v2 v2.8.17 github.com/sijms/go-ora/v2 v2.8.19
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.17.1
github.com/veops/go-ansiterm v0.0.5 github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver v1.15.0 // mongo go.mongodb.org/mongo-driver v1.16.0 // mongo
golang.org/x/crypto v0.23.0 // ssh golang.org/x/crypto v0.25.0 // ssh
golang.org/x/oauth2 v0.20.0 golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0 golang.org/x/sync v0.7.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
// gorm // gorm
gorm.io/driver/mysql v1.5.6 gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.10 gorm.io/gorm v1.25.11
) )
require ( require (
@@ -77,12 +77,14 @@ require (
github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.3 // indirect github.com/rivo/uniseg v0.4.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
@@ -94,8 +96,8 @@ require (
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
golang.org/x/image v0.13.0 // indirect golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
google.golang.org/grpc v1.52.3 // indirect google.golang.org/grpc v1.52.3 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect

View File

@@ -57,10 +57,21 @@ func (d *Db) Dbs(rc *req.Ctx) {
res, err := d.DbApp.GetPageList(queryCond, page, &dbvos) res, err := d.DbApp.GetPageList(queryCond, page, &dbvos)
biz.ErrIsNil(err) biz.ErrIsNil(err)
// 填充标签信息 instances, _ := d.InstanceApp.GetByIds(collx.ArrayMap(dbvos, func(i *vo.DbListVO) uint64 {
d.TagApp.FillTagInfo(tagentity.TagTypeDbName, collx.ArrayMap(dbvos, func(dbvo *vo.DbListVO) tagentity.ITagResource { return i.InstanceId
return dbvo }))
})...) instancesMap := collx.ArrayToMap(instances, func(i *entity.DbInstance) uint64 {
return i.Id
})
for _, dbvo := range dbvos {
di := instancesMap[dbvo.InstanceId]
if di != nil {
dbvo.InstanceCode = di.Code
dbvo.InstanceType = di.Type
dbvo.Host = di.Host
dbvo.Port = di.Port
}
}
rc.ResData = res rc.ResData = res
} }

View File

@@ -2,26 +2,23 @@ package vo
import ( import (
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
tagentity "mayfly-go/internal/tag/domain/entity"
"time" "time"
) )
type DbListVO struct { type DbListVO struct {
tagentity.ResourceTags
Id *int64 `json:"id"` Id *int64 `json:"id"`
Code string `json:"code"` Code string `json:"code"`
Name *string `json:"name"` Name *string `json:"name"`
GetDatabaseMode entity.DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式 GetDatabaseMode entity.DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式
Database *string `json:"database"` Database *string `json:"database"`
Remark *string `json:"remark"` Remark *string `json:"remark"`
InstanceId uint64 `json:"instanceId"`
InstanceId *int64 `json:"instanceId"`
AuthCertName string `json:"authCertName"` AuthCertName string `json:"authCertName"`
InstanceName *string `json:"instanceName"`
InstanceType *string `json:"type"` InstanceCode string `json:"instanceCode" gorm:"-"`
Host string `json:"host"` InstanceType string `json:"type" gorm:"-"`
Port int `json:"port"` Host string `json:"host" gorm:"-"`
Port int `json:"port" gorm:"-"`
CreateTime *time.Time `json:"createTime"` CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"` Creator *string `json:"creator"`
@@ -30,7 +27,3 @@ type DbListVO struct {
Modifier *string `json:"modifier"` Modifier *string `json:"modifier"`
ModifierId *int64 `json:"modifierId"` ModifierId *int64 `json:"modifierId"`
} }
func (d DbListVO) GetCode() string {
return d.Code
}

View File

@@ -297,7 +297,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
// 生成insert sql数据在索引前加速insert // 生成insert sql数据在索引前加速insert
if reqParam.DumpData { if reqParam.DumpData {
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", tableName)) writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表数据: %s \n-- ----------------------------\n", tableName))
dumpHelper.BeforeInsert(writer, quoteTableName) dumpHelper.BeforeInsert(writer, quoteTableName)
// 获取列信息 // 获取列信息

View File

@@ -2,7 +2,6 @@ package application
import ( import (
"context" "context"
"errors"
"mayfly-go/internal/common/consts" "mayfly-go/internal/common/consts"
"mayfly-go/internal/db/application/dto" "mayfly-go/internal/db/application/dto"
"mayfly-go/internal/db/dbm" "mayfly-go/internal/db/dbm"
@@ -13,14 +12,11 @@ import (
tagdto "mayfly-go/internal/tag/application/dto" tagdto "mayfly-go/internal/tag/application/dto"
tagentity "mayfly-go/internal/tag/domain/entity" tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx" "mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/structx" "mayfly-go/pkg/utils/structx"
"gorm.io/gorm"
) )
type Instance interface { type Instance interface {
@@ -71,18 +67,10 @@ func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, pagePar
func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) error { func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) error {
instanceEntity.Network = instanceEntity.GetNetwork() instanceEntity.Network = instanceEntity.GetNetwork()
if authCert.Id != 0 { authCert, err := app.resourceAuthCertApp.GetRealAuthCert(authCert)
// 密文可能被清除,故需要重新获取
authCert, _ = app.resourceAuthCertApp.GetAuthCert(authCert.Name)
} else {
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
publicAuthCert, err := app.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
if err != nil { if err != nil {
return err return err
} }
authCert = publicAuthCert
}
}
dbConn, err := dbm.Conn(app.toDbInfoByAc(instanceEntity, authCert, "")) dbConn, err := dbm.Conn(app.toDbInfoByAc(instanceEntity, authCert, ""))
if err != nil { if err != nil {
@@ -171,7 +159,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa
} }
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error { func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
instance, err := app.GetById(instanceId, "name") instance, err := app.GetById(instanceId)
if err != nil { if err != nil {
return errorx.NewBiz("获取数据库实例错误数据库实例ID为: %d", instance.Id) return errorx.NewBiz("获取数据库实例错误数据库实例ID为: %d", instance.Id)
} }
@@ -180,26 +168,16 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
DbInstanceId: instanceId, DbInstanceId: instanceId,
} }
err = app.restoreApp.restoreRepo.GetByCond(restore) err = app.restoreApp.restoreRepo.GetByCond(restore)
switch { if err == nil {
case err == nil: return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
biz.ErrNotNil(err, "不能删除数据库实例【%s】请先删除关联的数据库恢复任务。", instance.Name)
case errors.Is(err, gorm.ErrRecordNotFound):
break
default:
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
} }
backup := &entity.DbBackup{ backup := &entity.DbBackup{
DbInstanceId: instanceId, DbInstanceId: instanceId,
} }
err = app.backupApp.backupRepo.GetByCond(backup) err = app.backupApp.backupRepo.GetByCond(backup)
switch { if err == nil {
case err == nil: return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
biz.ErrNotNil(err, "不能删除数据库实例【%s】请先删除关联的数据库备份任务。", instance.Name)
case errors.Is(err, gorm.ErrRecordNotFound):
break
default:
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
} }
dbs, _ := app.dbApp.ListByCond(&entity.Db{ dbs, _ := app.dbApp.ListByCond(&entity.Db{

View File

@@ -2,6 +2,7 @@ package application
import ( import (
"context" "context"
"encoding/hex"
"fmt" "fmt"
"mayfly-go/internal/db/dbm/dbi" "mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
@@ -87,16 +88,20 @@ func (app *dbTransferAppImpl) CreateLog(ctx context.Context, taskId uint64) (uin
} }
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint64) { func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint64) {
defer app.logApp.Flush(logId, true)
task, err := app.GetById(taskId) task, err := app.GetById(taskId)
if err != nil { if err != nil {
logx.Errorf("创建DBMS-执行数据迁移日志失败:%v", err) logx.Errorf("创建DBMS-执行数据迁移日志失败:%v", err)
return return
} }
if app.IsRunning(taskId) {
logx.Warnf("[%d]该任务正在运行中...", taskId)
return
}
start := time.Now() start := time.Now()
defer app.logApp.Flush(logId, true)
// 修改状态与关联日志id // 修改状态与关联日志id
task.LogId = logId task.LogId = logId
task.RunningState = entity.DbTransferTaskRunStateRunning task.RunningState = entity.DbTransferTaskRunStateRunning
@@ -322,6 +327,8 @@ func (app *dbTransferAppImpl) transfer2Target(taskId uint64, targetConn *dbi.DbC
columnNames = append(columnNames, targetMeta.QuoteIdentifier(col.ColumnName)) columnNames = append(columnNames, targetMeta.QuoteIdentifier(col.ColumnName))
} }
dataHelper := targetMeta.GetDataHelper()
// 从目标库数据中取出源库字段对应的值 // 从目标库数据中取出源库字段对应的值
values := make([][]any, 0) values := make([][]any, 0)
for _, record := range result { for _, record := range result {
@@ -338,6 +345,14 @@ func (app *dbTransferAppImpl) transfer2Target(taskId uint64, targetConn *dbi.DbC
} }
} }
} }
if dataHelper.GetDataType(string(tc.DataType)) == dbi.DataTypeBlob {
decodeBytes, err := hex.DecodeString(val.(string))
if err == nil {
val = decodeBytes
}
}
rawValue = append(rawValue, val) rawValue = append(rawValue, val)
} }
values = append(values, rawValue) values = append(values, rawValue)

View File

@@ -214,6 +214,9 @@ func valueConvert(data []byte, colType *sql.ColumnType) any {
if strings.Contains(colDatabaseTypeName, "bit") { if strings.Contains(colDatabaseTypeName, "bit") {
return data[0] return data[0]
} }
if colDatabaseTypeName == "blob" {
return fmt.Sprintf("%x", data)
}
// 这里把[]byte数据转成string // 这里把[]byte数据转成string
stringV := string(data) stringV := string(data)

View File

@@ -28,17 +28,19 @@ type Dialect interface {
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复 // GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
GetDbProgram() (DbProgram, error) GetDbProgram() (DbProgram, error)
// 批量保存数据 // BatchInsert 批量insert数据
BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any, duplicateStrategy int) (int64, error) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any, duplicateStrategy int) (int64, error)
// 拷贝表 // CopyTable 拷贝表
CopyTable(copy *DbCopyTable) error CopyTable(copy *DbCopyTable) error
// CreateTable 创建表
CreateTable(columns []Column, tableInfo Table, dropOldTable bool) (int, error) CreateTable(columns []Column, tableInfo Table, dropOldTable bool) (int, error)
// CreateIndex 创建索引
CreateIndex(tableInfo Table, indexs []Index) error CreateIndex(tableInfo Table, indexs []Index) error
// 有些数据库迁移完数据之后,需要更新表自增序列为当前表最大值 // UpdateSequence 有些数据库迁移完数据之后,需要更新表自增序列为当前表最大值
UpdateSequence(tableName string, columns []Column) UpdateSequence(tableName string, columns []Column)
} }

View File

@@ -13,37 +13,36 @@ import (
type MetaData interface { type MetaData interface {
BaseMetaData BaseMetaData
// 获取数据库服务实例信息 // GetDbServer 获取数据库服务实例信息
GetDbServer() (*DbServer, error) GetDbServer() (*DbServer, error)
// 获取数据库名称列表 // GetDbNames 获取数据库名称列表
GetDbNames() ([]string, error) GetDbNames() ([]string, error)
// 获取表信息 // GetTables 获取表信息
GetTables(tableNames ...string) ([]Table, error) GetTables(tableNames ...string) ([]Table, error)
// 获取指定表名的所有列元信息 // GetColumns 获取指定表名的所有列元信息
GetColumns(tableNames ...string) ([]Column, error) GetColumns(tableNames ...string) ([]Column, error)
// 根据数据库类型修复字段长度、精度等 // GetPrimaryKey 获取表主键字段名,没有主键标识则默认第一个字段
// FixColumn(column *Column)
// 获取表主键字段名,没有主键标识则默认第一个字段
GetPrimaryKey(tableName string) (string, error) GetPrimaryKey(tableName string) (string, error)
// 获取表索引信息 // GetTableIndex 获取表索引信息
GetTableIndex(tableName string) ([]Index, error) GetTableIndex(tableName string) ([]Index, error)
// 获取建表ddl // GetTableDDL 获取建表ddl
GetTableDDL(tableName string, dropBeforeCreate bool) (string, error) GetTableDDL(tableName string, dropBeforeCreate bool) (string, error)
// GenerateTableDDL 生成建表ddl
GenerateTableDDL(columns []Column, tableInfo Table, dropBeforeCreate bool) []string GenerateTableDDL(columns []Column, tableInfo Table, dropBeforeCreate bool) []string
// GenerateIndexDDL 生成索引ddl
GenerateIndexDDL(indexs []Index, tableInfo Table) []string GenerateIndexDDL(indexs []Index, tableInfo Table) []string
GetSchemas() ([]string, error) GetSchemas() ([]string, error)
// 获取数据处理助手 用于解析格式化列数据等 // GetDataHelper 获取数据处理助手 用于解析格式化列数据等
GetDataHelper() DataHelper GetDataHelper() DataHelper
} }
@@ -84,6 +83,13 @@ type Column struct {
// 拼接数据类型与长度等。如varchar(2000)decimal(20,2) // 拼接数据类型与长度等。如varchar(2000)decimal(20,2)
func (c *Column) GetColumnType() string { func (c *Column) GetColumnType() string {
// 哪些mysql数据类型不需要添加字段长度
if collx.ArrayAnyMatches([]string{"int", "blob", "float", "double", "date", "year", "json"}, string(c.DataType)) {
return string(c.DataType)
}
if c.DataType == "timestamp" {
return "timestamp(6)"
}
if c.CharMaxLength > 0 { if c.CharMaxLength > 0 {
return fmt.Sprintf("%s(%d)", c.DataType, c.CharMaxLength) return fmt.Sprintf("%s(%d)", c.DataType, c.CharMaxLength)
} }
@@ -124,6 +130,7 @@ const (
CommonTypeVarbinary ColumnDataType = "varbinary" CommonTypeVarbinary ColumnDataType = "varbinary"
CommonTypeInt ColumnDataType = "int" CommonTypeInt ColumnDataType = "int"
CommonTypeBit ColumnDataType = "bit"
CommonTypeSmallint ColumnDataType = "smallint" CommonTypeSmallint ColumnDataType = "smallint"
CommonTypeTinyint ColumnDataType = "tinyint" CommonTypeTinyint ColumnDataType = "tinyint"
CommonTypeNumber ColumnDataType = "number" CommonTypeNumber ColumnDataType = "number"
@@ -146,6 +153,7 @@ const (
DataTypeDate DataType = "date" DataTypeDate DataType = "date"
DataTypeTime DataType = "time" DataTypeTime DataType = "time"
DataTypeDateTime DataType = "datetime" DataTypeDateTime DataType = "datetime"
DataTypeBlob DataType = "blob"
) )
// 列数据处理帮助方法 // 列数据处理帮助方法

View File

@@ -42,13 +42,13 @@ SELECT a.indexname AS "i
indexdef AS "indexDef", indexdef AS "indexDef",
c.attname AS "columnName", c.attname AS "columnName",
c.attnum AS "seqInIndex", c.attnum AS "seqInIndex",
case when a.indexname like '%_pkey' then 1 else 0 end AS "isPrimaryKey" case when a.indexname like '%%_pkey' then 1 else 0 end AS "isPrimaryKey"
FROM pg_indexes a FROM pg_indexes a
join pg_class b on a.indexname = b.relname join pg_class b on a.indexname = b.relname
join pg_attribute c on b.oid = c.attrelid join pg_attribute c on b.oid = c.attrelid
WHERE a.schemaname = (select current_schema()) WHERE a.schemaname = (select current_schema())
AND a.tablename = '%s' AND a.tablename = '%s'
AND a.indexname not like '%_pkey' AND a.indexname not like '%%_pkey'
--------------------------------------- ---------------------------------------
--PGSQL_COLUMN_MA 表列信息 --PGSQL_COLUMN_MA 表列信息
SELECT a.table_name AS "tableName", SELECT a.table_name AS "tableName",

View File

@@ -19,6 +19,8 @@ var (
// 时间类型 // 时间类型
timeRegexp = regexp.MustCompile(`(?i)time`) timeRegexp = regexp.MustCompile(`(?i)time`)
blobRegexp = regexp.MustCompile(`(?i)blob`)
// mysql数据类型 映射 公共数据类型 // mysql数据类型 映射 公共数据类型
commonColumnTypeMap = map[string]dbi.ColumnDataType{ commonColumnTypeMap = map[string]dbi.ColumnDataType{
"bigint": dbi.CommonTypeBigint, "bigint": dbi.CommonTypeBigint,
@@ -37,6 +39,7 @@ var (
"longtext": dbi.CommonTypeLongtext, "longtext": dbi.CommonTypeLongtext,
"mediumblob": dbi.CommonTypeBlob, "mediumblob": dbi.CommonTypeBlob,
"mediumtext": dbi.CommonTypeText, "mediumtext": dbi.CommonTypeText,
"bit": dbi.CommonTypeBit,
"set": dbi.CommonTypeVarchar, "set": dbi.CommonTypeVarchar,
"smallint": dbi.CommonTypeSmallint, "smallint": dbi.CommonTypeSmallint,
"text": dbi.CommonTypeText, "text": dbi.CommonTypeText,
@@ -60,6 +63,7 @@ var (
dbi.CommonTypeMediumtext: "text", dbi.CommonTypeMediumtext: "text",
dbi.CommonTypeVarbinary: "varbinary", dbi.CommonTypeVarbinary: "varbinary",
dbi.CommonTypeInt: "int", dbi.CommonTypeInt: "int",
dbi.CommonTypeBit: "bit",
dbi.CommonTypeSmallint: "smallint", dbi.CommonTypeSmallint: "smallint",
dbi.CommonTypeTinyint: "tinyint", dbi.CommonTypeTinyint: "tinyint",
dbi.CommonTypeNumber: "decimal", dbi.CommonTypeNumber: "decimal",
@@ -92,6 +96,10 @@ func (dc *DataHelper) GetDataType(dbColumnType string) dbi.DataType {
if timeRegexp.MatchString(dbColumnType) { if timeRegexp.MatchString(dbColumnType) {
return dbi.DataTypeTime return dbi.DataTypeTime
} }
// blob类型
if blobRegexp.MatchString(dbColumnType) {
return dbi.DataTypeBlob
}
return dbi.DataTypeString return dbi.DataTypeString
} }
@@ -157,6 +165,8 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
case dbi.DataTypeDate, dbi.DataTypeDateTime, dbi.DataTypeTime: case dbi.DataTypeDate, dbi.DataTypeDateTime, dbi.DataTypeTime:
// mysql时间类型无需格式化 // mysql时间类型无需格式化
return fmt.Sprintf("'%s'", dbColumnValue) return fmt.Sprintf("'%s'", dbColumnValue)
case dbi.DataTypeBlob:
return fmt.Sprintf("unhex('%s')", dbColumnValue)
} }
return fmt.Sprintf("'%s'", dbColumnValue) return fmt.Sprintf("'%s'", dbColumnValue)
} }

View File

@@ -16,7 +16,7 @@ type DbInstance struct {
Network string `json:"network"` Network string `json:"network"`
Extra *string `json:"extra"` // 连接需要的其他额外参数json格式, 如oracle需要sid等 Extra *string `json:"extra"` // 连接需要的其他额外参数json格式, 如oracle需要sid等
Params *string `json:"params"` // 使用指针类型,可更新为零值(空字符串) Params *string `json:"params"` // 使用指针类型,可更新为零值(空字符串)
Remark string `json:"remark"` Remark *string `json:"remark"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
} }

View File

@@ -28,15 +28,9 @@ type DbTransferLogQuery struct {
// 数据库查询实体,不与数据库表字段一一对应 // 数据库查询实体,不与数据库表字段一一对应
type DbQuery struct { type DbQuery struct {
Id uint64 `form:"id"` Id uint64 `form:"id"`
Code string `json:"code" form:"code"`
Name string `orm:"column(name)" json:"name"`
Database string `orm:"column(database)" json:"database"`
Remark string `json:"remark"`
Codes []string
TagIds []uint64 `orm:"column(tag_id)"`
TagPath string `form:"tagPath"` TagPath string `form:"tagPath"`
Code string `json:"code" form:"code"`
Codes []string
InstanceId uint64 `form:"instanceId"` InstanceId uint64 `form:"instanceId"`
} }

View File

@@ -4,7 +4,6 @@ import (
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/gormx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
) )
@@ -18,16 +17,6 @@ func newDbRepo() repository.Db {
// 分页获取数据库信息列表 // 分页获取数据库信息列表
func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQueryWithTableName("t_db db").Joins("JOIN t_db_instance inst ON db.instance_id = inst.id"). pd := model.NewCond().Eq("instance_id", condition.InstanceId).In("code", condition.Codes).Eq("id", condition.Id)
WithCond(model.NewCond().Columns("db.*, inst.name instance_name, inst.type instance_type, inst.host, inst.port "). return d.PageByCondToAny(pd, pageParam, toEntity)
Eq("db.instance_id", condition.InstanceId).
Eq("db.id", condition.Id).
Like("db.database", condition.Database).
Eq("db.code", condition.Code).
In("db.code", condition.Codes).
Eq0("db."+model.DeletedColumn, model.ModelUndeleted).
Eq0("inst."+model.DeletedColumn, model.ModelUndeleted),
)
return gormx.PageQuery(qd, pageParam, toEntity)
} }

View File

@@ -173,6 +173,22 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
biz.ErrIsNil(err, "终止进程失败: %s", res) biz.ErrIsNil(err, "终止进程失败: %s", res)
} }
func (m *Machine) GetUsers(rc *req.Ctx) {
cli, err := m.MachineApp.GetCli(GetMachineId(rc))
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
res, err := cli.GetUsers()
biz.ErrIsNil(err)
rc.ResData = res
}
func (m *Machine) GetGroups(rc *req.Ctx) {
cli, err := m.MachineApp.GetCli(GetMachineId(rc))
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
res, err := cli.GetGroups()
biz.ErrIsNil(err)
rc.ResData = res
}
func (m *Machine) WsSSH(g *gin.Context) { func (m *Machine) WsSSH(g *gin.Context) {
wsConn, err := ws.Upgrader.Upgrade(g.Writer, g.Request, nil) wsConn, err := ws.Upgrader.Upgrade(g.Writer, g.Request, nil)
defer func() { defer func() {
@@ -261,7 +277,7 @@ func (m *Machine) WsGuacamole(g *gin.Context) {
return return
} }
err = mi.IfUseSshTunnelChangeIpPort() err = mi.IfUseSshTunnelChangeIpPort(true)
if err != nil { if err != nil {
return return
} }

View File

@@ -28,9 +28,11 @@ import (
"sync" "sync"
"github.com/may-fly/cast" "github.com/may-fly/cast"
"github.com/pkg/sftp"
) )
type MachineFile struct { type MachineFile struct {
MachineApp application.Machine `inject:""`
MachineFileApp application.MachineFile `inject:""` MachineFileApp application.MachineFile `inject:""`
MsgApp msgapp.Msg `inject:""` MsgApp msgapp.Msg `inject:""`
} }
@@ -159,15 +161,21 @@ func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
path = readPath + name path = readPath + name
} }
fisVO = append(fisVO, vo.MachineFileInfo{ mfi := vo.MachineFileInfo{
Name: fi.Name(), Name: fi.Name(),
Size: fi.Size(), Size: fi.Size(),
Path: path, Path: path,
Type: getFileType(fi.Mode()), Type: getFileType(fi.Mode()),
Mode: fi.Mode().String(), Mode: fi.Mode().String(),
ModTime: timex.DefaultFormat(fi.ModTime()), ModTime: timex.DefaultFormat(fi.ModTime()),
}) }
if sftpFs, ok := fi.Sys().(*sftp.FileStat); ok {
mfi.UID = sftpFs.UID
mfi.GID = sftpFs.GID
}
fisVO = append(fisVO, mfi)
} }
sort.Sort(vo.MachineFileInfos(fisVO)) sort.Sort(vo.MachineFileInfos(fisVO))
rc.ResData = fisVO rc.ResData = fisVO

View File

@@ -78,6 +78,9 @@ type MachineFileInfo struct {
Type string `json:"type"` Type string `json:"type"`
Mode string `json:"mode"` Mode string `json:"mode"`
ModTime string `json:"modTime"` ModTime string `json:"modTime"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
} }
type MachineFileInfos []MachineFileInfo type MachineFileInfos []MachineFileInfo

View File

@@ -159,18 +159,10 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, param *dto.SaveMachine
func (m *machineAppImpl) TestConn(me *entity.Machine, authCert *tagentity.ResourceAuthCert) error { func (m *machineAppImpl) TestConn(me *entity.Machine, authCert *tagentity.ResourceAuthCert) error {
me.Id = 0 me.Id = 0
if authCert.Id != 0 { authCert, err := m.resourceAuthCertApp.GetRealAuthCert(authCert)
// 密文可能被清除,故需要重新获取
authCert, _ = m.resourceAuthCertApp.GetAuthCert(authCert.Name)
} else {
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
publicAuthCert, err := m.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
if err != nil { if err != nil {
return err return err
} }
authCert = publicAuthCert
}
}
mi, err := m.toMi(me, authCert) mi, err := m.toMi(me, authCert)
if err != nil { if err != nil {

View File

@@ -222,8 +222,7 @@ func (m *machineFileAppImpl) MkDir(ctx context.Context, opParam *dto.MachineFile
return nil, err return nil, err
} }
sftpCli.MkdirAll(path) return mi, sftpCli.MkdirAll(path)
return mi, err
} }
func (m *machineFileAppImpl) CreateFile(ctx context.Context, opParam *dto.MachineFileOp) (*mcm.MachineInfo, error) { func (m *machineFileAppImpl) CreateFile(ctx context.Context, opParam *dto.MachineFileOp) (*mcm.MachineInfo, error) {

View File

@@ -23,7 +23,7 @@ func (m *machineRepoImpl) GetMachineList(condition *entity.MachineQuery, pagePar
Like("ip", condition.Ip). Like("ip", condition.Ip).
Like("name", condition.Name). Like("name", condition.Name).
In("code", condition.Codes). In("code", condition.Codes).
Like("code", condition.Code). Eq("code", condition.Code).
Eq("protocol", condition.Protocol) Eq("protocol", condition.Protocol)
return m.PageByCondToAny(qd, pageParam, toEntity) return m.PageByCondToAny(qd, pageParam, toEntity)

View File

@@ -5,6 +5,7 @@ import (
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"strings" "strings"
"github.com/may-fly/cast"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@@ -66,30 +67,6 @@ func (c *Cli) Run(shell string) (string, error) {
return string(buf), nil return string(buf), nil
} }
// GetAllStats 获取机器的所有状态信息
func (c *Cli) GetAllStats() *Stats {
stats := new(Stats)
res, err := c.Run(StatsShell)
if err != nil {
logx.Errorf("执行机器[id=%d, name=%s]运行状态信息脚本失败: %s", c.Info.Id, c.Info.Name, err.Error())
return stats
}
infos := strings.Split(res, "-----")
if len(infos) < 8 {
return stats
}
getUptime(infos[0], stats)
getHostname(infos[1], stats)
getLoad(infos[2], stats)
getMemInfo(infos[3], stats)
getFSInfo(infos[4], stats)
getInterfaces(infos[5], stats)
getInterfaceInfo(infos[6], stats)
getCPU(infos[7], stats)
return stats
}
// Close 关闭client并从缓存中移除如果使用隧道则也关闭 // Close 关闭client并从缓存中移除如果使用隧道则也关闭
func (c *Cli) Close() { func (c *Cli) Close() {
m := c.Info m := c.Info
@@ -115,3 +92,77 @@ func (c *Cli) Close() {
CloseSshTunnelMachine(int(sshTunnelMachineId), m.GetTunnelId()) CloseSshTunnelMachine(int(sshTunnelMachineId), m.GetTunnelId())
} }
} }
// GetAllStats 获取机器的所有状态信息
func (c *Cli) GetAllStats() *Stats {
stats := new(Stats)
res, err := c.Run(StatsShell)
if err != nil {
logx.Errorf("执行机器[id=%d, name=%s]运行状态信息脚本失败: %s", c.Info.Id, c.Info.Name, err.Error())
return stats
}
infos := strings.Split(res, "-----")
if len(infos) < 8 {
return stats
}
getUptime(infos[0], stats)
getHostname(infos[1], stats)
getLoad(infos[2], stats)
getMemInfo(infos[3], stats)
getFSInfo(infos[4], stats)
getInterfaces(infos[5], stats)
getInterfaceInfo(infos[6], stats)
getCPU(infos[7], stats)
return stats
}
// GetUsers 读取/etc/passwd获取系统所有用户信息
func (c *Cli) GetUsers() ([]*UserInfo, error) {
res, err := c.Run("cat /etc/passwd")
if err != nil {
return nil, err
}
var users []*UserInfo
userLines := strings.Split(res, "\n")
for _, userLine := range userLines {
if userLine == "" {
continue
}
fields := strings.Split(userLine, ":")
user := &UserInfo{
Username: fields[0],
UID: cast.ToUint32(fields[2]),
GID: cast.ToUint32(fields[3]),
HomeDir: fields[5],
Shell: fields[6],
}
users = append(users, user)
}
return users, nil
}
// GetGroups 读取/etc/group获取系统所有组信息
func (c *Cli) GetGroups() ([]*GroupInfo, error) {
res, err := c.Run("cat /etc/group")
if err != nil {
return nil, err
}
var groups []*GroupInfo
groupLines := strings.Split(res, "\n")
for _, groupLine := range groupLines {
if groupLine == "" {
continue
}
fields := strings.Split(groupLine, ":")
group := &GroupInfo{
Groupname: fields[0],
GID: cast.ToUint32(fields[2]),
}
groups = append(groups, group)
}
return groups, nil
}

View File

@@ -5,6 +5,7 @@ import (
tagentity "mayfly-go/internal/tag/domain/entity" tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/netx"
"net" "net"
"time" "time"
@@ -15,8 +16,8 @@ import (
type MachineInfo struct { type MachineInfo struct {
Key string `json:"key"` // 缓存key Key string `json:"key"` // 缓存key
Id uint64 `json:"id"` Id uint64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"` Name string `json:"name"`
Code string `json:"code"`
Protocol int `json:"protocol"` Protocol int `json:"protocol"`
Ip string `json:"ip"` // IP地址 Ip string `json:"ip"` // IP地址
@@ -35,12 +36,12 @@ type MachineInfo struct {
CodePath []string `json:"codePath"` CodePath []string `json:"codePath"`
} }
func (m *MachineInfo) UseSshTunnel() bool { func (mi *MachineInfo) UseSshTunnel() bool {
return m.SshTunnelMachine != nil return mi.SshTunnelMachine != nil
} }
func (m *MachineInfo) GetTunnelId() string { func (mi *MachineInfo) GetTunnelId() string {
return fmt.Sprintf("machine:%d", m.Id) return fmt.Sprintf("machine:%d", mi.Id)
} }
// 连接 // 连接
@@ -48,7 +49,7 @@ func (mi *MachineInfo) Conn() (*Cli, error) {
logx.Infof("[%s]机器连接:%s:%d", mi.Name, mi.Ip, mi.Port) logx.Infof("[%s]机器连接:%s:%d", mi.Name, mi.Ip, mi.Port)
// 如果使用了ssh隧道则修改机器ip port为暴露的ip port // 如果使用了ssh隧道则修改机器ip port为暴露的ip port
err := mi.IfUseSshTunnelChangeIpPort() err := mi.IfUseSshTunnelChangeIpPort(false)
if err != nil { if err != nil {
return nil, errorx.NewBiz("ssh隧道连接失败: %s", err.Error()) return nil, errorx.NewBiz("ssh隧道连接失败: %s", err.Error())
} }
@@ -66,33 +67,39 @@ func (mi *MachineInfo) Conn() (*Cli, error) {
} }
// 如果使用了ssh隧道则修改机器ip port为暴露的ip port // 如果使用了ssh隧道则修改机器ip port为暴露的ip port
func (me *MachineInfo) IfUseSshTunnelChangeIpPort() error { func (mi *MachineInfo) IfUseSshTunnelChangeIpPort(out bool) error {
if !me.UseSshTunnel() { if !mi.UseSshTunnel() {
return nil return nil
} }
originId := me.Id originId := mi.Id
if originId == 0 { if originId == 0 {
// 随机设置一个id如果使用了隧道则用于临时保存隧道 // 随机设置一个id如果使用了隧道则用于临时保存隧道
me.Id = uint64(time.Now().Nanosecond()) mi.Id = uint64(time.Now().Nanosecond())
} }
sshTunnelMachine, err := GetSshTunnelMachine(int(me.SshTunnelMachine.Id), func(u uint64) (*MachineInfo, error) { sshTunnelMachine, err := GetSshTunnelMachine(int(mi.SshTunnelMachine.Id), func(u uint64) (*MachineInfo, error) {
return me.SshTunnelMachine, nil return mi.SshTunnelMachine, nil
}) })
if err != nil { if err != nil {
return err return err
} }
exposeIp, exposePort, err := sshTunnelMachine.OpenSshTunnel(me.GetTunnelId(), me.Ip, me.Port) exposeIp, exposePort, err := sshTunnelMachine.OpenSshTunnel(mi.GetTunnelId(), mi.Ip, mi.Port)
if err != nil { if err != nil {
return err return err
} }
// 是否获取局域网的本地IP
if out {
exposeIp = netx.GetOutBoundIP()
}
// 修改机器ip地址 // 修改机器ip地址
me.Ip = exposeIp mi.Ip = exposeIp
me.Port = exposePort mi.Port = exposePort
// 代理之后置空跳板机信息,防止重复跳 // 代理之后置空跳板机信息,防止重复跳
me.TempSshMachineId = me.SshTunnelMachine.Id mi.TempSshMachineId = mi.SshTunnelMachine.Id
me.SshTunnelMachine = nil mi.SshTunnelMachine = nil
return nil return nil
} }

View File

@@ -89,7 +89,7 @@ func (stm *SshTunnelMachine) OpenSshTunnel(id string, ip string, port int) (expo
return "", 0, err return "", 0, err
} }
localHost := "127.0.0.1" localHost := "0.0.0.0"
localAddr := fmt.Sprintf("%s:%d", localHost, localPort) localAddr := fmt.Sprintf("%s:%d", localHost, localPort)
listener, err := net.Listen("tcp", localAddr) listener, err := net.Listen("tcp", localAddr)
if err != nil { if err != nil {

View File

@@ -264,6 +264,9 @@ func getInterfaceInfo(iInfo string, stats *Stats) (err error) {
} }
func getCPU(cpuInfo string, stats *Stats) (err error) { func getCPU(cpuInfo string, stats *Stats) (err error) {
if !strings.Contains(cpuInfo, ":") {
return
}
// %Cpu(s): 6.1 us, 3.0 sy, 0.0 ni, 90.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st // %Cpu(s): 6.1 us, 3.0 sy, 0.0 ni, 90.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
value := strings.Split(cpuInfo, ":")[1] value := strings.Split(cpuInfo, ":")[1]
values := strings.Split(value, ",") values := strings.Split(value, ",")
@@ -287,3 +290,16 @@ func getCPU(cpuInfo string, stats *Stats) (err error) {
return nil return nil
} }
type UserInfo struct {
UID uint32 `json:"uid"`
Username string `json:"uname"`
GID uint32 `json:"gid"`
HomeDir string `json:"homeDir"` // 用户登录后的起始工作目录
Shell string `json:"shell"` // 用户登录时使用的 shell 程序
}
type GroupInfo struct {
GID uint32 `json:"gid"`
Groupname string `json:"gname"`
}

View File

@@ -98,18 +98,19 @@ func NewParserByteStream(width, height int) *ansiterm.ByteStream {
var ( var (
enterMarks = [][]byte{ enterMarks = [][]byte{
[]byte("\x1b[?1049h"), []byte("\x1b[?1049h"), // 从备用屏幕缓冲区恢复屏幕内容
[]byte("\x1b[?1048h"), []byte("\x1b[?1048h"),
[]byte("\x1b[?1047h"), []byte("\x1b[?1047h"),
[]byte("\x1b[?47h"), []byte("\x1b[?47h"),
[]byte("\x1b[?25l"), []byte("\x1b[?25l"), // 隐藏光标
} }
exitMarks = [][]byte{ exitMarks = [][]byte{
[]byte("\x1b[?1049l"), []byte("\x1b[?1049l"), // 从备用屏幕缓冲区恢复屏幕内容
[]byte("\x1b[?1048l"), []byte("\x1b[?1048l"),
[]byte("\x1b[?1047l"), []byte("\x1b[?1047l"),
[]byte("\x1b[?47l"), []byte("\x1b[?47l"),
[]byte("\x1b[?25h"), // 显示光标
} }
screenMarks = [][]byte{ screenMarks = [][]byte{

View File

@@ -29,6 +29,10 @@ func InitMachineRouter(router *gin.RouterGroup) {
req.NewGet(":machineId/process", m.GetProcess), req.NewGet(":machineId/process", m.GetProcess),
req.NewGet(":machineId/users", m.GetUsers),
req.NewGet(":machineId/groups", m.GetGroups),
req.NewDelete(":machineId/process", m.KillProcess).Log(req.NewLogSave("终止进程")).RequiredPermissionCode("machine:killprocess"), req.NewDelete(":machineId/process", m.KillProcess).Log(req.NewLogSave("终止进程")).RequiredPermissionCode("machine:killprocess"),
req.NewPost("", m.SaveMachine).Log(req.NewLogSave("保存机器信息")).RequiredPermission(saveMachineP), req.NewPost("", m.SaveMachine).Log(req.NewLogSave("保存机器信息")).RequiredPermission(saveMachineP),

View File

@@ -76,19 +76,10 @@ func (r *redisAppImpl) TestConn(param *dto.SaveRedis) error {
db = cast.ToInt(strings.Split(re.Db, ",")[0]) db = cast.ToInt(strings.Split(re.Db, ",")[0])
} }
authCert := param.AuthCert authCert, err := r.resourceAuthCertApp.GetRealAuthCert(param.AuthCert)
if authCert.Id != 0 {
// 密文可能被清除,故需要重新获取
authCert, _ = r.resourceAuthCertApp.GetAuthCert(authCert.Name)
} else {
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
publicAuthCert, err := r.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
if err != nil { if err != nil {
return err return err
} }
authCert = publicAuthCert
}
}
rc, err := re.ToRedisInfo(db, authCert).Conn() rc, err := re.ToRedisInfo(db, authCert).Conn()
if err != nil { if err != nil {

View File

@@ -112,6 +112,7 @@ func (re *RedisInfo) connSentinel() (*RedisConn, error) {
SentinelAddrs: strings.Split(masterNameAndHosts[1], ","), SentinelAddrs: strings.Split(masterNameAndHosts[1], ","),
Username: re.Username, Username: re.Username,
Password: re.Password, // no password set Password: re.Password, // no password set
SentinelUsername: re.Username,
SentinelPassword: re.Password, // 哨兵节点密码需与redis节点密码一致 SentinelPassword: re.Password, // 哨兵节点密码需与redis节点密码一致
DB: re.Db, // use default DB DB: re.Db, // use default DB
DialTimeout: 8 * time.Second, DialTimeout: 8 * time.Second,

View File

@@ -82,6 +82,11 @@ func (p *TagTree) ListByQuery(rc *req.Ctx) {
cond.CodePaths = strings.Split(tagPaths, ",") cond.CodePaths = strings.Split(tagPaths, ",")
} }
cond.Id = uint64(rc.QueryInt("id")) cond.Id = uint64(rc.QueryInt("id"))
cond.Type = entity.TagType(rc.QueryInt("type"))
codes := rc.Query("codes")
if codes != "" {
cond.Codes = strings.Split(codes, ",")
}
var tagTrees []entity.TagTree var tagTrees []entity.TagTree
p.TagTreeApp.ListByQuery(cond, &tagTrees) p.TagTreeApp.ListByQuery(cond, &tagTrees)

View File

@@ -26,7 +26,10 @@ type ResourceAuthCert interface {
// GetAuthCert 根据授权凭证名称获取授权凭证 // GetAuthCert 根据授权凭证名称获取授权凭证
GetAuthCert(authCertName string) (*entity.ResourceAuthCert, error) GetAuthCert(authCertName string) (*entity.ResourceAuthCert, error)
// GetResourceAuthCert 获取资源授权凭证,默认获取特权账号,若没有则返回第一个 //GetRealAuthCert 获取真实可连接鉴权的授权凭证,主要用于资源测试连接时
GetRealAuthCert(authCert *entity.ResourceAuthCert) (*entity.ResourceAuthCert, error)
// GetResourceAuthCert 获取资源授权凭证,优先获取默认账号,若不存在默认账号则返回特权账号,都不存在则返回第一个
GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error) GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error)
// FillAuthCertByAcs 根据授权凭证列表填充资源的授权凭证信息 // FillAuthCertByAcs 根据授权凭证列表填充资源的授权凭证信息
@@ -211,6 +214,25 @@ func (r *resourceAuthCertAppImpl) GetAuthCert(authCertName string) (*entity.Reso
return r.decryptAuthCert(authCert) return r.decryptAuthCert(authCert)
} }
func (r *resourceAuthCertAppImpl) GetRealAuthCert(authCert *entity.ResourceAuthCert) (*entity.ResourceAuthCert, error) {
// 如果使用的是公共授权凭证,则密文为凭证名称
if authCert.CiphertextType == entity.AuthCertCiphertextTypePublic {
return r.GetAuthCert(authCert.Ciphertext)
}
if authCert.Id != 0 && authCert.Ciphertext == "" {
// 密文可能被清除,故需要重新获取
ac, err := r.GetAuthCert(authCert.Name)
if err != nil {
return nil, err
}
authCert.Ciphertext = ac.Ciphertext
return authCert, nil
}
return authCert, nil
}
func (r *resourceAuthCertAppImpl) GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error) { func (r *resourceAuthCertAppImpl) GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error) {
resourceAuthCerts, err := r.ListByCond(&entity.ResourceAuthCert{ resourceAuthCerts, err := r.ListByCond(&entity.ResourceAuthCert{
ResourceType: int8(resourceType), ResourceType: int8(resourceType),
@@ -224,6 +246,12 @@ func (r *resourceAuthCertAppImpl) GetResourceAuthCert(resourceType entity.TagTyp
return nil, errorx.NewBiz("该资源不存在授权凭证账号") return nil, errorx.NewBiz("该资源不存在授权凭证账号")
} }
for _, resourceAuthCert := range resourceAuthCerts {
if resourceAuthCert.Type == entity.AuthCertTypePrivateDefault {
return r.decryptAuthCert(resourceAuthCert)
}
}
for _, resourceAuthCert := range resourceAuthCerts { for _, resourceAuthCert := range resourceAuthCerts {
if resourceAuthCert.Type == entity.AuthCertTypePrivileged { if resourceAuthCert.Type == entity.AuthCertTypePrivileged {
return r.decryptAuthCert(resourceAuthCert) return r.decryptAuthCert(resourceAuthCert)

View File

@@ -312,6 +312,7 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes
} }
delTagType := param.ChildType delTagType := param.ChildType
var childrenTagIds []uint64
for _, resourceTag := range resourceTags { for _, resourceTag := range resourceTags {
// 获取所有关联的子标签 // 获取所有关联的子标签
childrenTag, _ := p.ListByCond(model.NewCond().RLike("code_path", resourceTag.CodePath).Eq("type", delTagType)) childrenTag, _ := p.ListByCond(model.NewCond().RLike("code_path", resourceTag.CodePath).Eq("type", delTagType))
@@ -319,15 +320,17 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes
continue continue
} }
childrenTagIds := collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 { childrenTagIds = append(childrenTagIds, collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 {
return item.Id return item.Id
}) })...)
// 删除code_path下的所有子标签
return p.deleteByIds(ctx, childrenTagIds)
} }
if len(childrenTagIds) == 0 {
return nil return nil
} }
// 删除code_path下的所有子标签
return p.deleteByIds(ctx, collx.ArrayDeduplicate(childrenTagIds))
}
func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity any) { func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity any) {
p.GetRepo().SelectByCondition(condition, toEntity) p.GetRepo().SelectByCondition(condition, toEntity)

View File

@@ -4,7 +4,7 @@ import "fmt"
const ( const (
AppName = "mayfly-go" AppName = "mayfly-go"
Version = "v1.8.5" Version = "v1.8.8"
) )
func GetAppInfo() string { func GetAppInfo() string {

View File

@@ -3,9 +3,8 @@ package jsonx
import ( import (
"encoding/json" "encoding/json"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"strings"
"github.com/buger/jsonparser" "github.com/tidwall/gjson"
) )
// json字符串转map // json字符串转map
@@ -42,35 +41,35 @@ func ToStr(val any) string {
// //
// @param fieldPath字段路径。如user.username等 // @param fieldPath字段路径。如user.username等
func GetStringByBytes(bytes []byte, fieldPath string) (string, error) { func GetStringByBytes(bytes []byte, fieldPath string) (string, error) {
return jsonparser.GetString(bytes, strings.Split(fieldPath, ".")...) return gjson.GetBytes(bytes, fieldPath).String(), nil
} }
// 根据json字符串获取对应字段路径的string类型值 // 根据json字符串获取对应字段路径的string类型值
// //
// @param fieldPath字段路径。如user.username等 // @param fieldPath字段路径。如user.username等
func GetString(jsonStr string, fieldPath string) (string, error) { func GetString(jsonStr string, fieldPath string) (string, error) {
return GetStringByBytes([]byte(jsonStr), fieldPath) return gjson.Get(jsonStr, fieldPath).String(), nil
} }
// 根据json字节数组获取对应字段路径的int类型值 // 根据json字节数组获取对应字段路径的int类型值
// //
// @param fieldPath字段路径。如user.age等 // @param fieldPath字段路径。如user.age等
func GetIntByBytes(bytes []byte, fieldPath string) (int64, error) { func GetIntByBytes(bytes []byte, fieldPath string) (int64, error) {
return jsonparser.GetInt(bytes, strings.Split(fieldPath, ".")...) return gjson.GetBytes(bytes, fieldPath).Int(), nil
} }
// 根据json字符串获取对应字段路径的int类型值 // 根据json字符串获取对应字段路径的int类型值
// //
// @param fieldPath字段路径。如user.age等 // @param fieldPath字段路径。如user.age等
func GetInt(jsonStr string, fieldPath string) (int64, error) { func GetInt(jsonStr string, fieldPath string) (int64, error) {
return GetIntByBytes([]byte(jsonStr), fieldPath) return gjson.Get(jsonStr, fieldPath).Int(), nil
} }
// 根据json字节数组获取对应字段路径的bool类型值 // 根据json字节数组获取对应字段路径的bool类型值
// //
// @param fieldPath字段路径。如user.isDeleted等 // @param fieldPath字段路径。如user.isDeleted等
func GetBoolByBytes(bytes []byte, fieldPath string) (bool, error) { func GetBoolByBytes(bytes []byte, fieldPath string) (bool, error) {
return jsonparser.GetBoolean(bytes, strings.Split(fieldPath, ".")...) return gjson.GetBytes(bytes, fieldPath).Bool(), nil
} }
// 根据json字符串获取对应字段路径的bool类型值 // 根据json字符串获取对应字段路径的bool类型值

View File

@@ -3,8 +3,6 @@ package jsonx
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/buger/jsonparser"
) )
const jsonStr = `{ const jsonStr = `{
@@ -36,7 +34,7 @@ func TestGetString(t *testing.T) {
// val, err := GetString(jsonStr, "username1") // val, err := GetString(jsonStr, "username1")
// 含有数组的 // 含有数组的
val, err := GetString(jsonStr, "person.avatars.[0].url") val, err := GetString(jsonStr, "person.avatars.0.url")
if err != nil { if err != nil {
fmt.Println("error: ", err.Error()) fmt.Println("error: ", err.Error())
@@ -50,60 +48,3 @@ func TestGetInt(t *testing.T) {
val2, _ := GetInt(jsonStr, "person.github.followers") val2, _ := GetInt(jsonStr, "person.github.followers")
fmt.Println(val, ",", val2) fmt.Println(val, ",", val2)
} }
// 官方demo
func TestJsonParser(t *testing.T) {
data := []byte(jsonStr)
// You can specify key path by providing arguments to Get function
jsonparser.Get(data, "person", "name", "fullName")
// There is `GetInt` and `GetBoolean` helpers if you exactly know key data type
jsonparser.GetInt(data, "person", "github", "followers")
// When you try to get object, it will return you []byte slice pointer to data containing it
// In `company` it will be `{"name": "Acme"}`
jsonparser.Get(data, "company")
// If the key doesn't exist it will throw an error
var size int64
if value, err := jsonparser.GetInt(data, "company", "size"); err == nil {
size = value
fmt.Println(size)
}
// You can use `ArrayEach` helper to iterate items [item1, item2 .... itemN]
jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
fmt.Println(jsonparser.Get(value, "url"))
}, "person", "avatars")
// Or use can access fields by index!
jsonparser.GetString(data, "person", "avatars", "[0]", "url")
// You can use `ObjectEach` helper to iterate objects { "key1":object1, "key2":object2, .... "keyN":objectN }
jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
fmt.Printf("Key: '%s'\n Value: '%s'\n Type: %s\n", string(key), string(value), dataType)
return nil
}, "person", "name")
// The most efficient way to extract multiple keys is `EachKey`
paths := [][]string{
[]string{"person", "name", "fullName"},
[]string{"person", "avatars", "[0]", "url"},
[]string{"company", "url"},
}
jsonparser.EachKey(data, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
switch idx {
case 0: // []string{"person", "name", "fullName"}
{
}
case 1: // []string{"person", "avatars", "[0]", "url"}
{
}
case 2: // []string{"company", "url"},
{
}
}
}, paths...)
}

View File

@@ -3,6 +3,7 @@ package netx
import ( import (
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"net" "net"
"strings"
"github.com/lionsoul2014/ip2region/binding/golang/xdb" "github.com/lionsoul2014/ip2region/binding/golang/xdb"
) )
@@ -68,3 +69,13 @@ func Ip2Region(ip string) string {
} }
return region return region
} }
func GetOutBoundIP() string {
conn, err := net.Dial("udp", "8.8.8.8:53")
if err != nil {
return "0.0.0.0"
}
localAddr := conn.LocalAddr().(*net.UDPAddr)
ip := strings.Split(localAddr.String(), ":")[0]
return ip
}

View File

@@ -0,0 +1,10 @@
package netx
import (
"fmt"
"testing"
)
func TestIp(t *testing.T) {
fmt.Println(GetOutBoundIP())
}

View File

@@ -761,11 +761,10 @@ INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight
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(45, 3, '12sSjal1/lskeiql1/Ljewisd3/', 2, 1, '脚本管理-保存脚本按钮', 'machine:script:save', 120000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:01', '2021-06-08 11:09:01', 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(45, 3, '12sSjal1/lskeiql1/Ljewisd3/', 2, 1, '脚本管理-保存脚本按钮', 'machine:script:save', 120000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:01', '2021-06-08 11:09:01', 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(46, 3, '12sSjal1/lskeiql1/Ljeew43/', 2, 1, '脚本管理-删除按钮', 'machine:script:del', 130000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:27', '2021-06-08 11:09:27', 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(46, 3, '12sSjal1/lskeiql1/Ljeew43/', 2, 1, '脚本管理-删除按钮', 'machine:script:del', 130000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:27', '2021-06-08 11:09:27', 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(47, 3, '12sSjal1/lskeiql1/ODewix43/', 2, 1, '脚本管理-执行按钮', 'machine:script:run', 140000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:50', '2021-06-08 11:09:50', 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(47, 3, '12sSjal1/lskeiql1/ODewix43/', 2, 1, '脚本管理-执行按钮', 'machine:script:run', 140000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:50', '2021-06-08 11:09:50', 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(49, 36, 'dbms23ax/xleaiec2/', 1, 1, '数据库管理', 'dbs', 20000000, '{"component":"ops/db/DbList","icon":"Coin","isKeepAlive":true,"routeName":"DbList"}', 1, 'admin', 1, 'admin', '2021-07-07 15:13:55', '2023-03-15 17:31:28', 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(54, 135, 'dbms23ax/X0f4BxT0/leix3Axl/', 2, 1, '数据库保存', 'db:save', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:36', '2021-07-08 17:31:05', 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(54, 49, 'dbms23ax/xleaiec2/leix3Axl/', 2, 1, '数据库保存', 'db:save', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:36', '2021-07-08 17:31:05', 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(55, 135, 'dbms23ax/X0f4BxT0/ygjL3sxA/', 2, 1, '数据库删除', 'db:del', 20000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:48', '2021-07-08 17:30:48', 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(55, 49, 'dbms23ax/xleaiec2/ygjL3sxA/', 2, 1, '数据库删除', 'db:del', 20000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:48', '2021-07-08 17:30:48', 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(57, 3, '12sSjal1/lskeiql1/OJewex43/', 2, 1, '基本权限', 'machine', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:02', '2021-07-09 10:48: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(57, 3, '12sSjal1/lskeiql1/OJewex43/', 2, 1, '基本权限', 'machine', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:02', '2021-07-09 10:48: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(58, 49, 'dbms23ax/xleaiec2/AceXe321/', 2, 1, '基本权限', 'db', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:22', '2021-07-09 10:48:22', 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(58, 135, 'dbms23ax/X0f4BxT0/AceXe321/', 2, 1, '数据库基本权限', 'db', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:22', '2021-07-09 10:48:22', 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(59, 38, 'dbms23ax/exaeca2x/ealcia23/', 2, 1, '基本权限', 'db:exec', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:50:13', '2021-07-09 10:50:13', 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(59, 38, 'dbms23ax/exaeca2x/ealcia23/', 2, 1, '基本权限', 'db:exec', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:50:13', '2021-07-09 10:50:13', 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(60, 0, 'RedisXq4/', 1, 1, 'Redis', '/redis', 50000001, '{"icon":"iconfont icon-redis","isKeepAlive":true,"routeName":"RDS"}', 1, 'admin', 1, 'admin', '2021-07-19 20:15:41', '2023-03-15 16:44:59', 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(60, 0, 'RedisXq4/', 1, 1, 'Redis', '/redis', 50000001, '{"icon":"iconfont icon-redis","isKeepAlive":true,"routeName":"RDS"}', 1, 'admin', 1, 'admin', '2021-07-19 20:15:41', '2023-03-15 16:44:59', 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(61, 60, 'RedisXq4/Exitx4al/', 1, 1, '数据操作', 'data-operation', 10000000, '{"component":"ops/redis/DataOperation","icon":"iconfont icon-redis","isKeepAlive":true,"routeName":"DataOperation"}', 1, 'admin', 1, 'admin', '2021-07-19 20:17:29', '2023-03-15 16:37:50', 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(61, 60, 'RedisXq4/Exitx4al/', 1, 1, '数据操作', 'data-operation', 10000000, '{"component":"ops/redis/DataOperation","icon":"iconfont icon-redis","isKeepAlive":true,"routeName":"DataOperation"}', 1, 'admin', 1, 'admin', '2021-07-19 20:17:29', '2023-03-15 16:37:50', 0, NULL);
@@ -814,8 +813,8 @@ INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight
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(152, 150, 'Jra0n7De/zvAMo2vk/', 2, 1, '编辑', 'db:sync:save', 1703641320, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:00', '2023-12-27 09:42:12', 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(152, 150, 'Jra0n7De/zvAMo2vk/', 2, 1, '编辑', 'db:sync:save', 1703641320, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:00', '2023-12-27 09:42:12', 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(151, 150, 'Jra0n7De/uAnHZxEV/', 2, 1, '基本权限', 'db:sync', 1703641202, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:40:02', '2023-12-27 09:40: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(151, 150, 'Jra0n7De/uAnHZxEV/', 2, 1, '基本权限', 'db:sync', 1703641202, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:40:02', '2023-12-27 09:40: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(150, 36, 'Jra0n7De/', 1, 1, '数据同步', 'sync', 1693040707, '{"component":"ops/db/SyncTaskList","icon":"Coin","isKeepAlive":true,"routeName":"SyncTaskList"}', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-22 09:51:34', '2023-12-27 10:16:57', 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(150, 36, 'Jra0n7De/', 1, 1, '数据同步', 'sync', 1693040707, '{"component":"ops/db/SyncTaskList","icon":"Coin","isKeepAlive":true,"routeName":"SyncTaskList"}', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-22 09:51:34', '2023-12-27 10:16:57', 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(160, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 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(160, 135, 'dbms23ax/X0f4BxT0/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 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(161, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 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(161, 135, 'dbms23ax/X0f4BxT0/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 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(1709208354, 1708911264, '6egfEVYr/fw0Hhvye/b4cNf3iq/', 2, 1, '删除流程', 'flow:procdef:del', 1709208354, 'null', 1, 'admin', 1, 'admin', '2024-02-29 20:05:54', '2024-02-29 20:05:54', 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(1709208354, 1708911264, '6egfEVYr/fw0Hhvye/b4cNf3iq/', 2, 1, '删除流程', 'flow:procdef:del', 1709208354, 'null', 1, 'admin', 1, 'admin', '2024-02-29 20:05:54', '2024-02-29 20:05:54', 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(1709208339, 1708911264, '6egfEVYr/fw0Hhvye/r9ZMTHqC/', 2, 1, '保存流程', 'flow:procdef:save', 1709208339, 'null', 1, 'admin', 1, 'admin', '2024-02-29 20:05:40', '2024-02-29 20:05:40', 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(1709208339, 1708911264, '6egfEVYr/fw0Hhvye/r9ZMTHqC/', 2, 1, '保存流程', 'flow:procdef:save', 1709208339, 'null', 1, 'admin', 1, 'admin', '2024-02-29 20:05:40', '2024-02-29 20:05:40', 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(1709103180, 1708910975, '6egfEVYr/oNCIbynR/', 1, 1, '我的流程', 'procinsts', 1708911263, '{"component":"flow/ProcinstList","icon":"Tickets","isKeepAlive":true,"routeName":"ProcinstList"}', 1, 'admin', 1, 'admin', '2024-02-28 14:53:00', '2024-02-29 20:36:07', 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(1709103180, 1708910975, '6egfEVYr/oNCIbynR/', 1, 1, '我的流程', 'procinsts', 1708911263, '{"component":"flow/ProcinstList","icon":"Tickets","isKeepAlive":true,"routeName":"ProcinstList"}', 1, 'admin', 1, 'admin', '2024-02-28 14:53:00', '2024-02-29 20:36:07', 0, NULL);

View File

@@ -0,0 +1,7 @@
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/leix3Axl/', `type`=2, status=1, name='数据库保存', code='db:save', weight=1693041085, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-08 17:30:36', update_time='2024-05-17 21:50:01', is_deleted=0, delete_time=NULL WHERE id=54;
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/ygjL3sxA/', `type`=2, status=1, name='数据库删除', code='db:del', weight=1693041086, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-08 17:30:48', update_time='2024-05-17 21:50:04', is_deleted=0, delete_time=NULL WHERE id=55;
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/AceXe321/', `type`=2, status=1, name='数据库基本权限', code='db', weight=1693041085, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-09 10:48:22', update_time='2024-05-17 21:52:52', is_deleted=0, delete_time=NULL WHERE id=58;
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/3NUXQFIO/', `type`=2, status=1, name='数据库备份', code='db:backup', weight=1693041087, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2024-01-23 09:37:56', update_time='2024-05-17 21:50:07', is_deleted=0, delete_time=NULL WHERE id=160;
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/ghErkTdb/', `type`=2, status=1, name='数据库恢复', code='db:restore', weight=1693041088, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2024-01-23 09:38:29', update_time='2024-05-17 21:50:10', is_deleted=0, delete_time=NULL WHERE id=161;
DELETE FROM t_sys_resource WHERE id=49;