mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
feat: 增加上传文件夹功能 (#2661)
Refs https://github.com/1Panel-dev/1Panel/issues/1364 Refs https://github.com/1Panel-dev/1Panel/issues/2053
This commit is contained in:
parent
f30f561723
commit
b045261f16
@ -568,12 +568,13 @@ func (b *BaseApi) Size(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int) error {
|
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int) error {
|
||||||
if _, err := os.Stat(path.Dir(dstDir)); err != nil && os.IsNotExist(err) {
|
op := files.NewFileOp()
|
||||||
if err = os.MkdirAll(path.Dir(dstDir), os.ModePerm); err != nil {
|
dstDir = strings.TrimSpace(dstDir)
|
||||||
|
if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) {
|
||||||
|
if err = op.CreateDir(dstDir, os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
targetFile, err := os.Create(filepath.Join(dstDir, fileName))
|
targetFile, err := os.Create(filepath.Join(dstDir, fileName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -975,6 +975,7 @@ const message = {
|
|||||||
favorite: 'favorites',
|
favorite: 'favorites',
|
||||||
removeFavorite: 'Remove from favorites?',
|
removeFavorite: 'Remove from favorites?',
|
||||||
addFavorite: 'Add to favorites',
|
addFavorite: 'Add to favorites',
|
||||||
|
clearList: 'Clear list',
|
||||||
},
|
},
|
||||||
ssh: {
|
ssh: {
|
||||||
autoStart: 'Auto Start',
|
autoStart: 'Auto Start',
|
||||||
|
@ -936,6 +936,7 @@ const message = {
|
|||||||
favorite: '收藏夾',
|
favorite: '收藏夾',
|
||||||
removeFavorite: '是否從收藏夾移出?',
|
removeFavorite: '是否從收藏夾移出?',
|
||||||
addFavorite: '加入收藏夾',
|
addFavorite: '加入收藏夾',
|
||||||
|
clearList: '清空列表',
|
||||||
},
|
},
|
||||||
ssh: {
|
ssh: {
|
||||||
autoStart: '開機自啟',
|
autoStart: '開機自啟',
|
||||||
|
@ -937,6 +937,7 @@ const message = {
|
|||||||
favorite: '收藏夹',
|
favorite: '收藏夹',
|
||||||
removeFavorite: '是否从收藏夹移出?',
|
removeFavorite: '是否从收藏夹移出?',
|
||||||
addFavorite: '加入收藏夹子',
|
addFavorite: '加入收藏夹子',
|
||||||
|
clearList: '清空列表',
|
||||||
},
|
},
|
||||||
ssh: {
|
ssh: {
|
||||||
autoStart: '开机自启',
|
autoStart: '开机自启',
|
||||||
|
@ -53,6 +53,7 @@ const forceDelete = ref(false);
|
|||||||
const acceptParams = (props: File.File[]) => {
|
const acceptParams = (props: File.File[]) => {
|
||||||
files.value = props;
|
files.value = props;
|
||||||
open.value = true;
|
open.value = true;
|
||||||
|
forceDelete.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConfirm = () => {
|
const onConfirm = () => {
|
||||||
|
@ -9,6 +9,26 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<DrawerHeader :header="$t('file.upload')" :back="handleClose" />
|
<DrawerHeader :header="$t('file.upload')" :back="handleClose" />
|
||||||
</template>
|
</template>
|
||||||
|
<div class="button-container">
|
||||||
|
<el-dropdown @command="upload">
|
||||||
|
<el-button type="primary">
|
||||||
|
{{ $t('file.upload') }}
|
||||||
|
<el-icon><arrow-down /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="file">
|
||||||
|
{{ $t('file.file') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="dir">
|
||||||
|
{{ $t('file.dir') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<el-button @click="clearFiles">{{ $t('file.clearList') }}</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-upload
|
<el-upload
|
||||||
action="#"
|
action="#"
|
||||||
drag
|
drag
|
||||||
@ -17,8 +37,9 @@
|
|||||||
:on-change="fileOnChange"
|
:on-change="fileOnChange"
|
||||||
:on-exceed="handleExceed"
|
:on-exceed="handleExceed"
|
||||||
:on-success="hadleSuccess"
|
:on-success="hadleSuccess"
|
||||||
show-file-list
|
:show-file-list="false"
|
||||||
multiple
|
multiple
|
||||||
|
v-model:file-list="uploaderFiles"
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="el-upload__text">
|
<div class="el-upload__text">
|
||||||
@ -30,6 +51,31 @@
|
|||||||
<el-progress v-if="loading" text-inside :stroke-width="20" :percentage="uploadPrecent"></el-progress>
|
<el-progress v-if="loading" text-inside :stroke-width="20" :percentage="uploadPrecent"></el-progress>
|
||||||
</template>
|
</template>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
v-for="(item, index) in uploaderFiles"
|
||||||
|
:key="index"
|
||||||
|
class="file-item"
|
||||||
|
@mouseover="hoverIndex = index"
|
||||||
|
@mouseout="hoverIndex = null"
|
||||||
|
>
|
||||||
|
<el-icon class="file-icon"><Document /></el-icon>
|
||||||
|
<span v-if="item.raw.webkitRelativePath != ''">{{ item.raw.webkitRelativePath }}</span>
|
||||||
|
<span v-else>{{ item.name }}</span>
|
||||||
|
<span v-if="item.status === 'success'" class="success-icon">
|
||||||
|
<el-icon><Select /></el-icon>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<el-button
|
||||||
|
class="delete-button"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="removeFile(index)"
|
||||||
|
:icon="Close"
|
||||||
|
></el-button>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
@ -42,12 +88,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { nextTick, reactive, ref } from 'vue';
|
||||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
|
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
|
||||||
import { ChunkUploadFileData, UploadFileData } from '@/api/modules/files';
|
import { ChunkUploadFileData, UploadFileData } from '@/api/modules/files';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
import { Close } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
interface UploadFileProps {
|
interface UploadFileProps {
|
||||||
path: string;
|
path: string;
|
||||||
@ -66,13 +113,35 @@ const handleClose = () => {
|
|||||||
uploadRef.value!.clearFiles();
|
uploadRef.value!.clearFiles();
|
||||||
em('close', false);
|
em('close', false);
|
||||||
};
|
};
|
||||||
|
const state = reactive({
|
||||||
|
uploadEle: null,
|
||||||
|
});
|
||||||
const uploaderFiles = ref<UploadFiles>([]);
|
const uploaderFiles = ref<UploadFiles>([]);
|
||||||
|
const isUploadFolder = ref(false);
|
||||||
|
const hoverIndex = ref(null);
|
||||||
|
|
||||||
|
const upload = (commnad: string) => {
|
||||||
|
if (commnad == 'dir') {
|
||||||
|
state.uploadEle.webkitdirectory = true;
|
||||||
|
} else {
|
||||||
|
state.uploadEle.webkitdirectory = false;
|
||||||
|
}
|
||||||
|
isUploadFolder.value = true;
|
||||||
|
uploadRef.value.$el.querySelector('input').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
uploaderFiles.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||||
uploaderFiles.value = uploadFiles;
|
uploaderFiles.value = uploadFiles;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearFiles = () => {
|
||||||
|
uploadRef.value!.clearFiles();
|
||||||
|
};
|
||||||
|
|
||||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||||
uploadRef.value!.clearFiles();
|
uploadRef.value!.clearFiles();
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
@ -111,7 +180,11 @@ const submit = async () => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append('filename', file.name);
|
formData.append('filename', file.name);
|
||||||
formData.append('path', path.value);
|
if (file.raw.webkitRelativePath != '') {
|
||||||
|
formData.append('path', path.value + '/' + getPathWithoutFilename(file.raw.webkitRelativePath));
|
||||||
|
} else {
|
||||||
|
formData.append('path', path.value);
|
||||||
|
}
|
||||||
formData.append('chunk', chunk);
|
formData.append('chunk', chunk);
|
||||||
formData.append('chunkIndex', c.toString());
|
formData.append('chunkIndex', c.toString());
|
||||||
formData.append('chunkCount', chunkCount.toString());
|
formData.append('chunkCount', chunkCount.toString());
|
||||||
@ -149,12 +222,58 @@ const submit = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPathWithoutFilename = (path: string) => {
|
||||||
|
return path ? path.split('/').slice(0, -1).join('/') : path;
|
||||||
|
};
|
||||||
|
|
||||||
const acceptParams = (props: UploadFileProps) => {
|
const acceptParams = (props: UploadFileProps) => {
|
||||||
path.value = props.path;
|
path.value = props.path;
|
||||||
open.value = true;
|
open.value = true;
|
||||||
uploadPrecent.value = 0;
|
uploadPrecent.value = 0;
|
||||||
uploadHelper.value = '';
|
uploadHelper.value = '';
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const uploadEle = document.querySelector('.el-upload__input');
|
||||||
|
state.uploadEle = uploadEle;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ acceptParams });
|
defineExpose({ acceptParams });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
color: green;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user