提交 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)
}
// 处理 token 过期或无效的情况
console.log('err' + error)
// 处理 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,11 +24,10 @@
<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">
......@@ -53,7 +52,8 @@
</div>
</div>
</div>
<div class="menu-box" v-show="isShowMenu" @mouseenter="handleHoverMenu(true)"
</div>
<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,36 +628,27 @@ const initRightChart = () => {
const option = {
color: colorMap,
// 避免自动换行导致“第二行不居中”:拆成两行 legend,每行各自居中
legend: [
{
// 图例尽量单行居中;放不下则横向滚动(不换行)
legend: {
show: true,
type: "plain",
data: legendFirstLine,
type: "scroll",
orient: "horizontal",
data: legendData,
top: 8,
left: "center",
width: "90%",
height: 24,
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
pageButtonPosition: "end",
pageIconSize: 12,
pageTextStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
}
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,
......@@ -667,20 +656,28 @@ const initRightChart = () => {
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,12 +36,13 @@
</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">
<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>
......@@ -66,6 +66,12 @@
<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",
"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,19 +50,44 @@ export const useChartInterpretation = () => {
for (const line of lines) {
if (line.startsWith("data: ")) {
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*"([^"]*)"/);
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 {
// 只有在非 AbortError 的情况下,才由 finally 统一关闭 loading
// 如果上面 catch 中已经 return 了,这里不会执行
if (err?.name !== 'AbortError') {
loading.value = false;
}
}
};
return {
......
......@@ -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>
<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 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>
<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-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>
......@@ -34,16 +41,16 @@
<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);
......@@ -57,35 +64,33 @@ onMounted(async () => {
console.log(route.query.name, moduleId.value, moduleName.value);
const { data: areaList } = await getAreaList();
AreaList.value = areaList ?? [];
await updateDate("")
await updateDate("");
});
const onCurrentChange = async e => {
await updateDate(currentAreaId.value, e - 1)
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,
currentPage: page
});
data?.content?.forEach(item => {
item.newsImage = item.coverUrl ?? ""
})
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;
......
......@@ -14,7 +14,7 @@
<div class="home-main-header-center">
<SearchContainer class="think-tank-search" style="margin-bottom: 0; margin-top: 51px; height: fit-content;"
v-if="containerRef" placeholder="搜索智库、报告或政策建议" :containerRef="containerRef" areaName="智库" />
v-if="containerRef" placeholder="搜索智库、报告或政策建议" :containerRef="containerRef" areaName="智库报告" />
<!-- <el-input v-model="searchThinktankText" @keyup.enter="handleSearch"
style="width: 838px; height: 100%" placeholder="搜索智库报告" />
<div class="search">
......
<svg viewBox="0 0 15.0039 14.5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15.003906" height="14.500000" fill="none" customFrame="#000000">
<rect id="容器 239" width="14.000000" height="14.000000" x="0.501953" y="0.500000" />
<path id="矢量 467" d="M0.501953 4.81104L4.27116 2.65573L8.0404 4.81118L14.502 0.5" stroke="rgb(255,255,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="矢量 468" d="M4.27118 6.96812L0.501953 9.12357L0.50198 14.5015L14.502 14.5013L14.502 4.8125L8.04045 9.12354L4.27118 6.96812Z" fill="rgb(255,255,255)" fill-rule="evenodd" />
</svg>
<svg viewBox="0 0 26.1548 23.3789" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26.154785" height="23.378906" fill="none" customFrame="#000000">
<defs>
<linearGradient id="paint_linear_0" x1="12.260066" x2="21.2057495" y1="1.21773767" y2="18.4094753" gradientUnits="userSpaceOnUse">
<stop stop-color="rgb(206,79,81)" offset="0" stop-opacity="1" />
<stop stop-color="rgb(240,202,202)" offset="1" stop-opacity="1" />
</linearGradient>
</defs>
<path id="减去顶层" d="M25.8883 20.3883L14.8139 1.00773C14.0461 -0.335911 12.1087 -0.335911 11.3409 1.00773L0.266462 20.3883C-0.495419 21.7216 0.467316 23.3806 2.00295 23.3806L24.1519 23.3806C25.6875 23.3806 26.6502 21.7216 25.8883 20.3883ZM14.2931 14.8346C14.2711 15.4323 13.7802 15.9054 13.1821 15.9054C12.5832 15.9054 12.0919 15.4311 12.0709 14.8326L11.8462 8.42741C11.8196 7.66793 12.428 7.03775 13.188 7.03775C13.9489 7.03775 14.5578 7.66948 14.5297 8.42989L14.2931 14.8346ZM11.7811 18.0226C11.8557 17.8691 11.9621 17.7314 12.1004 17.6094C12.1956 17.5255 12.2977 17.4548 12.4066 17.3975C12.6412 17.2741 12.9075 17.2124 13.2057 17.2124C13.5225 17.2124 13.8007 17.2811 14.0405 17.4184C14.1353 17.4727 14.2241 17.5378 14.3068 17.6136C14.442 17.7374 14.5458 17.8762 14.6184 18.0301C14.7027 18.2087 14.7448 18.4077 14.7448 18.6269C14.7448 18.8242 14.7098 19.0065 14.6397 19.1738C14.5688 19.3433 14.4619 19.4974 14.3191 19.6361C14.1832 19.7681 14.0342 19.8685 13.8721 19.9373C13.6955 20.0122 13.5034 20.0497 13.2957 20.0497C12.8581 20.0497 12.4982 19.945 12.216 19.7355C12.2122 19.7327 12.2085 19.7299 12.2047 19.7271C12.1672 19.6986 12.131 19.6683 12.0963 19.6361C11.9352 19.4868 11.8178 19.3197 11.7439 19.1349C11.6814 18.9783 11.6501 18.809 11.6501 18.6269C11.6501 18.4038 11.6938 18.2024 11.7811 18.0226Z" fill="url(#paint_linear_0)" fill-rule="evenodd" />
</svg>
......@@ -5,11 +5,11 @@
<div class="center-center">
<div class="center-header">
<div class="center-header-left">
<img class="iconstyle" src="./assets/images/warning.png" />
<img class="iconstyle" src="./assets/images/warning.svg" />
<div class="center-header-title">风险信号管理</div>
</div>
<div class="center-header-right">
<img class="img" src="./assets/images/danger.png" />
<img class="img" src="./assets/images/danger.svg" />
<div class="text">风险统计</div>
</div>
</div>
......@@ -22,7 +22,7 @@
<div class="text1">本年新增风险</div>
</div>
<div class="text2" style="color: rgba(95, 101, 108, 1)">
{{ basicInfo.yearAdded + " 项" }}
<span class="text2-inner">{{ basicInfo.yearAdded + " 项" }}</span>
</div>
</div>
<div class="item">
......@@ -31,7 +31,7 @@
<div class="text1">本月新增风险</div>
</div>
<div class="text2" style="color: rgba(95, 101, 108, 1)">
{{ basicInfo.monthAdded + " 项" }}
<span class="text2-inner">{{ basicInfo.monthAdded + " 项" }}</span>
</div>
</div>
</div>
......@@ -41,7 +41,8 @@
<div class="dot" style="background-color: rgba(5, 95, 194, 1)"></div>
<div class="text1">已处理风险</div>
</div>
<div class="text2" style="color: rgba(5, 95, 194, 1)">{{ basicInfo.dealCount + " 项" }}</div>
<div class="text2" style="color: rgba(5, 95, 194, 1)"><span class="text2-inner">{{ basicInfo.dealCount
+ " 项" }}</span></div>
</div>
<div class="item">
<div class="top">
......@@ -49,7 +50,7 @@
<div class="text1">待处理风险</div>
</div>
<div class="text2" style="color: rgba(206, 79, 81, 1)">
{{ basicInfo.pendingCount + " 项" }}
<span class="text2-inner">{{ basicInfo.pendingCount + " 项" }}</span>
</div>
</div>
</div>
......@@ -125,7 +126,7 @@
<div class="title">{{ "风险类型" }}</div>
</div>
<div class="select-main">
<el-checkbox-group class="checkbox-group" :model-value="selectedRiskTypeModel"
<el-checkbox-group class="checkbox-group risk-type-checkbox-group" :model-value="selectedRiskTypeModel"
@change="handleRiskTypeGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RISK_FILTER_ALL_TYPE">
{{ RISK_FILTER_ALL_TYPE }}
......@@ -188,12 +189,14 @@
</div>
</div>
</div>
<template v-if="riskList && riskList.length">
<div class="right-main">
<div class="itemlist itemlist--clickable" v-for="(val, idx) in riskList" :key="val.rowKey"
@click="handleOpenRiskDetail(val)">
<div class="box-title">
<div class="risktitle" v-html="highlightRiskText(val.title)" />
<div class="risktype" :class="'risktype--' + getRiskListItemLevelKey(val.risktype)">
<div v-if="!isRiskLevelNoData(val.risktype)" class="risktype"
:class="'risktype--' + getRiskListItemLevelKey(val.risktype)">
<div class="icon" :class="'icon--' + getRiskListItemLevelKey(val.risktype)" />
<div class="text">{{ getRiskListItemLevelLabel(val.risktype) }}</div>
</div>
......@@ -217,6 +220,12 @@
:total="totalNum" background layout="prev, pager, next" />
</div>
</div>
</template>
<template v-else>
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div>
</div>
</div>
......@@ -606,6 +615,11 @@ const getRiskListItemLevelLabel = (level) => {
return t;
};
const isRiskLevelNoData = (level) => {
const t = String(level ?? "").trim();
return !t || t === "暂无数值" || t === "暂无数据";
};
const route = useRoute();
const router = useRouter();
......@@ -935,6 +949,15 @@ onMounted(async () => {
vertical-align: middle;
}
.risk-type-checkbox-group :deep(.el-checkbox__label) {
display: inline-block;
max-width: 8em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.home-wrapper {
width: 100%;
height: 100%;
......@@ -997,8 +1020,8 @@ onMounted(async () => {
cursor: pointer;
.img {
width: 16px;
height: 15px;
width: 14px;
height: 14px;
}
.text {
......@@ -1051,6 +1074,7 @@ onMounted(async () => {
line-height: 24px;
color: rgba(59, 65, 75, 1);
text-align: right;
white-space: nowrap;
}
}
......@@ -1061,6 +1085,14 @@ onMounted(async () => {
text-align: right;
/* 强制不换行 */
white-space: nowrap;
// 让右边界固定在块内,超长只会向左溢出(且不省略、完整显示)
width: 100%;
direction: rtl;
}
.text2-inner {
direction: ltr;
unicode-bidi: isolate;
}
}
}
......@@ -1188,6 +1220,8 @@ onMounted(async () => {
margin-bottom: 24px;
width: 1224px;
position: relative;
display: flex;
flex-direction: column;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
......@@ -1277,6 +1311,7 @@ onMounted(async () => {
.right-main {
width: 1224px;
flex: 1;
padding-left: 24px;
padding-top: 6px;
......@@ -1451,6 +1486,24 @@ onMounted(async () => {
margin-right: 58px;
}
}
.right-empty {
flex: 1;
min-height: 520px;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
:deep(.el-empty__image) {
margin-bottom: 0;
}
.right-el-empty {
padding: 0;
margin: 0;
}
}
}
}
}
......@@ -1839,7 +1892,7 @@ onMounted(async () => {
.risk-signal-detail-dialog .risk-signal-detail-dialog__header-badge-close {
position: absolute;
right: 115px;
right: 61px;
top: 50%;
transform: translateY(-50%);
width: 16px;
......@@ -1864,7 +1917,7 @@ onMounted(async () => {
.risk-signal-detail-dialog .risk-signal-detail-dialog__read-indicator {
position: absolute;
right: 115px;
right: 61px;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
......
......@@ -206,7 +206,7 @@ const getCalendarHeatChart = (rawData) => {
return "";
}
const v = p.data?.[3] ?? 0;
return `${dateStr}<br/>数值: ${v}`;
return `${dateStr}<br/>风险数量: ${v}`;
}
}
};
......
......@@ -19,14 +19,8 @@ export default defineConfig({
resolvers: [ElementPlusResolver()],
}),
],
build: {
// minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 移除所有 console.*
drop_debugger: true, // 移除 debugger
},
},
esbuild: {
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [] // 正式环境去除掉console
},
resolve: {
alias: {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论