mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-12-16 04:36:35 +08:00
feat: 机器文件支持文件夹上传&数据库列表组件拆分
This commit is contained in:
@@ -90,94 +90,7 @@
|
||||
</page-table>
|
||||
|
||||
<el-dialog width="80%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible">
|
||||
<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="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>
|
||||
<db-table-list :db-id="dbId" :db="db" :db-type="state.row.type" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||
@@ -227,64 +140,7 @@
|
||||
:close-on-click-modal="false"
|
||||
v-model="sqlExecLogDialog.visible"
|
||||
>
|
||||
<page-table
|
||||
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>
|
||||
<db-sql-exec-log :db-id="sqlExecLogDialog.dbId" :dbs="sqlExecLogDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog" :close-on-click-modal="false">
|
||||
@@ -308,26 +164,13 @@
|
||||
</el-dialog>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<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 { formatByteSize } from '@/common/utils/format';
|
||||
import { dbApi } from './api';
|
||||
import { DbSqlExecTypeEnum } from './enums';
|
||||
import SqlExecBox from './component/SqlExecBox';
|
||||
import config from '@/common/config';
|
||||
import { getSession } from '@/common/utils/storage';
|
||||
import { isTrue } from '@/common/assert';
|
||||
@@ -337,9 +180,10 @@ import TagInfo from '../component/TagInfo.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import DbSqlExecLog from './DbSqlExecLog.vue';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
const CreateTable = defineAsyncComponent(() => import('./CreateTable.vue'));
|
||||
const DbTableList = defineAsyncComponent(() => import('./table/DbTableList.vue'));
|
||||
|
||||
const perms = {
|
||||
base: 'db',
|
||||
@@ -364,7 +208,7 @@ const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(15
|
||||
const pageTableRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
row: {},
|
||||
row: {} as any,
|
||||
dbId: 0,
|
||||
db: '',
|
||||
tags: [],
|
||||
@@ -401,49 +245,15 @@ const state = reactive({
|
||||
},
|
||||
// sql执行记录弹框
|
||||
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: '',
|
||||
visible: false,
|
||||
data: [],
|
||||
total: 0,
|
||||
dbs: [],
|
||||
query: {
|
||||
dbId: 0,
|
||||
db: '',
|
||||
table: '',
|
||||
type: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
},
|
||||
rollbackSqlDialog: {
|
||||
visible: false,
|
||||
sql: '',
|
||||
dbId: 0,
|
||||
},
|
||||
chooseTableName: '',
|
||||
tableInfoDialog: {
|
||||
loading: false,
|
||||
visible: false,
|
||||
infos: [],
|
||||
tableNameSearch: '',
|
||||
tableCommentSearch: '',
|
||||
},
|
||||
|
||||
exportDialog: {
|
||||
visible: false,
|
||||
dbId: 0,
|
||||
@@ -453,38 +263,11 @@ const state = reactive({
|
||||
contents: [] as any,
|
||||
extName: '',
|
||||
},
|
||||
|
||||
columnDialog: {
|
||||
visible: false,
|
||||
columns: [],
|
||||
},
|
||||
indexDialog: {
|
||||
visible: false,
|
||||
indexs: [],
|
||||
},
|
||||
ddlDialog: {
|
||||
visible: false,
|
||||
ddl: '',
|
||||
},
|
||||
dbEditDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
title: '新增数据库',
|
||||
},
|
||||
tableCreateDialog: {
|
||||
title: '创建表',
|
||||
visible: false,
|
||||
activeName: '1',
|
||||
type: '',
|
||||
enableEditTypes: ['mysql'], // 支持"编辑表"的数据库类型
|
||||
data: {
|
||||
// 修改表时,传递修改数据
|
||||
edit: false,
|
||||
row: {},
|
||||
indexs: [],
|
||||
columns: [],
|
||||
},
|
||||
},
|
||||
filterDb: {
|
||||
param: '',
|
||||
cache: [],
|
||||
@@ -492,30 +275,8 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
dbId,
|
||||
db,
|
||||
tags,
|
||||
instances,
|
||||
selectionData,
|
||||
query,
|
||||
datas,
|
||||
total,
|
||||
infoDialog,
|
||||
showDumpInfo,
|
||||
dumpInfo,
|
||||
sqlExecLogDialog,
|
||||
rollbackSqlDialog,
|
||||
chooseTableName,
|
||||
tableInfoDialog,
|
||||
exportDialog,
|
||||
columnDialog,
|
||||
indexDialog,
|
||||
ddlDialog,
|
||||
dbEditDialog,
|
||||
tableCreateDialog,
|
||||
filterDb,
|
||||
} = toRefs(state);
|
||||
const { dbId, db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, tableInfoDialog, exportDialog, dbEditDialog, filterDb } =
|
||||
toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -524,26 +285,6 @@ onMounted(async () => {
|
||||
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 () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
@@ -619,51 +360,15 @@ const deleteDb = async () => {
|
||||
|
||||
const onShowSqlExec = async (row: any) => {
|
||||
state.sqlExecLogDialog.title = `${row.name}`;
|
||||
state.sqlExecLogDialog.query.dbId = row.id;
|
||||
state.sqlExecLogDialog.dbId = row.id;
|
||||
state.sqlExecLogDialog.dbs = row.database.split(' ');
|
||||
searchSqlExecLog();
|
||||
state.sqlExecLogDialog.visible = true;
|
||||
};
|
||||
|
||||
const onBeforeCloseSqlExecDialog = () => {
|
||||
state.sqlExecLogDialog.visible = false;
|
||||
state.sqlExecLogDialog.data = [];
|
||||
state.sqlExecLogDialog.dbs = [];
|
||||
state.sqlExecLogDialog.total = 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;
|
||||
state.sqlExecLogDialog.dbId = 0;
|
||||
};
|
||||
|
||||
const onDumpDbs = async (row: any) => {
|
||||
@@ -707,136 +412,16 @@ const dumpDbs = () => {
|
||||
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) => {
|
||||
state.tableInfoDialog.loading = true;
|
||||
state.dbId = row.id;
|
||||
state.row = row;
|
||||
state.db = db;
|
||||
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.row = row;
|
||||
state.db = db;
|
||||
} catch (e) {
|
||||
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 = () => {
|
||||
state.showDumpInfo = 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;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开编辑表
|
||||
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>
|
||||
<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 { TYPE_LIST, CHARACTER_SET_NAME_LIST, COLLATION_SUFFIX_LIST } from './service';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import SqlExecBox from './component/SqlExecBox';
|
||||
import SqlExecBox from '../component/SqlExecBox';
|
||||
|
||||
const props = defineProps({
|
||||
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="操作">
|
||||
<template #default="scope">
|
||||
<el-popconfirm title="确定终止该进程?" @confirm="confirmKillProcess(scope.row.pid)">
|
||||
<el-popconfirm title="确定终止该进程?" @confirm="confirmKillProcess(scope.row.pid)" width="160">
|
||||
<template #reference>
|
||||
<el-button v-auth="'machine:killprocess'" type="danger" icon="delete" size="small" plain>终止</el-button>
|
||||
</template>
|
||||
|
||||
@@ -29,19 +29,53 @@
|
||||
<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-upload
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
action=""
|
||||
:http-request="getUploadFile"
|
||||
:headers="{ token }"
|
||||
:show-file-list="false"
|
||||
name="file"
|
||||
class="machine-file-upload-exec"
|
||||
>
|
||||
<el-button v-auth="'machine:file:upload'" class="ml5" type="primary" circle size="small" icon="Upload" title="上传">
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<!-- 文件&文件夹上传 -->
|
||||
<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
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
action=""
|
||||
:http-request="getUploadFile"
|
||||
:headers="{ token }"
|
||||
:show-file-list="false"
|
||||
name="file"
|
||||
class="machine-file-upload-exec"
|
||||
>
|
||||
<el-link>文件</el-link>
|
||||
</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
|
||||
:disabled="state.selectionFiles.length == 0"
|
||||
@@ -234,7 +268,7 @@
|
||||
</template>
|
||||
|
||||
<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 { machineApi } from '../api';
|
||||
|
||||
@@ -252,6 +286,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const token = getSession('token');
|
||||
const folderUploadRef: any = ref();
|
||||
|
||||
const folderType = 'd';
|
||||
const fileType = '-';
|
||||
@@ -573,6 +608,48 @@ const downloadFile = (data: any) => {
|
||||
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) => {
|
||||
state.uploadProgressShow = true;
|
||||
let complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
|
||||
@@ -593,7 +670,7 @@ const getUploadFile = (content: any) => {
|
||||
headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
|
||||
onUploadProgress: onUploadProgress,
|
||||
baseURL: '',
|
||||
timeout: 60 * 60 * 1000,
|
||||
timeout: 3 * 60 * 60 * 1000,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('上传成功');
|
||||
|
||||
@@ -148,8 +148,8 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
|
||||
cli := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.LoginAccount.Id, cli.GetMachine().TagPath), "%s")
|
||||
|
||||
_, err := cli.Run("sudo kill -9 " + pid)
|
||||
biz.ErrIsNilAppendErr(err, "终止进程失败: %s")
|
||||
res, err := cli.Run("sudo kill -9 " + pid)
|
||||
biz.ErrIsNil(err, "终止进程失败: %s", res)
|
||||
}
|
||||
|
||||
func (m *Machine) WsSSH(g *gin.Context) {
|
||||
|
||||
@@ -11,13 +11,18 @@ import (
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/jsonx"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"mayfly-go/pkg/ws"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
const MaxUploadFileSize int64 = 1024 * 1024 * 1024
|
||||
|
||||
func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
fid := GetMachineFileId(g)
|
||||
@@ -165,6 +172,7 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
||||
|
||||
fileheader, err := g.FormFile("file")
|
||||
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
|
||||
biz.IsTrue(fileheader.Size <= MaxUploadFileSize, "文件大小不能超过%d字节", MaxUploadFileSize)
|
||||
|
||||
file, _ := fileheader.Open()
|
||||
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)
|
||||
|
||||
la := rc.LoginAccount
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
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())))
|
||||
}
|
||||
defer func() {
|
||||
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())))
|
||||
}
|
||||
}()
|
||||
defer file.Close()
|
||||
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)))
|
||||
}
|
||||
}()
|
||||
|
||||
defer file.Close()
|
||||
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)))
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -158,7 +158,7 @@ func (m *machineFileAppImpl) MkDir(fid uint64, path string) {
|
||||
}
|
||||
|
||||
sftpCli := m.getSftpCli(machineId)
|
||||
err := sftpCli.Mkdir(path)
|
||||
err := sftpCli.MkdirAll(path)
|
||||
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-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/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
|
||||
}
|
||||
|
||||
// 将数组或切片按固定大小分割成小数组
|
||||
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(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