提交 6804f72d authored 作者: yanpeng's avatar yanpeng

页面长时间停留后页面卡顿问题

上级 4f806fd1
// 引入 axios 请求
import axios from 'axios'
// 引入 element-plus 里面的消息提示
import { ElMessage } from 'element-plus'
import { getToken, setToken, removeToken, formatBearerAuthorization } from '@/api/request.js'
export { getToken, setToken, removeToken }
// 【新增】全局 AbortController,用于管理所有通过此 service 发出的请求
let globalAbortController = new AbortController()
// 【新增】暴露一个方法,供外部(如路由守卫)调用以取消所有正在进行的请求
export const cancelAllRequests = () => {
if (globalAbortController) {
globalAbortController.abort()
// 创建一个新的 Controller 以备下次使用
globalAbortController = new AbortController()
}
}
// 创建 axios 实例
const service = axios.create({
timeout: 30 * 1000 // 请求超时时间
})
// request 拦截器:与主 request 一致,就地改 config,避免 mergeConfig 破坏 url
service.interceptors.request.use(config => {
const raw = getToken()
const token = raw ? String(raw).trim() : ""
if (!config.headers) {
config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers)
}
if (token) {
config.headers.set('token', token, true)
} else {
config.headers.delete('token')
config.headers.delete('Token')
}
const reqUrl = String(config.url ?? '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) {
if (!config.headers) {
config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers)
}
config.headers.set('X-API-Key', aiApiKey)
}
}
// 【新增】将全局控制器的 signal 注入到当前请求中
// 注意:如果 config 中已经手动传入了 signal(例如组件内单独控制),则优先使用组件内的
if (!config.signal) {
config.signal = globalAbortController.signal
}
return config
}, error => {
console.log(error)
return Promise.reject(error)
})
// response 拦截器
service.interceptors.response.use(
response => {
const res = response?.data
if (!res) {
return Promise.reject(new Error('响应数据为空'))
}
// 根据需求:接口返回 code 不等于 200 的时候报错
if (res.code !== 200) {
ElMessage({
message: res.message || '请求失败',
type: 'error',
duration: 3 * 1000
})
return Promise.reject(res)
}
return res.data
},
error => {
console.log('err' + error)
const isCanceledError =
error?.code === 'ERR_CANCELED' ||
error?.name === 'CanceledError' ||
error?.name === 'AbortError' ||
(typeof error?.message === 'string' && /canceled/i.test(error.message))
if (isCanceledError) return Promise.reject(error)
// 处理 token 过期或无效的情况
const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
!isAiAnalysisRequest
) {
ElMessage({
message: 'Token 已过期,请重新登录',
type: 'error',
duration: 3 * 1000
})
const h = error.config?.headers
const hadToken =
h &&
(typeof h.get === 'function'
? Boolean(
h.get('token') || h.get('Token')
)
: Boolean(
h.token || h.Token
))
if (hadToken) removeToken()
} else {
ElMessage({
message: typeof error?.message === 'string' ? error.message : '请求失败',
type: 'error',
duration: 3 * 1000
})
}
return Promise.reject(error)
}
)
// 封装通用请求函数(支持 http(config) 和 http.get/post 等调用方式)
function http(config) {
return service(config)
}
// 为 http 函数添加快捷方法
http.get = function(url, params) {
return service({ url, method: 'get', params })
}
http.post = function(url, data) {
return service({ url, method: 'post', data })
}
http.put = function(url, data) {
return service({ url, method: 'put', data })
}
http.delete = function(url, params) {
return service({ url, method: 'delete', params })
}
export { http }
export default service
\ No newline at end of file
// 引入 axios 请求 // src/api/finance/service.js
import axios from 'axios' import axios from 'axios'
// 引入 element-plus 里面的消息提示
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getToken, setToken, removeToken, formatBearerAuthorization } from '@/api/request.js' import { getToken, setToken, removeToken } from '@/api/request.js'
export { getToken, setToken, removeToken } export { getToken, setToken, removeToken }
// 定义全局控制器,以便在取消后重新赋值
let currentAbortController = new AbortController()
/**
* 获取当前有效的 AbortSignal
* 供 axios 拦截器和 fetch 请求共同使用
*/
export const getAbortSignal = () => {
return currentAbortController.signal
}
/**
* 取消所有正在进行的请求
* 路由守卫中调用此方法
*/
export const cancelAllRequests = () => {
// 1. 终止当前控制器的所有请求
currentAbortController.abort()
// 2. 创建一个新的控制器,供后续新请求使用
currentAbortController = new AbortController()
}
// 创建 axios 实例 // 创建 axios 实例
const service = axios.create({ const service = axios.create({
timeout: 300 * 1000 // 请求超时时间 timeout: 30 * 1000 // 请求超时时间
}) })
// request 拦截器:与主 request 一致,就地改 config,避免 mergeConfig 破坏 url // request 拦截器
service.interceptors.request.use(config => { service.interceptors.request.use(config => {
const raw = getToken() const raw = getToken()
const token = raw ? String(raw).trim() : "" const token = raw ? String(raw).trim() : ""
// 处理 Headers
if (!config.headers) { if (!config.headers) {
config.headers = new axios.AxiosHeaders() config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) { } else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers) config.headers = axios.AxiosHeaders.from(config.headers)
} }
// 设置 Token
if (token) { if (token) {
config.headers.set('token', token, true) config.headers.set('token', token, true)
} else { } else {
config.headers.delete('token') config.headers.delete('token')
config.headers.delete('Token') config.headers.delete('Token')
// ===== 旧逻辑保留(勿删):Authorization: Bearer <token> =====
// config.headers.delete('Authorization')
// config.headers.delete('authorization')
} }
// 处理 AI 分析接口的特殊 Header
const reqUrl = String(config.url ?? '') const reqUrl = String(config.url ?? '')
if (reqUrl.includes('aiAnalysis')) { if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) { if (aiApiKey) {
// 确保 headers 存在
if (!config.headers) { if (!config.headers) {
config.headers = new axios.AxiosHeaders() config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) { } else if (!(config.headers instanceof axios.AxiosHeaders)) {
...@@ -42,6 +67,13 @@ service.interceptors.request.use(config => { ...@@ -42,6 +67,13 @@ service.interceptors.request.use(config => {
config.headers.set('X-API-Key', aiApiKey) config.headers.set('X-API-Key', aiApiKey)
} }
} }
// 将全局控制器的 signal 注入到 axios 请求中
// 如果用户手动传入了 signal,则优先使用用户的(虽然少见)
if (!config.signal) {
config.signal = getAbortSignal()
}
return config return config
}, error => { }, error => {
console.log(error) console.log(error)
...@@ -55,7 +87,6 @@ service.interceptors.response.use( ...@@ -55,7 +87,6 @@ service.interceptors.response.use(
if (!res) { if (!res) {
return Promise.reject(new Error('响应数据为空')) return Promise.reject(new Error('响应数据为空'))
} }
// 根据需求:接口返回 code 不等于 200 的时候报错
if (res.code !== 200) { if (res.code !== 200) {
ElMessage({ ElMessage({
message: res.message || '请求失败', message: res.message || '请求失败',
...@@ -67,19 +98,25 @@ service.interceptors.response.use( ...@@ -67,19 +98,25 @@ service.interceptors.response.use(
return res.data return res.data
}, },
error => { error => {
console.log('err' + error) // 精准识别取消错误,避免弹窗骚扰用户
const isCanceledError = const isCanceledError =
axios.isCancel(error) ||
error?.code === 'ERR_CANCELED' || error?.code === 'ERR_CANCELED' ||
error?.name === 'CanceledError' || error?.name === 'CanceledError' ||
error?.name === 'AbortError' || error?.name === 'AbortError' ||
(typeof error?.message === 'string' && /canceled/i.test(error.message)) (typeof error?.message === 'string' && /canceled/i.test(error.message))
if (isCanceledError) return Promise.reject(error) if (isCanceledError) {
// 静默处理取消错误,不弹窗,不打印.error
return Promise.reject(error)
}
console.log('err' + error)
// 处理 token 过期或无效的情况 // 处理 Token 过期
const errUrl = String(error.config?.url || '') const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis') const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if ( if (
error.response && error.response &&
(error.response.status === 401 || error.response.status === 403) && (error.response.status === 401 || error.response.status === 403) &&
...@@ -94,14 +131,11 @@ service.interceptors.response.use( ...@@ -94,14 +131,11 @@ service.interceptors.response.use(
const hadToken = const hadToken =
h && h &&
(typeof h.get === 'function' (typeof h.get === 'function'
? Boolean( ? Boolean(h.get('token') || h.get('Token'))
h.get('token') || h.get('Token') : Boolean(h.token || h.Token))
)
: Boolean(
h.token || h.Token
))
if (hadToken) removeToken() if (hadToken) removeToken()
} else { } else {
// 只有非取消、非 Token 过期的错误才弹出通用提示
ElMessage({ ElMessage({
message: typeof error?.message === 'string' ? error.message : '请求失败', message: typeof error?.message === 'string' ? error.message : '请求失败',
type: 'error', type: 'error',
...@@ -112,12 +146,11 @@ service.interceptors.response.use( ...@@ -112,12 +146,11 @@ service.interceptors.response.use(
} }
) )
// 封装通用请求函数(支持 http(config) 和 http.get/post 等调用方式) // 封装通用请求函数
function http(config) { function http(config) {
return service(config) return service(config)
} }
// 为 http 函数添加快捷方法
http.get = function(url, params) { http.get = function(url, params) {
return service({ url, method: 'get', params }) return service({ url, method: 'get', params })
} }
......
...@@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from "vue-router"; ...@@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from "vue-router";
import { setToken, removeToken, getToken } from "@/api/request.js"; import { setToken, removeToken, getToken } from "@/api/request.js";
import { AUTH_LOGOUT_CHANNEL } from "@/utils/authCrossTabLogout.js"; import { AUTH_LOGOUT_CHANNEL } from "@/utils/authCrossTabLogout.js";
import { cancelAllRequests } from "@/api/finance/service.js"
/** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */ /** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */
const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id"; const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id";
/** 退出后强制回登录页(跨标签页/刷新生效) */ /** 退出后强制回登录页(跨标签页/刷新生效) */
...@@ -145,51 +147,9 @@ const router = createRouter({ ...@@ -145,51 +147,9 @@ const router = createRouter({
// 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态) // 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态)
// 3)已有本地 token:正常走前端路由 // 3)已有本地 token:正常走前端路由
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
// ===== SSO/重定向逻辑(切线上接口时停用,保留注释) ===== // 【新增】在每次路由跳转开始前,取消上一个页面所有未完成的请求
// const queryToken = to.query && to.query.token != null && String(to.query.token).trim() !== "" // 这能防止旧页面的数据回来覆盖新页面,也能减少服务器压力
// ? String(to.query.token).trim() cancelAllRequests();
// : "";
//
// if (queryToken) {
// setToken(queryToken);
// // 成功回跳拿到 token,说明统一登录链路已完成,清除强制标记
// try {
// if (typeof window !== "undefined") {
// window.sessionStorage.removeItem(FORCE_SSO_LOGIN_KEY);
// }
// } catch {
// // ignore
// }
// persistViteBootIdOnly();
// const restQuery = { ...to.query };
// delete restQuery.token;
// const isGatewayCallback =
// to.path === "/callback" || to.path.replace(/\/$/, "") === "/callback";
// const targetPath = isGatewayCallback ? SSO_POST_LOGIN_PATH : to.path;
// next({
// path: targetPath,
// query: restQuery,
// hash: to.hash,
// replace: true,
// });
// return;
// }
//
// // 若用户点了“退出登录”,即使本地还有残留 token/或别处写回,也强制先走统一登录链路
// try {
// if (typeof window !== "undefined" && window.sessionStorage.getItem(FORCE_SSO_LOGIN_KEY) === "1") {
// removeToken();
// const targetUrl = `${SSO_GATEWAY_ORIGIN}/api/v2${to.fullPath || "/"}`;
// window.location.replace(targetUrl);
// next(false);
// return;
// }
// } catch {
// // ignore
// }
// 外网/线上版本:不因重启清登录态;仅开发环境需要此逻辑
// clearTokenIfNewDevBoot();
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
clearTokenIfNewDevBoot(); clearTokenIfNewDevBoot();
} }
......
...@@ -1822,14 +1822,20 @@ const handleToDataLibrary = item => { ...@@ -1822,14 +1822,20 @@ const handleToDataLibrary = item => {
onMounted(async () => { onMounted(async () => {
console.log("finance 页面 mounted"); console.log("finance 页面 mounted");
try { try {
// 获取趋势图数据
fetchTrendData();
fetchRiskSignals("0109");
// 获取社交媒体信息
fetchSocialMediaInfo();
// 获取新闻资讯
fetchNewsInfo();
const [dataCount, entitiesDataInfo, industryCountByYear, cclList] = await Promise.all([ const [dataCount, entitiesDataInfo, industryCountByYear, cclList] = await Promise.all([
getDataCount(), getDataCount(),
getLatestEntityListInfo(), getLatestEntityListInfo(),
getReleaseCount(2) getReleaseCount(2)
// getReleaseCount(3) // getReleaseCount(3)
]); ]);
// 交换第二个和第三个元素
// [dataCount[1], dataCount[2]] = [dataCount[2], dataCount[1]];
console.log("dataCount", dataCount); console.log("dataCount", dataCount);
infoList.value = dataCount.slice(0, 2).map((item, idx) => { infoList.value = dataCount.slice(0, 2).map((item, idx) => {
return { return {
...@@ -1870,6 +1876,11 @@ onMounted(async () => { ...@@ -1870,6 +1876,11 @@ onMounted(async () => {
tags: item.domain tags: item.domain
}; };
}); });
await fetchSanctionProcess(sanctionPage.value, 10);
// 获取雷达图数据
await fetchRadarData(domainChecked.value);
// 获取出口管制制裁措施
await fetchSanctionList();
entityListReleaseFreqChart.interpret({ entityListReleaseFreqChart.interpret({
type: "柱状图", type: "柱状图",
name: "美国商务部发布实体清单的频次", name: "美国商务部发布实体清单的频次",
...@@ -1888,21 +1899,6 @@ onMounted(async () => { ...@@ -1888,21 +1899,6 @@ onMounted(async () => {
name: "美国商务部发布商业管制清单的频次", name: "美国商务部发布商业管制清单的频次",
data: commerceControlListReleaseFreq.value data: commerceControlListReleaseFreq.value
}); });
// 获取趋势图数据
fetchTrendData();
fetchRiskSignals("0109");
// 获取社交媒体信息
fetchSocialMediaInfo();
// 获取新闻资讯
fetchNewsInfo();
// fetchEntitiesList(currentPage.value, pageSize.value);
await fetchSanctionProcess(sanctionPage.value, 10);
// 获取雷达图数据
await fetchRadarData(domainChecked.value);
// 获取出口管制制裁措施
await fetchSanctionList();
} catch (err) { } catch (err) {
console.log("此处报错?"); console.log("此处报错?");
console.log(err); console.log(err);
......
import { ref } from "vue";
export const useChartInterpretation = () => {
const loading = ref(false);
const interpretation = ref("");
const error = ref(null);
const interpret = async text => {
loading.value = true;
error.value = null;
interpretation.value = "";
try {
const response = await fetch("/aiAnalysis/chart_interpretation", {
method: "POST",
headers: {
"X-API-Key": "aircasKEY19491001",
"Content-Type": "application/json"
},
body: JSON.stringify({ text })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const content = line.substring(6);
const textMatch = content.match(/"解读":\s*"([^"]*)"/);
if (textMatch && textMatch[1]) {
interpretation.value = textMatch[1];
}
}
}
}
} catch (err) {
error.value = err.message || "AI 解读失败";
console.error("AI Chart Interpretation Error:", err);
} finally {
loading.value = false;
}
};
return {
loading,
interpretation,
error,
interpret
};
};
// src/views/finance/utils/common.js
import { ref } from "vue"; import { ref } from "vue";
// 【新增】引入获取全局 Signal 的方法
import { getAbortSignal } from "@/api/finance/service.js";
export const useChartInterpretation = () => { export const useChartInterpretation = () => {
const loading = ref(false); const loading = ref(false);
...@@ -10,14 +14,20 @@ export const useChartInterpretation = () => { ...@@ -10,14 +14,20 @@ export const useChartInterpretation = () => {
error.value = null; error.value = null;
interpretation.value = ""; interpretation.value = "";
// 【新增】在请求发起前获取当前的 Signal
// 注意:必须在每次调用 interpret 时重新获取,以确保拿到最新的 controller 的 signal
const signal = getAbortSignal();
try { try {
// 【修改】在 fetch 中传入 signal
const response = await fetch("/aiAnalysis/chart_interpretation", { const response = await fetch("/aiAnalysis/chart_interpretation", {
method: "POST", method: "POST",
headers: { headers: {
"X-API-Key": "aircasKEY19491001", "X-API-Key": "aircasKEY19491001",
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
body: JSON.stringify({ text }) body: JSON.stringify({ text }),
signal: signal // 【关键】绑定取消信号
}); });
if (!response.ok) { if (!response.ok) {
...@@ -29,6 +39,7 @@ export const useChartInterpretation = () => { ...@@ -29,6 +39,7 @@ export const useChartInterpretation = () => {
let buffer = ""; let buffer = "";
while (true) { while (true) {
// reader.read() 会在 signal abort 时抛出 AbortError
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) break;
...@@ -39,18 +50,43 @@ export const useChartInterpretation = () => { ...@@ -39,18 +50,43 @@ export const useChartInterpretation = () => {
for (const line of lines) { for (const line of lines) {
if (line.startsWith("data: ")) { if (line.startsWith("data: ")) {
const content = line.substring(6); const content = line.substring(6);
const textMatch = content.match(/"解读":\s*"([^"]*)"/); // 尝试解析 JSON
if (textMatch && textMatch[1]) { try {
interpretation.value = textMatch[1]; const jsonMatch = content.match(/\{.*\}/);
} if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
if (parsed["解读"]) {
interpretation.value = parsed["解读"];
}
} else {
// 兼容旧的正则匹配
const textMatch = content.match(/"解读":\s*"([^"]*)"/);
if (textMatch && textMatch[1]) {
interpretation.value = textMatch[1];
}
}
} catch (e) {
// 忽略解析错误
}
} }
} }
} }
} catch (err) { } catch (err) {
// 【关键】判断是否是因路由切换导致的取消
if (err.name === 'AbortError') {
console.log('AI 解读请求已取消');
loading.value = false; // 关闭 loading
return; // 直接返回,不设置 error,不弹窗
}
error.value = err.message || "AI 解读失败"; error.value = err.message || "AI 解读失败";
console.error("AI Chart Interpretation Error:", err); console.error("AI Chart Interpretation Error:", err);
} finally { } finally {
loading.value = false; // 只有在非 AbortError 的情况下,才由 finally 统一关闭 loading
// 如果上面 catch 中已经 return 了,这里不会执行
if (err?.name !== 'AbortError') {
loading.value = false;
}
} }
}; };
...@@ -60,4 +96,4 @@ export const useChartInterpretation = () => { ...@@ -60,4 +96,4 @@ export const useChartInterpretation = () => {
error, error,
interpret interpret
}; };
}; };
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论