Files
mayfly-go/mayfly_go_web/src/views/ops/machine/FileManage.vue
2022-11-05 15:13:40 +08:00

595 lines
20 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="file-manage">
<el-dialog :title="title" v-model="dialogVisible" :show-close="true" :before-close="handleClose" width="800px">
<div class="toolbar">
<div style="float: right">
<el-button v-auth="'machine:file:add'" type="primary" @click="add" icon="plus" size="small" plain>添加
</el-button>
</div>
</div>
<el-table :data="fileTable" stripe style="width: 100%">
<el-table-column prop="name" label="名称" width>
<template #default="scope">
<el-input v-model="scope.row.name" size="small" :disabled="scope.row.id != null" clearable>
</el-input>
</template>
</el-table-column>
<el-table-column prop="name" label="类型" min-width="50px">
<template #default="scope">
<el-select :disabled="scope.row.id != null" size="small" v-model="scope.row.type"
style="width: 100px" placeholder="请选择">
<el-option v-for="item in enums.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="路径" width>
<template #default="scope">
<el-input v-model="scope.row.path" :disabled="scope.row.id != null" size="small" clearable>
</el-input>
</template>
</el-table-column>
<el-table-column label="操作" width>
<template #default="scope">
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success"
icon="success-filled" size="small" plain>确定</el-button>
<el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets"
size="small" plain>查看</el-button>
<el-button v-auth="'machine:file:del'" type="danger" @click="deleteRow(scope.$index, scope.row)"
icon="delete" size="small" plain>删除</el-button>
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 10px" type="flex" justify="end">
<el-pagination small 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: 45vh; 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="true">
<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 name="folder" />
</span>
<span v-if="data.type == 'd' && node.expanded">
<SvgIcon name="folder-opened" />
</span>
<span v-if="data.type == '-'">
<SvgIcon name="document" />
</span>
<span>
{{ 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" v-if="data.type == '-'">[{{ formatFileSize(data.size)
}}]</span>
<span v-if="data.mode" style="color: #67c23a">&nbsp;[{{ data.mode }} {{ data.modTime
}}]</span>
</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 enums 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,
},
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,
query,
total,
fileTable,
fileContent,
tree,
progressNum,
uploadProgressShow,
createFileDialog,
} = toRefs(state)
watch(props, async (newValue) => {
if (newValue.machineId && newValue.visible) {
await getFiles();
}
state.dialogVisible = newValue.visible;
});
const getFiles = async () => {
state.query.id = props.machineId as any;
const res = await files.request(state.query);
state.fileTable = res.list;
state.total = res.total;
};
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 showCreateFileDialog = (node: any) => {
isTrue(node.expanded, '请先点击展开该节点后再创建');
state.createFileDialog.node = node;
state.createFileDialog.visible = true;
};
const createFile = async () => {
const node = state.createFileDialog.node;
console.log(node.data);
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>