7 Commits

Author SHA1 Message Date
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
59 changed files with 814 additions and 702 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.10.0",
"asciinema-player": "^3.7.1", "asciinema-player": "^3.7.1",
"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.0",
"element-plus": "^2.7.3", "element-plus": "^2.7.4",
"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.49.0",
"monaco-sql-languages": "^0.11.0", "monaco-sql-languages": "^0.12.0",
"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",
@@ -57,7 +57,7 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"sass": "^1.77.1", "sass": "^1.77.1",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vite": "^5.2.11", "vite": "^5.2.12",
"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.7',
}; };
export default config; export default config;

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

@@ -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,93 +1,112 @@
<template> <template>
<div class="db-list"> <div class="db-list">
<page-table <el-drawer
ref="pageTableRef" :title="title"
:page-api="dbApi.dbs" v-model="dialogVisible"
:before-query-fn="checkRouteTagPath" @open="search"
:search-items="searchItems" :before-close="cancel"
v-model:query-form="query" :destroy-on-close="true"
:columns="columns" :close-on-click-modal="true"
lazy size="60%"
> >
<template #instanceSelect> <template #header>
<el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable> <DrawerHeader :header="title" :back="cancel">
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id"> <template #extra>
{{ item.name }} <div class="mr20">
<el-divider direction="vertical" border-style="dashed" /> <span>{{ $props.instance?.tags?.[0]?.codePath }}</span>
<el-divider direction="vertical" border-style="dashed" />
{{ item.type }} / {{ item.host }}:{{ item.port }} <SvgIcon :name="getDbDialect($props.instance?.type).getInfo()?.icon" :size="20" />
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
{{ item.username }} <span>{{ $props.instance?.host }}:{{ $props.instance?.port }}</span>
</el-option> </div>
</el-select>
</template>
<template #type="{ data }">
<el-tooltip :content="data.type" placement="top">
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
</el-tooltip>
</template>
<template #host="{ data }">
{{ `${data.host}:${data.port}` }}
</template>
<template #database="{ data }">
<el-popover placement="bottom" :width="200" trigger="click">
<template #reference>
<el-button @click="getDbNames(data)" type="primary" link>查看库</el-button>
</template> </template>
<el-table :data="filterDbs" v-loading="state.loadingDbNames" size="small"> </DrawerHeader>
<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> </template>
<template #tagPath="{ data }"> <page-table
<ResourceTags :tags="data.tags" /> ref="pageTableRef"
</template> :page-api="dbApi.dbs"
v-model:query-form="query"
:columns="columns"
lazy
show-selection
v-model:selection-data="state.selectionData"
>
<template #tableHeader>
<el-button v-auth="perms.saveDb" type="primary" circle icon="Plus" @click="editDb(null)"> </el-button>
<el-button v-auth="perms.delDb" :disabled="state.selectionData.length < 1" @click="deleteDb" type="danger" circle icon="delete"></el-button>
</template>
<template #action="{ data }"> <template #type="{ data }">
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button> <el-tooltip :content="data.type" placement="top">
<el-divider direction="vertical" border-style="dashed" /> <SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
</el-tooltip>
</template>
<el-dropdown @command="handleMoreActionCommand"> <template #database="{ data }">
<span class="el-dropdown-link-more"> <el-popover placement="bottom" :width="200" trigger="click">
更多 <template #reference>
<el-icon class="el-icon--right"> <el-button @click="getDbNames(data)" type="primary" link>查看库</el-button>
<arrow-down /> </template>
</el-icon> <el-table :data="filterDbs" v-loading="state.loadingDbNames" size="small">
</span> <el-table-column prop="dbName" label="数据库">
<template #dropdown> <template #header>
<el-dropdown-menu> <el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item> </template>
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item> </el-table-column>
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"> </el-table>
备份任务 </el-popover>
</el-dropdown-item> </template>
<el-dropdown-item
:command="{ type: 'backupHistory', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
备份历史
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'restoreDb', data }"
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
>
恢复任务
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</page-table>
<el-dialog width="750px" :title="`${db} 数据库导出`" v-model="exportDialog.visible"> <template #tagPath="{ data }">
<ResourceTags :tags="data.tags" />
</template>
<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-divider direction="vertical" border-style="dashed" />
<el-dropdown @command="handleMoreActionCommand">
<span class="el-dropdown-link-more">
更多
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<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>
<el-dropdown-item
:command="{ type: 'backupHistory', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
备份历史
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'restoreDb', data }"
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
>
恢复任务
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</page-table>
</el-drawer>
<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>
@@ -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', { showColumnComment: false, locationTreeNode: false });
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,7 +23,10 @@ export const dbApi = {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log(param.sql); console.log(param.sql);
} }
param.sql = Base64.encode(param.sql); // 非base64编码sql则进行base64编码refreshToken时会重复调用该方法故简单判断下
if (!Base64.isValid(param.sql)) {
param.sql = Base64.encode(param.sql);
}
} }
return param; return param;
}), }),

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

@@ -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>
@@ -486,7 +485,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,
@@ -841,11 +840,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

@@ -259,7 +259,7 @@ 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, useStorage } 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({
@@ -476,10 +476,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 +531,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 +621,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; if (tableNameSearch) {
let tcMatch = true; return fuzzyMatchField(tableNameSearch, tables, (table: any) => table.tableName);
if (tableNameSearch) { }
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase()); return fuzzyMatchField(tableCommentSearch, tables, (table: any) => table.tableComment);
}
if (tableCommentSearch) {
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);
} }

View File

@@ -71,9 +71,9 @@ export enum DataType {
/** 列数据类型角标 */ /** 列数据类型角标 */
export const ColumnTypeSubscript = { export const ColumnTypeSubscript = {
/** 字符串 */ /** 字符串 */
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,31 +100,34 @@ 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(
state.dvisible = props.visible; () => props.visible,
if (!state.dvisible) { () => {
return; state.dvisible = props.visible;
} if (!state.dvisible) {
return;
}
if (props.data) { if (props.data) {
state.form = { ...(props.data as any) }; state.form = { ...(props.data as any) };
if (state.form.params) { if (state.form.params) {
state.params = JSON.parse(state.form.params); state.params = JSON.parse(state.form.params);
} else {
state.params = [];
}
} else { } else {
state.form = { permission: 'all' } as any;
state.params = []; state.params = [];
} }
} else {
state.form = { permission: 'all' } as any;
state.params = [];
}
if (state.form.permission != 'all') { if (state.form.permission != 'all') {
const accounts = state.form.permission.split(','); const accounts = state.form.permission.split(',');
state.permissionAccount = accounts.slice(0, accounts.length - 1); state.permissionAccount = accounts.slice(0, accounts.length - 1);
} 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

@@ -28,12 +28,12 @@ require (
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.1
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/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.15.0 // mongo
golang.org/x/crypto v0.23.0 // ssh golang.org/x/crypto v0.24.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
@@ -94,8 +94,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.21.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,20 @@ 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.InstanceType = di.Type
dbvo.Host = di.Host
dbvo.Port = di.Port
}
}
rc.ResData = res rc.ResData = res
} }

View File

@@ -2,26 +2,22 @@ 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"`
AuthCertName string `json:"authCertName"`
InstanceId *int64 `json:"instanceId"` InstanceType string `json:"type" gorm:"-"`
AuthCertName string `json:"authCertName"` Host string `json:"host" gorm:"-"`
InstanceName *string `json:"instanceName"` Port int `json:"port" gorm:"-"`
InstanceType *string `json:"type"`
Host string `json:"host"`
Port int `json:"port"`
CreateTime *time.Time `json:"createTime"` CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"` Creator *string `json:"creator"`
@@ -30,7 +26,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 {
@@ -171,7 +167,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 +176,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

@@ -84,6 +84,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 +131,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 +154,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

@@ -27,16 +27,10 @@ type DbTransferLogQuery struct {
// 数据库查询实体,不与数据库表字段一一对应 // 数据库查询实体,不与数据库表字段一一对应
type DbQuery struct { type DbQuery struct {
Id uint64 `form:"id"` Id uint64 `form:"id"`
Code string `json:"code" form:"code"` TagPath string `form:"tagPath"`
Name string `orm:"column(name)" json:"name"` Code string `json:"code" form:"code"`
Database string `orm:"column(database)" json:"database"` Codes []string
Remark string `json:"remark"`
Codes []string
TagIds []uint64 `orm:"column(tag_id)"`
TagPath string `form:"tagPath"`
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)
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

@@ -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

@@ -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

@@ -26,7 +26,7 @@ type ResourceAuthCert interface {
// GetAuthCert 根据授权凭证名称获取授权凭证 // GetAuthCert 根据授权凭证名称获取授权凭证
GetAuthCert(authCertName string) (*entity.ResourceAuthCert, error) GetAuthCert(authCertName string) (*entity.ResourceAuthCert, error)
// GetResourceAuthCert 获取资源授权凭证,默认获取特权账号,若没有则返回第一个 // GetResourceAuthCert 获取资源授权凭证,优先获取默认账号,若不存在默认账号则返回特权账号,都不存在则返回第一个
GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error) GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error)
// FillAuthCertByAcs 根据授权凭证列表填充资源的授权凭证信息 // FillAuthCertByAcs 根据授权凭证列表填充资源的授权凭证信息
@@ -224,6 +224,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,14 +320,16 @@ 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)
} }
return nil if len(childrenTagIds) == 0 {
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) {

View File

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

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;