feat(文件管理): 上传组件调整&增加后台上传store

This commit is contained in:
baiqi 2023-09-11 18:17:14 +08:00 committed by 刘瑞斌
parent d350d9fa96
commit 4ef82f980b
14 changed files with 436 additions and 94 deletions

View File

@ -1,9 +1,9 @@
<template>
<a-config-provider :locale="locale">
<router-view />
<!-- <template #empty>
<template #empty>
<MsEmpty />
</template> -->
</template>
<!-- <global-setting /> -->
</a-config-provider>
</template>

View File

@ -675,3 +675,14 @@
.arco-trigger-arrow {
border-bottom-right-radius: var(--border-radius-mini) !important;
}
/** 消息通知 **/
.arco-notification-left {
padding-right: 8px;
.arco-notification-icon {
margin-top: 2px;
.arco-icon {
font-size: 20px;
}
}
}

View File

@ -41,6 +41,7 @@
color: rgb(var(--primary-5));
}
.ms-button-icon {
padding: 4px;
color: var(--color-text-4);
&:hover {
color: rgb(var(--primary-5));
@ -52,19 +53,19 @@
}
.ms-button--secondary {
color: var(--color-text-2);
&:hover {
&:not(.ms-button-text):hover {
background-color: var(--color-text-n8);
}
}
.ms-button--primary {
color: rgb(var(--primary-5));
&:hover {
&:not(.ms-button-text):hover {
background-color: rgb(var(--primary-9));
}
}
.ms-button--danger {
color: rgb(var(--danger-6));
&:hover {
&:not(.ms-button-text):hover {
color: rgb(var(--danger-6));
background-color: rgb(var(--danger-1));
}

View File

@ -0,0 +1,29 @@
<template>
<div class="flex flex-wrap items-center">
<div class="mr-[8px]">{{ props.content }}</div>
<MsButton v-if="props.showDetail" @click="goDetail">{{ t('ms.upload.detail') }}</MsButton>
</div>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import MsButton from '@/components/pure/ms-button/index.vue';
const props = withDefaults(
defineProps<{
mode?: 'message' | 'notification';
content: string;
showDetail?: boolean;
}>(),
{
mode: 'message',
showDetail: true,
}
);
const emit = defineEmits(['goDetail']);
const { t } = useI18n();
function goDetail() {
emit('goDetail');
}
</script>

View File

@ -2,9 +2,9 @@
<div class="sticky top-[0] z-[9999] mb-[8px] flex justify-between bg-white">
<a-radio-group v-model:model-value="fileListTab" type="button" size="small">
<a-radio value="all">{{ `${t('ms.upload.all')} (${innerFileList.length})` }}</a-radio>
<a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${waitingList.length})` }}</a-radio>
<a-radio value="success">{{ `${t('ms.upload.success')} (${successList.length})` }}</a-radio>
<a-radio value="error">{{ `${t('ms.upload.fail')} (${failList.length})` }}</a-radio>
<a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${totalWaitingFileList.length})` }}</a-radio>
<a-radio value="success">{{ `${t('ms.upload.success')} (${totalSuccessFileList.length})` }}</a-radio>
<a-radio value="error">{{ `${t('ms.upload.fail')} (${totalFailFileList.length})` }}</a-radio>
</a-radio-group>
<slot name="tabExtra"></slot>
</div>
@ -47,7 +47,7 @@
</div>
<a-progress
v-else-if="item.status === UploadStatus.uploading"
:percent="progress / 100"
:percent="asyncTaskStore.uploadFileTask.singleProgress / 100"
:show-text="false"
size="large"
class="w-[200px]"
@ -99,6 +99,7 @@
import dayjs from 'dayjs';
import { useI18n } from '@/hooks/useI18n';
import { formatFileSize } from '@/utils';
import useAsyncTaskStore from '@/store/modules/app/asyncTask';
import MsList from '@/components/pure/ms-list/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -109,6 +110,8 @@
const props = defineProps<{
fileList: MsFileItem[];
route?: string; //
routeQuery?: Record<string, string>; //
handleDelete?: (item: MsFileItem) => void;
handleReupload?: (item: MsFileItem) => void;
}>();
@ -119,6 +122,7 @@
(e: 'start'): void;
}>();
const asyncTaskStore = useAsyncTaskStore();
const { t } = useI18n();
const fileListTab = ref('all');
@ -146,102 +150,59 @@
}
);
const waitingList = computed(() => {
const totalWaitingFileList = computed(() => {
return innerFileList.value.filter(
(e) => e.status && (e.status === UploadStatus.init || e.status === UploadStatus.uploading)
);
});
const successList = computed(() => {
const totalSuccessFileList = computed(() => {
return innerFileList.value.filter((e) => e.status && e.status === UploadStatus.done);
});
const failList = computed(() => {
const totalFailFileList = computed(() => {
return innerFileList.value.filter((e) => e.status && e.status === UploadStatus.error);
});
const filterFileList = computed(() => {
switch (fileListTab.value) {
case 'waiting':
return waitingList.value;
return totalWaitingFileList.value;
case 'success':
return successList.value;
return totalSuccessFileList.value;
case 'error':
return failList.value;
return totalFailFileList.value;
default:
return innerFileList.value;
}
});
const uploadQueue = ref<MsFileItem[]>([]);
const progress = ref(0);
let timer: any = null;
/**
* 开始上传队列中的文件
* @param fileItem 文件项
*/
async function uploadFileFromQueue(fileItem?: MsFileItem) {
if (fileItem) {
fileItem.status = UploadStatus.uploading; //
}
if (timer === null) {
//
timer = setInterval(() => {
if (progress.value < 50) {
// 0-50%
const randomIncrement = Math.floor(Math.random() * 10) + 1; // 5-10
progress.value += randomIncrement;
} else if (progress.value < 100) {
// 50%-100%
const randomIncrement = Math.floor(Math.random() * 10) + 1; // 1-5
progress.value = Math.min(progress.value + randomIncrement, 99);
} else {
clearInterval(timer);
timer = null;
}
}, 100); // 100
}
try {
await new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, 3000);
});
if (fileItem?.file?.type.includes('jpeg')) {
throw new Error('上传失败');
}
if (fileItem) {
fileItem.status = UploadStatus.done;
fileItem.uploadedTime = Date.now();
}
} catch (error) {
console.log(error);
if (fileItem) {
fileItem.status = UploadStatus.error;
}
} finally {
// /
progress.value = 0;
clearInterval(timer);
timer = null;
if (uploadQueue.value.length > 0) {
//
uploadFileFromQueue(uploadQueue.value.shift());
} else {
emit('finish');
}
}
}
/**
* 开始上传
*/
function startUpload() {
emit('start');
// init
uploadQueue.value = innerFileList.value.filter((item) => item.status === UploadStatus.init);
uploadFileFromQueue(uploadQueue.value.shift());
asyncTaskStore.startUpload(innerFileList.value, props.route, props.routeQuery);
}
/**
* 后台上传
*/
function backstageUpload() {
asyncTaskStore.uploadFileTask.isBackstageUpload = true;
if (asyncTaskStore.uploadFileTask.uploadQueue.length === 0) {
// startUpload
startUpload();
}
}
watch(
() => asyncTaskStore.uploadFileTask.finishedTime,
(val) => {
if (val) {
emit('finish');
}
}
);
const previewVisible = ref(false);
const previewCurrent = ref(0);
@ -271,9 +232,9 @@
props.handleReupload(item);
} else {
item.status = UploadStatus.init;
if (uploadQueue.value.length > 0) {
if (asyncTaskStore.uploadFileTask.uploadQueue.length > 0) {
// push
uploadQueue.value.push(item);
asyncTaskStore.uploadFileTask.uploadQueue.push(item);
} else {
//
startUpload();
@ -283,14 +244,15 @@
//
onBeforeUnmount(() => {
if (timer !== null) {
clearInterval(timer);
timer = null;
if (asyncTaskStore.uploadFileTask.timer !== null) {
clearInterval(asyncTaskStore.uploadFileTask.timer);
asyncTaskStore.uploadFileTask.timer = null;
}
});
defineExpose({
startUpload,
backstageUpload,
});
</script>

View File

@ -7,6 +7,11 @@ export default {
'ms.upload.reUpload': 'Reupload',
'ms.upload.preview': 'Preview',
'ms.upload.uploadAt': 'Uploaded at',
'ms.upload.fail': 'Upload failed',
'ms.upload.fail': 'Failed',
'ms.upload.delete': 'Delete',
'ms.upload.uploadFail': 'Upload failed',
'ms.upload.all': 'All',
'ms.upload.uploading': 'Waiting/Uploading',
'ms.upload.success': 'Success',
'ms.upload.detail': 'Detail',
};

View File

@ -13,4 +13,5 @@ export default {
'ms.upload.uploading': '等待/上传中',
'ms.upload.success': '成功',
'ms.upload.fail': '失败',
'ms.upload.detail': '查看详情',
};

View File

@ -26,4 +26,8 @@ export default {
'api.errMsg503': 'The service is unavailable, the server is temporarily overloaded or maintained!',
'api.errMsg504': 'Network timeout!',
'api.errMsg505': 'The http version does not support the request!',
// 异步任务提示
'asyncTask.uploadFileProgress': 'File upload progress {percent}; {done} successful, {fail} failed',
'asyncTask.uploadFileSuccess': 'File upload completed: {done} successfully, {fail} failed',
'asyncTask.uploadFileSuccessTitle': 'Upload completed',
};

View File

@ -16,7 +16,7 @@ export default {
'api.responseError': '接口返回错误,请重试',
'api.requestError': '请求错误',
'api.errMsg401': '用户没有权限(令牌、用户名、密码错误)!',
'api.errMsg403': '用户得到授权,但是访问是被禁止的!',
'api.errMsg403': '用户得到授权,但是访问是被禁止的!',
'api.errMsg404': '网络请求错误,未找到该资源!',
'api.errMsg405': '网络请求错误,请求方法未允许!',
'api.errMsg408': '网络请求超时!',
@ -26,4 +26,8 @@ export default {
'api.errMsg503': '服务不可用,服务器暂时过载或维护!',
'api.errMsg504': '网络超时!',
'api.errMsg505': 'http版本不支持该请求!',
// 异步任务提示
'asyncTask.uploadFileProgress': '文件上传进度 {percent};成功 {done} 个,失败 {fail} 个',
'asyncTask.uploadFileSuccess': '文件上传完成:成功 {done} 个,失败 {fail} 个',
'asyncTask.uploadFileSuccessTitle': '上传完成',
};

View File

@ -0,0 +1,201 @@
import { h } from 'vue';
import { defineStore } from 'pinia';
import { Message, Notification } from '@arco-design/web-vue';
import { UploadStatus } from '@/enums/uploadEnum';
import { useI18n } from '@/hooks/useI18n';
import BackstageMsg from '@/components/pure/ms-upload/backstageMsg.vue';
import router from '@/router';
import type { AsyncTaskState } from './types';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
// 全局异步任务 store用于一些后台运行或者耗时任务展示任务的进度提示例如文件上传的后台上传任务、耗时的导出操作等。注意每次只能执行一个任务。TODO: 后续可以考虑支持多个任务
const useAsyncTaskStore = defineStore('asyncTask', {
state: (): AsyncTaskState => ({
uploadFileTask: {
// 上传文件的任务
isBackstageUpload: false, // 是否后台上传,后台上传会展示全局提示类型的进度提示
isHideMessage: false, // 后台上传时展示的消息提示在点击关闭后无需再弹
fileList: [], // 文件总队列,包含已经上传的历史记录(不做持久化存储,刷新丢失)
uploadQueue: [], // 每次添加的上传队列,用于展示每次任务的进度使用
eachTaskQueue: [], // 上传队列,每个文件上传完成后会从队列中移除,初始值为上传队列的副本
singleProgress: 0, // 单个上传文件的上传进度,非总进度,是每个文件在上传时的模拟进度
timer: null, // 上传进度定时器
finishedTime: null, // 任务完成时间
},
}),
getters: {
// totalXXX 为文件总队列计算出的数量,包含历史记录
totalWaitingFileList: (state: AsyncTaskState) => {
return state.uploadFileTask.fileList.filter(
(e) => e.status && (e.status === UploadStatus.init || e.status === UploadStatus.uploading)
);
},
totalSuccessFileList: (state: AsyncTaskState) => {
return state.uploadFileTask.fileList.filter((e) => e.status && e.status === UploadStatus.done);
},
totalFailFileList: (state: AsyncTaskState) => {
return state.uploadFileTask.fileList.filter((e) => e.status && e.status === UploadStatus.error);
},
// 下面的是每次上传任务计算的数量
eachWaitingFileList: (state: AsyncTaskState) => {
return state.uploadFileTask.eachTaskQueue.filter(
(e) => e.status && (e.status === UploadStatus.init || e.status === UploadStatus.uploading)
);
},
eachSuccessFileList: (state: AsyncTaskState) => {
return state.uploadFileTask.eachTaskQueue.filter((e) => e.status && e.status === UploadStatus.done);
},
eachFailFileList: (state: AsyncTaskState) => {
return state.uploadFileTask.eachTaskQueue.filter((e) => e.status && e.status === UploadStatus.error);
},
eachUploadTaskProgress(): number {
// 每次上传任务的总进度
const { uploadFileTask } = this;
const { eachTaskQueue } = uploadFileTask;
const total = eachTaskQueue.length;
if (total === 0) {
return 0;
}
return Math.floor((this.totalSuccessFileList.length / total) * 100);
},
},
actions: {
beforeEachUpload(fileItem?: MsFileItem, route?: string, routeQuery?: Record<string, any>) {
const { t } = useI18n();
const { uploadFileTask } = this;
if (uploadFileTask.isBackstageUpload && !uploadFileTask.isHideMessage) {
// 开启了后台下载模式,展示全局的进度提示,不模拟进度条
Message.info({
id: 'asyncTaskUploadFile',
content: () =>
h(BackstageMsg, {
content: t('asyncTask.uploadFileProgress', {
percent: `${uploadFileTask.eachTaskQueue.length - this.eachWaitingFileList.length} / ${
uploadFileTask.eachTaskQueue.length
}`,
done: this.eachSuccessFileList.length,
fail: this.eachFailFileList.length,
}),
onGoDetail() {
router.push({
name: route,
query: routeQuery,
});
Message.clear();
uploadFileTask.isBackstageUpload = false;
},
}),
duration: 999999999, // 一直展示,除非手动关闭
closable: true,
onClose() {
uploadFileTask.isHideMessage = true;
},
});
} else if (uploadFileTask.timer === null) {
// 模拟上传进度
uploadFileTask.timer = setInterval(() => {
if (uploadFileTask.singleProgress < 50) {
// 进度在0-50%之间较快
const randomIncrement = Math.floor(Math.random() * 10) + 1; // 随机增加 5-10 的百分比
uploadFileTask.singleProgress += randomIncrement;
} else if (uploadFileTask.singleProgress < 100) {
// 进度在50%-100%之间较慢
const randomIncrement = Math.floor(Math.random() * 10) + 1; // 随机增加 1-5 的百分比
uploadFileTask.singleProgress = Math.min(uploadFileTask.singleProgress + randomIncrement, 99);
} else {
clearInterval(uploadFileTask.timer as unknown as number);
uploadFileTask.timer = null;
}
}, 100); // 定时器间隔为 100 毫秒
}
if (fileItem) {
fileItem.status = UploadStatus.uploading; // 设置文件状态为上传中
}
uploadFileTask.finishedTime = null; // 重置任务完成时间
},
afterEachUploadTask(route?: string, routeQuery?: Record<string, any>) {
const { t } = useI18n();
const { uploadFileTask } = this;
if (uploadFileTask.timer) {
uploadFileTask.singleProgress = 0;
clearInterval(uploadFileTask.timer);
uploadFileTask.timer = null;
}
if (uploadFileTask.uploadQueue.length > 0) {
// 如果待上传队列中还有文件,继续上传
this.uploadFileFromQueue(uploadFileTask.uploadQueue.shift(), route, routeQuery);
} else {
uploadFileTask.finishedTime = Date.now();
Message.clear(); // 清除全局提示
if (uploadFileTask.isBackstageUpload) {
Notification.success({
title: t('asyncTask.uploadFileSuccessTitle'),
content: () =>
h(BackstageMsg, {
content: t('asyncTask.uploadFileSuccess', {
done: this.eachSuccessFileList.length,
fail: this.eachFailFileList.length,
}),
onGoDetail() {
router.push({
name: route,
query: routeQuery,
});
Notification.clear();
uploadFileTask.isBackstageUpload = false;
},
}),
style: { width: 'auto' },
closable: true,
duration: 3000,
});
}
this.$patch({
uploadFileTask: {
isBackstageUpload: false,
isHideMessage: false,
},
});
}
},
async uploadFileFromQueue(fileItem?: MsFileItem, route?: string, routeQuery?: Record<string, any>) {
this.beforeEachUpload(fileItem, route, routeQuery);
try {
// TODO: 模拟上传,待接口联调后替换为真实上传逻辑
await new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, 3000);
});
if (fileItem) {
fileItem.status = UploadStatus.done;
fileItem.uploadedTime = Date.now();
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
if (fileItem) {
fileItem.status = UploadStatus.error;
}
} finally {
// 上传完成/失败,重置进度和定时器
this.afterEachUploadTask(route, routeQuery);
}
},
startUpload(fileList: MsFileItem[], route?: string, routeQuery?: Record<string, any>) {
// 正式开始上传任务之前,同步一次文件列表,取出所有状态为 init 的文件
const totalWaitingFileList = fileList.filter((item) => item.status === UploadStatus.init);
this.$patch({
uploadFileTask: {
fileList,
uploadQueue: totalWaitingFileList,
eachTaskQueue: [...totalWaitingFileList],
},
});
this.uploadFileFromQueue(this.uploadFileTask.uploadQueue.shift(), route, routeQuery);
},
},
});
export default useAsyncTaskStore;

View File

@ -1,6 +1,7 @@
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
import type { PageConfig, ThemeConfig, LoginConfig, PlatformConfig } from '@/models/setting/config';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
export interface AppState {
colorWeak: boolean;
@ -33,4 +34,17 @@ export interface AppState {
pageConfig: PageConfig;
}
export type CustomTheme = 'theme-default' | 'theme-green';
export interface UploadFileTaskState {
isBackstageUpload: boolean; // 是否后台上传,后台上传会展示全局提示类型的进度提示
isHideMessage: boolean; // 后台上传时展示的消息提示在点击关闭后无需再弹
fileList: MsFileItem[]; // 文件总队列,包含已经上传的历史记录(不做持久化存储,刷新丢失)
eachTaskQueue: MsFileItem[]; // 每次添加的上传队列,用于展示每次任务的进度使用
uploadQueue: MsFileItem[]; // 上传队列,每个文件上传完成后会从队列中移除,初始值为上传队列的副本
singleProgress: number; // 单个上传文件的上传进度,非总进度,是每个文件在上传时的模拟进度
timer: NodeJS.Timer | null; // 上传进度定时器
finishedTime: number | null; // 任务完成时间
}
export interface AsyncTaskState {
uploadFileTask: UploadFileTaskState;
}

View File

@ -5,7 +5,7 @@ export const phoneRegex = /^\d{11}$/;
// 密码校验8-32位
export const passwordLengthRegex = /^.{8,32}$/;
// 密码校验,必须包含数字和字母
export const passwordWordRegex = /^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$/;
export const passwordWordRegex = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9!@#$%^&*]+$/;
// Git地址校验
export const gitRepositoryUrlRegex =
/^(?:(?:git:\/\/|https?:\/\/)(?:www\.)?)?(github\.com|gitee\.com)\/([^/]+)\/([^/]+)\.git$/;

View File

@ -86,7 +86,14 @@
class="mb-[16px] w-full"
@change="handleFileChange"
/>
<MsFileList ref="fileListRef" v-model:file-list="fileList" @start="handleUploadStart" @finish="uploadFinish">
<MsFileList
ref="fileListRef"
v-model:file-list="fileList"
:route="RouteEnum.PROJECT_MANAGEMENT_FILE_MANAGEMENT"
:route-query="{ position: 'uploadDrawer' }"
@start="handleUploadStart"
@finish="uploadFinish"
>
<template #tabExtra>
<div v-if="acceptType === 'jar'" class="flex items-center gap-[4px]">
<a-switch size="small" @change="enableAllJar"></a-switch>
@ -104,10 +111,10 @@
<a-button type="secondary" @click="uploadDrawerVisible = false">
{{ t('project.fileManagement.cancel') }}
</a-button>
<a-button type="secondary" :disabled="noWaitingUpload" @click="uploadDrawerVisible = false">
<a-button type="secondary" :disabled="noWaitingUpload" @click="backstageUpload">
{{ t('project.fileManagement.backendUpload') }}
</a-button>
<a-button type="primary" :disabled="noWaitingUpload || isUploading" @click="startUpload">
<a-button type="primary" :disabled="isUploading || noWaitingUpload" @click="startUpload">
{{ t('project.fileManagement.startUpload') }}
</a-button>
</template>
@ -115,7 +122,8 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { debounce } from 'lodash-es';
import { useI18n } from '@/hooks/useI18n';
import useTableStore from '@/store/modules/ms-table';
@ -129,6 +137,8 @@
import MsUpload from '@/components/pure/ms-upload/index.vue';
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import { UploadStatus } from '@/enums/uploadEnum';
import { RouteEnum } from '@/enums/routeEnum';
import useAsyncTaskStore from '@/store/modules/app/asyncTask';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import type { MsFileItem, UploadType } from '@/components/pure/ms-upload/types';
@ -137,7 +147,10 @@
activeFolder: string | number;
activeFolderType: 'folder' | 'module' | 'storage';
}>();
const route = useRoute();
const { t } = useI18n();
const asyncTaskStore = useAsyncTaskStore();
const keyword = ref('');
const fileType = ref('module');
@ -232,7 +245,7 @@
async function openFileDetail(id: string) {}
const uploadDrawerVisible = ref(false);
const fileList = ref<MsFileItem[]>([]);
const fileList = ref<MsFileItem[]>(asyncTaskStore.uploadFileTask.fileList);
const noWaitingUpload = computed(
() =>
@ -278,6 +291,11 @@
isUploading.value = true;
}
function backstageUpload() {
fileListRef.value?.backstageUpload();
uploadDrawerVisible.value = false;
}
function startUpload() {
fileListRef.value?.startUpload();
}
@ -285,6 +303,33 @@
function uploadFinish() {
isUploading.value = false;
}
type RouteQueryPosition = 'uploadDrawer' | null;
onBeforeMount(() => {
if (route.query.position) {
switch (
route.query.position as RouteQueryPosition //
) {
case 'uploadDrawer':
uploadDrawerVisible.value = true;
break;
default:
break;
}
}
});
// onBeforeMount
watch(
() => asyncTaskStore.uploadFileTask.isBackstageUpload,
(val) => {
if (!val && route.query.position === 'uploadDrawer') {
uploadDrawerVisible.value = true;
}
}
);
</script>
<style lang="less" scoped>

View File

@ -1 +1,66 @@
export default {};
export default {
'project.fileManagement.myFile': 'My files',
'project.fileManagement.allFile': 'All files',
'project.fileManagement.defaultFile': 'Default files',
'project.fileManagement.expandAll': 'Expand all submodules',
'project.fileManagement.collapseAll': 'Collapse all submodules',
'project.fileManagement.addSubModule': 'Add module',
'project.fileManagement.addStorage': 'Add repository',
'project.fileManagement.rename': 'Rename',
'project.fileManagement.nameNotNull': 'name cannot be empty',
'project.fileManagement.namePlaceholder': 'Please enter the group name and press Enter to save',
'project.fileManagement.renameSuccess': 'Rename successful',
'project.fileManagement.addSubModuleSuccess': 'Added successfully',
'project.fileManagement.nameExist': 'This module name already exists at this level',
'project.fileManagement.module': 'Module',
'project.fileManagement.storage': 'Repository',
'project.fileManagement.folderSearchPlaceholder': 'Enter name to search',
'project.fileManagement.delete': 'Delete',
'project.fileManagement.deleteSuccess': 'Delete successful',
'project.fileManagement.deleteFolderTipTitle': 'Remove the `{name}` module?',
'project.fileManagement.deleteFolderTipContent':
'This operation will delete the module and all resources under it, please operate with caution!',
'project.fileManagement.deleteConfirm': 'Confirm delete',
'project.fileManagement.noFolder': 'No matching related modules.',
'project.fileManagement.noStorage': 'No matching related repository',
'project.fileManagement.addFile': 'Add files',
'project.fileManagement.deleteStorageTipTitle': 'Delete the `{name}` repository?',
'project.fileManagement.deleteStorageTipContent':
'This operation will delete this repository and all resources under it, please operate with caution!',
'project.fileManagement.updateStorageTitle': 'Edit repository',
'project.fileManagement.save': 'Save',
'project.fileManagement.add': 'Add',
'project.fileManagement.edit': 'Edit',
'project.fileManagement.cancel': 'Cancel',
'project.fileManagement.testLink': 'Test connection',
'project.fileManagement.storageName': 'Repository name',
'project.fileManagement.storageNamePlaceholder': 'Please enter a repository name',
'project.fileManagement.storageNameNotNull': 'Repository name cannot be empty',
'project.fileManagement.storagePlatform': 'Docking platform',
'project.fileManagement.storageUrl': 'Repository address',
'project.fileManagement.storageExUrl': 'Example: {url}',
'project.fileManagement.storageUrlPlaceholder': 'Please enter the repository address',
'project.fileManagement.storageUrlNotNull': 'Repository address cannot be empty',
'project.fileManagement.storageUrlError': 'Repository address format is wrong',
'project.fileManagement.storageToken': 'Token',
'project.fileManagement.storageTokenPlaceholder': 'Please enter Token',
'project.fileManagement.storageTokenNotNull': 'Token can not be empty',
'project.fileManagement.storageUsername': 'Username',
'project.fileManagement.storageUsernamePlaceholder': 'Please enter user name',
'project.fileManagement.storageUsernameNotNull': 'Username can not be empty',
'project.fileManagement.tableNoFile': 'No data yet, please',
'project.fileManagement.fileType': 'File type',
'project.fileManagement.normalFile': 'Regular files',
'project.fileManagement.normalFileDesc': 'All file types',
'project.fileManagement.jarFile': 'JAR files',
'project.fileManagement.jarFileDesc': 'Files used for interface testing',
'project.fileManagement.uploadTip':
'Interface test script execution needs to be enabled, which can be enabled with one click; files can be opened individually',
'project.fileManagement.fileTypeTip': 'Switch the file type and the selected/uploaded file list will be cleared.',
'project.fileManagement.normalFileSubText': 'Supports any file type up to {size} MB in size',
'project.fileManagement.enableAll': 'Turn all on',
'project.fileManagement.uploadingTip': 'File type cannot be changed during upload',
'project.fileManagement.emptyFileList': 'No files yet',
'project.fileManagement.backendUpload': 'Backstage upload',
'project.fileManagement.startUpload': 'Start upload',
};