mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +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: '',
 | 
			
		||||
    },
 | 
			
		||||
    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.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 });
 | 
			
		||||
    state.tableInfoDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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,6 +29,22 @@
 | 
			
		||||
                                <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-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"
 | 
			
		||||
@@ -39,9 +55,27 @@
 | 
			
		||||
                                                    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-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 {
 | 
			
		||||
			logx.Errorf("文件上传失败: %s", err)
 | 
			
		||||
			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())))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	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