feat: 机器文件支持文件夹上传&数据库列表组件拆分

This commit is contained in:
meilin.huang
2023-09-08 22:24:45 +08:00
parent d7a10d4032
commit 08c381fa60
14 changed files with 810 additions and 551 deletions

View File

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

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

View File

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

View File

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

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

View File

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

View File

@@ -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('上传成功');

View File

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

View File

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

View File

@@ -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")
}

View File

@@ -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"),

View File

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

View File

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