mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-12-01 21:40:25 +08:00
feat: 机器文件支持文件夹上传&数据库列表组件拆分
This commit is contained in:
@@ -90,94 +90,7 @@
|
|||||||
</page-table>
|
</page-table>
|
||||||
|
|
||||||
<el-dialog width="80%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible">
|
<el-dialog width="80%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible">
|
||||||
<el-row class="mb10">
|
<db-table-list :db-id="dbId" :db="db" :db-type="state.row.type" />
|
||||||
<el-popover v-model:visible="showDumpInfo" :width="470" placement="right" trigger="click">
|
|
||||||
<template #reference>
|
|
||||||
<el-button class="ml5" type="success" size="small">导出</el-button>
|
|
||||||
</template>
|
|
||||||
<el-form-item label="导出内容: ">
|
|
||||||
<el-radio-group v-model="dumpInfo.type">
|
|
||||||
<el-radio :label="1" size="small">结构</el-radio>
|
|
||||||
<el-radio :label="2" size="small">数据</el-radio>
|
|
||||||
<el-radio :label="3" size="small">结构+数据</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="导出表: ">
|
|
||||||
<el-table @selection-change="handleDumpTableSelectionChange" max-height="300" size="small" :data="tableInfoDialog.infos">
|
|
||||||
<el-table-column type="selection" width="45" />
|
|
||||||
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip> </el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<div style="text-align: right">
|
|
||||||
<el-button @click="showDumpInfo = false" size="small">取消</el-button>
|
|
||||||
<el-button @click="dump(db)" type="success" size="small">确定</el-button>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
|
|
||||||
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
|
|
||||||
</el-row>
|
|
||||||
<el-table v-loading="tableInfoDialog.loading" border stripe :data="filterTableInfos" size="small" max-height="680">
|
|
||||||
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
|
|
||||||
<template #header>
|
|
||||||
<el-input v-model="tableInfoDialog.tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip>
|
|
||||||
<template #header>
|
|
||||||
<el-input v-model="tableInfoDialog.tableCommentSearch" size="small" placeholder="备注: 输入可过滤" clearable />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
prop="tableRows"
|
|
||||||
label="Rows"
|
|
||||||
min-width="70"
|
|
||||||
sortable
|
|
||||||
:sort-method="(a: any, b: any) => parseInt(a.tableRows) - parseInt(b.tableRows)"
|
|
||||||
></el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
property="dataLength"
|
|
||||||
label="数据大小"
|
|
||||||
sortable
|
|
||||||
:sort-method="(a: any, b: any) => parseInt(a.dataLength) - parseInt(b.dataLength)"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatByteSize(scope.row.dataLength) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
property="indexLength"
|
|
||||||
label="索引大小"
|
|
||||||
sortable
|
|
||||||
:sort-method="(a: any, b: any) => parseInt(a.indexLength) - parseInt(b.indexLength)"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatByteSize(scope.row.indexLength) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column property="createTime" label="创建时间" min-width="150"> </el-table-column>
|
|
||||||
<el-table-column label="更多信息" min-width="140">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
|
|
||||||
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
|
|
||||||
<el-link
|
|
||||||
class="ml5"
|
|
||||||
v-if="tableCreateDialog.enableEditTypes.indexOf(tableCreateDialog.type) > -1"
|
|
||||||
@click.prevent="openEditTable(scope.row)"
|
|
||||||
type="warning"
|
|
||||||
>编辑表</el-link
|
|
||||||
>
|
|
||||||
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" min-width="80">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-link @click.prevent="dropTable(scope.row)" type="danger">删除</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
<el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||||
@@ -227,64 +140,7 @@
|
|||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
v-model="sqlExecLogDialog.visible"
|
v-model="sqlExecLogDialog.visible"
|
||||||
>
|
>
|
||||||
<page-table
|
<db-sql-exec-log :db-id="sqlExecLogDialog.dbId" :dbs="sqlExecLogDialog.dbs" />
|
||||||
height="100%"
|
|
||||||
ref="sqlExecDialogPageTableRef"
|
|
||||||
:query="sqlExecLogDialog.queryConfig"
|
|
||||||
v-model:query-form="sqlExecLogDialog.query"
|
|
||||||
:data="sqlExecLogDialog.data"
|
|
||||||
:columns="sqlExecLogDialog.columns"
|
|
||||||
:total="sqlExecLogDialog.total"
|
|
||||||
v-model:page-size="sqlExecLogDialog.query.pageSize"
|
|
||||||
v-model:page-num="sqlExecLogDialog.query.pageNum"
|
|
||||||
@pageChange="searchSqlExecLog()"
|
|
||||||
>
|
|
||||||
<template #dbSelect>
|
|
||||||
<el-select v-model="sqlExecLogDialog.query.db" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
|
||||||
<el-option v-for="item in sqlExecLogDialog.dbs" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action="{ data }">
|
|
||||||
<el-link
|
|
||||||
v-if="data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value"
|
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
size="small"
|
|
||||||
:underline="false"
|
|
||||||
@click="onShowRollbackSql(data)"
|
|
||||||
>
|
|
||||||
还原SQL</el-link
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</page-table>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog width="55%" :title="`还原SQL`" v-model="rollbackSqlDialog.visible">
|
|
||||||
<el-input type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="rollbackSqlDialog.sql" size="small"> </el-input>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible">
|
|
||||||
<el-table border stripe :data="columnDialog.columns" size="small">
|
|
||||||
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column width="80" prop="nullable" label="是否可为空" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog width="40%" :title="`${chooseTableName} 索引信息`" v-model="indexDialog.visible">
|
|
||||||
<el-table border stripe :data="indexDialog.indexs" size="small">
|
|
||||||
<el-table-column prop="indexName" label="索引名" min-width="120" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column prop="columnName" label="列名" min-width="120" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column prop="seqInIndex" label="列序列号" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column prop="indexType" label="类型"> </el-table-column>
|
|
||||||
<el-table-column prop="indexComment" label="备注" min-width="130" show-overflow-tooltip> </el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog width="55%" :title="`${chooseTableName} Create-DDL`" v-model="ddlDialog.visible">
|
|
||||||
<el-input disabled type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="ddlDialog.ddl" size="small"> </el-input>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog" :close-on-click-modal="false">
|
<el-dialog v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog" :close-on-click-modal="false">
|
||||||
@@ -308,26 +164,13 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<db-edit @val-change="valChange" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
|
<db-edit @val-change="valChange" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
|
||||||
<create-table
|
|
||||||
:title="tableCreateDialog.title"
|
|
||||||
:active-name="tableCreateDialog.activeName"
|
|
||||||
:dbId="dbId"
|
|
||||||
:db="db"
|
|
||||||
:data="tableCreateDialog.data"
|
|
||||||
v-model:visible="tableCreateDialog.visible"
|
|
||||||
@submit-sql="onSubmitSql"
|
|
||||||
>
|
|
||||||
</create-table>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRefs, reactive, computed, onMounted, defineAsyncComponent } from 'vue';
|
import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { formatByteSize } from '@/common/utils/format';
|
|
||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { DbSqlExecTypeEnum } from './enums';
|
|
||||||
import SqlExecBox from './component/SqlExecBox';
|
|
||||||
import config from '@/common/config';
|
import config from '@/common/config';
|
||||||
import { getSession } from '@/common/utils/storage';
|
import { getSession } from '@/common/utils/storage';
|
||||||
import { isTrue } from '@/common/assert';
|
import { isTrue } from '@/common/assert';
|
||||||
@@ -337,9 +180,10 @@ import TagInfo from '../component/TagInfo.vue';
|
|||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
|
import DbSqlExecLog from './DbSqlExecLog.vue';
|
||||||
|
|
||||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||||
const CreateTable = defineAsyncComponent(() => import('./CreateTable.vue'));
|
const DbTableList = defineAsyncComponent(() => import('./table/DbTableList.vue'));
|
||||||
|
|
||||||
const perms = {
|
const perms = {
|
||||||
base: 'db',
|
base: 'db',
|
||||||
@@ -364,7 +208,7 @@ const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(15
|
|||||||
const pageTableRef: any = ref(null);
|
const pageTableRef: any = ref(null);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
row: {},
|
row: {} as any,
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
db: '',
|
db: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -401,49 +245,15 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
// sql执行记录弹框
|
// sql执行记录弹框
|
||||||
sqlExecLogDialog: {
|
sqlExecLogDialog: {
|
||||||
queryConfig: [
|
|
||||||
TableQuery.slot('db', '数据库', 'dbSelect'),
|
|
||||||
TableQuery.text('table', '表名'),
|
|
||||||
TableQuery.select('type', '操作类型').setOptions(Object.values(DbSqlExecTypeEnum)),
|
|
||||||
],
|
|
||||||
columns: [
|
|
||||||
TableColumn.new('db', '数据库'),
|
|
||||||
TableColumn.new('table', '表'),
|
|
||||||
TableColumn.new('type', '类型').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
|
|
||||||
TableColumn.new('creator', '执行人'),
|
|
||||||
TableColumn.new('sql', 'SQL').canBeautify(),
|
|
||||||
TableColumn.new('oldValue', '原值').canBeautify(),
|
|
||||||
TableColumn.new('createTime', '执行时间').isTime(),
|
|
||||||
TableColumn.new('remark', '备注'),
|
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(80).fixedRight().alignCenter(),
|
|
||||||
],
|
|
||||||
title: '',
|
title: '',
|
||||||
visible: false,
|
visible: false,
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
dbs: [],
|
dbs: [],
|
||||||
query: {
|
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
db: '',
|
|
||||||
table: '',
|
|
||||||
type: null,
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rollbackSqlDialog: {
|
|
||||||
visible: false,
|
|
||||||
sql: '',
|
|
||||||
},
|
},
|
||||||
chooseTableName: '',
|
chooseTableName: '',
|
||||||
tableInfoDialog: {
|
tableInfoDialog: {
|
||||||
loading: false,
|
|
||||||
visible: false,
|
visible: false,
|
||||||
infos: [],
|
|
||||||
tableNameSearch: '',
|
|
||||||
tableCommentSearch: '',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
exportDialog: {
|
exportDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
@@ -453,38 +263,11 @@ const state = reactive({
|
|||||||
contents: [] as any,
|
contents: [] as any,
|
||||||
extName: '',
|
extName: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
columnDialog: {
|
|
||||||
visible: false,
|
|
||||||
columns: [],
|
|
||||||
},
|
|
||||||
indexDialog: {
|
|
||||||
visible: false,
|
|
||||||
indexs: [],
|
|
||||||
},
|
|
||||||
ddlDialog: {
|
|
||||||
visible: false,
|
|
||||||
ddl: '',
|
|
||||||
},
|
|
||||||
dbEditDialog: {
|
dbEditDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
data: null as any,
|
data: null as any,
|
||||||
title: '新增数据库',
|
title: '新增数据库',
|
||||||
},
|
},
|
||||||
tableCreateDialog: {
|
|
||||||
title: '创建表',
|
|
||||||
visible: false,
|
|
||||||
activeName: '1',
|
|
||||||
type: '',
|
|
||||||
enableEditTypes: ['mysql'], // 支持"编辑表"的数据库类型
|
|
||||||
data: {
|
|
||||||
// 修改表时,传递修改数据
|
|
||||||
edit: false,
|
|
||||||
row: {},
|
|
||||||
indexs: [],
|
|
||||||
columns: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
filterDb: {
|
filterDb: {
|
||||||
param: '',
|
param: '',
|
||||||
cache: [],
|
cache: [],
|
||||||
@@ -492,30 +275,8 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { dbId, db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, tableInfoDialog, exportDialog, dbEditDialog, filterDb } =
|
||||||
dbId,
|
toRefs(state);
|
||||||
db,
|
|
||||||
tags,
|
|
||||||
instances,
|
|
||||||
selectionData,
|
|
||||||
query,
|
|
||||||
datas,
|
|
||||||
total,
|
|
||||||
infoDialog,
|
|
||||||
showDumpInfo,
|
|
||||||
dumpInfo,
|
|
||||||
sqlExecLogDialog,
|
|
||||||
rollbackSqlDialog,
|
|
||||||
chooseTableName,
|
|
||||||
tableInfoDialog,
|
|
||||||
exportDialog,
|
|
||||||
columnDialog,
|
|
||||||
indexDialog,
|
|
||||||
ddlDialog,
|
|
||||||
dbEditDialog,
|
|
||||||
tableCreateDialog,
|
|
||||||
filterDb,
|
|
||||||
} = toRefs(state);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (Object.keys(actionBtns).length > 0) {
|
if (Object.keys(actionBtns).length > 0) {
|
||||||
@@ -524,26 +285,6 @@ onMounted(async () => {
|
|||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterTableInfos = computed(() => {
|
|
||||||
const infos = state.tableInfoDialog.infos;
|
|
||||||
const tableNameSearch = state.tableInfoDialog.tableNameSearch;
|
|
||||||
const tableCommentSearch = state.tableInfoDialog.tableCommentSearch;
|
|
||||||
if (!tableNameSearch && !tableCommentSearch) {
|
|
||||||
return infos;
|
|
||||||
}
|
|
||||||
return infos.filter((data: any) => {
|
|
||||||
let tnMatch = true;
|
|
||||||
let tcMatch = true;
|
|
||||||
if (tableNameSearch) {
|
|
||||||
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase());
|
|
||||||
}
|
|
||||||
if (tableCommentSearch) {
|
|
||||||
tcMatch = data.tableComment.includes(tableCommentSearch);
|
|
||||||
}
|
|
||||||
return tnMatch && tcMatch;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
try {
|
try {
|
||||||
pageTableRef.value.loading(true);
|
pageTableRef.value.loading(true);
|
||||||
@@ -619,51 +360,15 @@ const deleteDb = async () => {
|
|||||||
|
|
||||||
const onShowSqlExec = async (row: any) => {
|
const onShowSqlExec = async (row: any) => {
|
||||||
state.sqlExecLogDialog.title = `${row.name}`;
|
state.sqlExecLogDialog.title = `${row.name}`;
|
||||||
state.sqlExecLogDialog.query.dbId = row.id;
|
state.sqlExecLogDialog.dbId = row.id;
|
||||||
state.sqlExecLogDialog.dbs = row.database.split(' ');
|
state.sqlExecLogDialog.dbs = row.database.split(' ');
|
||||||
searchSqlExecLog();
|
|
||||||
state.sqlExecLogDialog.visible = true;
|
state.sqlExecLogDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBeforeCloseSqlExecDialog = () => {
|
const onBeforeCloseSqlExecDialog = () => {
|
||||||
state.sqlExecLogDialog.visible = false;
|
state.sqlExecLogDialog.visible = false;
|
||||||
state.sqlExecLogDialog.data = [];
|
|
||||||
state.sqlExecLogDialog.dbs = [];
|
state.sqlExecLogDialog.dbs = [];
|
||||||
state.sqlExecLogDialog.total = 0;
|
state.sqlExecLogDialog.dbId = 0;
|
||||||
state.sqlExecLogDialog.query.dbId = 0;
|
|
||||||
state.sqlExecLogDialog.query.pageNum = 1;
|
|
||||||
state.sqlExecLogDialog.query.table = '';
|
|
||||||
state.sqlExecLogDialog.query.db = '';
|
|
||||||
state.sqlExecLogDialog.query.type = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchSqlExecLog = async () => {
|
|
||||||
const res = await dbApi.getSqlExecs.request(state.sqlExecLogDialog.query);
|
|
||||||
state.sqlExecLogDialog.data = res.list;
|
|
||||||
state.sqlExecLogDialog.total = res.total;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选择导出数据库表
|
|
||||||
*/
|
|
||||||
const handleDumpTableSelectionChange = (vals: any) => {
|
|
||||||
state.dumpInfo.tables = vals.map((x: any) => x.tableName);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据库信息导出
|
|
||||||
*/
|
|
||||||
const dump = (db: string) => {
|
|
||||||
isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表');
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.setAttribute(
|
|
||||||
'href',
|
|
||||||
`${config.baseApiUrl}/dbs/${state.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&token=${getSession(
|
|
||||||
'token'
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
a.click();
|
|
||||||
state.showDumpInfo = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDumpDbs = async (row: any) => {
|
const onDumpDbs = async (row: any) => {
|
||||||
@@ -707,136 +412,16 @@ const dumpDbs = () => {
|
|||||||
state.exportDialog.visible = false;
|
state.exportDialog.visible = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onShowRollbackSql = async (sqlExecLog: any) => {
|
|
||||||
const columns = await dbApi.columnMetadata.request({ id: sqlExecLog.dbId, db: sqlExecLog.db, tableName: sqlExecLog.table });
|
|
||||||
const primaryKey = getPrimaryKey(columns);
|
|
||||||
const oldValue = JSON.parse(sqlExecLog.oldValue);
|
|
||||||
|
|
||||||
const rollbackSqls = [];
|
|
||||||
if (sqlExecLog.type == DbSqlExecTypeEnum['UPDATE'].value) {
|
|
||||||
for (let ov of oldValue) {
|
|
||||||
const setItems = [];
|
|
||||||
for (let key in ov) {
|
|
||||||
if (key == primaryKey) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
setItems.push(`${key} = ${wrapValue(ov[key])}`);
|
|
||||||
}
|
|
||||||
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
|
|
||||||
}
|
|
||||||
} else if (sqlExecLog.type == DbSqlExecTypeEnum['DELETE'].value) {
|
|
||||||
const columnNames = columns.map((c: any) => c.columnName);
|
|
||||||
for (let ov of oldValue) {
|
|
||||||
const values = [];
|
|
||||||
for (let column of columnNames) {
|
|
||||||
values.push(wrapValue(ov[column]));
|
|
||||||
}
|
|
||||||
rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.rollbackSqlDialog.sql = rollbackSqls.join('\n');
|
|
||||||
state.rollbackSqlDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPrimaryKey = (columns: any) => {
|
|
||||||
const col = columns.find((c: any) => c.columnKey == 'PRI');
|
|
||||||
if (col) {
|
|
||||||
return col.columnName;
|
|
||||||
}
|
|
||||||
return columns[0].columnName;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 包装值,如果值类型为number则直接返回,其他则需要使用''包装
|
|
||||||
*/
|
|
||||||
const wrapValue = (val: any) => {
|
|
||||||
if (typeof val == 'number') {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
return `'${val}'`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showTableInfo = async (row: any, db: string) => {
|
const showTableInfo = async (row: any, db: string) => {
|
||||||
state.tableInfoDialog.loading = true;
|
|
||||||
state.tableInfoDialog.visible = true;
|
|
||||||
try {
|
|
||||||
state.tableInfoDialog.infos = await dbApi.tableInfos.request({ id: row.id, db });
|
|
||||||
state.tableCreateDialog.type = row.type;
|
|
||||||
state.dbId = row.id;
|
state.dbId = row.id;
|
||||||
state.row = row;
|
state.row = row;
|
||||||
state.db = db;
|
state.db = db;
|
||||||
} catch (e) {
|
state.tableInfoDialog.visible = true;
|
||||||
state.tableInfoDialog.visible = false;
|
|
||||||
} finally {
|
|
||||||
state.tableInfoDialog.loading = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmitSql = async (row: { tableName: string }) => {
|
|
||||||
await openEditTable(row);
|
|
||||||
state.tableInfoDialog.infos = await dbApi.tableInfos.request({ id: state.dbId, db: state.db });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeTableInfo = () => {
|
const closeTableInfo = () => {
|
||||||
state.showDumpInfo = false;
|
state.showDumpInfo = false;
|
||||||
state.tableInfoDialog.visible = false;
|
state.tableInfoDialog.visible = false;
|
||||||
state.tableInfoDialog.infos = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const showColumns = async (row: any) => {
|
|
||||||
state.chooseTableName = row.tableName;
|
|
||||||
state.columnDialog.columns = await dbApi.columnMetadata.request({
|
|
||||||
id: state.dbId,
|
|
||||||
db: state.db,
|
|
||||||
tableName: row.tableName,
|
|
||||||
});
|
|
||||||
|
|
||||||
state.columnDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showTableIndex = async (row: any) => {
|
|
||||||
state.chooseTableName = row.tableName;
|
|
||||||
state.indexDialog.indexs = await dbApi.tableIndex.request({
|
|
||||||
id: state.dbId,
|
|
||||||
db: state.db,
|
|
||||||
tableName: row.tableName,
|
|
||||||
});
|
|
||||||
|
|
||||||
state.indexDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showCreateDdl = async (row: any) => {
|
|
||||||
state.chooseTableName = row.tableName;
|
|
||||||
const res = await dbApi.tableDdl.request({
|
|
||||||
id: state.dbId,
|
|
||||||
db: state.db,
|
|
||||||
tableName: row.tableName,
|
|
||||||
});
|
|
||||||
state.ddlDialog.ddl = res;
|
|
||||||
state.ddlDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除表
|
|
||||||
*/
|
|
||||||
const dropTable = async (row: any) => {
|
|
||||||
try {
|
|
||||||
const tableName = row.tableName;
|
|
||||||
await ElMessageBox.confirm(`确定删除'${tableName}'表?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
SqlExecBox({
|
|
||||||
sql: `DROP TABLE ${tableName}`,
|
|
||||||
dbId: state.dbId,
|
|
||||||
db: state.db,
|
|
||||||
runSuccessCallback: async () => {
|
|
||||||
state.tableInfoDialog.infos = await dbApi.tableInfos.request({ id: state.dbId, db: state.db });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 点击查看时初始化数据
|
// 点击查看时初始化数据
|
||||||
@@ -856,31 +441,5 @@ const filterSchema = () => {
|
|||||||
state.filterDb.list = state.filterDb.cache;
|
state.filterDb.list = state.filterDb.cache;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开编辑表
|
|
||||||
const openEditTable = async (row: any) => {
|
|
||||||
state.tableCreateDialog.visible = true;
|
|
||||||
state.tableCreateDialog.activeName = '1';
|
|
||||||
|
|
||||||
if (row === false) {
|
|
||||||
state.tableCreateDialog.data = { edit: false, row: {}, indexs: [], columns: [] };
|
|
||||||
state.tableCreateDialog.title = '创建表';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.tableName) {
|
|
||||||
state.tableCreateDialog.title = '修改表';
|
|
||||||
let indexs = await dbApi.tableIndex.request({
|
|
||||||
id: state.dbId,
|
|
||||||
db: state.db,
|
|
||||||
tableName: row.tableName,
|
|
||||||
});
|
|
||||||
let columns = await dbApi.columnMetadata.request({
|
|
||||||
id: state.dbId,
|
|
||||||
db: state.db,
|
|
||||||
tableName: row.tableName,
|
|
||||||
});
|
|
||||||
state.tableCreateDialog.data = { edit: true, row, indexs, columns };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
168
mayfly_go_web/src/views/ops/db/DbSqlExecLog.vue
Normal file
168
mayfly_go_web/src/views/ops/db/DbSqlExecLog.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<div class="db-sql-exec-log">
|
||||||
|
<page-table
|
||||||
|
height="100%"
|
||||||
|
ref="sqlExecDialogPageTableRef"
|
||||||
|
:query="queryConfig"
|
||||||
|
v-model:query-form="query"
|
||||||
|
:data="data"
|
||||||
|
:columns="columns"
|
||||||
|
:total="total"
|
||||||
|
v-model:page-size="query.pageSize"
|
||||||
|
v-model:page-num="query.pageNum"
|
||||||
|
@pageChange="searchSqlExecLog()"
|
||||||
|
>
|
||||||
|
<template #dbSelect>
|
||||||
|
<el-select v-model="query.db" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
||||||
|
<el-option v-for="item in dbs" :key="item" :label="`${item}`" :value="item"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<el-link
|
||||||
|
v-if="data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:underline="false"
|
||||||
|
@click="onShowRollbackSql(data)"
|
||||||
|
>
|
||||||
|
还原SQL</el-link
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<el-dialog width="55%" :title="`还原SQL`" v-model="rollbackSqlDialog.visible">
|
||||||
|
<el-input type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="rollbackSqlDialog.sql" size="small"> </el-input>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs,watch, reactive, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||||
|
import { dbApi } from './api';
|
||||||
|
import { DbSqlExecTypeEnum } from './enums';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dbId: {
|
||||||
|
type: [Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
dbs: {
|
||||||
|
type: [Array<String>],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryConfig = [
|
||||||
|
TableQuery.slot('db', '数据库', 'dbSelect'),
|
||||||
|
TableQuery.text('table', '表名'),
|
||||||
|
TableQuery.select('type', '操作类型').setOptions(Object.values(DbSqlExecTypeEnum)),
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('db', '数据库'),
|
||||||
|
TableColumn.new('table', '表'),
|
||||||
|
TableColumn.new('type', '类型').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
|
||||||
|
TableColumn.new('creator', '执行人'),
|
||||||
|
TableColumn.new('sql', 'SQL').canBeautify(),
|
||||||
|
TableColumn.new('oldValue', '原值').canBeautify(),
|
||||||
|
TableColumn.new('createTime', '执行时间').isTime(),
|
||||||
|
TableColumn.new('remark', '备注'),
|
||||||
|
TableColumn.new('action', '操作').isSlot().setMinWidth(90).fixedRight().alignCenter(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
dbs: [],
|
||||||
|
query: {
|
||||||
|
dbId: 0,
|
||||||
|
db: '',
|
||||||
|
table: '',
|
||||||
|
type: null,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
rollbackSqlDialog: {
|
||||||
|
visible: false,
|
||||||
|
sql: '',
|
||||||
|
},
|
||||||
|
filterDb: {
|
||||||
|
param: '',
|
||||||
|
cache: [],
|
||||||
|
list: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, query, total, rollbackSqlDialog } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
searchSqlExecLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(props, async (newValue: any) => {
|
||||||
|
await searchSqlExecLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const searchSqlExecLog = async () => {
|
||||||
|
state.query.dbId = props.dbId
|
||||||
|
const res = await dbApi.getSqlExecs.request(state.query);
|
||||||
|
state.data = res.list;
|
||||||
|
state.total = res.total;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||||
|
const columns = await dbApi.columnMetadata.request({ id: sqlExecLog.dbId, db: sqlExecLog.db, tableName: sqlExecLog.table });
|
||||||
|
const primaryKey = getPrimaryKey(columns);
|
||||||
|
const oldValue = JSON.parse(sqlExecLog.oldValue);
|
||||||
|
|
||||||
|
const rollbackSqls = [];
|
||||||
|
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
|
||||||
|
for (let ov of oldValue) {
|
||||||
|
const setItems = [];
|
||||||
|
for (let key in ov) {
|
||||||
|
if (key == primaryKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
setItems.push(`${key} = ${wrapValue(ov[key])}`);
|
||||||
|
}
|
||||||
|
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
|
||||||
|
}
|
||||||
|
} else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
|
||||||
|
const columnNames = columns.map((c: any) => c.columnName);
|
||||||
|
for (let ov of oldValue) {
|
||||||
|
const values = [];
|
||||||
|
for (let column of columnNames) {
|
||||||
|
values.push(wrapValue(ov[column]));
|
||||||
|
}
|
||||||
|
rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.rollbackSqlDialog.sql = rollbackSqls.join('\n');
|
||||||
|
state.rollbackSqlDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPrimaryKey = (columns: any) => {
|
||||||
|
const col = columns.find((c: any) => c.columnKey == 'PRI');
|
||||||
|
if (col) {
|
||||||
|
return col.columnName;
|
||||||
|
}
|
||||||
|
return columns[0].columnName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包装值,如果值类型为number则直接返回,其他则需要使用''包装
|
||||||
|
*/
|
||||||
|
const wrapValue = (val: any) => {
|
||||||
|
if (typeof val == 'number') {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
return `'${val}'`;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog :title="`${title} 详情`" v-model="dialogVisible" :before-close="cancel" width="90%">
|
|
||||||
<el-table @cell-click="cellClick" :data="data.res">
|
|
||||||
<el-table-column :width="200" :prop="item" :label="item" v-for="item in data.colNames" :key="item"> </el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { watch, toRefs, reactive } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//定义事件
|
|
||||||
const emit = defineEmits(['update:visible']);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
dialogVisible: false,
|
|
||||||
data: {
|
|
||||||
res: [],
|
|
||||||
colNames: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { dialogVisible, data } = toRefs(state);
|
|
||||||
|
|
||||||
watch(props, async (newValue: any) => {
|
|
||||||
state.dialogVisible = newValue.visible;
|
|
||||||
state.data.res = newValue.data.res;
|
|
||||||
state.data.colNames = newValue.data.colNames;
|
|
||||||
});
|
|
||||||
|
|
||||||
const cellClick = (row: any, column: any, cell: any) => {
|
|
||||||
let isDiv = cell.children[0].tagName === 'DIV';
|
|
||||||
let text = cell.children[0].innerText;
|
|
||||||
let div = cell.children[0];
|
|
||||||
if (isDiv) {
|
|
||||||
let input = document.createElement('input');
|
|
||||||
input.setAttribute('value', text);
|
|
||||||
cell.replaceChildren(input);
|
|
||||||
input.focus();
|
|
||||||
input.addEventListener('blur', () => {
|
|
||||||
div.innerText = input.value;
|
|
||||||
cell.replaceChildren(div);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
emit('update:visible', false);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
import { watch, toRefs, reactive, ref } from 'vue';
|
import { watch, toRefs, reactive, ref } from 'vue';
|
||||||
import { TYPE_LIST, CHARACTER_SET_NAME_LIST, COLLATION_SUFFIX_LIST } from './service';
|
import { TYPE_LIST, CHARACTER_SET_NAME_LIST, COLLATION_SUFFIX_LIST } from './service';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import SqlExecBox from './component/SqlExecBox';
|
import SqlExecBox from '../component/SqlExecBox';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
356
mayfly_go_web/src/views/ops/db/table/DbTableList.vue
Normal file
356
mayfly_go_web/src/views/ops/db/table/DbTableList.vue
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
<template>
|
||||||
|
<div class="db-table">
|
||||||
|
<el-row class="mb10">
|
||||||
|
<el-popover v-model:visible="showDumpInfo" :width="470" placement="right" trigger="click">
|
||||||
|
<template #reference>
|
||||||
|
<el-button class="ml5" type="success" size="small">导出</el-button>
|
||||||
|
</template>
|
||||||
|
<el-form-item label="导出内容: ">
|
||||||
|
<el-radio-group v-model="dumpInfo.type">
|
||||||
|
<el-radio :label="1" size="small">结构</el-radio>
|
||||||
|
<el-radio :label="2" size="small">数据</el-radio>
|
||||||
|
<el-radio :label="3" size="small">结构+数据</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="导出表: ">
|
||||||
|
<el-table @selection-change="handleDumpTableSelectionChange" max-height="300" size="small" :data="tables">
|
||||||
|
<el-table-column type="selection" width="45" />
|
||||||
|
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip> </el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div style="text-align: right">
|
||||||
|
<el-button @click="showDumpInfo = false" size="small">取消</el-button>
|
||||||
|
<el-button @click="dump(db)" type="success" size="small">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
|
||||||
|
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" border stripe :data="filterTableInfos" size="small" height="65vh">
|
||||||
|
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
|
||||||
|
<template #header>
|
||||||
|
<el-input v-model="tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip>
|
||||||
|
<template #header>
|
||||||
|
<el-input v-model="tableCommentSearch" size="small" placeholder="备注: 输入可过滤" clearable />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="tableRows"
|
||||||
|
label="Rows"
|
||||||
|
min-width="70"
|
||||||
|
sortable
|
||||||
|
:sort-method="(a: any, b: any) => parseInt(a.tableRows) - parseInt(b.tableRows)"
|
||||||
|
></el-table-column>
|
||||||
|
<el-table-column property="dataLength" label="数据大小" sortable :sort-method="(a: any, b: any) => parseInt(a.dataLength) - parseInt(b.dataLength)">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatByteSize(scope.row.dataLength) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
property="indexLength"
|
||||||
|
label="索引大小"
|
||||||
|
sortable
|
||||||
|
:sort-method="(a: any, b: any) => parseInt(a.indexLength) - parseInt(b.indexLength)"
|
||||||
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatByteSize(scope.row.indexLength) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column property="createTime" label="创建时间" min-width="150"> </el-table-column>
|
||||||
|
<el-table-column label="更多信息" min-width="140">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
|
||||||
|
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
|
||||||
|
<el-link class="ml5" v-if="tableCreateDialog.enableEditTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning"
|
||||||
|
>编辑表</el-link
|
||||||
|
>
|
||||||
|
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" min-width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click.prevent="dropTable(scope.row)" type="danger">删除</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible">
|
||||||
|
<el-table border stripe :data="columnDialog.columns" size="small">
|
||||||
|
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column width="80" prop="nullable" label="是否可为空" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog width="40%" :title="`${chooseTableName} 索引信息`" v-model="indexDialog.visible">
|
||||||
|
<el-table border stripe :data="indexDialog.indexs" size="small">
|
||||||
|
<el-table-column prop="indexName" label="索引名" min-width="120" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column prop="columnName" label="列名" min-width="120" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column prop="seqInIndex" label="列序列号" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column prop="indexType" label="类型"> </el-table-column>
|
||||||
|
<el-table-column prop="indexComment" label="备注" min-width="130" show-overflow-tooltip> </el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog width="55%" :title="`${chooseTableName} Create-DDL`" v-model="ddlDialog.visible">
|
||||||
|
<el-input disabled type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="ddlDialog.ddl" size="small"> </el-input>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<db-table-edit
|
||||||
|
:title="tableCreateDialog.title"
|
||||||
|
:active-name="tableCreateDialog.activeName"
|
||||||
|
:dbId="dbId"
|
||||||
|
:db="db"
|
||||||
|
:data="tableCreateDialog.data"
|
||||||
|
v-model:visible="tableCreateDialog.visible"
|
||||||
|
@submit-sql="onSubmitSql"
|
||||||
|
>
|
||||||
|
</db-table-edit>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, computed, onMounted, defineAsyncComponent, nextTick } from 'vue';
|
||||||
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
import { dbApi } from '../api';
|
||||||
|
import SqlExecBox from '../component/SqlExecBox';
|
||||||
|
import config from '@/common/config';
|
||||||
|
import { getSession } from '@/common/utils/storage';
|
||||||
|
import { isTrue } from '@/common/assert';
|
||||||
|
|
||||||
|
const DbTableEdit = defineAsyncComponent(() => import('./DbTableEdit.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dbId: {
|
||||||
|
type: [Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
type: [String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
dbType: {
|
||||||
|
type: [String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
row: {},
|
||||||
|
loading: false,
|
||||||
|
tables: [],
|
||||||
|
tableNameSearch: '',
|
||||||
|
tableCommentSearch: '',
|
||||||
|
showDumpInfo: false,
|
||||||
|
dumpInfo: {
|
||||||
|
id: 0,
|
||||||
|
db: '',
|
||||||
|
type: 3,
|
||||||
|
tables: [],
|
||||||
|
},
|
||||||
|
chooseTableName: '',
|
||||||
|
columnDialog: {
|
||||||
|
visible: false,
|
||||||
|
columns: [],
|
||||||
|
},
|
||||||
|
indexDialog: {
|
||||||
|
visible: false,
|
||||||
|
indexs: [],
|
||||||
|
},
|
||||||
|
ddlDialog: {
|
||||||
|
visible: false,
|
||||||
|
ddl: '',
|
||||||
|
},
|
||||||
|
tableCreateDialog: {
|
||||||
|
title: '创建表',
|
||||||
|
visible: false,
|
||||||
|
activeName: '1',
|
||||||
|
type: '',
|
||||||
|
enableEditTypes: ['mysql'], // 支持"编辑表"的数据库类型
|
||||||
|
data: {
|
||||||
|
// 修改表时,传递修改数据
|
||||||
|
edit: false,
|
||||||
|
row: {},
|
||||||
|
indexs: [],
|
||||||
|
columns: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filterDb: {
|
||||||
|
param: '',
|
||||||
|
cache: [],
|
||||||
|
list: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
tables,
|
||||||
|
tableNameSearch,
|
||||||
|
tableCommentSearch,
|
||||||
|
showDumpInfo,
|
||||||
|
dumpInfo,
|
||||||
|
chooseTableName,
|
||||||
|
columnDialog,
|
||||||
|
indexDialog,
|
||||||
|
ddlDialog,
|
||||||
|
tableCreateDialog,
|
||||||
|
} = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
getTables();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(props, async (newValue: any) => {
|
||||||
|
await getTables();
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterTableInfos = computed(() => {
|
||||||
|
const tables = state.tables;
|
||||||
|
const tableNameSearch = state.tableNameSearch;
|
||||||
|
const tableCommentSearch = state.tableCommentSearch;
|
||||||
|
if (!tableNameSearch && !tableCommentSearch) {
|
||||||
|
return tables;
|
||||||
|
}
|
||||||
|
return tables.filter((data: any) => {
|
||||||
|
let tnMatch = true;
|
||||||
|
let tcMatch = true;
|
||||||
|
if (tableNameSearch) {
|
||||||
|
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase());
|
||||||
|
}
|
||||||
|
if (tableCommentSearch) {
|
||||||
|
tcMatch = data.tableComment.includes(tableCommentSearch);
|
||||||
|
}
|
||||||
|
return tnMatch && tcMatch;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTables = async () => {
|
||||||
|
state.loading = true;
|
||||||
|
try {
|
||||||
|
state.tables = await dbApi.tableInfos.request({ id: props.dbId, db: props.db });
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择导出数据库表
|
||||||
|
*/
|
||||||
|
const handleDumpTableSelectionChange = (vals: any) => {
|
||||||
|
state.dumpInfo.tables = vals.map((x: any) => x.tableName);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库信息导出
|
||||||
|
*/
|
||||||
|
const dump = (db: string) => {
|
||||||
|
isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表');
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute(
|
||||||
|
'href',
|
||||||
|
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&token=${getSession(
|
||||||
|
'token'
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
a.click();
|
||||||
|
state.showDumpInfo = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showColumns = async (row: any) => {
|
||||||
|
state.chooseTableName = row.tableName;
|
||||||
|
state.columnDialog.columns = await dbApi.columnMetadata.request({
|
||||||
|
id: props.dbId,
|
||||||
|
db: props.db,
|
||||||
|
tableName: row.tableName,
|
||||||
|
});
|
||||||
|
|
||||||
|
state.columnDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showTableIndex = async (row: any) => {
|
||||||
|
state.chooseTableName = row.tableName;
|
||||||
|
state.indexDialog.indexs = await dbApi.tableIndex.request({
|
||||||
|
id: props.dbId,
|
||||||
|
db: props.db,
|
||||||
|
tableName: row.tableName,
|
||||||
|
});
|
||||||
|
|
||||||
|
state.indexDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showCreateDdl = async (row: any) => {
|
||||||
|
state.chooseTableName = row.tableName;
|
||||||
|
const res = await dbApi.tableDdl.request({
|
||||||
|
id: props.dbId,
|
||||||
|
db: props.db,
|
||||||
|
tableName: row.tableName,
|
||||||
|
});
|
||||||
|
state.ddlDialog.ddl = res;
|
||||||
|
state.ddlDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除表
|
||||||
|
*/
|
||||||
|
const dropTable = async (row: any) => {
|
||||||
|
try {
|
||||||
|
const tableName = row.tableName;
|
||||||
|
await ElMessageBox.confirm(`确定删除'${tableName}'表?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
SqlExecBox({
|
||||||
|
sql: `DROP TABLE ${tableName}`,
|
||||||
|
dbId: props.dbId as any,
|
||||||
|
db: props.db as any,
|
||||||
|
runSuccessCallback: async () => {
|
||||||
|
state.tables = await dbApi.tableInfos.request({ id: props.dbId, db: props.db });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开编辑表
|
||||||
|
const openEditTable = async (row: any) => {
|
||||||
|
state.tableCreateDialog.visible = true;
|
||||||
|
state.tableCreateDialog.activeName = '1';
|
||||||
|
|
||||||
|
if (row === false) {
|
||||||
|
state.tableCreateDialog.data = { edit: false, row: {}, indexs: [], columns: [] };
|
||||||
|
state.tableCreateDialog.title = '创建表';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.tableName) {
|
||||||
|
state.tableCreateDialog.title = '修改表';
|
||||||
|
let indexs = await dbApi.tableIndex.request({
|
||||||
|
id: props.dbId,
|
||||||
|
db: props.db,
|
||||||
|
tableName: row.tableName,
|
||||||
|
});
|
||||||
|
let columns = await dbApi.columnMetadata.request({
|
||||||
|
id: props.dbId,
|
||||||
|
db: props.db,
|
||||||
|
tableName: row.tableName,
|
||||||
|
});
|
||||||
|
state.tableCreateDialog.data = { edit: true, row, indexs, columns };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitSql = async (row: { tableName: string }) => {
|
||||||
|
await openEditTable(row);
|
||||||
|
state.tableCreateDialog.visible = false;
|
||||||
|
state.tables = await dbApi.tableInfos.request({ id: props.dbId, db: props.db });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
|
|
||||||
<el-table-column label="操作">
|
<el-table-column label="操作">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-popconfirm title="确定终止该进程?" @confirm="confirmKillProcess(scope.row.pid)">
|
<el-popconfirm title="确定终止该进程?" @confirm="confirmKillProcess(scope.row.pid)" width="160">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button v-auth="'machine:killprocess'" type="danger" icon="delete" size="small" plain>终止</el-button>
|
<el-button v-auth="'machine:killprocess'" type="danger" icon="delete" size="small" plain>终止</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -29,6 +29,22 @@
|
|||||||
<el-button :disabled="nowPath == basePath" type="primary" circle size="small" icon="Back" @click="back()"> </el-button>
|
<el-button :disabled="nowPath == basePath" type="primary" circle size="small" icon="Back" @click="back()"> </el-button>
|
||||||
<el-button class="ml5" type="primary" circle size="small" icon="Refresh" @click="refresh()"> </el-button>
|
<el-button class="ml5" type="primary" circle size="small" icon="Refresh" @click="refresh()"> </el-button>
|
||||||
|
|
||||||
|
<!-- 文件&文件夹上传 -->
|
||||||
|
<el-dropdown class="machine-file-upload-exec" trigger="click" size="small">
|
||||||
|
<span>
|
||||||
|
<el-button
|
||||||
|
v-auth="'machine:file:upload'"
|
||||||
|
class="ml5"
|
||||||
|
type="primary"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
icon="Upload"
|
||||||
|
title="上传"
|
||||||
|
></el-button>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item>
|
||||||
<el-upload
|
<el-upload
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
:on-success="uploadSuccess"
|
:on-success="uploadSuccess"
|
||||||
@@ -39,9 +55,27 @@
|
|||||||
name="file"
|
name="file"
|
||||||
class="machine-file-upload-exec"
|
class="machine-file-upload-exec"
|
||||||
>
|
>
|
||||||
<el-button v-auth="'machine:file:upload'" class="ml5" type="primary" circle size="small" icon="Upload" title="上传">
|
<el-link>文件</el-link>
|
||||||
</el-button>
|
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
</el-dropdown-item>
|
||||||
|
|
||||||
|
<el-dropdown-item>
|
||||||
|
<div>
|
||||||
|
<el-link @click="addFinderToList">文件夹</el-link>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="folderUploadInput"
|
||||||
|
ref="folderUploadRef"
|
||||||
|
webkitdirectory
|
||||||
|
directory
|
||||||
|
@change="getFolder"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
:disabled="state.selectionFiles.length == 0"
|
:disabled="state.selectionFiles.length == 0"
|
||||||
@@ -234,7 +268,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, onMounted, computed } from 'vue';
|
import { ref, toRefs, reactive, onMounted, computed } from 'vue';
|
||||||
import { ElMessage, ElMessageBox, ElInput } from 'element-plus';
|
import { ElMessage, ElMessageBox, ElInput } from 'element-plus';
|
||||||
import { machineApi } from '../api';
|
import { machineApi } from '../api';
|
||||||
|
|
||||||
@@ -252,6 +286,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const token = getSession('token');
|
const token = getSession('token');
|
||||||
|
const folderUploadRef: any = ref();
|
||||||
|
|
||||||
const folderType = 'd';
|
const folderType = 'd';
|
||||||
const fileType = '-';
|
const fileType = '-';
|
||||||
@@ -573,6 +608,48 @@ const downloadFile = (data: any) => {
|
|||||||
a.click();
|
a.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function addFinderToList() {
|
||||||
|
folderUploadRef.value.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFolder(e: any) {
|
||||||
|
//e.target.files为文件夹里面的文件
|
||||||
|
// 把文件夹数据放到formData里面,下面的files和paths字段根据接口来定
|
||||||
|
var form = new FormData();
|
||||||
|
form.append('basePath', state.nowPath);
|
||||||
|
for (let file of e.target.files) {
|
||||||
|
form.append('files', file);
|
||||||
|
form.append('paths', file.webkitRelativePath);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 上传操作
|
||||||
|
machineApi.uploadFile
|
||||||
|
.request(form, {
|
||||||
|
url: `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/upload-folder?token=${token}`,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
|
||||||
|
onUploadProgress: onUploadProgress,
|
||||||
|
baseURL: '',
|
||||||
|
timeout: 3 * 60 * 60 * 1000,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success('上传成功');
|
||||||
|
setTimeout(() => {
|
||||||
|
refresh();
|
||||||
|
state.uploadProgressShow = false;
|
||||||
|
}, 3000);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
state.uploadProgressShow = false;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
//无论上传成功与否,都把已选择的文件夹清空,否则选择同一文件夹没有反应
|
||||||
|
const folderEle: any = document.getElementById('folderUploadInput');
|
||||||
|
if (folderEle) {
|
||||||
|
folderEle.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onUploadProgress = (progressEvent: any) => {
|
const onUploadProgress = (progressEvent: any) => {
|
||||||
state.uploadProgressShow = true;
|
state.uploadProgressShow = true;
|
||||||
let complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
|
let complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
|
||||||
@@ -593,7 +670,7 @@ const getUploadFile = (content: any) => {
|
|||||||
headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
|
headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
|
||||||
onUploadProgress: onUploadProgress,
|
onUploadProgress: onUploadProgress,
|
||||||
baseURL: '',
|
baseURL: '',
|
||||||
timeout: 60 * 60 * 1000,
|
timeout: 3 * 60 * 60 * 1000,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
ElMessage.success('上传成功');
|
ElMessage.success('上传成功');
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
|
|||||||
cli := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
|
cli := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
|
||||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.LoginAccount.Id, cli.GetMachine().TagPath), "%s")
|
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.LoginAccount.Id, cli.GetMachine().TagPath), "%s")
|
||||||
|
|
||||||
_, err := cli.Run("sudo kill -9 " + pid)
|
res, err := cli.Run("sudo kill -9 " + pid)
|
||||||
biz.ErrIsNilAppendErr(err, "终止进程失败: %s")
|
biz.ErrIsNil(err, "终止进程失败: %s", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Machine) WsSSH(g *gin.Context) {
|
func (m *Machine) WsSSH(g *gin.Context) {
|
||||||
|
|||||||
@@ -11,13 +11,18 @@ import (
|
|||||||
msgapp "mayfly-go/internal/msg/application"
|
msgapp "mayfly-go/internal/msg/application"
|
||||||
"mayfly-go/pkg/biz"
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/ginx"
|
"mayfly-go/pkg/ginx"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
"mayfly-go/pkg/req"
|
"mayfly-go/pkg/req"
|
||||||
|
"mayfly-go/pkg/utils/collx"
|
||||||
"mayfly-go/pkg/utils/jsonx"
|
"mayfly-go/pkg/utils/jsonx"
|
||||||
"mayfly-go/pkg/utils/timex"
|
"mayfly-go/pkg/utils/timex"
|
||||||
"mayfly-go/pkg/ws"
|
"mayfly-go/pkg/ws"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -158,6 +163,8 @@ func (m *MachineFile) WriteFileContent(rc *req.Ctx) {
|
|||||||
rc.ReqParam = fmt.Sprintf("%s -> 修改文件内容: %s", mi.GetLogDesc(), path)
|
rc.ReqParam = fmt.Sprintf("%s -> 修改文件内容: %s", mi.GetLogDesc(), path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MaxUploadFileSize int64 = 1024 * 1024 * 1024
|
||||||
|
|
||||||
func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
||||||
g := rc.GinCtx
|
g := rc.GinCtx
|
||||||
fid := GetMachineFileId(g)
|
fid := GetMachineFileId(g)
|
||||||
@@ -165,6 +172,7 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
|||||||
|
|
||||||
fileheader, err := g.FormFile("file")
|
fileheader, err := g.FormFile("file")
|
||||||
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
|
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
|
||||||
|
biz.IsTrue(fileheader.Size <= MaxUploadFileSize, "文件大小不能超过%d字节", MaxUploadFileSize)
|
||||||
|
|
||||||
file, _ := fileheader.Open()
|
file, _ := fileheader.Open()
|
||||||
rc.ReqParam = fmt.Sprintf("path: %s", path)
|
rc.ReqParam = fmt.Sprintf("path: %s", path)
|
||||||
@@ -173,20 +181,104 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
|||||||
rc.ReqParam = fmt.Sprintf("%s -> 上传文件: %s/%s", mi.GetLogDesc(), path, fileheader.Filename)
|
rc.ReqParam = fmt.Sprintf("%s -> 上传文件: %s/%s", mi.GetLogDesc(), path, fileheader.Filename)
|
||||||
|
|
||||||
la := rc.LoginAccount
|
la := rc.LoginAccount
|
||||||
go func() {
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
|
logx.Errorf("文件上传失败: %s", err)
|
||||||
switch t := err.(type) {
|
switch t := err.(type) {
|
||||||
case *biz.BizError:
|
case biz.BizError:
|
||||||
m.MsgApp.CreateAndSend(la, ws.ErrMsg("文件上传失败", fmt.Sprintf("执行文件上传失败:\n<-e errCode: %d, errMsg: %s", t.Code(), t.Error())))
|
m.MsgApp.CreateAndSend(la, ws.ErrMsg("文件上传失败", fmt.Sprintf("执行文件上传失败:\n<-e errCode: %d, errMsg: %s", t.Code(), t.Error())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
m.MachineFileApp.UploadFile(fid, path, fileheader.Filename, file)
|
m.MachineFileApp.UploadFile(fid, path, fileheader.Filename, file)
|
||||||
// 保存消息并发送文件上传成功通知
|
// 保存消息并发送文件上传成功通知
|
||||||
m.MsgApp.CreateAndSend(la, ws.SuccessMsg("文件上传成功", fmt.Sprintf("[%s]文件已成功上传至 %s[%s:%s]", fileheader.Filename, mi.Name, mi.Ip, path)))
|
m.MsgApp.CreateAndSend(la, ws.SuccessMsg("文件上传成功", fmt.Sprintf("[%s]文件已成功上传至 %s[%s:%s]", fileheader.Filename, mi.Name, mi.Ip, path)))
|
||||||
|
}
|
||||||
|
|
||||||
|
type FolderFile struct {
|
||||||
|
Dir string
|
||||||
|
Fileheader *multipart.FileHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MachineFile) UploadFolder(rc *req.Ctx) {
|
||||||
|
g := rc.GinCtx
|
||||||
|
fid := GetMachineFileId(g)
|
||||||
|
|
||||||
|
mf, err := g.MultipartForm()
|
||||||
|
biz.ErrIsNilAppendErr(err, "获取表单信息失败: %s")
|
||||||
|
basePath := mf.Value["basePath"][0]
|
||||||
|
biz.NotEmpty(basePath, "基础路径不能为空")
|
||||||
|
|
||||||
|
fileheaders := mf.File["files"]
|
||||||
|
biz.IsTrue(len(fileheaders) > 0, "文件不能为空")
|
||||||
|
allFileSize := collx.ArrayReduce(fileheaders, 0, func(i int64, fh *multipart.FileHeader) int64 {
|
||||||
|
return i + fh.Size
|
||||||
|
})
|
||||||
|
biz.IsTrue(allFileSize <= MaxUploadFileSize, "文件夹总大小不能超过%d字节", MaxUploadFileSize)
|
||||||
|
|
||||||
|
paths := mf.Value["paths"]
|
||||||
|
|
||||||
|
folderName := filepath.Dir(paths[0])
|
||||||
|
mi := m.MachineFileApp.GetMachine(fid)
|
||||||
|
rc.ReqParam = fmt.Sprintf("%s -> 上传文件夹: %s/%s", mi.GetLogDesc(), basePath, folderName)
|
||||||
|
|
||||||
|
folderFiles := make([]FolderFile, len(paths))
|
||||||
|
// 先创建目录,并将其包装为folderFile结构
|
||||||
|
mkdirs := make(map[string]bool, 0)
|
||||||
|
for i, path := range paths {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
// 目录已建,则无需重复建
|
||||||
|
if !mkdirs[dir] {
|
||||||
|
m.MachineFileApp.MkDir(fid, basePath+"/"+dir)
|
||||||
|
mkdirs[dir] = true
|
||||||
|
}
|
||||||
|
folderFiles[i] = FolderFile{
|
||||||
|
Dir: dir,
|
||||||
|
Fileheader: fileheaders[i],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组处理
|
||||||
|
groupNum := 10
|
||||||
|
chunks := collx.ArraySplit(folderFiles, groupNum)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
// 设置要等待的协程数量
|
||||||
|
wg.Add(len(chunks))
|
||||||
|
|
||||||
|
la := rc.LoginAccount
|
||||||
|
for _, chunk := range chunks {
|
||||||
|
go func(files []FolderFile, wg *sync.WaitGroup) {
|
||||||
|
defer func() {
|
||||||
|
// 协程执行完成后调用Done方法
|
||||||
|
wg.Done()
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
logx.Errorf("文件上传失败: %s", err)
|
||||||
|
switch t := err.(type) {
|
||||||
|
case biz.BizError:
|
||||||
|
m.MsgApp.CreateAndSend(la, ws.ErrMsg("文件上传失败", fmt.Sprintf("执行文件上传失败:\n<-e errCode: %d, errMsg: %s", t.Code(), t.Error())))
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
fileHeader := file.Fileheader
|
||||||
|
dir := file.Dir
|
||||||
|
file, _ := fileHeader.Open()
|
||||||
|
defer file.Close()
|
||||||
|
logx.Debugf("上传文件夹: dir=%s -> filename=%s", dir, fileHeader.Filename)
|
||||||
|
m.MachineFileApp.UploadFile(fid, basePath+"/"+dir, fileHeader.Filename, file)
|
||||||
|
}
|
||||||
|
}(chunk, &wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待所有协程执行完成
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// 保存消息并发送文件上传成功通知
|
||||||
|
m.MsgApp.CreateAndSend(rc.LoginAccount, ws.SuccessMsg("文件上传成功", fmt.Sprintf("[%s]文件夹已成功上传至 %s[%s:%s]", folderName, mi.Name, mi.Ip, basePath)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MachineFile) RemoveFile(rc *req.Ctx) {
|
func (m *MachineFile) RemoveFile(rc *req.Ctx) {
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func (m *machineFileAppImpl) MkDir(fid uint64, path string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sftpCli := m.getSftpCli(machineId)
|
sftpCli := m.getSftpCli(machineId)
|
||||||
err := sftpCli.Mkdir(path)
|
err := sftpCli.MkdirAll(path)
|
||||||
biz.ErrIsNilAppendErr(err, "创建目录失败: %s")
|
biz.ErrIsNilAppendErr(err, "创建目录失败: %s")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ func InitMachineFileRouter(router *gin.RouterGroup) {
|
|||||||
|
|
||||||
req.NewPost(":machineId/files/:fileId/upload", mf.UploadFile).Log(req.NewLogSave("机器-文件上传")).RequiredPermissionCode("machine:file:upload"),
|
req.NewPost(":machineId/files/:fileId/upload", mf.UploadFile).Log(req.NewLogSave("机器-文件上传")).RequiredPermissionCode("machine:file:upload"),
|
||||||
|
|
||||||
|
req.NewPost(":machineId/files/:fileId/upload-folder", mf.UploadFolder).Log(req.NewLogSave("机器-文件夹上传")).RequiredPermissionCode("machine:file:upload"),
|
||||||
|
|
||||||
req.NewPost(":machineId/files/:fileId/remove", mf.RemoveFile).Log(req.NewLogSave("机器-删除文件or文件夹")).RequiredPermissionCode("machine:file:rm"),
|
req.NewPost(":machineId/files/:fileId/remove", mf.RemoveFile).Log(req.NewLogSave("机器-删除文件or文件夹")).RequiredPermissionCode("machine:file:rm"),
|
||||||
|
|
||||||
req.NewPost(":machineId/files/:fileId/cp", mf.CopyFile).Log(req.NewLogSave("机器-拷贝文件")).RequiredPermissionCode("machine:file:rm"),
|
req.NewPost(":machineId/files/:fileId/cp", mf.CopyFile).Log(req.NewLogSave("机器-拷贝文件")).RequiredPermissionCode("machine:file:rm"),
|
||||||
|
|||||||
@@ -65,3 +65,52 @@ func ArrayMap[T any, K comparable](arr []T, mapFunc func(val T) K) []K {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将数组或切片按固定大小分割成小数组
|
||||||
|
func ArrayChunk[T any](arr []T, chunkSize int) [][]T {
|
||||||
|
var chunks [][]T
|
||||||
|
for i := 0; i < len(arr); i += chunkSize {
|
||||||
|
end := i + chunkSize
|
||||||
|
if end > len(arr) {
|
||||||
|
end = len(arr)
|
||||||
|
}
|
||||||
|
chunks = append(chunks, arr[i:end])
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将数组切割为指定个数的子数组,并尽可能均匀
|
||||||
|
func ArraySplit[T any](arr []T, numGroups int) [][]T {
|
||||||
|
if numGroups > len(arr) {
|
||||||
|
numGroups = len(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算每个子数组的大小
|
||||||
|
size := len(arr) / numGroups
|
||||||
|
remainder := len(arr) % numGroups
|
||||||
|
|
||||||
|
// 创建一个存放子数组的切片
|
||||||
|
subArrays := make([][]T, numGroups)
|
||||||
|
|
||||||
|
// 分割数组为子数组
|
||||||
|
start := 0
|
||||||
|
for i := range subArrays {
|
||||||
|
subSize := size
|
||||||
|
if i < remainder {
|
||||||
|
subSize++
|
||||||
|
}
|
||||||
|
subArrays[i] = arr[start : start+subSize]
|
||||||
|
start += subSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return subArrays
|
||||||
|
}
|
||||||
|
|
||||||
|
// reduce操作
|
||||||
|
func ArrayReduce[T any, V any](arr []T, initialValue V, reducer func(V, T) V) V {
|
||||||
|
value := initialValue
|
||||||
|
for _, a := range arr {
|
||||||
|
value = reducer(value, a)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,3 +15,23 @@ func TestArrayCompare(t *testing.T) {
|
|||||||
fmt.Println(del...)
|
fmt.Println(del...)
|
||||||
fmt.Println(unmodifier...)
|
fmt.Println(unmodifier...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestArrayChunk(t *testing.T) {
|
||||||
|
arr := []int{1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
||||||
|
res := ArrayChunk[int](arr, 3)
|
||||||
|
fmt.Println(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArraySplit(t *testing.T) {
|
||||||
|
// arr := []int{1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
||||||
|
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
|
||||||
|
// arr := []int{1, 2, 3}
|
||||||
|
res := ArraySplit(arr, 10)
|
||||||
|
fmt.Println(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArrayReduce(t *testing.T) {
|
||||||
|
arr := []int{1, 2, 3, 5}
|
||||||
|
res := ArrayReduce[int, int](arr, 0, func(i1, i2 int) int { return i1 + i2 })
|
||||||
|
fmt.Println(res)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user