From cfd8b072dd5114e7e4f98120abee3dd08b5115e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B4=BE=E5=90=8C=E5=AD=A6?= Date: Thu, 11 Sep 2025 11:03:14 +0800 Subject: [PATCH] =?UTF-8?q?ADD:=20=E4=BF=AE=E6=94=B9=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20event-source-polyfill=E5=8C=85=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E9=95=BF=E8=BF=9E=E6=8E=A5=E9=80=9A=E4=BF=A1=EF=BC=8C=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=90=88=E5=B9=B6=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 2 + frontend/src/api/index.ts | 368 ++++++++++-------- frontend/src/api/plan/plan.ts | 15 +- frontend/src/components/ImportZip/index.vue | 190 +++++++-- frontend/src/views/log/index.vue | 186 +++++---- .../plan/planList/components/childrenPlan.vue | 43 +- 6 files changed, 459 insertions(+), 345 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 167ae9c..94f2c90 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", + "@types/event-source-polyfill": "^1.0.5", "@vue-flow/core": "^1.45.0", "@vueuse/core": "^10.4.1", "axios": "^1.7.3", @@ -23,6 +24,7 @@ "echarts": "^5.4.3", "echarts-liquidfill": "^3.1.0", "element-plus": "^2.7.8", + "event-source-polyfill": "^1.0.31", "html2canvas": "^1.4.1", "md5": "^2.3.0", "mitt": "^3.0.1", diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index e634f4e..1a9636b 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,190 +1,226 @@ -import { ElMessage, ElTreeSelect } from 'element-plus'; -import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from 'axios' +import { ElMessage } from 'element-plus' +import axios, { + AxiosError, + type AxiosInstance, + type AxiosRequestConfig, + type AxiosResponse, + type InternalAxiosRequestConfig +} from 'axios' import { showFullScreenLoading, tryHideFullScreenLoading } from '@/components/Loading/fullScreen' import { LOGIN_URL } from '@/config' -import { ElMessage } from 'element-plus' -import { ResultData } from '@/api/interface' +import { type ResultData } from '@/api/interface' import { ResultEnum } from '@/enums/httpEnum' import { checkStatus } from './helper/checkStatus' import { useUserStore } from '@/stores/modules/user' import router from '@/routers' -import {refreshToken} from '@/api/user/login' +import { refreshToken } from '@/api/user/login' +import { EventSourcePolyfill } from 'event-source-polyfill' export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { - loading?: boolean; + loading?: boolean } const config = { - // 默认地址请求地址,可在 .env.** 文件中修改 - baseURL: import.meta.env.VITE_API_URL as string, - // 设置超时时间 - timeout: ResultEnum.TIMEOUT as number, - // 跨域时候允许携带凭证 - withCredentials: true, - // post请求指定数据类型以及编码 - headers: { 'Content-Type': 'application/json;charset=utf-8' }, + // 默认地址请求地址,可在 .env.** 文件中修改 + baseURL: import.meta.env.VITE_API_URL as string, + // 设置超时时间 + timeout: ResultEnum.TIMEOUT as number, + // 跨域时候允许携带凭证 + withCredentials: true, + // post请求指定数据类型以及编码 + headers: { 'Content-Type': 'application/json;charset=utf-8' } } class RequestHttp { - service: AxiosInstance + service: AxiosInstance - public constructor(config: AxiosRequestConfig) { - // 创建实例 - this.service = axios.create(config) + public constructor(config: AxiosRequestConfig) { + // 创建实例 + this.service = axios.create(config) - /** - * @description 请求拦截器 - * 客户端发送请求 -> [请求拦截器] -> 服务器 - * token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中 - */ - this.service.interceptors.request.use( - (config: CustomAxiosRequestConfig) => { - isFirst = true - const userStore = useUserStore() - // 当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { loading: false } 来控制 - config.loading ?? (config.loading = true) - config.loading && showFullScreenLoading() - if (config.headers && typeof config.headers.set === 'function') { - config.headers.set('Authorization', 'Bearer ' + userStore.accessToken) - config.headers.set('Is-Refresh-Token', userStore.isRefreshToken+"") - } - return config - }, - (error: AxiosError) => { - return Promise.reject(error) - }, - ) - - let isFirst = true - /** - * @description 响应拦截器 - * 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息 - */ - this.service.interceptors.response.use( - async (response: AxiosResponse) => { - const { data } = response - const userStore = useUserStore() - tryHideFullScreenLoading() - - if(data.code === ResultEnum.ACCESSTOKEN_EXPIRED){ - // 用长token去换短token - userStore.setAccessToken(userStore.refreshToken) - userStore.setIsRefreshToken(true) - const result = await refreshToken() - if (result) { //获取新token成功的话 - // 有新的token后,重新请求 - userStore.setAccessToken(result.data.accessToken) - userStore.setRefreshToken(result.data.refreshToken) - userStore.setIsRefreshToken(false) - userStore.setExp(1000 * 60 * 60 * 24 * 30) - response.config.headers.Authorization = `Bearer ${result.data.accessToken}`//重新请求前需要将更新后的新token更换掉之前无效的token,不然会死循环 - const resp = await this.service.request(response.config) - return resp - } else { - // 刷新失效,跳转登录页 + /** + * @description 请求拦截器 + * 客户端发送请求 -> [请求拦截器] -> 服务器 + * token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中 + */ + this.service.interceptors.request.use( + (config: CustomAxiosRequestConfig) => { + isFirst = true + const userStore = useUserStore() + // 当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { loading: false } 来控制 + config.loading ?? (config.loading = true) + config.loading && showFullScreenLoading() + if (config.headers && typeof config.headers.set === 'function') { + config.headers.set('Authorization', 'Bearer ' + userStore.accessToken) + config.headers.set('Is-Refresh-Token', userStore.isRefreshToken + '') + } + return config + }, + (error: AxiosError) => { + return Promise.reject(error) } - } - // 登陆失效 - if (data.code === ResultEnum.OVERDUE) { - console.log("登陆失效") - userStore.setAccessToken('') - userStore.setRefreshToken('') - userStore.setIsRefreshToken(false) - userStore.setUserInfo({ id:'',name: '' }) - userStore.setExp(0) - await router.replace(LOGIN_URL) - if(isFirst){//临时处理token失效弹窗多次 - ElMessage.error(data.message) - isFirst = false - } - return Promise.reject(data) + ) + + let isFirst = true + /** + * @description 响应拦截器 + * 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息 + */ + this.service.interceptors.response.use( + async (response: AxiosResponse) => { + const { data } = response + const userStore = useUserStore() + tryHideFullScreenLoading() + + if (data.code === ResultEnum.ACCESSTOKEN_EXPIRED) { + // 用长token去换短token + userStore.setAccessToken(userStore.refreshToken) + userStore.setIsRefreshToken(true) + const result = await refreshToken() + if (result) { + //获取新token成功的话 + // 有新的token后,重新请求 + userStore.setAccessToken(result.data.accessToken) + userStore.setRefreshToken(result.data.refreshToken) + userStore.setIsRefreshToken(false) + userStore.setExp(1000 * 60 * 60 * 24 * 30) + response.config.headers.Authorization = `Bearer ${result.data.accessToken}` //重新请求前需要将更新后的新token更换掉之前无效的token,不然会死循环 + const resp = await this.service.request(response.config) + return resp + } else { + // 刷新失效,跳转登录页 + } + } + // 登陆失效 + if (data.code === ResultEnum.OVERDUE) { + console.log('登陆失效') + userStore.setAccessToken('') + userStore.setRefreshToken('') + userStore.setIsRefreshToken(false) + userStore.setUserInfo({ id: '', name: '' }) + userStore.setExp(0) + await router.replace(LOGIN_URL) + if (isFirst) { + //临时处理token失效弹窗多次 + ElMessage.error(data.message) + isFirst = false + } + return Promise.reject(data) + } + // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错) + if (data.code && data.code !== ResultEnum.SUCCESS) { + if (data.message.includes('&')) { + let formattedMessage = data.message.split('&').join('
') + if (data.message.includes(':')) { + formattedMessage = formattedMessage.replace(':', '') + } + ElMessage.error({ message: formattedMessage, dangerouslyUseHTMLString: true }) + return Promise.reject(data) + } + + ElMessage.error(data.message) + return Promise.reject(data) + } + // 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑) + + if (userStore.exp <= Date.now() && userStore.exp !== 0) { + userStore.setAccessToken('') + userStore.setRefreshToken('') + userStore.setIsRefreshToken(false) + userStore.setUserInfo({ id: '', name: '' }) + userStore.setExp(0) + ElMessage.error('登录已过期,请重新登录!') + await router.replace(LOGIN_URL) + return Promise.reject(data) + } + return data + }, + async (error: AxiosError) => { + const { response } = error + tryHideFullScreenLoading() + console.log('error', error.message) + // 请求超时 && 网络错误单独判断,没有 response + if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试') + if (error.message.indexOf('Network Error') !== -1) ElMessage.error('网络错误!请您稍后重试') + // 根据服务器响应的错误状态码,做不同的处理 + if (response) checkStatus(response.status) + // 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面 + if (!window.navigator.onLine) router.replace('/500') + return Promise.reject(error) + } + ) + } + + /** + * @description 常用请求方法封装 + */ + get(url: string, params?: object, _object = {}): Promise> { + return this.service.get(url, { params, ..._object }) + } + + post(url: string, params?: object | string, _object = {}): Promise> { + return this.service.post(url, params, _object) + } + + put(url: string, params?: object, _object = {}): Promise> { + return this.service.put(url, params, _object) + } + + delete(url: string, params?: any, _object = {}): Promise> { + return this.service.delete(url, { params, ..._object }) + } + + download(url: string, params?: object, _object = {}): Promise { + return this.service.post(url, params, { ..._object, responseType: 'blob' }) + } + + upload(url: string, params?: object, _object = {}): Promise { + return this.service.post(url, params, { + ..._object, + headers: { 'Content-Type': 'multipart/form-data' } + }) + } + + /** + * 针对excel的上传,默认返回的是blob类型,Excel没问题时返回json特殊处理 + */ + uploadExcel(url: string, params?: object, _object = {}): Promise { + return this.service.post(url, params, { + ..._object, + headers: { 'Content-Type': 'multipart/form-data' }, + responseType: 'blob' + }) + } + + // 添加SSE连接方法 + sse(url: string, params?: any): EventSource { + const userStore = useUserStore() + // 构造带参数的URL + let requestUrl = config.baseURL + url + if (params) { + const searchParams = new URLSearchParams() + for (const key in params) { + if (Object.prototype.hasOwnProperty.call(params, key)) { + searchParams.append(key, String(params[key])) + } + } + requestUrl += '?' + searchParams.toString() } - // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错) - if (data.code && data.code !== ResultEnum.SUCCESS) { - if(data.message.includes('&')){ - let formattedMessage = data.message.split('&').join('
'); - if (data.message.includes(':')) { - formattedMessage = formattedMessage.replace(':', '') - } - ElMessage.error({ message: formattedMessage, dangerouslyUseHTMLString: true }); - return Promise.reject(data) - } - ElMessage.error(data.message) - return Promise.reject(data) - } - // 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑) - - if (userStore.exp <= Date.now() && userStore.exp !== 0) { - userStore.setAccessToken('') - userStore.setRefreshToken('') - userStore.setIsRefreshToken(false) - userStore.setUserInfo({ id:'',name: '' }) - userStore.setExp(0) - ElMessage.error('登录已过期,请重新登录!') - await router.replace(LOGIN_URL) - return Promise.reject(data) - } - return data - }, - async (error: AxiosError) => { - const { response } = error - tryHideFullScreenLoading() - console.log('error', error.message) - // 请求超时 && 网络错误单独判断,没有 response - if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试') - if (error.message.indexOf('Network Error') !== -1) ElMessage.error('网络错误!请您稍后重试') - // 根据服务器响应的错误状态码,做不同的处理 - if (response) checkStatus(response.status) - // 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面 - if (!window.navigator.onLine) router.replace('/500') - return Promise.reject(error) - }, - ) - } - - /** - * @description 常用请求方法封装 - */ - get(url: string, params?: object, _object = {}): Promise> { - return this.service.get(url, { params, ..._object }) - } - - post(url: string, params?: object | string, _object = {}): Promise> { - return this.service.post(url, params, _object) - } - - put(url: string, params?: object, _object = {}): Promise> { - return this.service.put(url, params, _object) - } - - delete(url: string, params?: any, _object = {}): Promise> { - return this.service.delete(url, { params, ..._object }) - } - - download(url: string, params?: object, _object = {}): Promise { - return this.service.post(url, params, { ..._object, responseType: 'blob' }) - } - - upload(url: string, params?: object, _object = {}): Promise { - return this.service.post(url, params, { - ..._object, - headers: { 'Content-Type': 'multipart/form-data' } - }) - } - - /** - * 针对excel的上传,默认返回的是blob类型,Excel没问题时返回json特殊处理 - */ - uploadExcel(url: string, params?: object, _object = {}): Promise { - return this.service.post(url, params, { - ..._object, - headers: { 'Content-Type': 'multipart/form-data' }, - responseType: 'blob', - }) - } + // 创建EventSource连接 + const eventSource = new EventSourcePolyfill(requestUrl, { + headers: { + Authorization: 'Bearer ' + userStore.accessToken + } + }) + // 设置默认的Authorization头部 + eventSource.addEventListener('open', function () { + console.log('SSE连接已建立') + }) + return eventSource + } } export default new RequestHttp(config) diff --git a/frontend/src/api/plan/plan.ts b/frontend/src/api/plan/plan.ts index a232ae7..43b0f1e 100644 --- a/frontend/src/api/plan/plan.ts +++ b/frontend/src/api/plan/plan.ts @@ -141,16 +141,6 @@ export const exportPlanCheckData = (params: any) => { ) } -// 导入子检测计划检测结果数据 -export const importSubPlanCheckData = (params: Plan.ResPlan) => { - return http.upload(`/adPlan/importSubPlanCheckData`, params) -} - -// 合并子检测计划检测结果数据 -export const mergeSubPlanCheckData = (params: Plan.ResPlan) => { - return http.upload(`/adPlan/mergePlanCheckData?planId=${params.id}`) -} - //根据误差体系id获取测试项 export const getPqErrSysTestItemList = (params: {errorSysId : string}) => { return http.get(`/pqErrSys/getTestItems?id=${params.errorSysId}`) @@ -159,4 +149,9 @@ export const getPqErrSysTestItemList = (params: {errorSysId : string}) => { // 获取计划项目成员 export const getMemberList = (params: {id : string}) => { return http.get(`/adPlan/getMemberList?planId=${params.id}`) +} + +// 导入并合并子检测计划检测结果数据 +export const importAndMergePlanCheckData = (params: Plan.ResPlan) => { + return http.upload(`/adPlan/importAndMergePlanCheckData`, params) } \ No newline at end of file diff --git a/frontend/src/components/ImportZip/index.vue b/frontend/src/components/ImportZip/index.vue index 2304cd2..8c2590a 100644 --- a/frontend/src/components/ImportZip/index.vue +++ b/frontend/src/components/ImportZip/index.vue @@ -1,33 +1,48 @@