diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 4489bb538d..de9856197c 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,9 +1,9 @@
-
+
diff --git a/frontend/src/assets/style/arco-reset.less b/frontend/src/assets/style/arco-reset.less
index 621c4a71ab..346ecc0f9e 100644
--- a/frontend/src/assets/style/arco-reset.less
+++ b/frontend/src/assets/style/arco-reset.less
@@ -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;
+ }
+ }
+}
diff --git a/frontend/src/components/pure/ms-button/index.vue b/frontend/src/components/pure/ms-button/index.vue
index cf5899b745..65664a38c1 100644
--- a/frontend/src/components/pure/ms-button/index.vue
+++ b/frontend/src/components/pure/ms-button/index.vue
@@ -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));
}
diff --git a/frontend/src/components/pure/ms-upload/backstageMsg.vue b/frontend/src/components/pure/ms-upload/backstageMsg.vue
new file mode 100644
index 0000000000..aa5505e81e
--- /dev/null
+++ b/frontend/src/components/pure/ms-upload/backstageMsg.vue
@@ -0,0 +1,29 @@
+
+
+
{{ props.content }}
+
{{ t('ms.upload.detail') }}
+
+
+
+
diff --git a/frontend/src/components/pure/ms-upload/fileList.vue b/frontend/src/components/pure/ms-upload/fileList.vue
index f06aeff5c6..8fd97359b8 100644
--- a/frontend/src/components/pure/ms-upload/fileList.vue
+++ b/frontend/src/components/pure/ms-upload/fileList.vue
@@ -2,9 +2,9 @@
{{ `${t('ms.upload.all')} (${innerFileList.length})` }}
- {{ `${t('ms.upload.uploading')} (${waitingList.length})` }}
- {{ `${t('ms.upload.success')} (${successList.length})` }}
- {{ `${t('ms.upload.fail')} (${failList.length})` }}
+ {{ `${t('ms.upload.uploading')} (${totalWaitingFileList.length})` }}
+ {{ `${t('ms.upload.success')} (${totalSuccessFileList.length})` }}
+ {{ `${t('ms.upload.fail')} (${totalFailFileList.length})` }}
@@ -47,7 +47,7 @@
; // 用于后台上传文件时,查看详情跳转的路由参数
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([]);
- 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,
});
diff --git a/frontend/src/components/pure/ms-upload/locale/en-US.ts b/frontend/src/components/pure/ms-upload/locale/en-US.ts
index da612084d2..f351cce989 100644
--- a/frontend/src/components/pure/ms-upload/locale/en-US.ts
+++ b/frontend/src/components/pure/ms-upload/locale/en-US.ts
@@ -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',
};
diff --git a/frontend/src/components/pure/ms-upload/locale/zh-CN.ts b/frontend/src/components/pure/ms-upload/locale/zh-CN.ts
index e9ae8e5354..55fee8e23e 100644
--- a/frontend/src/components/pure/ms-upload/locale/zh-CN.ts
+++ b/frontend/src/components/pure/ms-upload/locale/zh-CN.ts
@@ -13,4 +13,5 @@ export default {
'ms.upload.uploading': '等待/上传中',
'ms.upload.success': '成功',
'ms.upload.fail': '失败',
+ 'ms.upload.detail': '查看详情',
};
diff --git a/frontend/src/locale/en-US/sys.ts b/frontend/src/locale/en-US/sys.ts
index fc969a1c96..5f53176342 100644
--- a/frontend/src/locale/en-US/sys.ts
+++ b/frontend/src/locale/en-US/sys.ts
@@ -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',
};
diff --git a/frontend/src/locale/zh-CN/sys.ts b/frontend/src/locale/zh-CN/sys.ts
index f2cf80442f..7bfe25e8ee 100644
--- a/frontend/src/locale/zh-CN/sys.ts
+++ b/frontend/src/locale/zh-CN/sys.ts
@@ -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': '上传完成',
};
diff --git a/frontend/src/store/modules/app/asyncTask.ts b/frontend/src/store/modules/app/asyncTask.ts
new file mode 100644
index 0000000000..4dcc02c88a
--- /dev/null
+++ b/frontend/src/store/modules/app/asyncTask.ts
@@ -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) {
+ 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) {
+ 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) {
+ 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) {
+ // 正式开始上传任务之前,同步一次文件列表,取出所有状态为 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;
diff --git a/frontend/src/store/modules/app/types.ts b/frontend/src/store/modules/app/types.ts
index 9cbba149ea..e12dfe9794 100644
--- a/frontend/src/store/modules/app/types.ts
+++ b/frontend/src/store/modules/app/types.ts
@@ -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;
+}
diff --git a/frontend/src/utils/validate.ts b/frontend/src/utils/validate.ts
index 4776f122b9..aaab181f9f 100644
--- a/frontend/src/utils/validate.ts
+++ b/frontend/src/utils/validate.ts
@@ -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$/;
diff --git a/frontend/src/views/project-management/fileManagement/components/rightBox.vue b/frontend/src/views/project-management/fileManagement/components/rightBox.vue
index 5b51b9d2cf..3a47fbebc4 100644
--- a/frontend/src/views/project-management/fileManagement/components/rightBox.vue
+++ b/frontend/src/views/project-management/fileManagement/components/rightBox.vue
@@ -86,7 +86,14 @@
class="mb-[16px] w-full"
@change="handleFileChange"
/>
-
+
@@ -104,10 +111,10 @@
{{ t('project.fileManagement.cancel') }}
-
+
{{ t('project.fileManagement.backendUpload') }}
-
+
{{ t('project.fileManagement.startUpload') }}
@@ -115,7 +122,8 @@