1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-16 18:54:43 +08:00

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 package v1
import ( import (
"fmt"
"github.com/1Panel-dev/1Panel/app/api/v1/helper" "github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto" "github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant" "github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"path"
) )
func (b *BaseApi) ListFiles(c *gin.Context) { func (b *BaseApi) ListFiles(c *gin.Context) {
@ -131,3 +134,23 @@ func (b *BaseApi) SaveContent(c *gin.Context) {
} }
helper.SuccessWithData(c, nil) 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() 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) { func GetParamID(c *gin.Context) (uint, error) {
idParam, ok := c.Params.Get("id") idParam, ok := c.Params.Get("id")
if !ok { if !ok {

View File

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

View File

@ -15,6 +15,7 @@
"echarts-liquidfill": "^3.1.0", "echarts-liquidfill": "^3.1.0",
"element-plus": "^2.2.13", "element-plus": "^2.2.13",
"fit2cloud-ui-plus": "^0.0.1-beta.15", "fit2cloud-ui-plus": "^0.0.1-beta.15",
"js-base64": "^3.7.2",
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"monaco-editor": "^0.34.0", "monaco-editor": "^0.34.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
@ -22,12 +23,16 @@
"pinia-plugin-persistedstate": "^1.6.1", "pinia-plugin-persistedstate": "^1.6.1",
"qs": "^6.10.3", "qs": "^6.10.3",
"sass-loader": "^13.0.2", "sass-loader": "^13.0.2",
"screenfull": "^6.0.2",
"unplugin-vue-define-options": "^0.7.3", "unplugin-vue-define-options": "^0.7.3",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.25", "vue": "^3.2.25",
"vue-i18n": "^9.1.9", "vue-i18n": "^9.1.9",
"vue-router": "^4.0.12", "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": { "devDependencies": {
"@commitlint/cli": "^17.0.1", "@commitlint/cli": "^17.0.1",
@ -5863,6 +5868,11 @@
"node": ">=10" "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": { "node_modules/js-md5": {
"version": "0.7.3", "version": "0.7.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
@ -7806,6 +7816,14 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"peer": true "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": { "node_modules/select": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@ -9774,6 +9792,27 @@
"node": ">=0.4" "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": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "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": { "js-md5": {
"version": "0.7.3", "version": "0.7.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", "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": { "select": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@ -17428,6 +17477,23 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true "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": { "y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "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) => { export const SaveFileContent = (params: File.FileEdit) => {
return http.post<File.File>('files/save', params); 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: { commons: {
button: { button: {
create: '新建', create: '新建',
create: '创建',
add: '添加', add: '添加',
delete: '删除', delete: '删除',
edit: '编辑', edit: '编辑',
confirm: '确认', confirm: '确认',
cancel: '取消', cancel: '取消',
reset: '重置', reset: '重置',
login: '登陆',
conn: '连接', conn: '连接',
login: '登录', login: '登录',
}, },
@ -197,5 +195,6 @@ export default {
softLink: '软链接', softLink: '软链接',
hardLink: '硬链接', hardLink: '硬链接',
linkPath: '链接路径', linkPath: '链接路径',
selectFile: '选择文件',
}, },
}; };

View File

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

View File

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

View File

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

View File

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

View File

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