feat: 增加文件上传功能

This commit is contained in:
zhengkunwang223 2022-09-03 18:41:52 +08:00
parent b172a5124e
commit 89b95e0d45
12 changed files with 236 additions and 22 deletions

View file

@ -1,10 +1,13 @@
package v1
import (
"fmt"
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/gin-gonic/gin"
"path"
)
func (b *BaseApi) ListFiles(c *gin.Context) {
@ -131,3 +134,23 @@ func (b *BaseApi) SaveContent(c *gin.Context) {
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) UploadFiles(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
files := form.File["file"]
paths := form.Value["path"]
success := 0
for _, file := range files {
err := c.SaveUploadedFile(file, path.Join(paths[0], file.Filename))
if err != nil {
global.LOG.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
continue
}
success++
}
helper.SuccessWithMsg(c, fmt.Sprintf("%d files upload success", success))
}

View file

@ -68,6 +68,15 @@ func SuccessWithData(ctx *gin.Context, data interface{}) {
ctx.Abort()
}
func SuccessWithMsg(ctx *gin.Context, msg string) {
res := dto.Response{
Code: constant.CodeSuccess,
Msg: msg,
}
ctx.JSON(http.StatusOK, res)
ctx.Abort()
}
func GetParamID(c *gin.Context) (uint, error) {
idParam, ok := c.Params.Get("id")
if !ok {

View file

@ -24,6 +24,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
fileRouter.POST("/decompress", baseApi.DeCompressFile)
fileRouter.POST("/content", baseApi.GetContent)
fileRouter.POST("/save", baseApi.SaveContent)
fileRouter.POST("/upload", baseApi.UploadFiles)
}
}

View file

@ -15,6 +15,7 @@
"echarts-liquidfill": "^3.1.0",
"element-plus": "^2.2.13",
"fit2cloud-ui-plus": "^0.0.1-beta.15",
"js-base64": "^3.7.2",
"js-md5": "^0.7.3",
"monaco-editor": "^0.34.0",
"nprogress": "^0.2.0",
@ -22,12 +23,16 @@
"pinia-plugin-persistedstate": "^1.6.1",
"qs": "^6.10.3",
"sass-loader": "^13.0.2",
"screenfull": "^6.0.2",
"unplugin-vue-define-options": "^0.7.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.25",
"vue-i18n": "^9.1.9",
"vue-router": "^4.0.12",
"vue3-seamless-scroll": "^1.2.0"
"vue3-seamless-scroll": "^1.2.0",
"xterm": "^4.19.0",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.5.0"
},
"devDependencies": {
"@commitlint/cli": "^17.0.1",
@ -5863,6 +5868,11 @@
"node": ">=10"
}
},
"node_modules/js-base64": {
"version": "3.7.2",
"resolved": "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.2.tgz",
"integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ=="
},
"node_modules/js-md5": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
@ -7806,6 +7816,14 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"peer": true
},
"node_modules/screenfull": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz",
"integrity": "sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==",
"engines": {
"node": "^14.13.1 || >=16.0.0"
}
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@ -9774,6 +9792,27 @@
"node": ">=0.4"
}
},
"node_modules/xterm": {
"version": "4.19.0",
"resolved": "https://registry.npmmirror.com/xterm/-/xterm-4.19.0.tgz",
"integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ=="
},
"node_modules/xterm-addon-attach": {
"version": "0.6.0",
"resolved": "https://registry.npmmirror.com/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz",
"integrity": "sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==",
"peerDependencies": {
"xterm": "^4.0.0"
}
},
"node_modules/xterm-addon-fit": {
"version": "0.5.0",
"resolved": "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
"integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==",
"peerDependencies": {
"xterm": "^4.0.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@ -14390,6 +14429,11 @@
}
}
},
"js-base64": {
"version": "3.7.2",
"resolved": "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.2.tgz",
"integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ=="
},
"js-md5": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
@ -15942,6 +15986,11 @@
}
}
},
"screenfull": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz",
"integrity": "sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw=="
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@ -17428,6 +17477,23 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
},
"xterm": {
"version": "4.19.0",
"resolved": "https://registry.npmmirror.com/xterm/-/xterm-4.19.0.tgz",
"integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ=="
},
"xterm-addon-attach": {
"version": "0.6.0",
"resolved": "https://registry.npmmirror.com/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz",
"integrity": "sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==",
"requires": {}
},
"xterm-addon-fit": {
"version": "0.5.0",
"resolved": "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
"integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==",
"requires": {}
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -36,3 +36,7 @@ export const GetFileContent = (params: File.ReqFile) => {
export const SaveFileContent = (params: File.FileEdit) => {
return http.post<File.File>('files/save', params);
};
export const UploadFileData = (params: FormData) => {
return http.post<File.File>('files/upload', params);
};

View file

@ -2,14 +2,12 @@ export default {
commons: {
button: {
create: '新建',
create: '创建',
add: '添加',
delete: '删除',
edit: '编辑',
confirm: '确认',
cancel: '取消',
reset: '重置',
login: '登陆',
conn: '连接',
login: '登录',
},
@ -197,5 +195,6 @@ export default {
softLink: '软链接',
hardLink: '硬链接',
linkPath: '链接路径',
selectFile: '选择文件',
},
};

View file

@ -1,12 +1,22 @@
<template>
<el-dialog v-model="open" :title="'code editor'" @opened="onOpen" :before-close="handleClose">
<el-dialog
v-model="open"
:title="$t('commons.button.edit')"
@opened="onOpen"
:before-close="handleClose"
destroy-on-close
width="70%"
draggable
>
<div>
<div id="codeBox" style="height: 600px"></div>
<div v-loading="loading">
<div id="codeBox" style="height: 60vh"></div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="saveContent">{{ $t('commons.button.confirm') }}</el-button>
<el-button type="primary" @click="save()">{{ $t('commons.button.confirm') }}</el-button>
</span>
</template>
</el-dialog>
@ -31,6 +41,10 @@ const props = defineProps({
type: String,
default: '',
},
loading: {
type: Boolean,
default: false,
},
});
let data = reactive({
@ -38,7 +52,7 @@ let data = reactive({
language: '',
});
const em = defineEmits(['close', 'save']);
const em = defineEmits(['close', 'qsave', 'save']);
const handleClose = () => {
if (editor) {
@ -47,6 +61,14 @@ const handleClose = () => {
em('close', false);
};
const save = () => {
em('save', data.content);
};
const saveNotClose = () => {
em('qsave', data.content);
};
const initEditor = () => {
if (editor) {
editor.dispose();
@ -67,10 +89,8 @@ const initEditor = () => {
data.content = editor.getValue();
}
});
};
const saveContent = () => {
em('save', data.content);
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, saveNotClose);
};
const onOpen = () => {

View file

@ -65,9 +65,9 @@ const props = defineProps({
});
const rules = reactive<FormRules>({
type: [Rules.required],
dst: [Rules.required],
name: [Rules.required],
type: [Rules.requiredSelect],
dst: [Rules.requiredInput],
name: [Rules.requiredInput],
});
const { open, files, type, dst, name } = toRefs(props);

View file

@ -71,10 +71,10 @@ const handleClose = () => {
};
const rules = reactive<FormRules>({
name: [Rules.required],
path: [Rules.required],
isSymlink: [Rules.required],
linkPath: [Rules.required],
name: [Rules.requiredInput],
path: [Rules.requiredInput],
isSymlink: [Rules.requiredInput],
linkPath: [Rules.requiredInput],
});
const getMode = (val: number) => {

View file

@ -60,7 +60,7 @@ const props = defineProps({
});
const rules = reactive<FormRules>({
dst: [Rules.required],
dst: [Rules.requiredInput],
});
const { open, dst, path, name, mimeType } = toRefs(props);

View file

@ -2,7 +2,7 @@
<LayoutContent>
<el-row :gutter="20">
<el-col :span="5">
<el-scrollbar height="800px">
<el-scrollbar height="80vh">
<el-tree
:data="fileTree"
:props="defaultProps"
@ -57,7 +57,7 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="primary" plain> {{ $t('file.upload') }}</el-button>
<el-button type="primary" plain @click="openUpload"> {{ $t('file.upload') }}</el-button>
<el-button type="primary" plain> {{ $t('file.search') }}</el-button>
<el-button type="primary" plain> {{ $t('file.remoteFile') }}</el-button>
<!-- <el-button type="primary" plain> {{ $t('file.sync') }}</el-button>
@ -85,6 +85,7 @@
prop="modTime"
:formatter="dateFromat"
min-width="100"
show-overflow-tooltip
>
</el-table-column>
@ -118,9 +119,12 @@
:open="editorPage.open"
:language="'json'"
:content="editorPage.content"
:loading="editorPage.loading"
@close="closeCodeEditor"
@qsave="quickSave"
@save="saveContent"
></CodeEditor>
<Upload :open="uploadPage.open" :path="uploadPage.path" @close="closeUpload"></Upload>
</el-row>
</LayoutContent>
</template>
@ -139,8 +143,10 @@ import CreateFile from './create/index.vue';
import ChangeRole from './change-role/index.vue';
import Compress from './compress/index.vue';
import Decompress from './decompress/index.vue';
import Upload from './upload/index.vue';
import { useDeleteData } from '@/hooks/use-delete-data';
import CodeEditor from './code-editor/index.vue';
import { ElMessage } from 'element-plus';
let data = ref();
let selects = ref<any>([]);
@ -155,8 +161,9 @@ let filePage = reactive({ open: false, createForm: { path: '/', isDir: false, mo
let modePage = reactive({ open: false, modeForm: { path: '/', isDir: false, mode: 0o755 } });
let compressPage = reactive({ open: false, files: [''], name: '', dst: '' });
let deCompressPage = reactive({ open: false, path: '', name: '', dst: '', mimeType: '' });
let editorPage = reactive({ open: false, content: '' });
let editorPage = reactive({ open: false, content: '', loading: false });
let codeReq = reactive({ path: '', expand: false });
const uploadPage = reactive({ open: false, path: '' });
const defaultProps = {
children: 'children',
@ -321,9 +328,30 @@ const closeCodeEditor = () => {
editorPage.open = false;
};
const openUpload = () => {
uploadPage.open = true;
uploadPage.path = req.path;
};
const closeUpload = () => {
uploadPage.open = false;
search(req);
};
const saveContent = (content: string) => {
SaveFileContent({ path: codeReq.path, content: content }).then(() => {
editorPage.loading = true;
SaveFileContent({ path: codeReq.path, content: content }).finally(() => {
editorPage.loading = false;
editorPage.open = false;
ElMessage.success(i18n.global.t('commons.msg.updateSuccess'));
});
};
const quickSave = (content: string) => {
editorPage.loading = true;
SaveFileContent({ path: codeReq.path, content: content }).finally(() => {
editorPage.loading = false;
ElMessage.success(i18n.global.t('commons.msg.updateSuccess'));
});
};

View file

@ -0,0 +1,64 @@
<template>
<el-dialog v-model="open" :title="$t('file.upload')" @open="onOpen" :before-close="handleClose">
<el-upload action="#" :auto-upload="false" ref="uploadRef" :multiple="true" :on-change="fileOnChange">
<template #trigger>
<el-button type="primary">{{ $t('file.selectFile') }}</el-button>
</template>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit()">{{ $t('commons.button.confirm') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElMessage, UploadFile, UploadFiles, UploadInstance } from 'element-plus';
import { UploadFileData } from '@/api/modules/files';
const props = defineProps({
open: {
type: Boolean,
default: false,
},
path: {
type: String,
default: '',
},
});
const uploadRef = ref<UploadInstance>();
// let loading = ref<Boolean>(false);
const em = defineEmits(['close']);
const handleClose = () => {
em('close', false);
};
const uploaderFiles = ref<UploadFiles>([]);
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
uploaderFiles.value = uploadFiles;
};
const submit = () => {
const formData = new FormData();
for (const file of uploaderFiles.value) {
if (file.raw != undefined) {
formData.append('file', file.raw);
}
}
formData.append('path', props.path);
UploadFileData(formData).then(() => {
ElMessage('upload success');
handleClose();
});
};
const onOpen = () => {};
</script>