提交 0204674d authored 作者: 张烨's avatar 张烨

Merge remote-tracking branch 'origin/pre' into zy-dev

流水线 #567 已通过 于阶段
in 1 分 34 秒
// 引入 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'
// 引入 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 }
// 定义全局控制器,以便在取消后重新赋值
let currentAbortController = new AbortController()
/**
* 获取当前有效的 AbortSignal
* 供 axios 拦截器和 fetch 请求共同使用
*/
export const getAbortSignal = () => {
return currentAbortController.signal
}
/**
* 取消所有正在进行的请求
* 路由守卫中调用此方法
*/
export const cancelAllRequests = () => {
// 1. 终止当前控制器的所有请求
currentAbortController.abort()
// 2. 创建一个新的控制器,供后续新请求使用
currentAbortController = new AbortController()
}
// 创建 axios 实例
const service = axios.create({
timeout: 300 * 1000 // 请求超时时间
timeout: 30 * 1000 // 请求超时时间 30s
})
// request 拦截器:与主 request 一致,就地改 config,避免 mergeConfig 破坏 url
// request 拦截器
service.interceptors.request.use(config => {
const raw = getToken()
const token = raw ? String(raw).trim() : ""
// 处理 Headers
if (!config.headers) {
config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers)
}
// 设置 Token
if (token) {
config.headers.set('token', token, true)
} else {
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 ?? '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) {
// 确保 headers 存在
if (!config.headers) {
config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) {
......@@ -42,6 +67,13 @@ service.interceptors.request.use(config => {
config.headers.set('X-API-Key', aiApiKey)
}
}
// 将全局控制器的 signal 注入到 axios 请求中
// 如果用户手动传入了 signal,则优先使用用户的(虽然少见)
if (!config.signal) {
config.signal = getAbortSignal()
}
return config
}, error => {
console.log(error)
......@@ -55,7 +87,6 @@ service.interceptors.response.use(
if (!res) {
return Promise.reject(new Error('响应数据为空'))
}
// 根据需求:接口返回 code 不等于 200 的时候报错
if (res.code !== 200) {
ElMessage({
message: res.message || '请求失败',
......@@ -67,19 +98,25 @@ service.interceptors.response.use(
return res.data
},
error => {
console.log('err' + error)
// 精准识别取消错误,避免弹窗骚扰用户
const isCanceledError =
axios.isCancel(error) ||
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)
if (isCanceledError) {
// 静默处理取消错误,不弹窗,不打印.error
return Promise.reject(error)
}
console.log('err' + error)
// 处理 token 过期或无效的情况
// 处理 Token 过期
const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
......@@ -94,14 +131,11 @@ service.interceptors.response.use(
const hadToken =
h &&
(typeof h.get === 'function'
? Boolean(
h.get('token') || h.get('Token')
)
: Boolean(
h.token || h.Token
))
? Boolean(h.get('token') || h.get('Token'))
: Boolean(h.token || h.Token))
if (hadToken) removeToken()
} else {
// 只有非取消、非 Token 过期的错误才弹出通用提示
ElMessage({
message: typeof error?.message === 'string' ? error.message : '请求失败',
type: 'error',
......@@ -112,12 +146,11 @@ service.interceptors.response.use(
}
)
// 封装通用请求函数(支持 http(config) 和 http.get/post 等调用方式)
// 封装通用请求函数
function http(config) {
return service(config)
}
// 为 http 函数添加快捷方法
http.get = function(url, params) {
return service({ url, method: 'get', params })
}
......
......@@ -17,6 +17,14 @@ export function getLatestRisks() {
});
}
// 中美博弈概览V2:首页最新风险动态(10条 + 各类数量/总数)
export function getLatestRisk() {
return request({
method: "GET",
url: `/api/rivalryIndexV2/LatestRisk`
});
}
// 中美博弈概览V2:美对华制裁措施数量趋势
export function geDomainContainmentTrend(params) {
return request({
......
......@@ -132,6 +132,28 @@
position: relative;
}
.risk-signal-detail-dialog .el-dialog__headerbtn {
top: 11px !important;
right: 16px !important;
width: 32px !important;
height: 32px !important;
display: inline-flex;
align-items: center;
justify-content: center;
}
.risk-signal-detail-dialog .el-dialog__headerbtn .el-dialog__close {
width: 16px;
height: 16px;
color: rgb(59, 65, 75) !important;
}
.risk-signal-detail-dialog .el-dialog__headerbtn .el-dialog__close svg,
.risk-signal-detail-dialog .el-dialog__headerbtn .el-dialog__close svg path {
fill: rgb(59, 65, 75) !important;
stroke: rgb(59, 65, 75) !important;
}
.risk-signal-detail-dialog .el-dialog__header::after {
content: "";
position: absolute;
......@@ -234,7 +256,7 @@
.risk-signal-detail-dialog .risk-signal-detail-dialog__read-indicator {
position: absolute;
right: 115px;
right: 61px;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
......
......@@ -25,7 +25,7 @@
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</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 v-if="shouldShowMoreCard" class="summary-item" @click="emit('more-click')">
......@@ -100,6 +100,10 @@ const props = defineProps({
loading: {
type: Boolean,
default: false
},
dotPrefix: {
type: String,
default: "+"
}
});
......
<template>
<div class="module-header-wrapper">
<div class="module-header-wrapper" @mouseleave="handleHideUserPanel">
<div class="nav-content" :class="{ 'nav-content-library': isShowDataLibrary }">
<div class="nav-left" :class="{ 'flex-start': isShowSearchBar }">
<div class="icon">
......@@ -24,36 +24,36 @@
<div class="mail" @click="handleClickToolBox">
<img src="@/assets/icons/overview/mail.png" alt="" />
</div>
<div class="user-trigger">
<div class="user" @click.stop="handleToggleUserPanel">
<div class="user-trigger" @mouseenter="handleShowUserPanel">
<div class="user" @click.stop>
<img src="@/assets/icons/overview/user.png" alt="" />
</div>
</div>
<div v-if="isShowUserPanel" class="user-panel">
<div class="user-panel-row">
<div class="blue-solid"></div>
<span class="user-panel-value user-panel-value--nickname">{{ userNickname }}</span>
<div class="role-box">
<span class="user-panel-value user-panel-value--role">{{ roleName }}</span>
<div v-if="isShowUserPanel" class="user-panel">
<div class="user-panel-row">
<div class="blue-solid"></div>
<span class="user-panel-value user-panel-value--nickname">{{ userNickname }}</span>
<div class="role-box">
<span class="user-panel-value user-panel-value--role">{{ roleName }}</span>
</div>
</div>
</div>
<div class="user-panel-row user-panel-row--single">
<span class="user-panel-value user-panel-value--organ">{{ organName }}</span>
</div>
<div class="solid"></div>
<div class="user-panel-row user-panel-row--single">
<span class="user-panel-value user-panel-value--organ">{{ organName }}</span>
</div>
<div class="solid"></div>
<div class="user-panel-logout" @click.stop="handleUserCommand('logout')"><img src="./back.png"
class="back-image" />{{ "退出登录" }}
<div class="user-panel-logout" @click.stop="handleUserCommand('logout')"><img src="./back.png"
class="back-image" />{{ "退出登录" }}
</div>
</div>
</div>
</div>
</div>
<div class="menu-box" v-show="isShowMenu" @mouseenter="handleHoverMenu(true)"
<div class="menu-box" v-if="isShowMenu" @mouseenter="handleHoverMenu(true)"
@mouseleave="handleHoverMenu(false)">
<div class="menu-content">
<div class="menu-item" v-for="(item, index) in menuList" :key="index" @click="handleToModule(item, 1)">
......@@ -64,9 +64,9 @@
</div>
</div>
</div>
<div class="tool-box" v-show="isShowTool" @mouseenter="handleHoverTool(true)"
<div class="tool-box" v-if="isShowTool" @mouseenter="handleHoverTool(true)"
@mouseleave="handleHoverTool(false)">
<div class="menu-content">
<div class="tool-content">
<div class="menu-item" v-for="(item, index) in toolList" :key="index" @click="handleToModule(item, 2)">
<div class="icon">
<img :src="item.icon" alt="" />
......@@ -404,8 +404,8 @@ const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
const handleToggleUserPanel = () => {
isShowUserPanel.value = !isShowUserPanel.value;
const handleShowUserPanel = () => {
isShowUserPanel.value = true;
};
const handleHideUserPanel = () => {
......@@ -662,10 +662,13 @@ onMounted(() => {
top: calc(32px + 21px);
width: 240px;
height: 141px;
// 与“中美科技博弈”下方菜单框(.menu-box)保持一致的透明/模糊效果
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
display: flex;
flex-direction: column;
......@@ -785,6 +788,13 @@ onMounted(() => {
display: flex;
align-items: center;
margin-left: 8px;
transition: color 0.15s ease, font-size 0.15s ease, font-weight 0.15s ease;
&:hover {
color: var(--color-main-active);
font-size: 18px;
font-weight: 700;
}
.back-image {
width: 16px;
......@@ -805,23 +815,6 @@ onMounted(() => {
}
.menu-box {
// position: absolute;
// z-index: 999999999;
// width: 713px;
// height: 413px;
// top: 52px;
// left: 0;
// box-sizing: border-box;
// border-radius: 10px;
// backdrop-filter: blur(10px);
// -webkit-backdrop-filter: blur(10px);
// box-shadow: 0px 8px 32px 0px rgba(31, 38, 135, 0.15);
// background: rgba(255, 255, 255, 0.25);
// backdrop-filter: blur(10px);
// -webkit-backdrop-filter: blur(10px);
// border: 1px solid rgba(255, 255, 255, 0.3);
// background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
// box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.2);
position: absolute;
z-index: 999999;
width: 713px;
......@@ -902,21 +895,17 @@ onMounted(() => {
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 0.8);
.menu-content {
width: 562px;
height: 348px;
.tool-content {
width: 130px;
height: 120px;
margin-top: 8px;
margin-left: 72px;
.menu-item {
margin-top: 36px;
width: 280px;
width: 100%;
height: 24px;
display: flex;
cursor: pointer;
&:hover {
.title {
color: var(--color-main-active);
......
......@@ -12,14 +12,14 @@
<div class="box2-main">
<div class="box2-main-item" v-for="(item, index) in showRiskSignalList" :key="index" @click="handleItemClick(item, index)">
<div :class="{
<div v-if="!isRiskLevelNoData(item?.[props.riskLevel])" :class="{
itemLeftStatus1: item[props.riskLevel] === '特别重大',
itemLeftStatus2: item[props.riskLevel] === '重大风险',
itemLeftStatus3: item[props.riskLevel] === '较大风险',
itemLeftStatus4: item[props.riskLevel] === '一般风险' || !item[props.riskLevel],
itemLeftStatus4: item[props.riskLevel] === '一般风险',
itemLeftStatus5: item[props.riskLevel] === '低风险',
}">
{{ item[props.riskLevel] || "暂无数据" }}
{{ item[props.riskLevel] }}
</div>
<div class="item-right">
<div class="text"> <span class="text-inner">{{ item[props.name] }}</span></div>
......@@ -76,6 +76,11 @@ const props = defineProps({
});
const isRiskLevelNoData = (level) => {
const t = String(level ?? "").trim();
return !t || t === "暂无数据" || t === "暂无数值";
};
const showRiskSignalList = computed(() => {
return props.list.slice(0,6)
})
......
......@@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from "vue-router";
import { setToken, removeToken, getToken } from "@/api/request.js";
import { AUTH_LOGOUT_CHANNEL } from "@/utils/authCrossTabLogout.js";
import { cancelAllRequests } from "@/api/finance/service.js"
/** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */
const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id";
/** 退出后强制回登录页(跨标签页/刷新生效) */
......@@ -145,51 +147,9 @@ const router = createRouter({
// 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态)
// 3)已有本地 token:正常走前端路由
router.beforeEach((to, from, next) => {
// ===== SSO/重定向逻辑(切线上接口时停用,保留注释) =====
// const queryToken = to.query && to.query.token != null && String(to.query.token).trim() !== ""
// ? String(to.query.token).trim()
// : "";
//
// 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();
// 【新增】在每次路由跳转开始前,取消上一个页面所有未完成的请求
// 这能防止旧页面的数据回来覆盖新页面,也能减少服务器压力
cancelAllRequests();
if (import.meta.env.DEV) {
clearTokenIfNewDevBoot();
}
......
......@@ -35,7 +35,7 @@ const billRoutes = [
component: BillAllCommittee,
meta: {
title: "法案委员会列表",
isShowHeader: true
isShowHeader: false
}
},
{
......
import { useRouter } from "vue-router";
const router = useRouter()
// 跳转法案详情
export const goToBill = (id, tabName) => {
window.sessionStorage.setItem("billId", id);
window.sessionStorage.setItem("curTabName", tabName);
const route = router.resolve({
path: "/billLayout",
query: {
billId: id
}
});
window.open(route.href, "_blank");
};
// 跳转政令详情
export const goToDecree = (id, tabName) => {
window.sessionStorage.setItem("decreeId", id);
window.sessionStorage.setItem("curTabName", tabName);
const route = router.resolve({
path: "/decreeLayout",
query: {
id: id
}
});
window.open(route.href, "_blank");
};
// 跳转智库
export const goToThinkTank = (id, tabName) => {
window.sessionStorage.setItem("curTabName", tabName);
const route = router.resolve({
name: "ReportDetail",
params: {
id: id
}
});
window.open(route.href, "_blank");
}
// 跳转机构详情
export const goToInstitution = (id, tabName) => {
window.sessionStorage.setItem('curTabName', tabName)
const curRoute = router.resolve({
path: "/institution",
query: {
id: id
}
});
window.open(curRoute.href, "_blank");
}
\ No newline at end of file
......@@ -223,14 +223,14 @@ export function getRiskDetailLevelModifier(level) {
if (t === "低风险") {
return "lv5";
}
if (t === "一般风险" || !t || t === "暂无数据") {
if (t === "一般风险" || !t || t === "暂无数据" || t === "风险信号暂无评估") {
return "lv4";
}
return "lv4";
}
/** 与 `@/components/base/riskSignal` 左侧等级一致:空 / null 等展示「暂无数据」 */
export const RISK_SIGNAL_LIST_LEVEL_EMPTY_TEXT = "暂无数据";
/** 详情弹窗标题区等级文案:空 / null 等展示「风险信号暂无评估」 */
export const RISK_SIGNAL_LIST_LEVEL_EMPTY_TEXT = "风险信号暂无评估";
/**
* 列表行风险等级 → 弹窗标题区文案(null、空串与「暂无数值」等与列表「暂无数据」对齐)
......@@ -241,7 +241,7 @@ export function normalizeRiskSignalListLevelText(raw) {
return RISK_SIGNAL_LIST_LEVEL_EMPTY_TEXT;
}
const s = String(raw).trim();
if (s === "" || s === "null" || s === "undefined" || s === "暂无数值") {
if (s === "" || s === "null" || s === "undefined" || s === "暂无数值" || s === "暂无数据") {
return RISK_SIGNAL_LIST_LEVEL_EMPTY_TEXT;
}
return s;
......
......@@ -43,15 +43,10 @@
<div style="display: flex">
<!-- 风险信号列表 -->
<div class="risk-signals" ref="riskSignalsRef">
<div
class="risk-signals-item"
v-for="(item, index) in warningList"
<div class="risk-signals-item" v-for="(item, index) in warningList"
:key="item.signalId != null ? String(item.signalId) : 'risk-' + index"
@mouseenter="onMouseEnter(item, index)"
@mouseleave="onMouseLeave"
@click.stop
:class="['risk-signals-item', { 'risk-signals-item-hightLight': riskSignalActiveIndex === index }]"
>
@mouseenter="onMouseEnter(item, index)" @mouseleave="onMouseLeave" @click.stop
:class="['risk-signals-item', { 'risk-signals-item-hightLight': riskSignalActiveIndex === index }]">
<div class="item-left" :class="{
'item-status-1': item.signalLevel === '特别重大',
'item-status-2': item.signalLevel === '重大风险',
......@@ -132,7 +127,8 @@
import { color } from "echarts";
import { onMounted, ref, onUnmounted, computed } from "vue";
import WaveBall from "./WaveBall.vue";
import { getLatestRiskUpdates, getLatestRisks } from "@/api/zmOverview/risk/index.js";
import { getLatestRisk } from "@/api/zmOverview/risk/index.js";
import { getRiskSignalInfoById } from "@/api/riskSignal/index.js";
import router from "@/router/index";
import { navigateToViewRiskSignal } from "@/utils/riskSignalOverviewNavigate";
import icon1 from "./icon/title-1.svg";
......@@ -315,12 +311,14 @@ const cardShowIndex4 = ref(0);
// 最新风险动态统计
const handleGetLatestRiskUpdates = async () => {
try {
const params = {
currentDate: "本周"
};
const res = await getLatestRiskUpdates(params);
console.log("最新风险动态统计1", res);
const res = await getLatestRisk();
console.log("最新风险动态", res);
if (res.code === 200 && res.data) {
const d = res.data;
const formatChange = (n) => {
const v = Number(n) || 0;
return v === 0 ? "无新增" : String(v);
};
sections.value = [
{
// title: res.data.policiesRegulations.hotspotTitle,
......@@ -328,16 +326,16 @@ const handleGetLatestRiskUpdates = async () => {
waveBall: [
{
percent: 30, // 估算的百分比
count: res.data.bill.total,
change: res.data.bill.dailyIncrement,
count: d.billRiskTotal ?? 0,
change: formatChange(d.billRiskNum),
unit: "项",
title: "法案(提出)",
type: "法案"
},
{
percent: 20, // 估算的百分比
count: res.data.administrativeOrder.total,
change: res.data.administrativeOrder.dailyIncrement,
count: d.orderRiskTotal ?? 0,
change: formatChange(d.orderRiskNum),
unit: "个",
title: "政令",
type: "行政令"
......@@ -350,8 +348,8 @@ const handleGetLatestRiskUpdates = async () => {
waveBall: [
{
percent: 10, // 估算的百分比
count: res.data.Entities.total,
change: res.data.Entities.dailyIncrement,
count: d.entityRiskTotal ?? 0,
change: formatChange(d.entityRiskNum),
unit: "次",
title: "实体清单",
......@@ -359,8 +357,8 @@ const handleGetLatestRiskUpdates = async () => {
},
{
percent: 20, // 估算的百分比
count: res.data.CCL.total,
change: res.data.CCL.dailyIncrement,
count: d.cclRiskTotal ?? 0,
change: formatChange(d.cclRiskNum),
unit: "次",
title: "CCL",
type: "CCL"
......@@ -373,16 +371,16 @@ const handleGetLatestRiskUpdates = async () => {
waveBall: [
{
percent: 15, // 估算的百分比
count: res.data.SDN.total,
change: res.data.SDN.dailyIncrement,
count: d.sdnRiskTotal ?? 0,
change: formatChange(d.sdnRiskNum),
unit: "次",
title: "SDN",
type: "SDN"
},
{
percent: 5, // 估算的百分比
count: res.data.militaryInvolvement.total,
change: res.data.militaryInvolvement.dailyIncrement,
count: d.armyRiskTotal ?? 0,
change: formatChange(d.armyRiskNum),
unit: "家",
title: "涉军企业",
type: "涉军企业"
......@@ -395,24 +393,24 @@ const handleGetLatestRiskUpdates = async () => {
waveBall: [
{
percent: 3, // 估算的百分比
count: res.data["337Survey"].total,
change: res.data["337Survey"].dailyIncrement,
count: d.m337RiskTotal ?? 0,
change: formatChange(d.m337RiskNum),
unit: "次",
title: "337调查",
type: "337调查"
},
{
percent: 3, // 估算的百分比
count: res.data["232Survey"].total,
change: res.data["232Survey"].dailyIncrement,
count: d.m232RiskTotal ?? 0,
change: formatChange(d.m232RiskNum),
unit: "次",
title: "232调查",
type: "232调查"
},
{
percent: 3, // 估算的百分比
count: res.data["301Survey"].total,
change: res.data["301Survey"].dailyIncrement,
count: d.m301RiskTotal ?? 0,
change: formatChange(d.m301RiskNum),
unit: "次",
title: "301调查",
type: "301调查"
......@@ -454,12 +452,60 @@ const hotNewsList = ref([
//最新风险信号
const handleGetLatestRisks = async () => {
try {
const res = await getLatestRisks();
console.log("最新风险信号", res);
const res = await getLatestRisk();
console.log("最新风险动态(列表)", res);
if (res.code === 200 && res.data) {
warningList.value = res.data.riskVOS;
hotNewsList.value = res.data.hotspotVOS;
riskTotal.value = res.data.riskCount;
const d = res.data;
const list = Array.isArray(d.riskSignals) ? d.riskSignals : [];
const getEventTypeFromModule = (code) => {
const c = String(code ?? "").trim();
if (c === "0100") return "法案";
if (c === "0101") return "行政令";
if (c === "0102") return "智库报告";
if (c === "0103") return "出口管制";
if (c === "0104") return "市场准入";
if (c === "0105") return "创新主体";
if (c === "0106") return "合作限制";
if (c === "0107") return "科研资助体系";
if (c === "0108") return "规则限制";
if (c === "0109") return "投融资限制";
if (c === "01031") return "实体清单";
if (c === "01032") return "CCL";
if (c === "01091") return "SDN";
if (c === "01092") return "涉军企业";
if (c === "01041") return "337调查";
if (c === "01042") return "232调查";
if (c === "01043") return "301调查";
return c;
};
warningList.value = list.map((x) => ({
signalId: x.id,
signalTitle: x.title,
signalTime: x.eventTime,
signalLevel: x.riskLevel,
signalCountryId: x.riskCountryId,
dealStatus: x.dealStatus,
eventType: getEventTypeFromModule(x.sanModuleCode)
}));
riskTotal.value = warningList.value.length;
// 右侧轮播:同样使用 LatestRisk 的 riskSignals(字段映射到现有模板使用的热点字段名)
hotNewsList.value = list.map((x) => ({
signalId: x.id,
hotspotID: x.id,
hotspotType: getEventTypeFromModule(x.sanModuleCode),
hotspotTitle: x.title,
// 轮播题目右侧标签:切换轮播时用详情接口 domains 覆盖
hotspotTag: "",
hotspotDesc: x.description || "",
hotspotDate: x.eventTime || "",
domainList: [],
eventTitle: x.issuingOrgId || ""
}));
// 首次加载:主动拉取当前轮播项详情以展示题目右侧标签
await fetchCarouselDetailForIndex(curHotNewsListIndex.value || 0);
cardList1.value = warningList.value
.filter(item => {
return item.eventType === "法案" || item.eventType === "行政令";
......@@ -520,11 +566,46 @@ const handleGetLatestRisks = async () => {
const curNews = ref({});
const carouselRef = ref(null);
const curHotNewsListIndex = ref(0);
const carouselDetailCache = ref({});
const applyHotspotTagById = (id, tagText) => {
const sid = String(id ?? "").trim();
if (!sid) return;
hotNewsList.value = (hotNewsList.value || []).map((n) => {
if (String(n.hotspotID ?? "").trim() !== sid) return n;
return { ...n, hotspotTag: tagText };
});
};
const fetchCarouselDetailForIndex = async (index) => {
const item = hotNewsList.value?.[index];
if (!item) return;
const id = String(item.hotspotID ?? "").trim();
if (!id) return;
if (carouselDetailCache.value[id]) {
applyHotspotTagById(id, carouselDetailCache.value[id]);
return;
}
try {
const res = await getRiskSignalInfoById(id);
if (res && res.code === 200 && res.data) {
const tagText = String(res.data.domains ?? "").trim();
if (tagText) {
carouselDetailCache.value = { ...carouselDetailCache.value, [id]: tagText };
applyHotspotTagById(id, tagText);
}
}
} catch (error) {
console.error("轮播风险信号详情获取失败", error);
}
};
const handleCarouselChange = index => {
curHotNewsListIndex.value = index;
if (hotNewsList.value && hotNewsList.value.length > 0) {
curNews.value = hotNewsList.value[index];
}
fetchCarouselDetailForIndex(index);
};
// 查看详情
......
......@@ -14,6 +14,12 @@
placeholder="搜索委员会"
/>
</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 class="date-box">
......@@ -30,7 +36,7 @@
<div class="item-name one-line-ellipsis">{{ item.name }}</div>
<div class="item-chamber one-line-ellipsis">{{ item.chamber }}</div>
</div>
<div class="item-total">{{ item.count }}</div>
<div class="item-total">{{ getDisplayCount(item) }}</div>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
......@@ -48,6 +54,12 @@
/>
</div>
</div>
<div class="back-bnt" @click="handleBack">
<el-icon>
<Back />
</el-icon>
<div class="back-text">返回</div>
</div>
</div>
</template>
......@@ -55,6 +67,7 @@
import { onMounted, reactive, ref } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ArrowRightBold } from "@element-plus/icons-vue";
import { Back } from "@element-plus/icons-vue";
import router from "@/router";
import TimeTabPane from "@/components/base/TimeTabPane/index.vue";
import { getStatisticsBillCountByCommittee } from "@/api/bill/billHome";
......@@ -66,6 +79,7 @@ const committeeInfo = reactive({
pageSize: 8,
total: 0,
keyWord: "",
metricType: 1,
dateDesc: "近一年",
list: []
});
......@@ -89,10 +103,11 @@ const onAllCommittee = async num => {
id: `${item.orgType || ""}-${item.orgName || ""}`,
name: item.orgName,
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))
.sort((a, b) => (b.count || 0) - (a.count || 0));
.sort((a, b) => getSortValue(b) - getSortValue(a));
committeeInfo.total = source.length;
const start = (committeeInfo.pageNum - 1) * committeeInfo.pageSize;
......@@ -108,20 +123,36 @@ const onAllCommittee = async num => {
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 => {
committeeInfo.dateDesc = event?.time || "近一年";
onAllCommittee();
};
const handleToDataLibrary = item => {
const route = router.resolve({
router.push({
path: "/dataLibrary/countryBill",
query: {
selectedOrg: item.name,
selectedCongress: item.chamber
}
});
window.open(route.href, "_blank");
};
const handleBack = () => {
if (window.history.length > 1) {
router.back();
return;
}
router.push("/billHome");
};
const refCommittee = ref();
......@@ -143,6 +174,28 @@ onMounted(() => {
background-size: 100% 100%;
display: flex;
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 {
width: 1600px;
......@@ -180,6 +233,11 @@ onMounted(() => {
width: 180px;
height: 32px;
}
.hard-select {
height: 42px;
padding: 5px 0;
}
}
.date-box {
......
......@@ -69,7 +69,7 @@ const getDoublePieChart = (data1, data2) => {
const name = truncateLabel(params?.name, 6)
const value = params?.value ?? 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,
edgeDistance: 10,
......
......@@ -10,6 +10,7 @@ const truncateLabel = (value, maxLen = 6) => {
const getPieChart = (data, colorList, options = {}) => {
const showCount = options.showCount !== false
const countUnit = options.countUnit || '条'
const chartColors = Array.isArray(colorList) && colorList.length ? colorList : MUTICHARTCOLORS
let option = {
color: chartColors,
......@@ -38,7 +39,7 @@ const getPieChart = (data, colorList, options = {}) => {
const name = truncateLabel(params?.name, 6)
const value = params?.value ?? 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}}`
},
minMargin: 5,
......
......@@ -602,9 +602,7 @@ const initRightChart = () => {
const domains = Array.from(domainsSet);
const types = Array.from(typesSet);
const legendSplitAt = Math.ceil(types.length / 2);
const legendFirstLine = types.slice(0, legendSplitAt);
const legendSecondLine = types.slice(legendSplitAt);
const legendData = types;
const indicators = domains.map((domain) => {
const domainData = rawData.filter((item) => item.AREA === domain);
......@@ -630,57 +628,56 @@ const initRightChart = () => {
const option = {
color: colorMap,
// 避免自动换行导致“第二行不居中”:拆成两行 legend,每行各自居中
legend: [
{
show: true,
type: "plain",
data: legendFirstLine,
top: 8,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
}
// 图例尽量单行居中;放不下则横向滚动(不换行)
legend: {
show: true,
type: "scroll",
orient: "horizontal",
data: legendData,
top: 8,
left: "center",
width: "90%",
height: 24,
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
pageButtonPosition: "end",
pageIconSize: 12,
pageTextStyle: {
color: "rgb(95, 101, 108)",
fontSize: 12,
fontFamily: "Microsoft YaHei"
},
{
show: legendSecondLine.length > 0,
type: "plain",
data: legendSecondLine,
top: 32,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
}
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
}
],
},
radar: {
// 对齐左侧折线图(grid top=34%)的“图例到图形”间距:下移雷达中心并略缩半径
center: ["50%", "62%"],
radius: "60%",
center: ["50%", "57%"],
radius: "58%",
indicator: indicators,
axisName: {
color: "rgba(132, 136, 142, 1)",
fontSize: 14,
fontWeight: 400
fontFamily: "Source Han Sans CN",
fontWeight: 700,
fontSize: 16,
lineHeight: 24,
letterSpacing: 1,
color: "rgb(59, 65, 75)"
},
splitLine: { lineStyle: { color: ["#e6e6e6"] } },
splitArea: { show: false }
splitArea: {
show: true,
areaStyle: {
// 从最内圈白色开始,向外层交替浅灰
color: ["#ffffff", "rgb(247, 248, 249)"]
}
}
},
series: [
{
......
......@@ -7,8 +7,7 @@
</div>
</div>
<el-select v-model="sortModel" placeholder="发布时间" class="select" popper-class="coop-select-dropdown"
:teleported="true" placement="bottom-start"
:popper-options="sortPopperOptions" @change="handleSortChange">
:teleported="true" placement="bottom-start" :popper-options="sortPopperOptions" @change="handleSortChange">
<template #prefix>
<img v-if="sortModel !== true" src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png"
class="select-prefix-img" alt="" @click.stop="toggleSortPrefix" />
......@@ -37,35 +36,42 @@
</div>
</div>
</div>
<div class="right">
<div class="right" :class="{ 'right--empty': !(mainDataList && mainDataList.length) }">
<div class="right-title">
<img src="./assets/icon01.png" alt="" />
<div>合作限制历程</div>
</div>
<div class="right-main">
<div class="main-content">
<div v-for="item in mainDataList" :key="item.id" class="main-item">
<div class="date">{{ formatDateCn(item.date) }}</div>
<img :src="item.img" alt="" class="img" />
<div class="box">
<div class="title" @click="handleClick(item)">{{ item.title }}</div>
<div class="content" @click="handleClick(item)">{{ item.content }}</div>
<div class="domain">
<AreaTag v-for="(domain, i) in item.domain" :key="i" " :tagName="domain">
</AreaTag>
</div>
<div class="type" :class="getTypeClass(item.type)">
{{ item.type }}
<template v-if="mainDataList && mainDataList.length">
<div class="main-content">
<div v-for="item in mainDataList" :key="item.id" class="main-item">
<div class="date">{{ formatDateCn(item.date) }}</div>
<img :src="item.img" alt="" class="img" />
<div class="box">
<div class="title" @click="handleClick(item)">{{ item.title }}</div>
<div class="content" @click="handleClick(item)">{{ item.content }}</div>
<div class="domain">
<AreaTag v-for="(domain, i) in item.domain" :key="i" " :tagName="domain">
</AreaTag>
</div>
<div class="type" :class="getTypeClass(item.type)">
{{ item.type }}
</div>
</div>
</div>
</div>
</div>
<div class="page">
<div class="count">共 {{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
<div class="page">
<div class="count">{{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
</template>
<template v-else>
<div class="right-main-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div>
</div>
</div>
......@@ -533,6 +539,13 @@ watch(currentPage, () => {
border-radius: 10px;
background-color: #fff;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
display: flex;
flex-direction: column;
&.right--empty {
// 与左侧筛选框等高
height: 432px;
}
.right-title {
width: 1224px;
......@@ -563,10 +576,28 @@ watch(currentPage, () => {
.right-main {
width: 1224px;
flex: 1;
padding: 12px 0px 80px 0px;
position: relative;
.right-main-empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
:deep(.el-empty__image) {
margin-bottom: 0;
}
.right-el-empty {
padding: 0;
margin: 0;
}
}
.main-content {
width: 1224px;
......@@ -594,15 +625,7 @@ watch(currentPage, () => {
}
&:last-child::after {
content: "";
position: absolute;
top: 37px;
bottom: -37px;
left: 108px;
width: 2px;
background-color: rgb(230, 231, 232);
z-index: 1;
height: calc(100% - 37px);
content: none;
}
.date {
......@@ -635,6 +658,7 @@ watch(currentPage, () => {
.title {
font-size: 20px;
width: 950px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
......
......@@ -5,13 +5,13 @@
<img :src="coopData?.IMAGEURL || defaultImg" alt="" />
<div class="content">
<div class="cl1">{{ coopData?.LIMITNAMEZH }}</div>
<div class="cl2">{{ coopData?.LIMITNAME }}</div>
<div v-if="hasLimitNameEn" class="cl2">{{ coopData?.LIMITNAME }}</div>
<div class="cl3">{{ coopData?.LIMITDATE }} · {{ coopData?.LIMITORGNAME }}</div>
</div>
<div class="btn">
<button class="btn1"><img src="./assets/icon01.png" alt="" />查看原文</button>
<!-- <button class="btn1"><img src="./assets/icon02.png" alt="" />查看官网</button> -->
<button class="btn1 active"><img src="./assets/icon03.png" alt="" />分析报告</button>
<button class="btn1 active" @click="handleOpenWrittingAsstaint"><img src="./assets/icon03.png" alt="" />分析报告</button>
</div>
</div>
</div>
......@@ -81,7 +81,8 @@
<img src="./assets/打开按钮.png" alt="">
</div>
<!-- contentList:单条按原样式展示;多条则逐条展示并加 1.2.3. 前缀 -->
<div v-if="Array.isArray(item.contentList) && item.contentList.length > 1" class="clause-item-content-list">
<div v-if="Array.isArray(item.contentList) && item.contentList.length > 1"
class="clause-item-content-list">
<div v-for="(row, i) in item.contentList" :key="i" class="clause-item-content-row">
<span class="row-index">{{ i + 1 }}.</span>
<span class="row-text">{{ row.CONTENT }}</span>
......@@ -117,6 +118,15 @@ import Rubio from "./assets/卢比奥.png";
import Bondi from "./assets/邦迪.png";
import Nome from "./assets/诺姆.png";
const hasLimitNameEn = computed(() => {
return Boolean(String(coopData.value?.LIMITNAME ?? "").trim());
});
const handleOpenWrittingAsstaint = () => {
const { href } = router.resolve({ path: "/writtingAsstaint" });
window.open(href, "_blank");
};
// 合作限制-查询限制条款
const limitClauseData = ref([]);
const getlimitClauseData = async () => {
......@@ -474,9 +484,13 @@ const dataList3 = ref([
.btn {
width: 376px;
height: 36px;
height: 48px;
display: flex;
justify-content: right;
position: absolute;
bottom: 0;
margin-left: 1224px;
.btn1 {
border-radius: 6px;
......
......@@ -164,6 +164,7 @@ const handleToPosi = id => {
background-size: 100% 100%;
position: absolute;
width: 100%;
height: 100%;
z-index: -100;
top: -64px;
......
......@@ -124,7 +124,7 @@
<el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange"
@select="handleSelect" @select-all="handleSelectAll" style="width: 100%" :row-style="{ height: '52px' }">
<el-table-column type="selection" width="40" />
<el-table-column label="法案名称" width="455">
<el-table-column label="法案名称" width="600">
<template #default="scope">
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{
scope.row.originalTitle
......@@ -134,14 +134,14 @@
<el-table-column label="日期" width="120" class-name="date-column">
<template #default="scope">{{ scope.row.date }}</template>
</el-table-column>
<el-table-column label="提案人" width="480">
<el-table-column label="提案人" width="300">
<template #default="scope">
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.sponsorPersonName
}}</span>
</template>
</el-table-column>
<el-table-column property="affiliation" label="所属党派" width="120" />
<el-table-column property="originDepart" label="提出委员会" width="180" />
<el-table-column property="originDepart" label="提出委员会" />
<el-table-column property="status" label="所处阶段" width="120" />
</el-table>
</div>
......
......@@ -23,6 +23,7 @@ const getLineChart = (dataX, dataY) => {
},
yAxis: {
type: 'value',
name: '数量',
splitLine: {
show: true,
lineStyle: {
......
......@@ -2,8 +2,8 @@ import { MUTICHARTCOLORS } from "@/common/constant";
const getPieChart = (data) => {
const colorList = MUTICHARTCOLORS
let showData = data
if(data.length > 14) {
showData = data.slice(0,13)
if(data.length > 12) {
showData = data.slice(0,11)
let num = 0
data.slice(13,).forEach(item => {
num = num + item.value
......@@ -22,8 +22,9 @@ const getPieChart = (data) => {
radius: [150, 180],
// height: '96%',
left: 'center',
top: 24,
top: 60,
width: '98%',
height: '90%',
itemStyle: {
borderColor: '#fff',
borderWidth: 1
......
......@@ -134,16 +134,15 @@ const siderList = ref([
active: false
},
{
name: "实体清单事件",
path: "/dataLibrary/dataEntityListEvent",
name: "商业管制清单",
path: "/dataLibrary/dataCommerceControlList",
active: false
},
{
name: "商业管制清单",
path: "/dataLibrary/dataCommerceControlList",
name: "实体清单事件",
path: "/dataLibrary/dataEntityListEvent",
active: false
},
{
name: "商业管制清单事件",
path: "/dataLibrary/dataCommerceControlListEvent",
......@@ -257,6 +256,10 @@ const siderList = ref([
]);
const handleSiderItem = item => {
if (item.name === '风险信号' || item.name === '市场准入限制') {
ElMessage.warning('当前模块开发中,敬请期待!')
return
}
siderList.value.forEach(val => {
val.active = false;
val.isExpanded = false;
......@@ -282,6 +285,10 @@ const handleSiderItem = item => {
};
const handleSiderSecondItem = item => {
if (item.name === '州法案' || item.name === '研究型大学' || item.name === '重点实验室') {
ElMessage.warning('当前模块开发中,敬请期待!')
return
}
siderList.value.forEach(item => {
if (item.children.length) {
item.children.forEach(val => {
......@@ -437,17 +444,16 @@ onMounted(() => {
siderList.value[3].isExpanded = true;
siderList.value[3].children[0].active = true;
break;
case "/dataLibrary/dataEntityListEvent":
case "/dataLibrary/dataCommerceControlList":
siderList.value[3].active = true;
siderList.value[3].isExpanded = true;
siderList.value[3].children[1].active = true;
break;
case "/dataLibrary/dataCommerceControlList":
case "/dataLibrary/dataEntityListEvent":
siderList.value[3].active = true;
siderList.value[3].isExpanded = true;
siderList.value[3].children[2].active = true;
break;
case "/dataLibrary/dataCommerceControlListEvent":
siderList.value[3].active = true;
siderList.value[3].isExpanded = true;
......@@ -559,6 +565,7 @@ onBeforeUnmount(() => {
height: 100%;
}
}
.title {
color: var(--color-primary-100);
}
......@@ -737,6 +744,7 @@ onBeforeUnmount(() => {
.tab-item-active {
border-bottom: 2px solid var(--color-primary-100) !important;
background: var(--color-primary-2);
}
}
......
......@@ -105,7 +105,7 @@
<el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange"
@select="handleSelect" @select-all="handleSelectAll" style="width: 100%" :row-style="{ height: '52px' }">
<el-table-column type="selection" width="40" />
<el-table-column label="新闻标题" width="600">
<el-table-column label="新闻标题" width="420">
<template #default="scope">
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{
scope.row.originalTitle
......
......@@ -9,7 +9,7 @@
<div class="en-title">{{ institutionInfo.enName }}</div>
<div class="desc">{{ institutionInfo.desc }}</div>
<div class="tag-box">
<div class="tag" v-for="(tag, index) in institutionInfo.tagList" :key="index">
<div class="tag" v-for="(tag, index) in showTagList" :key="index">
{{ tag }}
</div>
</div>
......@@ -22,13 +22,8 @@
</div> -->
</div>
<div class="tab-box">
<div
class="tab"
@click="handleClickTab(item)"
:class="{ tabActive: activeTabName == item.name }"
v-for="(item, index) in tabList"
:key="index"
>
<div class="tab" @click="handleClickTab(item)" :class="{ tabActive: activeTabName == item.name }"
v-for="(item, index) in tabList" :key="index">
{{ item.name }}
</div>
</div>
......@@ -61,6 +56,14 @@ const institutionInfo = ref({
logo: ""
});
const showTagList = computed(() => {
if(institutionInfo.value.tagList.length > 10) {
return institutionInfo.value.tagList.slice(0,10)
} else {
return institutionInfo.value.tagList
}
})
const handleGetInfo = async () => {
const params = {
id: route.query.id
......@@ -75,7 +78,7 @@ const handleGetInfo = async () => {
institutionInfo.value.desc = res.data.orgIntroduction;
institutionInfo.value.name = res.data.orgName;
}
} catch (error) {}
} catch (error) { }
};
handleGetInfo();
......@@ -95,7 +98,7 @@ const tabList = ref([
]);
const handleClickTab = val => {
if(val.name === '深度挖掘' || val.name === '对华制裁') {
if (val.name === '深度挖掘' || val.name === '对华制裁') {
ElMessage.warning('当前功能开发中,敬请期待!')
return
}
......@@ -125,6 +128,7 @@ onUnmounted(() => {
background-repeat: no-repeat;
background-size: 100% 100%;
padding-top: 16px;
.header {
width: 1600px;
height: 200px;
......@@ -136,20 +140,23 @@ onUnmounted(() => {
background: rgba(255, 255, 255, 0.8);
display: flex;
position: relative;
.header-left {
width: 160px;
height: 160px;
margin: 20px;
img {
width: 100%;
height: 100%;
}
}
.header-right {
margin-left: 24px;
width: 1350px;
overflow: hidden;
overflow-y: auto;
.title {
margin-top: 26px;
height: 42px;
......@@ -164,6 +171,7 @@ onUnmounted(() => {
text-overflow: ellipsis;
white-space: nowrap;
}
.en-title {
margin-top: 8px;
height: 24px;
......@@ -178,7 +186,9 @@ onUnmounted(() => {
text-overflow: ellipsis;
white-space: nowrap;
}
.desc {
height: 48px;
margin-top: 6px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
......@@ -187,11 +197,25 @@ onUnmounted(() => {
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
overflow: hidden;
display: -webkit-box;
/* 3. 限制显示的行数(修改数字即可改变行数) */
-webkit-line-clamp: 2;
/* 4. 设置文字垂直排列方向 */
-webkit-box-orient: vertical;
/* 5. 隐藏超出部分 */
overflow: hidden;
/* 6. 显示省略号 */
text-overflow: ellipsis;
/* 可选:修复文字间距/换行问题 */
word-break: break-all;
}
.tag-box {
margin-top: 14px;
display: flex;
gap: 8px;
.tag {
height: 24px;
padding: 0px 8px;
......@@ -206,6 +230,7 @@ onUnmounted(() => {
}
}
}
.header-btn {
position: absolute;
top: 26px;
......@@ -219,14 +244,17 @@ onUnmounted(() => {
justify-content: center;
align-items: center;
cursor: pointer;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 22px;
color: rgba(255, 255, 255, 1);
......@@ -237,6 +265,7 @@ onUnmounted(() => {
}
}
}
.tab-box {
width: 1600px;
height: 64px;
......@@ -249,6 +278,7 @@ onUnmounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
.tab {
width: 526px;
height: 54px;
......@@ -263,10 +293,12 @@ onUnmounted(() => {
font-weight: 400;
letter-spacing: 0px;
cursor: pointer;
&:hover {
background: rgba(231, 243, 255, 1);
}
}
.tabActive {
border: 2px solid rgba(174, 214, 255, 1);
background: rgba(231, 243, 255, 1);
......@@ -275,6 +307,7 @@ onUnmounted(() => {
font-weight: 700;
}
}
.main {
height: 800px;
width: 1600px;
......
......@@ -50,7 +50,7 @@
<el-carousel
ref="carouselRef"
height="370px"
:autoplay="true"
:autoplay="false"
:interval="3000"
arrow="never"
indicator-position="none"
......@@ -59,9 +59,7 @@
<el-carousel-item v-for="(item, index) in entitiesDataInfoList" :key="item.id + index">
<div>
<div class="box1-top">
<div class="box1-top-title">
{{ item.postDate }}——BIS《实体清单增列与修订条目》
</div>
<div class="box1-top-title">{{ item.postDate }}——{{ item.name }}</div>
<div class="box1-top-content">
<div class="box1-top-content-item">
<span class="box1-top-content-item-title">· 发布机构:</span>
......@@ -73,13 +71,7 @@
</div>
<div class="box1-top-content-item">
<span class="box1-top-content-item-title">· 涉及领域:</span>
<!-- <div
class="box1-top-content-item-tags"
v-for="(domainItem, index) in item.domains"
:key="index"
>
<el-tag :type="getTagType(domainItem)">{{ domainItem }}</el-tag>
</div> -->
<AreaTag
v-for="(domainItem, index) in item.domains"
:key="index"
......@@ -146,7 +138,7 @@
}}</span>
</div>
<div class="box1-absolute-num">
{{ item.cnEntityCount }}{{ item.sanTypeId == allSanTypeIds[0] ? "家" : "" }}
{{ item.cnEntityCount }}{{ item.sanTypeId == allSanTypeIds[0] ? "家" : "" }}
</div>
</div>
</div>
......@@ -1961,11 +1953,12 @@ const handleMediaClick = item => {
flex-direction: column;
gap: 20px;
position: relative;
width: 1036px;
.box1-left-arrow {
position: absolute;
z-index: 9999;
left: -20px;
left: -24px;
top: 135px;
width: 24px !important;
height: 48px;
......@@ -1989,7 +1982,7 @@ const handleMediaClick = item => {
.box1-right-arrow {
position: absolute;
z-index: 9999;
right: -20px;
right: 0px;
top: 135px;
width: 24px;
height: 48px;
......@@ -2053,6 +2046,10 @@ const handleMediaClick = item => {
color: $base-color;
margin-top: 10px;
margin-bottom: 15px;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-content {
......@@ -2079,7 +2076,7 @@ const handleMediaClick = item => {
height: 172px;
padding-top: 16px;
box-sizing: border-box;
padding-right: 24px;
&-title {
font-size: 16px;
font-weight: 700;
......
......@@ -60,9 +60,10 @@
<div>
<div class="box1-top">
<div class="box1-top-title">
{{ item.postDate }}——{{
<!-- {{ item.postDate }}——{{
item.sanTypeId == allSanTypeIds[0] ? "OFAC" : "DoD"
}}《实体清单增列与修订条目》
}}《实体清单增列与修订条目》 -->
{{ item.postDate }}——{{ item.name }}
</div>
<div class="box1-top-content">
<div class="box1-top-content-item">
......@@ -1073,7 +1074,7 @@ const handleToEntityList = item => {
console.log("这是什么数据1 =>", item);
let id = item?.id;
let sanTypeId = item?.sanTypeId || 1;
let date = entitiesDataInfoList.value[currentCarouselIndex.value].postDate
let date = entitiesDataInfoList.value[currentCarouselIndex.value].postDate;
if (!id) {
const currentItem = entitiesDataInfoList.value[currentCarouselIndex.value];
id = currentItem?.id;
......@@ -1826,14 +1827,20 @@ const handleToDataLibrary = item => {
onMounted(async () => {
console.log("finance 页面 mounted");
try {
// 获取趋势图数据
fetchTrendData();
fetchRiskSignals("0109");
// 获取社交媒体信息
fetchSocialMediaInfo();
// 获取新闻资讯
fetchNewsInfo();
const [dataCount, entitiesDataInfo, industryCountByYear, cclList] = await Promise.all([
getDataCount(),
getLatestEntityListInfo(),
getReleaseCount(2)
// getReleaseCount(3)
]);
// 交换第二个和第三个元素
// [dataCount[1], dataCount[2]] = [dataCount[2], dataCount[1]];
console.log("dataCount", dataCount);
infoList.value = dataCount.slice(0, 2).map((item, idx) => {
return {
......@@ -1874,6 +1881,11 @@ onMounted(async () => {
tags: item.domain
};
});
await fetchSanctionProcess(sanctionPage.value, 10);
// 获取雷达图数据
await fetchRadarData(domainChecked.value);
// 获取出口管制制裁措施
await fetchSanctionList();
entityListReleaseFreqChart.interpret({
type: "柱状图",
name: "美国商务部发布实体清单的频次",
......@@ -1892,21 +1904,6 @@ onMounted(async () => {
name: "美国商务部发布商业管制清单的频次",
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) {
console.log("此处报错?");
console.log(err);
......@@ -1957,11 +1954,12 @@ const handleMediaClick = item => {
flex-direction: column;
gap: 20px;
position: relative;
width: 1036px;
.box1-left-arrow {
position: absolute;
z-index: 9999;
left: -20px;
left: -24px;
top: 135px;
width: 24px !important;
height: 48px;
......@@ -1985,7 +1983,7 @@ const handleMediaClick = item => {
.box1-right-arrow {
position: absolute;
z-index: 9999;
right: -20px;
right: 0px;
top: 135px;
width: 24px;
height: 48px;
......@@ -2050,6 +2048,10 @@ const handleMediaClick = item => {
color: $base-color;
margin-top: 10px;
margin-bottom: 15px;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-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";
// 【新增】引入获取全局 Signal 的方法
import { getAbortSignal } from "@/api/finance/service.js";
export const useChartInterpretation = () => {
const loading = ref(false);
......@@ -10,14 +14,20 @@ export const useChartInterpretation = () => {
error.value = null;
interpretation.value = "";
// 【新增】在请求发起前获取当前的 Signal
// 注意:必须在每次调用 interpret 时重新获取,以确保拿到最新的 controller 的 signal
const signal = getAbortSignal();
try {
// 【修改】在 fetch 中传入 signal
const response = await fetch("/aiAnalysis/chart_interpretation", {
method: "POST",
headers: {
"X-API-Key": "aircasKEY19491001",
"X-API-Key": "aircasKEY19491001",
"Content-Type": "application/json"
},
body: JSON.stringify({ text })
body: JSON.stringify({ text }),
signal: signal // 【关键】绑定取消信号
});
if (!response.ok) {
......@@ -29,6 +39,7 @@ export const useChartInterpretation = () => {
let buffer = "";
while (true) {
// reader.read() 会在 signal abort 时抛出 AbortError
const { done, value } = await reader.read();
if (done) break;
......@@ -39,18 +50,43 @@ export const useChartInterpretation = () => {
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];
}
// 尝试解析 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*"([^"]*)"/);
if (textMatch && textMatch[1]) {
interpretation.value = textMatch[1];
}
}
} catch (e) {
// 忽略解析错误
}
}
}
}
} catch (err) {
// 【关键】判断是否是因路由切换导致的取消
if (err.name === 'AbortError') {
console.log('AI 解读请求已取消');
loading.value = false; // 关闭 loading
return; // 直接返回,不设置 error,不弹窗
}
error.value = err.message || "AI 解读失败";
console.error("AI Chart Interpretation Error:", err);
} finally {
loading.value = false;
// 只有在非 AbortError 的情况下,才由 finally 统一关闭 loading
// 如果上面 catch 中已经 return 了,这里不会执行
if (err?.name !== 'AbortError') {
loading.value = false;
}
}
};
......@@ -60,4 +96,4 @@ export const useChartInterpretation = () => {
error,
interpret
};
};
};
\ No newline at end of file
......@@ -2029,6 +2029,7 @@ onMounted(async () => {
font-family: Source Han Sans CN;
font-size: 16px;
color: var(--text-primary-65-color);
font-weight: 400;
}
}
}
......
<template>
<el-space direction="vertical" class="full-width news-image-background">
<el-button class="float-btn" @click="gotoNewsBrief(false)"><el-icon>
<back />
</el-icon> 返回</el-button>
<el-space style="width: 993px;" direction="vertical" alignment="flex-start">
<div id="ref-news-list" style="margin-top: 50px; margin-bottom: 24px; margin-left: 24px;">
<common-text class="text-title-0-show" color="var(--text-primary-90-color)">{{
moduleName
}}</common-text>
<common-text class="text-regular"
color="var(--text-primary-65-color)">基于情报价值评估预测算法,掌握全球重要潜在动向</common-text>
</div>
<el-space direction="vertical" fill alignment="flex-start" class="background-as-card common-padding">
<el-radio-group v-model="currentAreaId" class="radio-group-as-gap-btn">
<el-space direction="horizontal" wrap alignment="center">
<el-radio-button :label="''" @click="changeArea('')">全部</el-radio-button>
<el-radio-button v-for="(t, i) in AreaList" :key="i" :label="t.id" @click="changeArea(t.id)">{{
t.name }}
</el-radio-button>
</el-space>
</el-radio-group>
<el-divider style="margin: 10px 0px;"></el-divider>
<div v-if="NewsData?.content?.length > 0">
<news-list :news="NewsData.content" />
</div>
<el-empty v-else></el-empty>
<el-pagination background layout="total, ->, prev, pager, next" :current-page="modulePage"
:total="NewsData?.totalElements ?? 0" v-on:current-change="onCurrentChange" />
</el-space>
</el-space>
</el-space>
<el-space direction="vertical" class="full-width news-image-background">
<el-button class="float-btn" @click="gotoNewsBrief(false)"
><el-icon>
<Back />
</el-icon>
返回</el-button
>
<el-space style="width: 993px" direction="vertical" alignment="flex-start">
<div id="ref-news-list" style="margin-top: 50px; margin-bottom: 24px; margin-left: 24px">
<common-text class="text-title-0-show" color="var(--text-primary-90-color)">{{ moduleName }}</common-text>
<common-text class="text-regular" color="var(--text-primary-65-color)"
>基于情报价值评估预测算法,掌握全球重要潜在动向</common-text
>
</div>
<el-space direction="vertical" fill alignment="flex-start" class="background-as-card common-padding">
<el-radio-group v-model="currentAreaId" class="radio-group-as-gap-btn">
<el-space direction="horizontal" wrap alignment="center">
<el-radio-button :label="''" @click="changeArea('')">全部</el-radio-button>
<el-radio-button v-for="(t, i) in AreaList" :key="i" :label="t.id" @click="changeArea(t.id)"
>{{ t.name }}
</el-radio-button>
</el-space>
</el-radio-group>
<el-divider style="margin: 10px 0px"></el-divider>
<div v-if="NewsData?.content?.length > 0">
<news-list :news="NewsData.content" />
</div>
<el-empty v-else></el-empty>
<el-pagination
background
layout="total, ->, prev, pager, next"
:current-page="modulePage"
:total="NewsData?.totalElements ?? 0"
v-on:current-change="onCurrentChange"
/>
</el-space>
</el-space>
</el-space>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from 'vue-router';
import '@/styles/container.scss';
import '@/styles/radio.scss';
import { useRoute } from "vue-router";
import "@/styles/container.scss";
import "@/styles/radio.scss";
import NewsList from "./NewsList.vue";
import { getAreaList, getHotNewsByArea } from "@/api/news/newsBrief";
import { ElSpace, ElDivider, ElRadioButton, ElRadioGroup, ElButton, ElIcon, ElEmpty, ElPagination } from "element-plus";
import { Back } from "@element-plus/icons-vue";
import CommonText from "@/components/base/texts/CommonText.vue";
import { useGotoNewsBrief } from "@/router/modules/news";
import { scrollToElement } from "@/router/common";
import { number } from "echarts";
const route = useRoute();
const gotoNewsBrief = useGotoNewsBrief();
const moduleId = ref(route.params.id);
......@@ -54,52 +61,50 @@ const AreaList = ref([]);
const currentAreaId = ref("");
onMounted(async () => {
console.log(route.query.name, moduleId.value, moduleName.value);
const { data: areaList } = await getAreaList();
AreaList.value = areaList ?? [];
await updateDate("")
console.log(route.query.name, moduleId.value, moduleName.value);
const { data: areaList } = await getAreaList();
AreaList.value = areaList ?? [];
await updateDate("");
});
const onCurrentChange = async e => {
await updateDate(currentAreaId.value, e - 1)
scrollToElement("ref-news-list");
}
await updateDate(currentAreaId.value, e - 1);
scrollToElement("ref-news-list");
};
async function updateDate(id, page = 0) {
const { data } = await getHotNewsByArea({
moduleId: moduleId.value,
industryId: id ? id : null,
currentPage: page,
});
data?.content?.forEach(item => {
item.newsImage = item.coverUrl ?? ""
})
NewsData.value = data ?? [];
modulePage.value = (data?.number ?? 0) + 1;
const { data } = await getHotNewsByArea({
moduleId: moduleId.value,
industryId: id ? id : null,
currentPage: page
});
data?.content?.forEach(item => {
item.newsImage = item.coverUrl ?? "";
});
NewsData.value = data ?? [];
modulePage.value = (data?.number ?? 0) + 1;
}
async function changeArea(id) {
await updateDate(id, 0)
await updateDate(id, 0);
}
</script>
<style lang="scss" scoped>
@use '@/styles/common.scss' as *;
@use "@/styles/common.scss" as *;
@import url("./style.css");
.float-btn {
position: absolute;
top: 24px;
left: 40px;
width: 92px;
height: 40px;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 32px;
@extend .text-regular;
color: var(--text-primary-65-color);
position: absolute;
top: 24px;
left: 40px;
width: 92px;
height: 40px;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 32px;
@extend .text-regular;
color: var(--text-primary-65-color);
}
</style>
\ No newline at end of file
</style>
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论