mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	refactor: 机器文件操作界面重构
This commit is contained in:
		@@ -81,11 +81,11 @@
 | 
			
		||||
            <template #more="{ data }">
 | 
			
		||||
                <el-button @click="showInfo(data)" link>详情</el-button>
 | 
			
		||||
                <el-button class="ml5" type="primary" @click="onShowSqlExec(data)" link>SQL执行记录</el-button>
 | 
			
		||||
                <el-button v-if="data.type=='mysql'" class="ml5" type="primary" @click="onDumpDbs(data)" link>导出</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>编辑</el-button>
 | 
			
		||||
                <el-button v-if="data.type == 'mysql'" class="ml5" type="primary" @click="onDumpDbs(data)" link>导出</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
 | 
			
		||||
@@ -183,15 +183,15 @@
 | 
			
		||||
        <el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
 | 
			
		||||
            <el-row justify="space-between">
 | 
			
		||||
                <el-col :span="9">
 | 
			
		||||
                    <el-form-item label="导出内容: " size="small">
 | 
			
		||||
                        <el-checkbox-group v-model="exportDialog.contents" :min=1>
 | 
			
		||||
                    <el-form-item label="导出内容: ">
 | 
			
		||||
                        <el-checkbox-group v-model="exportDialog.contents" :min="1">
 | 
			
		||||
                            <el-checkbox label="结构" />
 | 
			
		||||
                            <el-checkbox label="数据" />
 | 
			
		||||
                        </el-checkbox-group>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
                </el-col>
 | 
			
		||||
                <el-col :span="9" >
 | 
			
		||||
                    <el-form-item label="扩展名: " size="small">
 | 
			
		||||
                <el-col :span="9">
 | 
			
		||||
                    <el-form-item label="扩展名: ">
 | 
			
		||||
                        <el-radio-group v-model="exportDialog.extName">
 | 
			
		||||
                            <el-radio label="sql" />
 | 
			
		||||
                            <el-radio label="gz" />
 | 
			
		||||
@@ -201,14 +201,23 @@
 | 
			
		||||
            </el-row>
 | 
			
		||||
 | 
			
		||||
            <el-form-item>
 | 
			
		||||
                <el-transfer :titles="['全部数据库', '导出数据库']"  max-height="300" size="small" v-model="exportDialog.value" :data="exportDialog.data">
 | 
			
		||||
                <el-transfer
 | 
			
		||||
                    :titles="['全部数据库', '导出数据库']"
 | 
			
		||||
                    max-height="300"
 | 
			
		||||
                    v-model="exportDialog.value"
 | 
			
		||||
                    :data="exportDialog.data"
 | 
			
		||||
                    filterable
 | 
			
		||||
                    filter-placeholder="按数据库名称筛选"
 | 
			
		||||
                >
 | 
			
		||||
                </el-transfer>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
 | 
			
		||||
            <div style="text-align: right">
 | 
			
		||||
                <el-button @click="exportDialog.visible = false" size="small">取消</el-button>
 | 
			
		||||
                <el-button @click="dumpDbs()" type="success" size="small">确定</el-button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div class="dialog-footer">
 | 
			
		||||
                    <el-button @click="exportDialog.visible = false">取消</el-button>
 | 
			
		||||
                    <el-button @click="dumpDbs()" type="primary">确定</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog
 | 
			
		||||
@@ -349,7 +358,7 @@ const columns = ref([
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限
 | 
			
		||||
const actionBtns = hasPerms([perms.saveDb]);
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(65).fixedRight().alignCenter();
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(150).fixedRight().alignCenter();
 | 
			
		||||
 | 
			
		||||
const pageTableRef: any = ref(null);
 | 
			
		||||
 | 
			
		||||
@@ -438,9 +447,9 @@ const state = reactive({
 | 
			
		||||
        visible: false,
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
        type: 3,
 | 
			
		||||
        data: [],
 | 
			
		||||
        data: [] as any,
 | 
			
		||||
        value: [],
 | 
			
		||||
        contents: [],
 | 
			
		||||
        contents: [] as any,
 | 
			
		||||
        extName: '',
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@@ -658,18 +667,18 @@ const dump = (db: string) => {
 | 
			
		||||
 | 
			
		||||
const onDumpDbs = async (row: any) => {
 | 
			
		||||
    const dbs = row.database.split(' ');
 | 
			
		||||
    const data = []
 | 
			
		||||
    const data = [];
 | 
			
		||||
    for (let name of dbs) {
 | 
			
		||||
        data.push({
 | 
			
		||||
            key: name,
 | 
			
		||||
            label: name,
 | 
			
		||||
        })
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    state.exportDialog.value = []
 | 
			
		||||
    state.exportDialog.data = data
 | 
			
		||||
    state.exportDialog.value = [];
 | 
			
		||||
    state.exportDialog.data = data;
 | 
			
		||||
    state.exportDialog.dbId = row.id;
 | 
			
		||||
    state.exportDialog.contents = ["结构", "数据"]
 | 
			
		||||
    state.exportDialog.extName = "sql"
 | 
			
		||||
    state.exportDialog.contents = ['结构', '数据'];
 | 
			
		||||
    state.exportDialog.extName = 'sql';
 | 
			
		||||
    state.exportDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -679,19 +688,19 @@ const onDumpDbs = async (row: any) => {
 | 
			
		||||
const dumpDbs = () => {
 | 
			
		||||
    isTrue(state.exportDialog.value.length > 0, '请添加要导出的数据库');
 | 
			
		||||
    const a = document.createElement('a');
 | 
			
		||||
    let type = 0
 | 
			
		||||
    let type = 0;
 | 
			
		||||
    for (let c of state.exportDialog.contents) {
 | 
			
		||||
        if (c == "结构") {
 | 
			
		||||
            type += 1
 | 
			
		||||
        } else if (c == "数据") {
 | 
			
		||||
            type += 2
 | 
			
		||||
        if (c == '结构') {
 | 
			
		||||
            type += 1;
 | 
			
		||||
        } else if (c == '数据') {
 | 
			
		||||
            type += 2;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    a.setAttribute(
 | 
			
		||||
        'href',
 | 
			
		||||
        `${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${state.exportDialog.extName}&token=${getSession(
 | 
			
		||||
            'token'
 | 
			
		||||
        )}`
 | 
			
		||||
        `${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${
 | 
			
		||||
            state.exportDialog.extName
 | 
			
		||||
        }&token=${getSession('token')}`
 | 
			
		||||
    );
 | 
			
		||||
    a.click();
 | 
			
		||||
    state.exportDialog.visible = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,631 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="file-manage">
 | 
			
		||||
        <el-dialog :title="title" v-model="dialogVisible" :show-close="true" :before-close="handleClose" width="50%">
 | 
			
		||||
            <div class="toolbar">
 | 
			
		||||
                <div style="float: right">
 | 
			
		||||
                    <el-button v-auth="'machine:file:add'" type="primary" @click="add" icon="plus" plain>添加 </el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <el-table :data="fileTable" stripe style="width: 100%" v-loading="loading">
 | 
			
		||||
                <el-table-column prop="name" label="名称" min-width="70px">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-input v-model="scope.row.name" :disabled="scope.row.id != null" clearable> </el-input>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column prop="name" label="类型" width="130px">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-select :disabled="scope.row.id != null" v-model="scope.row.type" style="width: 100px" placeholder="请选择">
 | 
			
		||||
                            <el-option v-for="item in FileTypeEnum as any" :key="item.value" :label="item.label" :value="item.value"></el-option>
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip>
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column label="操作" min-wdith="180px">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
 | 
			
		||||
                        <el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>
 | 
			
		||||
                        <el-button v-auth="'machine:file:del'" type="danger" @click="deleteRow(scope.$index, scope.row)" icon="delete" plain></el-button>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
            </el-table>
 | 
			
		||||
            <el-row style="margin-top: 10px" type="flex" justify="end">
 | 
			
		||||
                <el-pagination
 | 
			
		||||
                    style="text-align: center"
 | 
			
		||||
                    :total="total"
 | 
			
		||||
                    layout="prev, pager, next, total, jumper"
 | 
			
		||||
                    v-model:current-page="query.pageNum"
 | 
			
		||||
                    :page-size="query.pageSize"
 | 
			
		||||
                    @current-change="handlePageChange"
 | 
			
		||||
                >
 | 
			
		||||
                </el-pagination>
 | 
			
		||||
            </el-row>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="70%">
 | 
			
		||||
            <el-progress v-if="uploadProgressShow" style="width: 90%; margin-left: 20px" :text-inside="true" :stroke-width="20" :percentage="progressNum" />
 | 
			
		||||
            <div style="height: 55vh; overflow: auto">
 | 
			
		||||
                <el-tree
 | 
			
		||||
                    v-if="tree.visible"
 | 
			
		||||
                    ref="fileTree"
 | 
			
		||||
                    :highlight-current="true"
 | 
			
		||||
                    :load="loadNode"
 | 
			
		||||
                    :props="treeProps"
 | 
			
		||||
                    lazy
 | 
			
		||||
                    node-key="id"
 | 
			
		||||
                    :expand-on-click-node="false"
 | 
			
		||||
                >
 | 
			
		||||
                    <template #default="{ node, data }">
 | 
			
		||||
                        <span class="custom-tree-node">
 | 
			
		||||
                            <el-dropdown size="small" @visible-change="getFilePath(data, $event)" trigger="contextmenu">
 | 
			
		||||
                                <span class="el-dropdown-link">
 | 
			
		||||
                                    <span v-if="data.type == 'd' && !node.expanded">
 | 
			
		||||
                                        <SvgIcon :size="15" name="folder" />
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                    <span v-if="data.type == 'd' && node.expanded">
 | 
			
		||||
                                        <SvgIcon :size="15" name="folder-opened" />
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                    <span v-if="data.type == '-'">
 | 
			
		||||
                                        <SvgIcon :size="15" name="document" />
 | 
			
		||||
                                    </span>
 | 
			
		||||
 | 
			
		||||
                                    <span class="ml5" style="font-weight: bold">
 | 
			
		||||
                                        {{ node.label }}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </span>
 | 
			
		||||
 | 
			
		||||
                                <template #dropdown>
 | 
			
		||||
                                    <el-dropdown-menu>
 | 
			
		||||
                                        <el-dropdown-item
 | 
			
		||||
                                            @click="getFileContent(tree.folder.id, data.path)"
 | 
			
		||||
                                            v-if="data.type == '-' && data.size < 1 * 1024 * 1024"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <el-link type="info" icon="view" :underline="false">查看</el-link>
 | 
			
		||||
                                        </el-dropdown-item>
 | 
			
		||||
 | 
			
		||||
                                        <span v-auth="'machine:file:write'">
 | 
			
		||||
                                            <el-dropdown-item @click="showCreateFileDialog(node)" v-if="data.type == 'd'">
 | 
			
		||||
                                                <el-link type="primary" icon="document" :underline="false" style="margin-left: 2px">新建</el-link>
 | 
			
		||||
                                            </el-dropdown-item>
 | 
			
		||||
                                        </span>
 | 
			
		||||
 | 
			
		||||
                                        <span v-auth="'machine:file:upload'">
 | 
			
		||||
                                            <el-dropdown-item v-if="data.type == 'd'">
 | 
			
		||||
                                                <el-upload
 | 
			
		||||
                                                    :before-upload="beforeUpload"
 | 
			
		||||
                                                    :on-success="uploadSuccess"
 | 
			
		||||
                                                    action=""
 | 
			
		||||
                                                    :http-request="getUploadFile"
 | 
			
		||||
                                                    :headers="{ token }"
 | 
			
		||||
                                                    :show-file-list="false"
 | 
			
		||||
                                                    name="file"
 | 
			
		||||
                                                    style="display: inline-block; margin-left: 2px"
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    <el-link icon="upload" :underline="false">上传</el-link>
 | 
			
		||||
                                                </el-upload>
 | 
			
		||||
                                            </el-dropdown-item>
 | 
			
		||||
                                        </span>
 | 
			
		||||
 | 
			
		||||
                                        <span v-auth="'machine:file:write'">
 | 
			
		||||
                                            <el-dropdown-item @click="downloadFile(node, data)" v-if="data.type == '-'">
 | 
			
		||||
                                                <el-link type="primary" icon="download" :underline="false" style="margin-left: 2px">下载</el-link>
 | 
			
		||||
                                            </el-dropdown-item>
 | 
			
		||||
                                        </span>
 | 
			
		||||
 | 
			
		||||
                                        <span v-auth="'machine:file:rm'">
 | 
			
		||||
                                            <el-dropdown-item @click="deleteFile(node, data)" v-if="!dontOperate(data)">
 | 
			
		||||
                                                <el-link type="danger" icon="delete" :underline="false" style="margin-left: 2px">删除</el-link>
 | 
			
		||||
                                            </el-dropdown-item>
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </el-dropdown-menu>
 | 
			
		||||
                                </template>
 | 
			
		||||
                            </el-dropdown>
 | 
			
		||||
                            <span style="display: inline-block" class="ml15">
 | 
			
		||||
                                <span style="color: #67c23a; font-weight: bold" v-if="data.type == '-'"> [{{ formatFileSize(data.size) }}] </span>
 | 
			
		||||
                                <span style="color: #67c23a; font-weight: bold" v-if="data.type == 'd' && data.dirSize"> [{{ data.dirSize }}] </span>
 | 
			
		||||
                                <span style="color: #67c23a; font-weight: bold" v-if="data.type == 'd' && !data.dirSize">
 | 
			
		||||
                                    [<el-button @click="getDirSize(data)" type="primary" link :loading="data.loadingDirSize">size</el-button>]
 | 
			
		||||
                                </span>
 | 
			
		||||
 | 
			
		||||
                                <el-popover placement="top-start" :title="`${data.path}-文件详情`" :width="520" trigger="click" @show="showFileStat(data)">
 | 
			
		||||
                                    <template #reference>
 | 
			
		||||
                                        <span style="color: #67c23a; font-weight: bold">
 | 
			
		||||
                                            [<el-button @click="showFileStat(data)" type="primary" link :loading="data.loadingStat">stat</el-button>]
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </template>
 | 
			
		||||
                                    <el-input :input-style="{ color: 'black' }" disabled autosize v-model="data.stat" type="textarea" />
 | 
			
		||||
                                </el-popover>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-tree>
 | 
			
		||||
            </div>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog
 | 
			
		||||
            :destroy-on-close="true"
 | 
			
		||||
            title="新建文件"
 | 
			
		||||
            v-model="createFileDialog.visible"
 | 
			
		||||
            :before-close="closeCreateFileDialog"
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            top="5vh"
 | 
			
		||||
            width="400px"
 | 
			
		||||
        >
 | 
			
		||||
            <div>
 | 
			
		||||
                <el-form-item prop="name" label="名称:">
 | 
			
		||||
                    <el-input v-model.trim="createFileDialog.name" placeholder="请输入名称" auto-complete="off"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <el-form-item prop="type" label="类型:">
 | 
			
		||||
                    <el-radio-group v-model="createFileDialog.type">
 | 
			
		||||
                        <el-radio label="d" size="small">文件夹</el-radio>
 | 
			
		||||
                        <el-radio label="-" size="small">文件</el-radio>
 | 
			
		||||
                    </el-radio-group>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <el-button @click="closeCreateFileDialog">关闭</el-button>
 | 
			
		||||
                    <el-button v-auth="'machine:file:write'" type="primary" @click="createFile">确定</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog
 | 
			
		||||
            :destroy-on-close="true"
 | 
			
		||||
            :title="fileContent.dialogTitle"
 | 
			
		||||
            v-model="fileContent.contentVisible"
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            top="5vh"
 | 
			
		||||
            width="70%"
 | 
			
		||||
        >
 | 
			
		||||
            <div>
 | 
			
		||||
                <monaco-editor :can-change-mode="true" v-model="fileContent.content" :language="fileContent.type" />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div class="dialog-footer">
 | 
			
		||||
                    <el-button @click="fileContent.contentVisible = false">关 闭</el-button>
 | 
			
		||||
                    <el-button v-auth="'machine:file:write'" type="primary" @click="updateContent">保 存</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, toRefs, reactive, watch } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { machineApi } from './api';
 | 
			
		||||
 | 
			
		||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { getSession } from '@/common/utils/storage';
 | 
			
		||||
import { FileTypeEnum } from './enums';
 | 
			
		||||
import config from '@/common/config';
 | 
			
		||||
import { isTrue } from '@/common/assert';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: { type: Boolean },
 | 
			
		||||
    machineId: { type: Number },
 | 
			
		||||
    title: { type: String },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
 | 
			
		||||
 | 
			
		||||
const treeProps = {
 | 
			
		||||
    label: 'name',
 | 
			
		||||
    children: 'zones',
 | 
			
		||||
    isLeaf: 'leaf',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addFile = machineApi.addConf;
 | 
			
		||||
const delFile = machineApi.delConf;
 | 
			
		||||
const updateFileContent = machineApi.updateFileContent;
 | 
			
		||||
const files = machineApi.files;
 | 
			
		||||
const fileTree: any = ref(null);
 | 
			
		||||
const token = getSession('token');
 | 
			
		||||
 | 
			
		||||
const folderType = 'd';
 | 
			
		||||
const fileType = '-';
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    query: {
 | 
			
		||||
        id: 0,
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 8,
 | 
			
		||||
    },
 | 
			
		||||
    loading: false,
 | 
			
		||||
    form: {
 | 
			
		||||
        id: null,
 | 
			
		||||
        type: null,
 | 
			
		||||
        name: '',
 | 
			
		||||
        remark: '',
 | 
			
		||||
    },
 | 
			
		||||
    total: 0,
 | 
			
		||||
    fileTable: [] as any,
 | 
			
		||||
    btnLoading: false,
 | 
			
		||||
    fileContent: {
 | 
			
		||||
        fileId: 0,
 | 
			
		||||
        content: '',
 | 
			
		||||
        contentVisible: false,
 | 
			
		||||
        dialogTitle: '',
 | 
			
		||||
        path: '',
 | 
			
		||||
        type: 'shell',
 | 
			
		||||
    },
 | 
			
		||||
    tree: {
 | 
			
		||||
        title: '',
 | 
			
		||||
        visible: false,
 | 
			
		||||
        folder: { id: 0 },
 | 
			
		||||
        node: {
 | 
			
		||||
            childNodes: [],
 | 
			
		||||
        },
 | 
			
		||||
        resolve: {},
 | 
			
		||||
    },
 | 
			
		||||
    dataObj: {
 | 
			
		||||
        name: '',
 | 
			
		||||
        path: '',
 | 
			
		||||
        type: '',
 | 
			
		||||
    },
 | 
			
		||||
    progressNum: 0,
 | 
			
		||||
    uploadProgressShow: false,
 | 
			
		||||
    createFileDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        name: '',
 | 
			
		||||
        type: folderType,
 | 
			
		||||
        node: null as any,
 | 
			
		||||
    },
 | 
			
		||||
    file: null as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, loading, query, total, fileTable, fileContent, tree, progressNum, uploadProgressShow, createFileDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watch(props, async (newValue) => {
 | 
			
		||||
    state.dialogVisible = newValue.visible;
 | 
			
		||||
    if (newValue.machineId && newValue.visible) {
 | 
			
		||||
        await getFiles();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getFiles = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        state.loading = true;
 | 
			
		||||
        state.query.id = props.machineId as any;
 | 
			
		||||
        const res = await files.request(state.query);
 | 
			
		||||
        state.fileTable = res.list || [];
 | 
			
		||||
        state.total = res.total;
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.loading = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handlePageChange = (curPage: number) => {
 | 
			
		||||
    state.query.pageNum = curPage;
 | 
			
		||||
    getFiles();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const add = () => {
 | 
			
		||||
    // 往数组头部添加元素
 | 
			
		||||
    state.fileTable = [{}].concat(state.fileTable);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addFiles = async (row: any) => {
 | 
			
		||||
    row.machineId = props.machineId;
 | 
			
		||||
    await addFile.request(row);
 | 
			
		||||
    ElMessage.success('添加成功');
 | 
			
		||||
    getFiles();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteRow = (idx: any, row: any) => {
 | 
			
		||||
    if (row.id) {
 | 
			
		||||
        ElMessageBox.confirm(`此操作将删除 [${row.name}], 是否继续?`, '提示', {
 | 
			
		||||
            confirmButtonText: '确定',
 | 
			
		||||
            cancelButtonText: '取消',
 | 
			
		||||
            type: 'warning',
 | 
			
		||||
        }).then(() => {
 | 
			
		||||
            // 删除配置文件
 | 
			
		||||
            delFile
 | 
			
		||||
                .request({
 | 
			
		||||
                    machineId: props.machineId,
 | 
			
		||||
                    id: row.id,
 | 
			
		||||
                })
 | 
			
		||||
                .then(() => {
 | 
			
		||||
                    getFiles();
 | 
			
		||||
                });
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
        state.fileTable.splice(idx, 1);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getConf = (row: any) => {
 | 
			
		||||
    if (row.type == 1) {
 | 
			
		||||
        state.tree.folder = row;
 | 
			
		||||
        state.tree.title = row.name;
 | 
			
		||||
        loadNode(state.tree.node, state.tree.resolve);
 | 
			
		||||
        state.tree.visible = true;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    getFileContent(row.id, row.path);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFileContent = async (fileId: number, path: string) => {
 | 
			
		||||
    const res = await machineApi.fileContent.request({
 | 
			
		||||
        fileId,
 | 
			
		||||
        path,
 | 
			
		||||
        machineId: props.machineId,
 | 
			
		||||
    });
 | 
			
		||||
    state.fileContent.content = res;
 | 
			
		||||
    state.fileContent.fileId = fileId;
 | 
			
		||||
    state.fileContent.dialogTitle = path;
 | 
			
		||||
    state.fileContent.path = path;
 | 
			
		||||
    state.fileContent.type = getFileType(path);
 | 
			
		||||
    state.fileContent.contentVisible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFileType = (path: string) => {
 | 
			
		||||
    if (path.endsWith('.sh')) {
 | 
			
		||||
        return 'shell';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('js')) {
 | 
			
		||||
        return 'javascript';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('json')) {
 | 
			
		||||
        return 'json';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('Dockerfile')) {
 | 
			
		||||
        return 'dockerfile';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('nginx.conf')) {
 | 
			
		||||
        return 'shell';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('sql')) {
 | 
			
		||||
        return 'sql';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('yaml') || path.endsWith('yml')) {
 | 
			
		||||
        return 'yaml';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('xml') || path.endsWith('html')) {
 | 
			
		||||
        return 'html';
 | 
			
		||||
    }
 | 
			
		||||
    return 'text';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateContent = async () => {
 | 
			
		||||
    await updateFileContent.request({
 | 
			
		||||
        content: state.fileContent.content,
 | 
			
		||||
        id: state.fileContent.fileId,
 | 
			
		||||
        path: state.fileContent.path,
 | 
			
		||||
        machineId: props.machineId,
 | 
			
		||||
    });
 | 
			
		||||
    ElMessage.success('修改成功');
 | 
			
		||||
    state.fileContent.contentVisible = false;
 | 
			
		||||
    state.fileContent.content = '';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 关闭取消按钮触发的事件
 | 
			
		||||
 */
 | 
			
		||||
const handleClose = () => {
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
    emit('update:machineId', null);
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
    state.fileTable = [];
 | 
			
		||||
    state.tree.folder = { id: 0 };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 加载文件树节点
 | 
			
		||||
 * @param {Object} node
 | 
			
		||||
 * @param {Object} resolve
 | 
			
		||||
 */
 | 
			
		||||
const loadNode = async (node: any, resolve: any) => {
 | 
			
		||||
    if (typeof resolve !== 'function') {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const folder: any = state.tree.folder;
 | 
			
		||||
    if (node.level === 0) {
 | 
			
		||||
        state.tree.node = node;
 | 
			
		||||
        state.tree.resolve = resolve;
 | 
			
		||||
 | 
			
		||||
        // let folder: any = this.tree.folder
 | 
			
		||||
        const path = folder ? folder.path : '/';
 | 
			
		||||
        return resolve([
 | 
			
		||||
            {
 | 
			
		||||
                name: path,
 | 
			
		||||
                type: folderType,
 | 
			
		||||
                path: path,
 | 
			
		||||
            },
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let path;
 | 
			
		||||
    const data = node.data;
 | 
			
		||||
    // 只有在第一级节点时,name==path,即上述level==0时设置的
 | 
			
		||||
    if (!data || data.name == data.path) {
 | 
			
		||||
        path = folder.path;
 | 
			
		||||
    } else {
 | 
			
		||||
        path = data.path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const res = await machineApi.lsFile.request({
 | 
			
		||||
        fileId: folder.id,
 | 
			
		||||
        machineId: props.machineId,
 | 
			
		||||
        path,
 | 
			
		||||
    });
 | 
			
		||||
    for (const file of res) {
 | 
			
		||||
        const type = file.type;
 | 
			
		||||
        if (type == fileType) {
 | 
			
		||||
            file.leaf = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return resolve(res);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getDirSize = async (data: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
        data.loadingDirSize = true;
 | 
			
		||||
        const res = await machineApi.dirSize.request({
 | 
			
		||||
            machineId: props.machineId,
 | 
			
		||||
            fileId: state.tree.folder.id,
 | 
			
		||||
            path: data.path,
 | 
			
		||||
        });
 | 
			
		||||
        data.dirSize = res;
 | 
			
		||||
    } finally {
 | 
			
		||||
        data.loadingDirSize = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showFileStat = async (data: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
        if (data.stat) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        data.loadingStat = true;
 | 
			
		||||
        const res = await machineApi.fileStat.request({
 | 
			
		||||
            machineId: props.machineId,
 | 
			
		||||
            fileId: state.tree.folder.id,
 | 
			
		||||
            path: data.path,
 | 
			
		||||
        });
 | 
			
		||||
        data.stat = res;
 | 
			
		||||
    } finally {
 | 
			
		||||
        data.loadingStat = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showCreateFileDialog = (node: any) => {
 | 
			
		||||
    isTrue(node.expanded, '请先点击展开该节点后再创建');
 | 
			
		||||
    state.createFileDialog.node = node;
 | 
			
		||||
    state.createFileDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createFile = async () => {
 | 
			
		||||
    const node = state.createFileDialog.node;
 | 
			
		||||
    const name = state.createFileDialog.name;
 | 
			
		||||
    const type = state.createFileDialog.type;
 | 
			
		||||
    const path = node.data.path + '/' + name;
 | 
			
		||||
    await machineApi.createFile.request({
 | 
			
		||||
        machineId: props.machineId,
 | 
			
		||||
        id: state.tree.folder.id,
 | 
			
		||||
        path,
 | 
			
		||||
        type,
 | 
			
		||||
    });
 | 
			
		||||
    fileTree.value.append({ name: name, path: path, type: type, leaf: type === fileType, size: 0 }, node);
 | 
			
		||||
    closeCreateFileDialog();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeCreateFileDialog = () => {
 | 
			
		||||
    state.createFileDialog.visible = false;
 | 
			
		||||
    state.createFileDialog.node = null;
 | 
			
		||||
    state.createFileDialog.name = '';
 | 
			
		||||
    state.createFileDialog.type = folderType;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteFile = (node: any, data: any) => {
 | 
			
		||||
    const file = data.path;
 | 
			
		||||
    ElMessageBox.confirm(`此操作将删除 [${file}], 是否继续?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
            machineApi.rmFile
 | 
			
		||||
                .request({
 | 
			
		||||
                    fileId: state.tree.folder.id,
 | 
			
		||||
                    path: file,
 | 
			
		||||
                    machineId: props.machineId,
 | 
			
		||||
                })
 | 
			
		||||
                .then(() => {
 | 
			
		||||
                    ElMessage.success('删除成功');
 | 
			
		||||
                    fileTree.value.remove(node);
 | 
			
		||||
                });
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
            // skip
 | 
			
		||||
        });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const downloadFile = (node: any, data: any) => {
 | 
			
		||||
    const a = document.createElement('a');
 | 
			
		||||
    a.setAttribute('href', `${config.baseApiUrl}/machines/${props.machineId}/files/${state.tree.folder.id}/read?type=1&path=${data.path}&token=${token}`);
 | 
			
		||||
    a.click();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onUploadProgress = (progressEvent: any) => {
 | 
			
		||||
    state.uploadProgressShow = true;
 | 
			
		||||
    let complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
 | 
			
		||||
    state.progressNum = complete;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getUploadFile = (content: any) => {
 | 
			
		||||
    const params = new FormData();
 | 
			
		||||
    params.append('file', content.file);
 | 
			
		||||
    params.append('path', state.dataObj.path);
 | 
			
		||||
    params.append('machineId', props.machineId as any);
 | 
			
		||||
    params.append('fileId', state.tree.folder.id as any);
 | 
			
		||||
    params.append('token', token);
 | 
			
		||||
    machineApi.uploadFile
 | 
			
		||||
        .request(params, {
 | 
			
		||||
            url: `${config.baseApiUrl}/machines/${props.machineId}/files/${state.tree.folder.id}/upload?token=${token}`,
 | 
			
		||||
            headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
 | 
			
		||||
            onUploadProgress: onUploadProgress,
 | 
			
		||||
            baseURL: '',
 | 
			
		||||
            timeout: 60 * 60 * 1000,
 | 
			
		||||
        })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
            ElMessage.success('上传成功');
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                state.uploadProgressShow = false;
 | 
			
		||||
            }, 3000);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
            state.uploadProgressShow = false;
 | 
			
		||||
        });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const uploadSuccess = (res: any) => {
 | 
			
		||||
    if (res.code !== 200) {
 | 
			
		||||
        ElMessage.error(res.msg);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const beforeUpload = (file: File) => {
 | 
			
		||||
    state.file = file;
 | 
			
		||||
};
 | 
			
		||||
const getFilePath = (data: object, visible: boolean) => {
 | 
			
		||||
    if (visible) {
 | 
			
		||||
        state.dataObj = data as any;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
const dontOperate = (data: any) => {
 | 
			
		||||
    const path = data.path;
 | 
			
		||||
    const ls = ['/', '//', '/usr', '/usr/', '/usr/bin', '/opt', '/run', '/etc', '/proc', '/var', '/mnt', '/boot', '/dev', '/home', '/media', '/root'];
 | 
			
		||||
    return ls.indexOf(path) != -1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 格式化文件大小
 | 
			
		||||
 * @param {*} value
 | 
			
		||||
 */
 | 
			
		||||
const formatFileSize = (size: any) => {
 | 
			
		||||
    const value = Number(size);
 | 
			
		||||
    if (size && !isNaN(value)) {
 | 
			
		||||
        const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
 | 
			
		||||
        let index = 0;
 | 
			
		||||
        let k = value;
 | 
			
		||||
        if (value >= 1024) {
 | 
			
		||||
            while (k > 1024) {
 | 
			
		||||
                k = k / 1024;
 | 
			
		||||
                index++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return `${k.toFixed(2)}${units[index]}`;
 | 
			
		||||
    }
 | 
			
		||||
    return '-';
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -150,7 +150,7 @@
 | 
			
		||||
 | 
			
		||||
        <script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
 | 
			
		||||
 | 
			
		||||
        <file-manage :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
 | 
			
		||||
        <file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
 | 
			
		||||
 | 
			
		||||
        <machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title"></machine-stats>
 | 
			
		||||
 | 
			
		||||
@@ -173,7 +173,7 @@ import { hasPerms } from '@/components/auth/auth';
 | 
			
		||||
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
 | 
			
		||||
const MachineEdit = defineAsyncComponent(() => import('./MachineEdit.vue'));
 | 
			
		||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
 | 
			
		||||
const FileManage = defineAsyncComponent(() => import('./FileManage.vue'));
 | 
			
		||||
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
 | 
			
		||||
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
 | 
			
		||||
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
 | 
			
		||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										211
									
								
								mayfly_go_web/src/views/ops/machine/file/FileConfList.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										211
									
								
								mayfly_go_web/src/views/ops/machine/file/FileConfList.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,211 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="file-manage">
 | 
			
		||||
        <el-dialog :title="title" v-model="dialogVisible" :show-close="true" :before-close="handleClose" width="50%">
 | 
			
		||||
            <el-table :data="fileTable" stripe style="width: 100%" v-loading="loading">
 | 
			
		||||
                <el-table-column prop="name" label="名称" min-width="70px">
 | 
			
		||||
                    <template #header>
 | 
			
		||||
                        <el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="add()"> </el-button>
 | 
			
		||||
                        <span class="ml10">名称</span>
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-input v-model="scope.row.name" :disabled="scope.row.id != null" clearable> </el-input>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column prop="name" label="类型" width="130px">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-select :disabled="scope.row.id != null" v-model="scope.row.type" style="width: 100px" placeholder="请选择">
 | 
			
		||||
                            <el-option v-for="item in FileTypeEnum as any" :key="item.value" :label="item.label" :value="item.value"></el-option>
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip>
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column label="操作" min-wdith="180px">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
 | 
			
		||||
                        <el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>
 | 
			
		||||
                        <el-button v-auth="'machine:file:del'" type="danger" @click="deleteRow(scope.$index, scope.row)" icon="delete" plain></el-button>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
            </el-table>
 | 
			
		||||
            <el-row style="margin-top: 10px" type="flex" justify="end">
 | 
			
		||||
                <el-pagination
 | 
			
		||||
                    style="text-align: center"
 | 
			
		||||
                    :total="total"
 | 
			
		||||
                    layout="prev, pager, next, total, jumper"
 | 
			
		||||
                    v-model:current-page="query.pageNum"
 | 
			
		||||
                    :page-size="query.pageSize"
 | 
			
		||||
                    @current-change="handlePageChange"
 | 
			
		||||
                >
 | 
			
		||||
                </el-pagination>
 | 
			
		||||
            </el-row>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog destroy-on-close :title="fileDialog.title" v-model="fileDialog.visible" :close-on-click-modal="false" width="65%">
 | 
			
		||||
            <machine-file :machine-id="machineId" :file-id="fileDialog.fileId" :path="fileDialog.path" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <machine-file-content v-model:visible="fileContent.contentVisible" :machine-id="machineId" :file-id="fileContent.fileId" :path="fileContent.path" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, watch } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { machineApi } from '../api';
 | 
			
		||||
 | 
			
		||||
import { FileTypeEnum } from '../enums';
 | 
			
		||||
import MachineFile from './MachineFile.vue';
 | 
			
		||||
import MachineFileContent from './MachineFileContent.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: { type: Boolean },
 | 
			
		||||
    machineId: { type: Number },
 | 
			
		||||
    title: { type: String },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
 | 
			
		||||
 | 
			
		||||
const addFile = machineApi.addConf;
 | 
			
		||||
const delFile = machineApi.delConf;
 | 
			
		||||
const files = machineApi.files;
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    query: {
 | 
			
		||||
        id: 0,
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 8,
 | 
			
		||||
    },
 | 
			
		||||
    loading: false,
 | 
			
		||||
    form: {
 | 
			
		||||
        id: null,
 | 
			
		||||
        type: null,
 | 
			
		||||
        name: '',
 | 
			
		||||
        remark: '',
 | 
			
		||||
    },
 | 
			
		||||
    total: 0,
 | 
			
		||||
    fileTable: [] as any,
 | 
			
		||||
    fileDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        title: '',
 | 
			
		||||
        fileId: 0,
 | 
			
		||||
        path: '',
 | 
			
		||||
    },
 | 
			
		||||
    fileContent: {
 | 
			
		||||
        fileId: 0,
 | 
			
		||||
        contentVisible: false,
 | 
			
		||||
        dialogTitle: '',
 | 
			
		||||
        path: '',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, loading, query, total, fileTable, fileDialog, fileContent } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watch(props, async (newValue) => {
 | 
			
		||||
    state.dialogVisible = newValue.visible;
 | 
			
		||||
    if (newValue.machineId && newValue.visible) {
 | 
			
		||||
        await getFiles();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getFiles = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        state.loading = true;
 | 
			
		||||
        state.query.id = props.machineId as any;
 | 
			
		||||
        const res = await files.request(state.query);
 | 
			
		||||
        state.fileTable = res.list || [];
 | 
			
		||||
        state.total = res.total;
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.loading = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handlePageChange = (curPage: number) => {
 | 
			
		||||
    state.query.pageNum = curPage;
 | 
			
		||||
    getFiles();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const add = () => {
 | 
			
		||||
    // 往数组头部添加元素
 | 
			
		||||
    state.fileTable = [{}].concat(state.fileTable);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addFiles = async (row: any) => {
 | 
			
		||||
    row.machineId = props.machineId;
 | 
			
		||||
    await addFile.request(row);
 | 
			
		||||
    ElMessage.success('添加成功');
 | 
			
		||||
    getFiles();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteRow = (idx: any, row: any) => {
 | 
			
		||||
    if (row.id) {
 | 
			
		||||
        ElMessageBox.confirm(`此操作将删除 [${row.name}], 是否继续?`, '提示', {
 | 
			
		||||
            confirmButtonText: '确定',
 | 
			
		||||
            cancelButtonText: '取消',
 | 
			
		||||
            type: 'warning',
 | 
			
		||||
        }).then(() => {
 | 
			
		||||
            // 删除配置文件
 | 
			
		||||
            delFile
 | 
			
		||||
                .request({
 | 
			
		||||
                    machineId: props.machineId,
 | 
			
		||||
                    id: row.id,
 | 
			
		||||
                })
 | 
			
		||||
                .then(() => {
 | 
			
		||||
                    getFiles();
 | 
			
		||||
                });
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
        state.fileTable.splice(idx, 1);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getConf = async (row: any) => {
 | 
			
		||||
    if (row.type == 1) {
 | 
			
		||||
        state.fileDialog.fileId = row.id;
 | 
			
		||||
        state.fileDialog.title = row.name;
 | 
			
		||||
        state.fileDialog.path = row.path;
 | 
			
		||||
        state.fileDialog.visible = true;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    showFileContent(row.id, row.path);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showFileContent = async (fileId: number, path: string) => {
 | 
			
		||||
    state.fileContent.fileId = fileId;
 | 
			
		||||
    state.fileContent.path = path;
 | 
			
		||||
    state.fileContent.contentVisible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 关闭取消按钮触发的事件
 | 
			
		||||
 */
 | 
			
		||||
const handleClose = () => {
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
    emit('update:machineId', null);
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
    state.fileTable = [];
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.machine-file-upload-exec {
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
.inline-block {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
.margin-change {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										478
									
								
								mayfly_go_web/src/views/ops/machine/file/MachineFile.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										478
									
								
								mayfly_go_web/src/views/ops/machine/file/MachineFile.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,478 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="machine-file">
 | 
			
		||||
        <div>
 | 
			
		||||
            <el-progress v-if="uploadProgressShow" style="width: 90%; margin-left: 20px" :text-inside="true" :stroke-width="20" :percentage="progressNum" />
 | 
			
		||||
            <el-row class="mb10">
 | 
			
		||||
                <el-breadcrumb separator-icon="ArrowRight">
 | 
			
		||||
                    <el-breadcrumb-item v-for="path in filePathNav">
 | 
			
		||||
                        <el-link @click="setFiles(path.path)">{{ path.name }}</el-link>
 | 
			
		||||
                    </el-breadcrumb-item>
 | 
			
		||||
                </el-breadcrumb>
 | 
			
		||||
            </el-row>
 | 
			
		||||
            <el-table ref="fileTableRef" height="65vh" :data="files" style="width: 100%" highlight-current-row v-loading="loading">
 | 
			
		||||
                <el-table-column prop="name" label="名称" show-overflow-tooltip>
 | 
			
		||||
                    <template #header>
 | 
			
		||||
                        <div class="machine-file-table-header">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <el-button :disabled="nowPath == basePath" type="primary" circle size="small" icon="Back" @click="back()"> </el-button>
 | 
			
		||||
                                <el-button class="ml0" 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="ml10" type="primary" circle size="small" icon="Upload"> </el-button>
 | 
			
		||||
                                </el-upload>
 | 
			
		||||
 | 
			
		||||
                                <el-button
 | 
			
		||||
                                    v-auth="'machine:file:write'"
 | 
			
		||||
                                    @click="showCreateFileDialog()"
 | 
			
		||||
                                    class="ml10"
 | 
			
		||||
                                    type="primary"
 | 
			
		||||
                                    circle
 | 
			
		||||
                                    size="small"
 | 
			
		||||
                                    icon="FolderAdd"
 | 
			
		||||
                                >
 | 
			
		||||
                                </el-button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </template>
 | 
			
		||||
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <span v-if="scope.row.isFolder">
 | 
			
		||||
                            <SvgIcon :size="15" name="folder" color="#007AFF" />
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <span v-else>
 | 
			
		||||
                            <SvgIcon :size="15" name="document" />
 | 
			
		||||
                        </span>
 | 
			
		||||
 | 
			
		||||
                        <span class="ml5" style="font-weight: bold">
 | 
			
		||||
                            <el-link @click="getFile(scope.row)" :underline="false">{{ scope.row.name }}</el-link>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
 | 
			
		||||
                <el-table-column prop="size" label="大小" width="100" sortable>
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatFileSize(scope.row.size) }} </span>
 | 
			
		||||
                        <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span>
 | 
			
		||||
                        <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && !scope.row.dirSize">
 | 
			
		||||
                            <el-button @click="getDirSize(scope.row)" type="primary" link :loading="scope.row.loadingDirSize">计算</el-button>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
 | 
			
		||||
                <el-table-column prop="mode" label="属性" width="110"> </el-table-column>
 | 
			
		||||
                <el-table-column prop="modTime" label="修改时间" width="165" sortable> </el-table-column>
 | 
			
		||||
 | 
			
		||||
                <el-table-column label="操作" width="100">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-link
 | 
			
		||||
                            @click="downloadFile(scope.row)"
 | 
			
		||||
                            v-if="scope.row.type == '-'"
 | 
			
		||||
                            v-auth="'machine:file:write'"
 | 
			
		||||
                            type="primary"
 | 
			
		||||
                            icon="download"
 | 
			
		||||
                            :underline="false"
 | 
			
		||||
                        ></el-link>
 | 
			
		||||
 | 
			
		||||
                        <el-link
 | 
			
		||||
                            @click="deleteFile(scope.row)"
 | 
			
		||||
                            v-if="!dontOperate(scope.row)"
 | 
			
		||||
                            v-auth="'machine:file:rm'"
 | 
			
		||||
                            type="danger"
 | 
			
		||||
                            icon="delete"
 | 
			
		||||
                            :underline="false"
 | 
			
		||||
                            class="ml10"
 | 
			
		||||
                        ></el-link>
 | 
			
		||||
 | 
			
		||||
                        <el-popover placement="top-start" :title="`${scope.row.path}-文件详情`" :width="520" trigger="click" @show="showFileStat(scope.row)">
 | 
			
		||||
                            <template #reference>
 | 
			
		||||
                                <span style="color: #67c23a; font-weight: bold">
 | 
			
		||||
                                    <el-link
 | 
			
		||||
                                        @click="showFileStat(scope.row)"
 | 
			
		||||
                                        icon="InfoFilled"
 | 
			
		||||
                                        :underline="false"
 | 
			
		||||
                                        link
 | 
			
		||||
                                        class="ml10"
 | 
			
		||||
                                        :loading="scope.row.loadingStat"
 | 
			
		||||
                                    ></el-link>
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </template>
 | 
			
		||||
                            <el-input :input-style="{ color: 'black' }" disabled autosize v-model="scope.row.stat" type="textarea" />
 | 
			
		||||
                        </el-popover>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
            </el-table>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <el-dialog
 | 
			
		||||
            :destroy-on-close="true"
 | 
			
		||||
            title="新建文件"
 | 
			
		||||
            v-model="createFileDialog.visible"
 | 
			
		||||
            :before-close="closeCreateFileDialog"
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            top="5vh"
 | 
			
		||||
            width="400px"
 | 
			
		||||
        >
 | 
			
		||||
            <div>
 | 
			
		||||
                <el-form-item prop="name" label="名称:">
 | 
			
		||||
                    <el-input v-model.trim="createFileDialog.name" placeholder="请输入名称" auto-complete="off"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <el-form-item prop="type" label="类型:">
 | 
			
		||||
                    <el-radio-group v-model="createFileDialog.type">
 | 
			
		||||
                        <el-radio label="d">文件夹</el-radio>
 | 
			
		||||
                        <el-radio label="-">文件</el-radio>
 | 
			
		||||
                    </el-radio-group>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <el-button @click="closeCreateFileDialog">关闭</el-button>
 | 
			
		||||
                    <el-button v-auth="'machine:file:write'" type="primary" @click="createFile">确定</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <machine-file-content v-model:visible="fileContent.contentVisible" :machine-id="machineId" :file-id="fileId" :path="fileContent.path" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, onMounted, computed } from 'vue';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import { machineApi } from '../api';
 | 
			
		||||
 | 
			
		||||
import { getSession } from '@/common/utils/storage';
 | 
			
		||||
import config from '@/common/config';
 | 
			
		||||
import { isTrue } from '@/common/assert';
 | 
			
		||||
import MachineFileContent from './MachineFileContent.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    machineId: { type: Number },
 | 
			
		||||
    fileId: { type: Number, default: 0 },
 | 
			
		||||
    path: { type: String, default: '' },
 | 
			
		||||
    isFolder: { type: Boolean, default: true },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const token = getSession('token');
 | 
			
		||||
 | 
			
		||||
const folderType = 'd';
 | 
			
		||||
const fileType = '-';
 | 
			
		||||
// 路径分隔符
 | 
			
		||||
const pathSep = '/';
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    basePath: '', // 基础路径
 | 
			
		||||
    nowPath: '', // 当前路径
 | 
			
		||||
    loading: true,
 | 
			
		||||
    progressNum: 0,
 | 
			
		||||
    uploadProgressShow: false,
 | 
			
		||||
    files: [] as any,
 | 
			
		||||
    fileContent: {
 | 
			
		||||
        content: '',
 | 
			
		||||
        contentVisible: false,
 | 
			
		||||
        dialogTitle: '',
 | 
			
		||||
        path: '',
 | 
			
		||||
        type: 'shell',
 | 
			
		||||
    },
 | 
			
		||||
    createFileDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        name: '',
 | 
			
		||||
        type: folderType,
 | 
			
		||||
        data: null as any,
 | 
			
		||||
    },
 | 
			
		||||
    file: null as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { basePath, nowPath, loading, files, progressNum, uploadProgressShow, fileContent, createFileDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    state.basePath = props.path;
 | 
			
		||||
    setFiles(props.path);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filePathNav = computed(() => {
 | 
			
		||||
    let basePath = state.basePath;
 | 
			
		||||
    const pathNavs = [
 | 
			
		||||
        {
 | 
			
		||||
            path: basePath,
 | 
			
		||||
            name: basePath,
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    if (basePath == state.nowPath) {
 | 
			
		||||
        return pathNavs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const paths = state.nowPath.split(pathSep).splice(1);
 | 
			
		||||
    let nowPath = '';
 | 
			
		||||
    for (let path of paths) {
 | 
			
		||||
        if (!nowPath) {
 | 
			
		||||
            nowPath = pathSep + path;
 | 
			
		||||
        } else {
 | 
			
		||||
            nowPath = nowPath + pathSep + path;
 | 
			
		||||
        }
 | 
			
		||||
        // 最多只能点击到basePath
 | 
			
		||||
        if (nowPath.length <= basePath.length) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        pathNavs.push({
 | 
			
		||||
            name: path,
 | 
			
		||||
            path: nowPath,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return pathNavs;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const showFileContent = async (path: string) => {
 | 
			
		||||
    state.fileContent.dialogTitle = path;
 | 
			
		||||
    state.fileContent.path = path;
 | 
			
		||||
    state.fileContent.contentVisible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFile = async (row: any) => {
 | 
			
		||||
    if (row.type == folderType) {
 | 
			
		||||
        await setFiles(row.path);
 | 
			
		||||
    } else {
 | 
			
		||||
        isTrue(row.size < 1 * 1024 * 1024, '文件太大, 请下载使用');
 | 
			
		||||
        await showFileContent(row.path);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setFiles = async (path: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
        if (!path) {
 | 
			
		||||
            path = pathSep;
 | 
			
		||||
        }
 | 
			
		||||
        state.loading = true;
 | 
			
		||||
        state.files = await lsFile(path);
 | 
			
		||||
        state.nowPath = path;
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.loading = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const lsFile = async (path: string) => {
 | 
			
		||||
    const res = await machineApi.lsFile.request({
 | 
			
		||||
        fileId: props.fileId,
 | 
			
		||||
        machineId: props.machineId,
 | 
			
		||||
        path,
 | 
			
		||||
    });
 | 
			
		||||
    for (const file of res) {
 | 
			
		||||
        const type = file.type;
 | 
			
		||||
        if (type == folderType) {
 | 
			
		||||
            file.isFolder = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            file.isFolder = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const back = () => {
 | 
			
		||||
    setFiles(getParentPath(state.nowPath));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const refresh = async () => {
 | 
			
		||||
    setFiles(state.nowPath);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getDirSize = async (data: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
        data.loadingDirSize = true;
 | 
			
		||||
        const res = await machineApi.dirSize.request({
 | 
			
		||||
            machineId: props.machineId,
 | 
			
		||||
            fileId: props.fileId,
 | 
			
		||||
            path: data.path,
 | 
			
		||||
        });
 | 
			
		||||
        data.dirSize = res;
 | 
			
		||||
    } finally {
 | 
			
		||||
        data.loadingDirSize = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showFileStat = async (data: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
        if (data.stat) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        data.loadingStat = true;
 | 
			
		||||
        const res = await machineApi.fileStat.request({
 | 
			
		||||
            machineId: props.machineId,
 | 
			
		||||
            fileId: props.fileId,
 | 
			
		||||
            path: data.path,
 | 
			
		||||
        });
 | 
			
		||||
        data.stat = res;
 | 
			
		||||
    } finally {
 | 
			
		||||
        data.loadingStat = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showCreateFileDialog = () => {
 | 
			
		||||
    state.createFileDialog.data = {};
 | 
			
		||||
    state.createFileDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createFile = async () => {
 | 
			
		||||
    const name = state.createFileDialog.name;
 | 
			
		||||
    const type = state.createFileDialog.type;
 | 
			
		||||
    const path = state.nowPath + pathSep + name;
 | 
			
		||||
    await machineApi.createFile.request({
 | 
			
		||||
        machineId: props.machineId,
 | 
			
		||||
        id: props.fileId,
 | 
			
		||||
        path,
 | 
			
		||||
        type,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    closeCreateFileDialog();
 | 
			
		||||
    refresh();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeCreateFileDialog = () => {
 | 
			
		||||
    state.createFileDialog.visible = false;
 | 
			
		||||
    state.createFileDialog.data = null;
 | 
			
		||||
    state.createFileDialog.name = '';
 | 
			
		||||
    state.createFileDialog.type = folderType;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getParentPath(filePath: string) {
 | 
			
		||||
    const segments = filePath.split(pathSep);
 | 
			
		||||
    segments.pop(); // 移除最后一个路径段
 | 
			
		||||
    return segments.join(pathSep);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const deleteFile = (data: any) => {
 | 
			
		||||
    const file = data.path;
 | 
			
		||||
    ElMessageBox.confirm(`此操作将删除 [${file}], 是否继续?`, '提示', {
 | 
			
		||||
        confirmButtonText: '确定',
 | 
			
		||||
        cancelButtonText: '取消',
 | 
			
		||||
        type: 'warning',
 | 
			
		||||
    })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
            machineApi.rmFile
 | 
			
		||||
                .request({
 | 
			
		||||
                    fileId: props.fileId,
 | 
			
		||||
                    path: file,
 | 
			
		||||
                    machineId: props.machineId,
 | 
			
		||||
                })
 | 
			
		||||
                .then(async () => {
 | 
			
		||||
                    ElMessage.success('删除成功');
 | 
			
		||||
                    refresh();
 | 
			
		||||
                });
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
            // skip
 | 
			
		||||
        });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const downloadFile = (data: any) => {
 | 
			
		||||
    const a = document.createElement('a');
 | 
			
		||||
    a.setAttribute('href', `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/read?type=1&path=${data.path}&token=${token}`);
 | 
			
		||||
    a.click();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onUploadProgress = (progressEvent: any) => {
 | 
			
		||||
    state.uploadProgressShow = true;
 | 
			
		||||
    let complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
 | 
			
		||||
    state.progressNum = complete;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getUploadFile = (content: any) => {
 | 
			
		||||
    const params = new FormData();
 | 
			
		||||
    const path = state.nowPath;
 | 
			
		||||
    params.append('file', content.file);
 | 
			
		||||
    params.append('path', path);
 | 
			
		||||
    params.append('machineId', props.machineId as any);
 | 
			
		||||
    params.append('fileId', props.fileId as any);
 | 
			
		||||
    params.append('token', token);
 | 
			
		||||
    machineApi.uploadFile
 | 
			
		||||
        .request(params, {
 | 
			
		||||
            url: `${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/upload?token=${token}`,
 | 
			
		||||
            headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
 | 
			
		||||
            onUploadProgress: onUploadProgress,
 | 
			
		||||
            baseURL: '',
 | 
			
		||||
            timeout: 60 * 60 * 1000,
 | 
			
		||||
        })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
            ElMessage.success('上传成功');
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                refresh();
 | 
			
		||||
                state.uploadProgressShow = false;
 | 
			
		||||
            }, 3000);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
            state.uploadProgressShow = false;
 | 
			
		||||
        });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const uploadSuccess = (res: any) => {
 | 
			
		||||
    if (res.code !== 200) {
 | 
			
		||||
        ElMessage.error(res.msg);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const beforeUpload = (file: File) => {
 | 
			
		||||
    state.file = file;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const dontOperate = (data: any) => {
 | 
			
		||||
    const path = data.path;
 | 
			
		||||
    const ls = ['/', '//', '/usr', '/usr/', '/usr/bin', '/opt', '/run', '/etc', '/proc', '/var', '/mnt', '/boot', '/dev', '/home', '/media', '/root'];
 | 
			
		||||
    return ls.indexOf(path) != -1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 格式化文件大小
 | 
			
		||||
 * @param {*} value
 | 
			
		||||
 */
 | 
			
		||||
const formatFileSize = (size: any) => {
 | 
			
		||||
    const value = Number(size);
 | 
			
		||||
    if (size && !isNaN(value)) {
 | 
			
		||||
        const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
 | 
			
		||||
        let index = 0;
 | 
			
		||||
        let k = value;
 | 
			
		||||
        if (value >= 1024) {
 | 
			
		||||
            while (k > 1024) {
 | 
			
		||||
                k = k / 1024;
 | 
			
		||||
                index++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return `${k.toFixed(2)}${units[index]}`;
 | 
			
		||||
    }
 | 
			
		||||
    return '-';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ showFileContent });
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.machine-file-upload-exec {
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
.machine-file-table-header {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
 | 
			
		||||
    .title-right-fixed {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        font-size: 20px;
 | 
			
		||||
        text-align: end;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										107
									
								
								mayfly_go_web/src/views/ops/machine/file/MachineFileContent.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										107
									
								
								mayfly_go_web/src/views/ops/machine/file/MachineFileContent.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="machine-file-content">
 | 
			
		||||
        <el-dialog :before-close="handleClose" :title="path" v-model="dialogVisible" :close-on-click-modal="false" top="5vh" width="65%">
 | 
			
		||||
            <div>
 | 
			
		||||
                <monaco-editor :can-change-mode="true" v-model="content" :language="fileType" />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div class="dialog-footer">
 | 
			
		||||
                    <el-button @click="handleClose">关 闭</el-button>
 | 
			
		||||
                    <el-button v-auth="'machine:file:write'" type="primary" @click="updateContent">保 存</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, watch } from 'vue';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import { machineApi } from '../api';
 | 
			
		||||
 | 
			
		||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: { type: Boolean, default: false },
 | 
			
		||||
    machineId: { type: Number },
 | 
			
		||||
    fileId: { type: Number, default: 0 },
 | 
			
		||||
    path: { type: String, default: '' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
 | 
			
		||||
 | 
			
		||||
const updateFileContent = machineApi.updateFileContent;
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    content: '',
 | 
			
		||||
    fileType: '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, content, fileType } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watch(props, async (newValue) => {
 | 
			
		||||
    if (newValue.visible) {
 | 
			
		||||
        await getFileContent();
 | 
			
		||||
    }
 | 
			
		||||
    state.dialogVisible = newValue.visible;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getFileContent = async () => {
 | 
			
		||||
    const path = props.path;
 | 
			
		||||
    const res = await machineApi.fileContent.request({
 | 
			
		||||
        fileId: props.fileId,
 | 
			
		||||
        path,
 | 
			
		||||
        machineId: props.machineId,
 | 
			
		||||
    });
 | 
			
		||||
    state.fileType = getFileType(path);
 | 
			
		||||
    state.content = res;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleClose = () => {
 | 
			
		||||
    state.dialogVisible = false;
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateContent = async () => {
 | 
			
		||||
    await updateFileContent.request({
 | 
			
		||||
        content: state.content,
 | 
			
		||||
        id: props.fileId,
 | 
			
		||||
        path: props.path,
 | 
			
		||||
        machineId: props.machineId,
 | 
			
		||||
    });
 | 
			
		||||
    ElMessage.success('修改成功');
 | 
			
		||||
    handleClose();
 | 
			
		||||
    state.content = '';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFileType = (path: string) => {
 | 
			
		||||
    if (path.endsWith('.sh')) {
 | 
			
		||||
        return 'shell';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('js')) {
 | 
			
		||||
        return 'javascript';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('json')) {
 | 
			
		||||
        return 'json';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('Dockerfile')) {
 | 
			
		||||
        return 'dockerfile';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('nginx.conf')) {
 | 
			
		||||
        return 'shell';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('sql')) {
 | 
			
		||||
        return 'sql';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('yaml') || path.endsWith('yml')) {
 | 
			
		||||
        return 'yaml';
 | 
			
		||||
    }
 | 
			
		||||
    if (path.endsWith('xml') || path.endsWith('html')) {
 | 
			
		||||
        return 'html';
 | 
			
		||||
    }
 | 
			
		||||
    return 'text';
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -23,8 +23,8 @@ require (
 | 
			
		||||
	github.com/robfig/cron/v3 v3.0.1 // 定时任务
 | 
			
		||||
	github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
 | 
			
		||||
	go.mongodb.org/mongo-driver v1.12.1 // mongo
 | 
			
		||||
	golang.org/x/crypto v0.12.0 // ssh
 | 
			
		||||
	golang.org/x/oauth2 v0.11.0
 | 
			
		||||
	golang.org/x/crypto v0.13.0 // ssh
 | 
			
		||||
	golang.org/x/oauth2 v0.12.0
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1
 | 
			
		||||
	// gorm
 | 
			
		||||
	gorm.io/driver/mysql v1.5.1
 | 
			
		||||
@@ -66,10 +66,10 @@ require (
 | 
			
		||||
	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
 | 
			
		||||
	golang.org/x/arch v0.3.0 // indirect
 | 
			
		||||
	golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
 | 
			
		||||
	golang.org/x/net v0.14.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.15.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.1.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.11.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.12.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.12.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.13.0 // indirect
 | 
			
		||||
	google.golang.org/appengine v1.6.7 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.31.0 // indirect
 | 
			
		||||
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 | 
			
		||||
 
 | 
			
		||||
@@ -94,8 +94,7 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
 | 
			
		||||
func (d *Db) getDbConnection(g *gin.Context) *application.DbConnection {
 | 
			
		||||
	dbName := g.Query("db")
 | 
			
		||||
	biz.NotEmpty(dbName, "db不能为空")
 | 
			
		||||
	dbId := getDbId(g)
 | 
			
		||||
	return d.DbApp.GetDbConnection(dbId, dbName)
 | 
			
		||||
	return d.DbApp.GetDbConnection(getDbId(g), dbName)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Db) TableInfos(rc *req.Ctx) {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"mayfly-go/pkg/biz"
 | 
			
		||||
	"mayfly-go/pkg/ginx"
 | 
			
		||||
	"mayfly-go/pkg/req"
 | 
			
		||||
	"mayfly-go/pkg/utils/timex"
 | 
			
		||||
	"mayfly-go/pkg/ws"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
@@ -113,11 +114,14 @@ func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
 | 
			
		||||
	fisVO := make([]vo.MachineFileInfo, 0)
 | 
			
		||||
	for _, fi := range fis {
 | 
			
		||||
		fisVO = append(fisVO, vo.MachineFileInfo{
 | 
			
		||||
			Name: fi.Name(),
 | 
			
		||||
			Size: fi.Size(),
 | 
			
		||||
			Path: readPath + fi.Name(),
 | 
			
		||||
			Type: getFileType(fi.Mode()),
 | 
			
		||||
			Name:    fi.Name(),
 | 
			
		||||
			Size:    fi.Size(),
 | 
			
		||||
			Path:    readPath + fi.Name(),
 | 
			
		||||
			Type:    getFileType(fi.Mode()),
 | 
			
		||||
			Mode:    fi.Mode().String(),
 | 
			
		||||
			ModTime: timex.DefaultFormat(fi.ModTime()),
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	sort.Sort(vo.MachineFileInfos(fisVO))
 | 
			
		||||
	rc.ResData = fisVO
 | 
			
		||||
 
 | 
			
		||||
@@ -65,10 +65,12 @@ type MachineFileVO struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MachineFileInfo struct {
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
	Path string `json:"path"`
 | 
			
		||||
	Size int64  `json:"size"`
 | 
			
		||||
	Type string `json:"type"`
 | 
			
		||||
	Name    string `json:"name"`
 | 
			
		||||
	Path    string `json:"path"`
 | 
			
		||||
	Size    int64  `json:"size"`
 | 
			
		||||
	Type    string `json:"type"`
 | 
			
		||||
	Mode    string `json:"mode"`
 | 
			
		||||
	ModTime string `json:"modTime"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MachineFileInfos []MachineFileInfo
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,9 @@ func (r TerminalSession) Stop() {
 | 
			
		||||
	r.cancel()
 | 
			
		||||
	if r.terminal != nil {
 | 
			
		||||
		if err := r.terminal.Close(); err != nil {
 | 
			
		||||
			logx.Errorf("关闭机器ssh终端失败: %s", err.Error())
 | 
			
		||||
			if err != io.EOF {
 | 
			
		||||
				logx.Errorf("关闭机器ssh终端失败: %s", err.Error())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user