提交 5c950255 authored 作者: coderBryanFu's avatar coderBryanFu

feat:新增组件:时间排序选择框

流水线 #568 已通过 于阶段
in 1 分 42 秒
// 引入 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: 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 })
} }
......
...@@ -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: "+"
} }
}); });
......
...@@ -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();
} }
......
...@@ -35,7 +35,7 @@ const billRoutes = [ ...@@ -35,7 +35,7 @@ const billRoutes = [
component: BillAllCommittee, component: BillAllCommittee,
meta: { meta: {
title: "法案委员会列表", title: "法案委员会列表",
isShowHeader: true isShowHeader: false
} }
}, },
{ {
......
...@@ -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 {
......
...@@ -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,
......
...@@ -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,
......
...@@ -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: -20px; left: -24px;
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: -20px; 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;
......
...@@ -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: -20px; left: -24px;
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: -20px; 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 {
......
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,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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论