Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
R
risk-monitor
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
蔡建
risk-monitor
Commits
5c950255
提交
5c950255
authored
4月 21, 2026
作者:
coderBryanFu
浏览文件
操作
浏览文件
下载
差异文件
feat:新增组件:时间排序选择框
上级
2959b3ad
f0cdfc5f
流水线
#568
已通过 于阶段
in 1 分 42 秒
变更
14
流水线
1
全部展开
显示空白字符变更
内嵌
并排
正在显示
14 个修改的文件
包含
431 行增加
和
113 行删除
+431
-113
service-back.js
src/api/finance/service-back.js
+153
-0
service.js
src/api/finance/service.js
+54
-21
index.vue
src/components/base/SummaryCardsPanel/index.vue
+5
-1
index.js
src/router/index.js
+5
-45
bill.js
src/router/modules/bill.js
+1
-1
index.vue
src/views/bill/allCommittee/index.vue
+63
-5
box8-header-icon.png
src/views/bill/billHome/assets/images/box8-header-icon.png
+0
-0
index.vue
src/views/bill/billHome/index.vue
+0
-0
doublePieChart.js
src/views/bill/billHome/utils/doublePieChart.js
+1
-1
piechart.js
src/views/bill/billHome/utils/piechart.js
+2
-1
index.vue
src/views/exportControl/index.vue
+23
-15
index.vue
src/views/finance/index.vue
+24
-22
common-back.js
src/views/finance/utils/common-back.js
+63
-0
common.js
src/views/finance/utils/common.js
+37
-1
没有找到文件。
src/api/finance/service-back.js
0 → 100644
浏览文件 @
5c950255
// 引入 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
src/api/finance/service.js
浏览文件 @
5c950255
// 引入 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
:
30
*
1000
// 请求超时时间 30s
timeout
:
30
*
1000
// 请求超时时间 30s
})
})
// 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
)
}
// 处理 token 过期或无效的情况
console
.
log
(
'err'
+
error
)
// 处理 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
})
}
}
...
...
src/components/base/SummaryCardsPanel/index.vue
浏览文件 @
5c950255
...
@@ -25,7 +25,7 @@
...
@@ -25,7 +25,7 @@
<el-icon
color=
"var(--color-primary-100)"
>
<el-icon
color=
"var(--color-primary-100)"
>
<ArrowRightBold
/>
<ArrowRightBold
/>
</el-icon>
</el-icon>
<div
class=
"item-dot"
v-if=
"item.delta"
>
+
{{ item.delta }}
</div>
<div
class=
"item-dot"
v-if=
"item.delta"
>
{{ dotPrefix }}
{{ item.delta }}
</div>
</div>
</div>
<div
v-if=
"shouldShowMoreCard"
class=
"summary-item"
@
click=
"emit('more-click')"
>
<div
v-if=
"shouldShowMoreCard"
class=
"summary-item"
@
click=
"emit('more-click')"
>
...
@@ -100,6 +100,10 @@ const props = defineProps({
...
@@ -100,6 +100,10 @@ const props = defineProps({
loading
:
{
loading
:
{
type
:
Boolean
,
type
:
Boolean
,
default
:
false
default
:
false
},
dotPrefix
:
{
type
:
String
,
default
:
"+"
}
}
});
});
...
...
src/router/index.js
浏览文件 @
5c950255
...
@@ -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
();
}
}
...
...
src/router/modules/bill.js
浏览文件 @
5c950255
...
@@ -35,7 +35,7 @@ const billRoutes = [
...
@@ -35,7 +35,7 @@ const billRoutes = [
component
:
BillAllCommittee
,
component
:
BillAllCommittee
,
meta
:
{
meta
:
{
title
:
"法案委员会列表"
,
title
:
"法案委员会列表"
,
isShowHeader
:
tru
e
isShowHeader
:
fals
e
}
}
},
},
{
{
...
...
src/views/bill/allCommittee/index.vue
浏览文件 @
5c950255
...
@@ -14,6 +14,12 @@
...
@@ -14,6 +14,12 @@
placeholder=
"搜索委员会"
placeholder=
"搜索委员会"
/>
/>
</div>
</div>
<div
class=
"hard-select"
>
<el-select
v-model=
"committeeInfo.metricType"
@
change=
"onAllCommittee()"
placeholder=
"统计口径"
style=
"width: 160px; margin-left: 8px"
>
<el-option
label=
"政令数据总量"
:value=
"1"
/>
<el-option
label=
"政令新增数量"
:value=
"2"
/>
</el-select>
</div>
</div>
</div>
<div
class=
"date-box"
>
<div
class=
"date-box"
>
...
@@ -30,7 +36,7 @@
...
@@ -30,7 +36,7 @@
<div
class=
"item-name one-line-ellipsis"
>
{{
item
.
name
}}
</div>
<div
class=
"item-name one-line-ellipsis"
>
{{
item
.
name
}}
</div>
<div
class=
"item-chamber one-line-ellipsis"
>
{{
item
.
chamber
}}
</div>
<div
class=
"item-chamber one-line-ellipsis"
>
{{
item
.
chamber
}}
</div>
</div>
</div>
<div
class=
"item-total"
>
{{
item
.
count
}}
项
</div>
<div
class=
"item-total"
>
{{
getDisplayCount
(
item
)
}}
项
</div>
<el-icon
color=
"var(--color-primary-100)"
>
<el-icon
color=
"var(--color-primary-100)"
>
<ArrowRightBold
/>
<ArrowRightBold
/>
</el-icon>
</el-icon>
...
@@ -48,6 +54,12 @@
...
@@ -48,6 +54,12 @@
/>
/>
</div>
</div>
</div>
</div>
<div
class=
"back-bnt"
@
click=
"handleBack"
>
<el-icon>
<Back
/>
</el-icon>
<div
class=
"back-text"
>
返回
</div>
</div>
</div>
</div>
</
template
>
</
template
>
...
@@ -55,6 +67,7 @@
...
@@ -55,6 +67,7 @@
import
{
onMounted
,
reactive
,
ref
}
from
"vue"
;
import
{
onMounted
,
reactive
,
ref
}
from
"vue"
;
import
{
Search
}
from
"@element-plus/icons-vue"
;
import
{
Search
}
from
"@element-plus/icons-vue"
;
import
{
ArrowRightBold
}
from
"@element-plus/icons-vue"
;
import
{
ArrowRightBold
}
from
"@element-plus/icons-vue"
;
import
{
Back
}
from
"@element-plus/icons-vue"
;
import
router
from
"@/router"
;
import
router
from
"@/router"
;
import
TimeTabPane
from
"@/components/base/TimeTabPane/index.vue"
;
import
TimeTabPane
from
"@/components/base/TimeTabPane/index.vue"
;
import
{
getStatisticsBillCountByCommittee
}
from
"@/api/bill/billHome"
;
import
{
getStatisticsBillCountByCommittee
}
from
"@/api/bill/billHome"
;
...
@@ -66,6 +79,7 @@ const committeeInfo = reactive({
...
@@ -66,6 +79,7 @@ const committeeInfo = reactive({
pageSize
:
8
,
pageSize
:
8
,
total
:
0
,
total
:
0
,
keyWord
:
""
,
keyWord
:
""
,
metricType
:
1
,
dateDesc
:
"近一年"
,
dateDesc
:
"近一年"
,
list
:
[]
list
:
[]
});
});
...
@@ -89,10 +103,11 @@ const onAllCommittee = async num => {
...
@@ -89,10 +103,11 @@ const onAllCommittee = async num => {
id
:
`
${
item
.
orgType
||
""
}
-
${
item
.
orgName
||
""
}
`
,
id
:
`
${
item
.
orgType
||
""
}
-
${
item
.
orgName
||
""
}
`
,
name
:
item
.
orgName
,
name
:
item
.
orgName
,
chamber
:
getChamberLabel
(
item
.
orgType
),
chamber
:
getChamberLabel
(
item
.
orgType
),
count
:
Number
(
item
.
count
||
0
)
totalCount
:
Number
(
item
.
count
||
0
),
recentCount
:
Number
(
item
.
countRecent
||
item
.
totalRecent
||
item
.
recentCount
||
item
.
recent
||
item
.
newCount
||
0
)
}))
}))
.
filter
(
item
=>
!
committeeInfo
.
keyWord
||
item
.
name
?.
includes
(
committeeInfo
.
keyWord
))
.
filter
(
item
=>
!
committeeInfo
.
keyWord
||
item
.
name
?.
includes
(
committeeInfo
.
keyWord
))
.
sort
((
a
,
b
)
=>
(
b
.
count
||
0
)
-
(
a
.
count
||
0
));
.
sort
((
a
,
b
)
=>
getSortValue
(
b
)
-
getSortValue
(
a
));
committeeInfo
.
total
=
source
.
length
;
committeeInfo
.
total
=
source
.
length
;
const
start
=
(
committeeInfo
.
pageNum
-
1
)
*
committeeInfo
.
pageSize
;
const
start
=
(
committeeInfo
.
pageNum
-
1
)
*
committeeInfo
.
pageSize
;
...
@@ -108,20 +123,36 @@ const onAllCommittee = async num => {
...
@@ -108,20 +123,36 @@ const onAllCommittee = async num => {
committeeInfo
.
loading
=
false
;
committeeInfo
.
loading
=
false
;
};
};
const
getSortValue
=
item
=>
{
if
(
committeeInfo
.
metricType
===
2
)
return
Number
(
item
?.
recentCount
||
0
);
return
Number
(
item
?.
totalCount
||
0
);
};
const
getDisplayCount
=
item
=>
{
return
getSortValue
(
item
);
};
const
handleDateChange
=
event
=>
{
const
handleDateChange
=
event
=>
{
committeeInfo
.
dateDesc
=
event
?.
time
||
"近一年"
;
committeeInfo
.
dateDesc
=
event
?.
time
||
"近一年"
;
onAllCommittee
();
onAllCommittee
();
};
};
const
handleToDataLibrary
=
item
=>
{
const
handleToDataLibrary
=
item
=>
{
const
route
=
router
.
resolve
({
router
.
push
({
path
:
"/dataLibrary/countryBill"
,
path
:
"/dataLibrary/countryBill"
,
query
:
{
query
:
{
selectedOrg
:
item
.
name
,
selectedOrg
:
item
.
name
,
selectedCongress
:
item
.
chamber
selectedCongress
:
item
.
chamber
}
}
});
});
window
.
open
(
route
.
href
,
"_blank"
);
};
const
handleBack
=
()
=>
{
if
(
window
.
history
.
length
>
1
)
{
router
.
back
();
return
;
}
router
.
push
(
"/billHome"
);
};
};
const
refCommittee
=
ref
();
const
refCommittee
=
ref
();
...
@@ -143,6 +174,28 @@ onMounted(() => {
...
@@ -143,6 +174,28 @@ onMounted(() => {
background-size
:
100%
100%
;
background-size
:
100%
100%
;
display
:
flex
;
display
:
flex
;
justify-content
:
center
;
justify-content
:
center
;
position
:
relative
;
.back-bnt
{
position
:
absolute
;
top
:
16px
;
left
:
30px
;
width
:
86px
;
height
:
38px
;
background-color
:
white
;
border-radius
:
19px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
color
:
var
(
--
text-primary-65-color
);
font-family
:
Source
Han
Sans
CN
;
font-size
:
16px
;
cursor
:
pointer
;
}
.back-text
{
margin-left
:
6px
;
}
.container-box
{
.container-box
{
width
:
1600px
;
width
:
1600px
;
...
@@ -180,6 +233,11 @@ onMounted(() => {
...
@@ -180,6 +233,11 @@ onMounted(() => {
width
:
180px
;
width
:
180px
;
height
:
32px
;
height
:
32px
;
}
}
.hard-select
{
height
:
42px
;
padding
:
5px
0
;
}
}
}
.date-box
{
.date-box
{
...
...
src/views/bill/billHome/assets/images/box8-header-icon.png
0 → 100644
浏览文件 @
5c950255
1.2 KB
src/views/bill/billHome/index.vue
浏览文件 @
5c950255
差异被折叠。
点击展开。
src/views/bill/billHome/utils/doublePieChart.js
浏览文件 @
5c950255
...
@@ -69,7 +69,7 @@ const getDoublePieChart = (data1, data2) => {
...
@@ -69,7 +69,7 @@ const getDoublePieChart = (data1, data2) => {
const
name
=
truncateLabel
(
params
?.
name
,
6
)
const
name
=
truncateLabel
(
params
?.
name
,
6
)
const
value
=
params
?.
value
??
0
const
value
=
params
?.
value
??
0
const
percent
=
typeof
params
?.
percent
===
'number'
?
params
.
percent
:
0
const
percent
=
typeof
params
?.
percent
===
'number'
?
params
.
percent
:
0
return
`{name|
${
name
}
}\n{time|
${
value
}
条
${
percent
}
%}`
return
`{name|
${
name
}
}\n{time|
${
value
}
项
${
percent
}
%}`
},
},
minMargin
:
5
,
minMargin
:
5
,
edgeDistance
:
10
,
edgeDistance
:
10
,
...
...
src/views/bill/billHome/utils/piechart.js
浏览文件 @
5c950255
...
@@ -10,6 +10,7 @@ const truncateLabel = (value, maxLen = 6) => {
...
@@ -10,6 +10,7 @@ const truncateLabel = (value, maxLen = 6) => {
const
getPieChart
=
(
data
,
colorList
,
options
=
{})
=>
{
const
getPieChart
=
(
data
,
colorList
,
options
=
{})
=>
{
const
showCount
=
options
.
showCount
!==
false
const
showCount
=
options
.
showCount
!==
false
const
countUnit
=
options
.
countUnit
||
'条'
const
chartColors
=
Array
.
isArray
(
colorList
)
&&
colorList
.
length
?
colorList
:
MUTICHARTCOLORS
const
chartColors
=
Array
.
isArray
(
colorList
)
&&
colorList
.
length
?
colorList
:
MUTICHARTCOLORS
let
option
=
{
let
option
=
{
color
:
chartColors
,
color
:
chartColors
,
...
@@ -38,7 +39,7 @@ const getPieChart = (data, colorList, options = {}) => {
...
@@ -38,7 +39,7 @@ const getPieChart = (data, colorList, options = {}) => {
const
name
=
truncateLabel
(
params
?.
name
,
6
)
const
name
=
truncateLabel
(
params
?.
name
,
6
)
const
value
=
params
?.
value
??
0
const
value
=
params
?.
value
??
0
const
percent
=
typeof
params
?.
percent
===
'number'
?
params
.
percent
:
0
const
percent
=
typeof
params
?.
percent
===
'number'
?
params
.
percent
:
0
const
labelText
=
showCount
?
`
${
value
}
条
${
percent
}
%`
:
`
${
percent
}
%`
const
labelText
=
showCount
?
`
${
value
}
${
countUnit
}
${
percent
}
%`
:
`
${
percent
}
%`
return
`{name|
${
name
}
}\n{time|
${
labelText
}
}`
return
`{name|
${
name
}
}\n{time|
${
labelText
}
}`
},
},
minMargin
:
5
,
minMargin
:
5
,
...
...
src/views/exportControl/index.vue
浏览文件 @
5c950255
...
@@ -34,14 +34,19 @@
...
@@ -34,14 +34,19 @@
<img
src=
"./assets/images/box1-right.png"
alt=
""
/>
<img
src=
"./assets/images/box1-right.png"
alt=
""
/>
</div>
</div>
</div>
</div>
<el-carousel
ref=
"carouselRef"
height=
"370px"
:autoplay=
"true"
:interval=
"3000"
arrow=
"never"
<el-carousel
indicator-position=
"none"
@
change=
"handleCarouselChange"
>
ref=
"carouselRef"
height=
"370px"
:autoplay=
"false"
:interval=
"3000"
arrow=
"never"
indicator-position=
"none"
@
change=
"handleCarouselChange"
>
<el-carousel-item
v-for=
"(item, index) in entitiesDataInfoList"
:key=
"item.id + index"
>
<el-carousel-item
v-for=
"(item, index) in entitiesDataInfoList"
:key=
"item.id + index"
>
<div>
<div>
<div
class=
"box1-top"
>
<div
class=
"box1-top"
>
<div
class=
"box1-top-title"
>
<div
class=
"box1-top-title"
>
{{
item
.
postDate
}}
——
{{
item
.
name
}}
</div>
{{
item
.
postDate
}}
——BIS《实体清单增列与修订条目》
</div>
<div
class=
"box1-top-content"
>
<div
class=
"box1-top-content"
>
<div
class=
"box1-top-content-item"
>
<div
class=
"box1-top-content-item"
>
<span
class=
"box1-top-content-item-title"
>
· 发布机构:
</span>
<span
class=
"box1-top-content-item-title"
>
· 发布机构:
</span>
...
@@ -53,14 +58,12 @@
...
@@ -53,14 +58,12 @@
</div>
</div>
<div
class=
"box1-top-content-item"
>
<div
class=
"box1-top-content-item"
>
<span
class=
"box1-top-content-item-title"
>
· 涉及领域:
</span>
<span
class=
"box1-top-content-item-title"
>
· 涉及领域:
</span>
<!--
<div
class=
"box1-top-content-item-tags"
<AreaTag
v-for=
"(domainItem, index) in item.domains"
v-for=
"(domainItem, index) in item.domains"
:key=
"index"
:key=
"index"
>
:tagName=
"domainItem"
<el-tag
:type=
"getTagType(domainItem)"
>
{{
domainItem
}}
</el-tag>
/>
</div>
-->
<AreaTag
v-for=
"(domainItem, index) in item.domains"
:key=
"index"
:tagName=
"domainItem"
/>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -110,7 +113,7 @@
...
@@ -110,7 +113,7 @@
}}
</span>
}}
</span>
</div>
</div>
<div
class=
"box1-absolute-num"
>
<div
class=
"box1-absolute-num"
>
{{
item
.
cnEntityCount
}}{{
item
.
sanTypeId
==
allSanTypeIds
[
0
]
?
"家"
:
"
类
"
}}
{{
item
.
cnEntityCount
}}{{
item
.
sanTypeId
==
allSanTypeIds
[
0
]
?
"家"
:
"
项
"
}}
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -1819,11 +1822,12 @@ const handleMediaClick = item => {
...
@@ -1819,11 +1822,12 @@ const handleMediaClick = item => {
flex-direction
:
column
;
flex-direction
:
column
;
gap
:
20px
;
gap
:
20px
;
position
:
relative
;
position
:
relative
;
width
:
1036px
;
.box1-left-arrow
{
.box1-left-arrow
{
position
:
absolute
;
position
:
absolute
;
z-index
:
9999
;
z-index
:
9999
;
left
:
-2
0
px
;
left
:
-2
4
px
;
top
:
135px
;
top
:
135px
;
width
:
24px
!
important
;
width
:
24px
!
important
;
height
:
48px
;
height
:
48px
;
...
@@ -1847,7 +1851,7 @@ const handleMediaClick = item => {
...
@@ -1847,7 +1851,7 @@ const handleMediaClick = item => {
.box1-right-arrow
{
.box1-right-arrow
{
position
:
absolute
;
position
:
absolute
;
z-index
:
9999
;
z-index
:
9999
;
right
:
-2
0px
;
right
:
0px
;
top
:
135px
;
top
:
135px
;
width
:
24px
;
width
:
24px
;
height
:
48px
;
height
:
48px
;
...
@@ -1912,6 +1916,10 @@ const handleMediaClick = item => {
...
@@ -1912,6 +1916,10 @@ const handleMediaClick = item => {
color
:
$base-color
;
color
:
$base-color
;
margin-top
:
10px
;
margin-top
:
10px
;
margin-bottom
:
15px
;
margin-bottom
:
15px
;
max-width
:
80%
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
}
&
-content
{
&
-content
{
...
@@ -1938,7 +1946,7 @@ const handleMediaClick = item => {
...
@@ -1938,7 +1946,7 @@ const handleMediaClick = item => {
height
:
172px
;
height
:
172px
;
padding-top
:
16px
;
padding-top
:
16px
;
box-sizing
:
border-box
;
box-sizing
:
border-box
;
padding-right
:
24px
;
&
-title
{
&
-title
{
font-size
:
16px
;
font-size
:
16px
;
font-weight
:
700
;
font-weight
:
700
;
...
...
src/views/finance/index.vue
浏览文件 @
5c950255
...
@@ -60,9 +60,10 @@
...
@@ -60,9 +60,10 @@
<div>
<div>
<div
class=
"box1-top"
>
<div
class=
"box1-top"
>
<div
class=
"box1-top-title"
>
<div
class=
"box1-top-title"
>
{{
item
.
postDate
}}
——
{{
<!--
{{
item
.
postDate
}}
——
{{
item
.
sanTypeId
==
allSanTypeIds
[
0
]
?
"OFAC"
:
"DoD"
item
.
sanTypeId
==
allSanTypeIds
[
0
]
?
"OFAC"
:
"DoD"
}}
《实体清单增列与修订条目》
}}
《实体清单增列与修订条目》 -->
{{
item
.
postDate
}}
——
{{
item
.
name
}}
</div>
</div>
<div
class=
"box1-top-content"
>
<div
class=
"box1-top-content"
>
<div
class=
"box1-top-content-item"
>
<div
class=
"box1-top-content-item"
>
...
@@ -1073,7 +1074,7 @@ const handleToEntityList = item => {
...
@@ -1073,7 +1074,7 @@ const handleToEntityList = item => {
console
.
log
(
"这是什么数据1 =>"
,
item
);
console
.
log
(
"这是什么数据1 =>"
,
item
);
let
id
=
item
?.
id
;
let
id
=
item
?.
id
;
let
sanTypeId
=
item
?.
sanTypeId
||
1
;
let
sanTypeId
=
item
?.
sanTypeId
||
1
;
let
date
=
entitiesDataInfoList
.
value
[
currentCarouselIndex
.
value
].
postDate
let
date
=
entitiesDataInfoList
.
value
[
currentCarouselIndex
.
value
].
postDate
;
if
(
!
id
)
{
if
(
!
id
)
{
const
currentItem
=
entitiesDataInfoList
.
value
[
currentCarouselIndex
.
value
];
const
currentItem
=
entitiesDataInfoList
.
value
[
currentCarouselIndex
.
value
];
id
=
currentItem
?.
id
;
id
=
currentItem
?.
id
;
...
@@ -1826,14 +1827,20 @@ const handleToDataLibrary = item => {
...
@@ -1826,14 +1827,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
{
...
@@ -1874,6 +1881,11 @@ onMounted(async () => {
...
@@ -1874,6 +1881,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
:
"美国商务部发布实体清单的频次"
,
...
@@ -1892,21 +1904,6 @@ onMounted(async () => {
...
@@ -1892,21 +1904,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
);
...
@@ -1957,11 +1954,12 @@ const handleMediaClick = item => {
...
@@ -1957,11 +1954,12 @@ const handleMediaClick = item => {
flex-direction
:
column
;
flex-direction
:
column
;
gap
:
20px
;
gap
:
20px
;
position
:
relative
;
position
:
relative
;
width
:
1036px
;
.box1-left-arrow
{
.box1-left-arrow
{
position
:
absolute
;
position
:
absolute
;
z-index
:
9999
;
z-index
:
9999
;
left
:
-2
0
px
;
left
:
-2
4
px
;
top
:
135px
;
top
:
135px
;
width
:
24px
!
important
;
width
:
24px
!
important
;
height
:
48px
;
height
:
48px
;
...
@@ -1985,7 +1983,7 @@ const handleMediaClick = item => {
...
@@ -1985,7 +1983,7 @@ const handleMediaClick = item => {
.box1-right-arrow
{
.box1-right-arrow
{
position
:
absolute
;
position
:
absolute
;
z-index
:
9999
;
z-index
:
9999
;
right
:
-2
0px
;
right
:
0px
;
top
:
135px
;
top
:
135px
;
width
:
24px
;
width
:
24px
;
height
:
48px
;
height
:
48px
;
...
@@ -2050,6 +2048,10 @@ const handleMediaClick = item => {
...
@@ -2050,6 +2048,10 @@ const handleMediaClick = item => {
color
:
$base-color
;
color
:
$base-color
;
margin-top
:
10px
;
margin-top
:
10px
;
margin-bottom
:
15px
;
margin-bottom
:
15px
;
max-width
:
80%
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
}
&
-content
{
&
-content
{
...
...
src/views/finance/utils/common-back.js
0 → 100644
浏览文件 @
5c950255
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
浏览文件 @
5c950255
// 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,19 +50,44 @@ export const useChartInterpretation = () => {
...
@@ -39,19 +50,44 @@ 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
);
// 尝试解析 JSON
try
{
const
jsonMatch
=
content
.
match
(
/
\{
.*
\}
/
);
if
(
jsonMatch
)
{
const
parsed
=
JSON
.
parse
(
jsonMatch
[
0
]);
if
(
parsed
[
"解读"
])
{
interpretation
.
value
=
parsed
[
"解读"
];
}
}
else
{
// 兼容旧的正则匹配
const
textMatch
=
content
.
match
(
/"解读":
\s
*"
([^
"
]
*
)
"/
);
const
textMatch
=
content
.
match
(
/"解读":
\s
*"
([^
"
]
*
)
"/
);
if
(
textMatch
&&
textMatch
[
1
])
{
if
(
textMatch
&&
textMatch
[
1
])
{
interpretation
.
value
=
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
{
// 只有在非 AbortError 的情况下,才由 finally 统一关闭 loading
// 如果上面 catch 中已经 return 了,这里不会执行
if
(
err
?.
name
!==
'AbortError'
)
{
loading
.
value
=
false
;
loading
.
value
=
false
;
}
}
}
};
};
return
{
return
{
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论