feat(文件管理): 上传组件调整&增加后台上传store
This commit is contained in:
parent
d350d9fa96
commit
4ef82f980b
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<a-config-provider :locale="locale">
|
<a-config-provider :locale="locale">
|
||||||
<router-view />
|
<router-view />
|
||||||
<!-- <template #empty>
|
<template #empty>
|
||||||
<MsEmpty />
|
<MsEmpty />
|
||||||
</template> -->
|
</template>
|
||||||
<!-- <global-setting /> -->
|
<!-- <global-setting /> -->
|
||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -675,3 +675,14 @@
|
||||||
.arco-trigger-arrow {
|
.arco-trigger-arrow {
|
||||||
border-bottom-right-radius: var(--border-radius-mini) !important;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
color: rgb(var(--primary-5));
|
color: rgb(var(--primary-5));
|
||||||
}
|
}
|
||||||
.ms-button-icon {
|
.ms-button-icon {
|
||||||
|
padding: 4px;
|
||||||
color: var(--color-text-4);
|
color: var(--color-text-4);
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgb(var(--primary-5));
|
color: rgb(var(--primary-5));
|
||||||
|
@ -52,19 +53,19 @@
|
||||||
}
|
}
|
||||||
.ms-button--secondary {
|
.ms-button--secondary {
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
&:hover {
|
&:not(.ms-button-text):hover {
|
||||||
background-color: var(--color-text-n8);
|
background-color: var(--color-text-n8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ms-button--primary {
|
.ms-button--primary {
|
||||||
color: rgb(var(--primary-5));
|
color: rgb(var(--primary-5));
|
||||||
&:hover {
|
&:not(.ms-button-text):hover {
|
||||||
background-color: rgb(var(--primary-9));
|
background-color: rgb(var(--primary-9));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ms-button--danger {
|
.ms-button--danger {
|
||||||
color: rgb(var(--danger-6));
|
color: rgb(var(--danger-6));
|
||||||
&:hover {
|
&:not(.ms-button-text):hover {
|
||||||
color: rgb(var(--danger-6));
|
color: rgb(var(--danger-6));
|
||||||
background-color: rgb(var(--danger-1));
|
background-color: rgb(var(--danger-1));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="sticky top-[0] z-[9999] mb-[8px] flex justify-between bg-white">
|
<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-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="all">{{ `${t('ms.upload.all')} (${innerFileList.length})` }}</a-radio>
|
||||||
<a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${waitingList.length})` }}</a-radio>
|
<a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${totalWaitingFileList.length})` }}</a-radio>
|
||||||
<a-radio value="success">{{ `${t('ms.upload.success')} (${successList.length})` }}</a-radio>
|
<a-radio value="success">{{ `${t('ms.upload.success')} (${totalSuccessFileList.length})` }}</a-radio>
|
||||||
<a-radio value="error">{{ `${t('ms.upload.fail')} (${failList.length})` }}</a-radio>
|
<a-radio value="error">{{ `${t('ms.upload.fail')} (${totalFailFileList.length})` }}</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<slot name="tabExtra"></slot>
|
<slot name="tabExtra"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
</div>
|
</div>
|
||||||
<a-progress
|
<a-progress
|
||||||
v-else-if="item.status === UploadStatus.uploading"
|
v-else-if="item.status === UploadStatus.uploading"
|
||||||
:percent="progress / 100"
|
:percent="asyncTaskStore.uploadFileTask.singleProgress / 100"
|
||||||
:show-text="false"
|
:show-text="false"
|
||||||
size="large"
|
size="large"
|
||||||
class="w-[200px]"
|
class="w-[200px]"
|
||||||
|
@ -99,6 +99,7 @@
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { formatFileSize } from '@/utils';
|
import { formatFileSize } from '@/utils';
|
||||||
|
import useAsyncTaskStore from '@/store/modules/app/asyncTask';
|
||||||
import MsList from '@/components/pure/ms-list/index.vue';
|
import MsList from '@/components/pure/ms-list/index.vue';
|
||||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
@ -109,6 +110,8 @@
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fileList: MsFileItem[];
|
fileList: MsFileItem[];
|
||||||
|
route?: string; // 用于后台上传文件时,查看详情跳转的路由
|
||||||
|
routeQuery?: Record<string, string>; // 用于后台上传文件时,查看详情跳转的路由参数
|
||||||
handleDelete?: (item: MsFileItem) => void;
|
handleDelete?: (item: MsFileItem) => void;
|
||||||
handleReupload?: (item: MsFileItem) => void;
|
handleReupload?: (item: MsFileItem) => void;
|
||||||
}>();
|
}>();
|
||||||
|
@ -119,6 +122,7 @@
|
||||||
(e: 'start'): void;
|
(e: 'start'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const asyncTaskStore = useAsyncTaskStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const fileListTab = ref('all');
|
const fileListTab = ref('all');
|
||||||
|
@ -146,102 +150,59 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const waitingList = computed(() => {
|
const totalWaitingFileList = computed(() => {
|
||||||
return innerFileList.value.filter(
|
return innerFileList.value.filter(
|
||||||
(e) => e.status && (e.status === UploadStatus.init || e.status === UploadStatus.uploading)
|
(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);
|
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);
|
return innerFileList.value.filter((e) => e.status && e.status === UploadStatus.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterFileList = computed(() => {
|
const filterFileList = computed(() => {
|
||||||
switch (fileListTab.value) {
|
switch (fileListTab.value) {
|
||||||
case 'waiting':
|
case 'waiting':
|
||||||
return waitingList.value;
|
return totalWaitingFileList.value;
|
||||||
case 'success':
|
case 'success':
|
||||||
return successList.value;
|
return totalSuccessFileList.value;
|
||||||
case 'error':
|
case 'error':
|
||||||
return failList.value;
|
return totalFailFileList.value;
|
||||||
default:
|
default:
|
||||||
return innerFileList.value;
|
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() {
|
function startUpload() {
|
||||||
emit('start');
|
emit('start');
|
||||||
// 正式开始上传任务之前,同步一次文件列表,取出所有状态为 init 的文件
|
asyncTaskStore.startUpload(innerFileList.value, props.route, props.routeQuery);
|
||||||
uploadQueue.value = innerFileList.value.filter((item) => item.status === UploadStatus.init);
|
|
||||||
uploadFileFromQueue(uploadQueue.value.shift());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台上传
|
||||||
|
*/
|
||||||
|
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 previewVisible = ref(false);
|
||||||
const previewCurrent = ref(0);
|
const previewCurrent = ref(0);
|
||||||
|
|
||||||
|
@ -271,9 +232,9 @@
|
||||||
props.handleReupload(item);
|
props.handleReupload(item);
|
||||||
} else {
|
} else {
|
||||||
item.status = UploadStatus.init;
|
item.status = UploadStatus.init;
|
||||||
if (uploadQueue.value.length > 0) {
|
if (asyncTaskStore.uploadFileTask.uploadQueue.length > 0) {
|
||||||
// 此时队列中还有任务,则 push 入队列末尾
|
// 此时队列中还有任务,则 push 入队列末尾
|
||||||
uploadQueue.value.push(item);
|
asyncTaskStore.uploadFileTask.uploadQueue.push(item);
|
||||||
} else {
|
} else {
|
||||||
// 此时队列任务已清空
|
// 此时队列任务已清空
|
||||||
startUpload();
|
startUpload();
|
||||||
|
@ -283,14 +244,15 @@
|
||||||
|
|
||||||
// 在组件销毁时清除定时器
|
// 在组件销毁时清除定时器
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timer !== null) {
|
if (asyncTaskStore.uploadFileTask.timer !== null) {
|
||||||
clearInterval(timer);
|
clearInterval(asyncTaskStore.uploadFileTask.timer);
|
||||||
timer = null;
|
asyncTaskStore.uploadFileTask.timer = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
startUpload,
|
startUpload,
|
||||||
|
backstageUpload,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,11 @@ export default {
|
||||||
'ms.upload.reUpload': 'Reupload',
|
'ms.upload.reUpload': 'Reupload',
|
||||||
'ms.upload.preview': 'Preview',
|
'ms.upload.preview': 'Preview',
|
||||||
'ms.upload.uploadAt': 'Uploaded at',
|
'ms.upload.uploadAt': 'Uploaded at',
|
||||||
'ms.upload.fail': 'Upload failed',
|
'ms.upload.fail': 'Failed',
|
||||||
'ms.upload.delete': 'Delete',
|
'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',
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,4 +13,5 @@ export default {
|
||||||
'ms.upload.uploading': '等待/上传中',
|
'ms.upload.uploading': '等待/上传中',
|
||||||
'ms.upload.success': '成功',
|
'ms.upload.success': '成功',
|
||||||
'ms.upload.fail': '失败',
|
'ms.upload.fail': '失败',
|
||||||
|
'ms.upload.detail': '查看详情',
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,4 +26,8 @@ export default {
|
||||||
'api.errMsg503': 'The service is unavailable, the server is temporarily overloaded or maintained!',
|
'api.errMsg503': 'The service is unavailable, the server is temporarily overloaded or maintained!',
|
||||||
'api.errMsg504': 'Network timeout!',
|
'api.errMsg504': 'Network timeout!',
|
||||||
'api.errMsg505': 'The http version does not support the request!',
|
'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',
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default {
|
||||||
'api.responseError': '接口返回错误,请重试',
|
'api.responseError': '接口返回错误,请重试',
|
||||||
'api.requestError': '请求错误',
|
'api.requestError': '请求错误',
|
||||||
'api.errMsg401': '用户没有权限(令牌、用户名、密码错误)!',
|
'api.errMsg401': '用户没有权限(令牌、用户名、密码错误)!',
|
||||||
'api.errMsg403': '用户得到授权,但是访问是被禁止的。!',
|
'api.errMsg403': '用户得到授权,但是访问是被禁止的!',
|
||||||
'api.errMsg404': '网络请求错误,未找到该资源!',
|
'api.errMsg404': '网络请求错误,未找到该资源!',
|
||||||
'api.errMsg405': '网络请求错误,请求方法未允许!',
|
'api.errMsg405': '网络请求错误,请求方法未允许!',
|
||||||
'api.errMsg408': '网络请求超时!',
|
'api.errMsg408': '网络请求超时!',
|
||||||
|
@ -26,4 +26,8 @@ export default {
|
||||||
'api.errMsg503': '服务不可用,服务器暂时过载或维护!',
|
'api.errMsg503': '服务不可用,服务器暂时过载或维护!',
|
||||||
'api.errMsg504': '网络超时!',
|
'api.errMsg504': '网络超时!',
|
||||||
'api.errMsg505': 'http版本不支持该请求!',
|
'api.errMsg505': 'http版本不支持该请求!',
|
||||||
|
// 异步任务提示
|
||||||
|
'asyncTask.uploadFileProgress': '文件上传进度 {percent};成功 {done} 个,失败 {fail} 个',
|
||||||
|
'asyncTask.uploadFileSuccess': '文件上传完成:成功 {done} 个,失败 {fail} 个',
|
||||||
|
'asyncTask.uploadFileSuccessTitle': '上传完成',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
@ -1,6 +1,7 @@
|
||||||
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
||||||
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
|
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
|
||||||
import type { PageConfig, ThemeConfig, LoginConfig, PlatformConfig } from '@/models/setting/config';
|
import type { PageConfig, ThemeConfig, LoginConfig, PlatformConfig } from '@/models/setting/config';
|
||||||
|
import type { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
colorWeak: boolean;
|
colorWeak: boolean;
|
||||||
|
@ -33,4 +34,17 @@ export interface AppState {
|
||||||
pageConfig: PageConfig;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ export const phoneRegex = /^\d{11}$/;
|
||||||
// 密码校验,8-32位
|
// 密码校验,8-32位
|
||||||
export const passwordLengthRegex = /^.{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地址校验
|
// Git地址校验
|
||||||
export const gitRepositoryUrlRegex =
|
export const gitRepositoryUrlRegex =
|
||||||
/^(?:(?:git:\/\/|https?:\/\/)(?:www\.)?)?(github\.com|gitee\.com)\/([^/]+)\/([^/]+)\.git$/;
|
/^(?:(?:git:\/\/|https?:\/\/)(?:www\.)?)?(github\.com|gitee\.com)\/([^/]+)\/([^/]+)\.git$/;
|
||||||
|
|
|
@ -86,7 +86,14 @@
|
||||||
class="mb-[16px] w-full"
|
class="mb-[16px] w-full"
|
||||||
@change="handleFileChange"
|
@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>
|
<template #tabExtra>
|
||||||
<div v-if="acceptType === 'jar'" class="flex items-center gap-[4px]">
|
<div v-if="acceptType === 'jar'" class="flex items-center gap-[4px]">
|
||||||
<a-switch size="small" @change="enableAllJar"></a-switch>
|
<a-switch size="small" @change="enableAllJar"></a-switch>
|
||||||
|
@ -104,10 +111,10 @@
|
||||||
<a-button type="secondary" @click="uploadDrawerVisible = false">
|
<a-button type="secondary" @click="uploadDrawerVisible = false">
|
||||||
{{ t('project.fileManagement.cancel') }}
|
{{ t('project.fileManagement.cancel') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="secondary" :disabled="noWaitingUpload" @click="uploadDrawerVisible = false">
|
<a-button type="secondary" :disabled="noWaitingUpload" @click="backstageUpload">
|
||||||
{{ t('project.fileManagement.backendUpload') }}
|
{{ t('project.fileManagement.backendUpload') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="primary" :disabled="noWaitingUpload || isUploading" @click="startUpload">
|
<a-button type="primary" :disabled="isUploading || noWaitingUpload" @click="startUpload">
|
||||||
{{ t('project.fileManagement.startUpload') }}
|
{{ t('project.fileManagement.startUpload') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -115,7 +122,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { debounce } from 'lodash-es';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useTableStore from '@/store/modules/ms-table';
|
import useTableStore from '@/store/modules/ms-table';
|
||||||
|
@ -129,6 +137,8 @@
|
||||||
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
||||||
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
|
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
|
||||||
import { UploadStatus } from '@/enums/uploadEnum';
|
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 { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
import type { MsFileItem, UploadType } from '@/components/pure/ms-upload/types';
|
import type { MsFileItem, UploadType } from '@/components/pure/ms-upload/types';
|
||||||
|
@ -137,7 +147,10 @@
|
||||||
activeFolder: string | number;
|
activeFolder: string | number;
|
||||||
activeFolderType: 'folder' | 'module' | 'storage';
|
activeFolderType: 'folder' | 'module' | 'storage';
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const asyncTaskStore = useAsyncTaskStore();
|
||||||
|
|
||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
const fileType = ref('module');
|
const fileType = ref('module');
|
||||||
|
@ -232,7 +245,7 @@
|
||||||
async function openFileDetail(id: string) {}
|
async function openFileDetail(id: string) {}
|
||||||
|
|
||||||
const uploadDrawerVisible = ref(false);
|
const uploadDrawerVisible = ref(false);
|
||||||
const fileList = ref<MsFileItem[]>([]);
|
const fileList = ref<MsFileItem[]>(asyncTaskStore.uploadFileTask.fileList);
|
||||||
|
|
||||||
const noWaitingUpload = computed(
|
const noWaitingUpload = computed(
|
||||||
() =>
|
() =>
|
||||||
|
@ -278,6 +291,11 @@
|
||||||
isUploading.value = true;
|
isUploading.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backstageUpload() {
|
||||||
|
fileListRef.value?.backstageUpload();
|
||||||
|
uploadDrawerVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
function startUpload() {
|
function startUpload() {
|
||||||
fileListRef.value?.startUpload();
|
fileListRef.value?.startUpload();
|
||||||
}
|
}
|
||||||
|
@ -285,6 +303,33 @@
|
||||||
function uploadFinish() {
|
function uploadFinish() {
|
||||||
isUploading.value = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
|
@ -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',
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue