feat: 机器文件新增批量删除、copy、mv、rename等操作

This commit is contained in:
meilin.huang
2023-09-07 16:33:53 +08:00
parent 25b0d276b3
commit 9e0db2bc99
10 changed files with 371 additions and 80 deletions

View File

@@ -1,6 +1,14 @@
<template>
<div id="terminalRecDialog">
<el-dialog :title="title" v-model="dialogVisible" :before-close="handleClose" :close-on-click-modal="false" :destroy-on-close="true" width="70%">
<el-dialog
:title="title"
v-if="dialogVisible"
v-model="dialogVisible"
:before-close="handleClose"
:close-on-click-modal="false"
:destroy-on-close="true"
width="70%"
>
<div class="toolbar">
<el-select @change="getUsers" v-model="operateDate" placeholder="操作日期" filterable>
<el-option v-for="item in operateDates" :key="item" :label="item" :value="item"> </el-option>

View File

@@ -29,7 +29,10 @@ export const machineApi = {
lsFile: Api.newGet('/machines/{machineId}/files/{fileId}/read-dir'),
dirSize: Api.newGet('/machines/{machineId}/files/{fileId}/dir-size'),
fileStat: Api.newGet('/machines/{machineId}/files/{fileId}/file-stat'),
rmFile: Api.newDelete('/machines/{machineId}/files/{fileId}/remove'),
rmFile: Api.newPost('/machines/{machineId}/files/{fileId}/remove'),
cpFile: Api.newPost('/machines/{machineId}/files/{fileId}/cp'),
renameFile: Api.newPost('/machines/{machineId}/files/{fileId}/rename'),
mvFile: Api.newPost('/machines/{machineId}/files/{fileId}/mv'),
uploadFile: Api.newPost('/machines/{machineId}/files/{fileId}/upload?token={token}'),
fileContent: Api.newGet('/machines/{machineId}/files/{fileId}/read'),
createFile: Api.newPost('/machines/{machineId}/files/{id}/create-file'),

View File

@@ -1,8 +1,8 @@
<template>
<div class="file-manage">
<el-dialog :title="title" v-model="dialogVisible" :show-close="true" :before-close="handleClose" width="50%">
<el-dialog v-if="dialogVisible" :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">
<el-table-column prop="name" label="名称" min-width="100px">
<template #header>
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="add()"> </el-button>
<span class="ml10">名称</span>
@@ -23,7 +23,7 @@
<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">
<el-table-column label="操作" min-wdith="120px">
<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>
@@ -42,13 +42,19 @@
>
</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>
<el-dialog destroy-on-close :title="fileDialog.title" v-model="fileDialog.visible" :close-on-click-modal="false" width="70%">
<machine-file :title="fileDialog.title" :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" />
<machine-file-content
:title="fileContent.title"
v-model:visible="fileContent.contentVisible"
:machine-id="machineId"
:file-id="fileContent.fileId"
:path="fileContent.path"
/>
</el-dialog>
</div>
</template>
@@ -56,7 +62,6 @@
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';
@@ -96,9 +101,9 @@ const state = reactive({
path: '',
},
fileContent: {
title: '',
fileId: 0,
contentVisible: false,
dialogTitle: '',
path: '',
},
});
@@ -168,15 +173,18 @@ const getConf = async (row: any) => {
state.fileDialog.fileId = row.id;
state.fileDialog.title = row.name;
state.fileDialog.path = row.path;
state.fileDialog.title = `${props.title} => ${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.title = `${props.title} => ${path}`;
state.fileContent.contentVisible = true;
};

View File

@@ -2,20 +2,32 @@
<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-link @click="setFiles(path.path)" style="font-weight: bold">{{ 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
ref="fileTableRef"
@cell-dblclick="cellDbclick"
@selection-change="handleSelectionChange"
height="65vh"
:data="filterFiles"
highlight-current-row
v-loading="loading"
>
<el-table-column type="selection" width="30" />
<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-button class="ml5" type="primary" circle size="small" icon="Refresh" @click="refresh()"> </el-button>
<el-upload
:before-upload="beforeUpload"
@@ -27,19 +39,78 @@
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-button v-auth="'machine:file:upload'" class="ml5" type="primary" circle size="small" icon="Upload" title="上传">
</el-button>
</el-upload>
<el-button
:disabled="state.selectionFiles.length == 0"
v-auth="'machine:file:rm'"
@click="copyFile(state.selectionFiles)"
class="ml5"
type="primary"
circle
size="small"
icon="CopyDocument"
title="复制"
>
</el-button>
<el-button
:disabled="state.selectionFiles.length == 0"
v-auth="'machine:file:rm'"
@click="mvFile(state.selectionFiles)"
class="ml5"
type="primary"
circle
size="small"
icon="Rank"
title="移动"
>
</el-button>
<el-button
v-auth="'machine:file:write'"
@click="showCreateFileDialog()"
class="ml10"
class="ml5"
type="primary"
circle
size="small"
icon="FolderAdd"
title="新建"
>
</el-button>
<el-button
:disabled="state.selectionFiles.length == 0"
v-auth="'machine:file:rm'"
@click="deleteFile(state.selectionFiles)"
class="ml5"
type="danger"
circle
size="small"
icon="delete"
title="删除"
>
</el-button>
<el-button-group v-if="state.copyOrMvFile.paths.length > 0" size="small" class="ml5">
<el-tooltip effect="customized" raw-content placement="top">
<template #content>
<div v-for="path in state.copyOrMvFile.paths">{{ path }}</div>
</template>
<el-button @click="pasteFile" type="primary"
>{{ isCpFile() ? '复制' : '移动' }}粘贴{{ state.copyOrMvFile.paths.length }}</el-button
>
</el-tooltip>
<el-button icon="CloseBold" @click="cancelCopy" />
</el-button-group>
</div>
<div style="width: 150px">
<el-input v-model="fileNameFilter" size="small" placeholder="名称: 输入可过滤" clearable />
</div>
</div>
</template>
@@ -52,8 +123,16 @@
<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 class="ml5" style="display: inline-block; width: 300px">
<div v-if="scope.row.nameEdit">
<el-input
@keyup.enter="fileRename(scope.row)"
:ref="(el: any) => el?.focus()"
@blur="filenameBlur(scope.row)"
v-model="scope.row.name"
/>
</div>
<el-link v-else @click="getFile(scope.row)" style="font-weight: bold" :underline="false">{{ scope.row.name }}</el-link>
</span>
</template>
</el-table-column>
@@ -71,7 +150,16 @@
<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">
<el-table-column width="100">
<template #header>
<el-popover placement="top" :width="270" trigger="hover">
<template #reference>
<SvgIcon name="QuestionFilled" :size="18" class="pointer-icon mr10" />
</template>
<div>rename: 双击文件名单元格后回车</div>
</el-popover>
操作
</template>
<template #default="scope">
<el-link
@click="downloadFile(scope.row)"
@@ -83,7 +171,7 @@
></el-link>
<el-link
@click="deleteFile(scope.row)"
@click="deleteFile([scope.row])"
v-if="!dontOperate(scope.row)"
v-auth="'machine:file:rm'"
type="danger"
@@ -147,13 +235,14 @@
<script lang="ts" setup>
import { toRefs, reactive, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ElMessage, ElMessageBox, ElInput } 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';
import { notBlank } from '@/common/assert';
const props = defineProps({
machineId: { type: Number },
@@ -175,7 +264,17 @@ const state = reactive({
loading: true,
progressNum: 0,
uploadProgressShow: false,
fileNameFilter: '',
files: [] as any,
selectionFiles: [] as any,
copyOrMvFile: {
paths: [] as any,
type: 'cp',
fromPath: '',
},
renameFile: {
oldname: '',
},
fileContent: {
content: '',
contentVisible: false,
@@ -192,13 +291,17 @@ const state = reactive({
file: null as any,
});
const { basePath, nowPath, loading, files, progressNum, uploadProgressShow, fileContent, createFileDialog } = toRefs(state);
const { basePath, nowPath, loading, fileNameFilter, progressNum, uploadProgressShow, fileContent, createFileDialog } = toRefs(state);
onMounted(() => {
state.basePath = props.path;
setFiles(props.path);
});
const filterFiles = computed(() =>
state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase()))
);
const filePathNav = computed(() => {
let basePath = state.basePath;
const pathNavs = [
@@ -233,6 +336,98 @@ const filePathNav = computed(() => {
return pathNavs;
});
const handleSelectionChange = (val: any) => {
state.selectionFiles = val;
};
const isCpFile = () => {
return state.copyOrMvFile.type == 'cp';
};
const copyFile = (files: any[]) => {
setCopyOrMvFile(files);
};
const mvFile = (files: any[]) => {
setCopyOrMvFile(files, 'mv');
};
const setCopyOrMvFile = (files: any[], type = 'cp') => {
for (let file of files) {
const path = file.path;
if (!state.copyOrMvFile.paths.includes(path)) {
state.copyOrMvFile.paths.push(path);
}
}
state.copyOrMvFile.type = type;
state.copyOrMvFile.fromPath = state.nowPath;
};
const pasteFile = async () => {
const cmFile = state.copyOrMvFile;
isTrue(state.nowPath != cmFile.fromPath, '同目录下不能粘贴');
const api = isCpFile() ? machineApi.cpFile : machineApi.mvFile;
try {
state.loading = true;
await api.request({
machineId: props.machineId,
fileId: props.fileId,
path: cmFile.paths,
toPath: state.nowPath,
});
ElMessage.success('粘贴成功');
state.copyOrMvFile.paths = [];
refresh();
} finally {
state.loading = false;
}
};
const cancelCopy = () => {
state.copyOrMvFile.paths = [];
};
const cellDbclick = (row: any, column: any) => {
// 双击名称列可修改名称
if (column.property == 'name') {
state.renameFile.oldname = row.name;
row.nameEdit = true;
}
};
const filenameBlur = (row: any) => {
const oldname = state.renameFile.oldname;
// 如果存在旧名称,则说明未回车修改文件名,则还原旧文件名
if (oldname) {
row.name = oldname;
state.renameFile.oldname = '';
}
row.nameEdit = false;
};
const fileRename = async (row: any) => {
if (row.name == state.renameFile.oldname) {
row.nameEdit = false;
return;
}
notBlank(row.name, '新名称不能为空');
try {
await machineApi.renameFile.request({
machineId: props.machineId,
fileId: props.fileId,
oldname: state.nowPath + pathSep + state.renameFile.oldname,
newname: state.nowPath + pathSep + row.name,
});
ElMessage.success('重命名成功');
// 修改路径上的文件名
row.path = state.nowPath + pathSep + row.name;
state.renameFile.oldname = '';
} catch (e) {
row.name = state.renameFile.oldname;
}
row.nameEdit = false;
};
const showFileContent = async (path: string) => {
state.fileContent.dialogTitle = path;
state.fileContent.path = path;
@@ -253,6 +448,7 @@ const setFiles = async (path: string) => {
if (!path) {
path = pathSep;
}
state.fileNameFilter = '';
state.loading = true;
state.files = await lsFile(path);
state.nowPath = path;
@@ -350,28 +546,25 @@ function getParentPath(filePath: string) {
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 deleteFile = async (files: any) => {
try {
await ElMessageBox.confirm(`此操作将删除 ${files.map((x: any) => `[${x.path}]`).join('\n')}, 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
state.loading = true;
await machineApi.rmFile.request({
fileId: props.fileId,
path: files.map((x: any) => x.path),
machineId: props.machineId,
});
ElMessage.success('删除成功');
refresh();
} catch (e) {
} finally {
state.loading = false;
}
};
const downloadFile = (data: any) => {

View File

@@ -1,6 +1,6 @@
<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%">
<el-dialog :before-close="handleClose" :title="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>
@@ -24,6 +24,7 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
const props = defineProps({
visible: { type: Boolean, default: false },
title: { type: String, default: '' },
machineId: { type: Number },
fileId: { type: Number, default: 0 },
path: { type: String, default: '' },