提交 a1036c92 authored 作者: coderBryanFu's avatar coderBryanFu

Merge branch 'pre' of http://8.140.26.4:10003/caijian/risk-monitor into fk-dev

流水线 #412 已通过 于阶段
in 1 分 30 秒
const baseUrl = `http://8.140.26.4:9085`
const outImgbaseUrl = `http://172.19.21.9:8003/out/img` // 写报图片URL
\ No newline at end of file
const outImgbaseUrl = `http://8.140.26.4:10017/out/img` // 写报图片URL
\ No newline at end of file
import { getToken } from "@/api/request.js";
import { getToken, formatBearerAuthorization } from "@/api/request.js";
const CHART_INTERPRETATION_URL = "/aiAnalysis/chart_interpretation";
const API_KEY = "aircasKEY19491001";
......@@ -183,6 +183,8 @@ export function getChartAnalysis(data, options = {}) {
(async () => {
try {
const { fetchEventSource } = await import("@microsoft/fetch-event-source");
const sseRaw = getToken();
const sseToken = sseRaw ? String(sseRaw).trim() : "";
await fetchEventSource(CHART_INTERPRETATION_URL, {
method: "POST",
......@@ -191,8 +193,7 @@ export function getChartAnalysis(data, options = {}) {
Accept: "text/event-stream",
"Cache-Control": "no-cache",
"X-API-Key": API_KEY,
// 后端同项目其它接口使用 token 字段名(axios 拦截器里就是这样注入的)
token: getToken()
...(sseToken ? { token: sseToken } : {})
},
body: JSON.stringify(data),
signal: abortController.signal,
......
......@@ -16,7 +16,7 @@ export function getBillIndustry(params) {
return request({
method: 'GET',
url: `/api/BillOverview/billIndustry/${params.year}`,
params: { status: params.status }
params: { stageName: params.stageName }
})
}
......
......@@ -498,14 +498,15 @@ export function getEntitiesUpdateCount(sanTypeId = 1) {
* @param {string} rule - 规则
* @param {string} type - 类型
*/
export function getSanDomainCount(rule, type) {
export function getSanDomainCount(rule, sanTypeIds, type) {
return request200(
request({
method: "GET",
url: "/api/entitiesDataCount/getSanDomainCount",
params: {
rule,
type
sanTypeIds
// type
}
})
);
......
......@@ -136,3 +136,27 @@ export function getRelateNews(sanRecordId) {
export function getReasonAndSan(sanRecordId) {
return http.get(`/api/sanctionList/invFin/getReasonAndSan?sanRecordId=${sanRecordId}`);
}
/**
* 制裁历程
* url:/entitiesDataCount/getSanRecord
*/
export function getSanRecord(params) {
return http.get("/api/entitiesDataCount/getSanRecord", params);
}
/**
* 限制关系分析-限制举措关系图
* url:/sanctionList/invFin/recordRelation
*/
export function getRecordRelation(sanRecordIds) {
return http.get(`/api/sanctionList/invFin/recordRelation?sanRecordIds=${sanRecordIds}`);
}
/**
* 查询投融资限制关联-图谱-节点详情
* url:/sanctionList/invFin/getVertexInfo
*/
export function getVertexInfo(sanRecordId) {
return http.get(`/api/sanctionList/invFin/getVertexInfo?sanRecordId=${sanRecordId}`);
}
\ No newline at end of file
......@@ -2,27 +2,8 @@
import axios from 'axios'
// 引入 element-plus 里面的消息提示
import { ElMessage } from 'element-plus'
import { getToken, setToken, removeToken, formatBearerAuthorization } from '@/api/request.js'
// Token 管理
const TOKEN_KEY = 'auth_token'
// 获取token
const getToken = () => {
return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw'
// return localStorage.getItem(TOKEN_KEY)
}
// 设置 token
const setToken = (token) => {
localStorage.setItem(TOKEN_KEY, token)
}
// 移除 token
const removeToken = () => {
localStorage.removeItem(TOKEN_KEY)
}
// 导出 token 管理方法
export { getToken, setToken, removeToken }
// 创建 axios 实例
......@@ -30,19 +11,35 @@ const service = axios.create({
timeout: 300 * 1000 // 请求超时时间
})
// request 拦截器
// request 拦截器:与主 request 一致,就地改 config,避免 mergeConfig 破坏 url
service.interceptors.request.use(config => {
// 获取 token 并添加到请求头
const token = getToken()
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['token'] = 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 分析服务(Vite 代理 /aiAnalysis)需要 X-API-Key
const reqUrl = String(config.url || '')
const reqUrl = String(config.url ?? '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) {
config.headers['X-API-Key'] = 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)
}
}
return config
......@@ -54,7 +51,10 @@ service.interceptors.request.use(config => {
// response 拦截器
service.interceptors.response.use(
response => {
const res = response.data
const res = response?.data
if (!res) {
return Promise.reject(new Error('响应数据为空'))
}
// 根据需求:接口返回 code 不等于 200 的时候报错
if (res.code !== 200) {
ElMessage({
......@@ -90,10 +90,20 @@ service.interceptors.response.use(
type: 'error',
duration: 3 * 1000
})
removeToken()
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: error.message,
message: typeof error?.message === 'string' ? error.message : '请求失败',
type: 'error',
duration: 3 * 1000
})
......
......@@ -8,8 +8,19 @@ import {
// Token管理
const TOKEN_KEY = 'auth_token'
// ===== 兼容导出(勿删):历史代码仍会 import formatBearerAuthorization =====
// 说明:当前线上版本后端用 `token` 头,不用 Authorization;但为了不影响其它模块编译/运行,这里保留该方法导出。
const formatBearerAuthorization = (raw) => {
const v = String(raw || "").trim()
if (!v) return ""
if (/^Bearer\s+/i.test(v)) return v
return `Bearer ${v}`
}
// ===== 兼容导出(勿删)=====
// 获取token
const getToken = () => {
// 固定 token(恢复原行为:所有请求都带这个 token)
return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw'
// return localStorage.getItem(TOKEN_KEY)
}
......@@ -25,7 +36,7 @@ const removeToken = () => {
}
// 导出token管理方法
export { getToken, setToken, removeToken }
export { getToken, setToken, removeToken, formatBearerAuthorization }
// const BASE_API = import.meta.env.VITE_BASE_API
// 创建axios实例
......@@ -90,6 +101,28 @@ service.interceptors.response.use(
// 重复请求触发的取消不提示错误
if (isCanceledError) return Promise.reject(error)
// 特殊处理:用户信息接口可能在某些环境不存在(404),此时不弹出错误提示
// 需求:GET /api/sso/user/info 返回 404 时静默
try {
const errUrl = String(error?.config?.url || '')
if (error?.response?.status === 404 && errUrl.includes('/api/sso/user/info')) {
return Promise.reject(error)
}
} catch {
// ignore
}
// 特殊处理:风险信号管理页面接口偶发 500,不弹出提示
// 覆盖接口:/api/riskSignal/getCountInfo | /api/riskSignal/getDailyCount | /api/riskSignal/pageQuery
try {
const errUrl = String(error?.config?.url || '')
if (error?.response?.status === 500 && errUrl.includes('/api/riskSignal/')) {
return Promise.reject(error)
}
} catch {
// ignore
}
// 处理token过期或无效的情况(排除 AI 分析服务:其 401 多为 API Key 问题)
const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
......@@ -119,3 +152,11 @@ service.interceptors.response.use(
)
export default service
/*
===========================
下面是“当前新版 request.js”代码备份(按你的要求:不删除,仅注释保留)
===========================
(此处保留了之前实现的 token 多来源读取、AxiosHeaders、默认头同步、401 防抖等逻辑)
*/
\ No newline at end of file
// 智库概览信息
import request, { getToken } from "@/api/request.js";
import request, { getToken, formatBearerAuthorization } from "@/api/request.js";
// 智库列表
export function getThinkTankList() {
......@@ -257,7 +257,8 @@ export function postReportDomainViewAnalysis(data) {
*/
export async function postReportDomainViewAnalysisStream(data, handlers = {}) {
const { onReasoningChunk, onMessage } = handlers
const token = getToken()
const raw = getToken()
const token = raw ? String(raw).trim() : ""
const response = await fetch('/intelligent-api/report-domain-view-analysis', {
method: 'POST',
headers: {
......
/**
* 必须在 router、业务组件之前执行,且不能 import @/api/request(会提前加载 element-plus,易触发 Vue 初始化异常)。
* 与 request.js 中 TOKEN_KEY / TOKEN_SESSION_KEY 保持一致。
*/
const TOKEN_KEY = "auth_token";
const TOKEN_SESSION_KEY = "auth_token";
if (typeof window !== "undefined") {
try {
const q = new URLSearchParams(window.location.search || "");
const t = q.get("token");
if (t && String(t).trim()) {
const v = String(t).trim();
localStorage.setItem(TOKEN_KEY, v);
sessionStorage.setItem(TOKEN_SESSION_KEY, v);
}
} catch {
// ignore
}
}
<template>
<div class="summary-cards-panel" :style="{ width: panelWidth }" v-loading="loading">
<div class="date-box">
<div class="date-icon" v-if="tipIcon">
<img :src="tipIcon" alt="" />
</div>
<div class="date-text">{{ descriptionText }}</div>
<TimeTabPane @time-click="onTimeClick" :activeTime="activeTime" />
</div>
<div class="cards-box" v-if="cards.length">
<div class="summary-item" v-for="item in cards" :key="item.id || item.name">
<div class="item-left">
<img :src="item.avatar || defaultAvatar" alt="" />
</div>
<div class="item-center" @click="emit('name-click', item)">
<div class="item-name one-line-ellipsis">{{ item.name }}</div>
<div v-if="item.subText" class="item-sub-text one-line-ellipsis">{{ item.subText }}</div>
</div>
<el-popover content="跳转至数据资源库" placement="top">
<template #reference>
<div class="item-total" @click="emit('count-click', item)">{{ item.count }}{{ countSuffix }}</div>
</template>
</el-popover>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
<div class="item-dot" v-if="item.delta">+{{ item.delta }}</div>
</div>
<div v-if="shouldShowMoreCard" class="summary-item" @click="emit('more-click')">
<div class="item-more">{{ moreText }} ({{ totalCount }}家)</div>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
</div>
</div>
<div v-else class="cards-empty">
<el-empty :description="emptyText" :image-size="80" />
</div>
</div>
</template>
<script setup>
import TimeTabPane from "@/components/base/TimeTabPane/index.vue";
import { ArrowRightBold } from "@element-plus/icons-vue";
import { computed } from "vue";
const props = defineProps({
descriptionText: {
type: String,
default: ""
},
cards: {
type: Array,
default: () => []
},
totalCount: {
type: Number,
default: 0
},
showMoreCard: {
type: Boolean,
default: true
},
moreCardMinCount: {
type: Number,
default: 8
},
emptyText: {
type: String,
default: "暂无数据"
},
activeTime: {
type: String,
default: "近一年"
},
tipIcon: {
type: String,
default: ""
},
defaultAvatar: {
type: String,
default: ""
},
countSuffix: {
type: String,
default: "项"
},
moreText: {
type: String,
default: "查看全部机构"
},
panelWidth: {
type: String,
default: "1600px"
},
loading: {
type: Boolean,
default: false
}
});
const emit = defineEmits(["time-click", "name-click", "count-click", "more-click"]);
const shouldShowMoreCard = computed(() => {
if (!props.showMoreCard) return false;
if (props.moreCardMinCount <= 0) return true;
return (props.cards?.length || 0) >= props.moreCardMinCount;
});
const onTimeClick = event => {
emit("time-click", event);
};
</script>
<style scoped lang="scss">
.summary-cards-panel {
margin: 48px auto 0;
.date-box {
display: flex;
align-items: center;
.date-icon {
width: 16px;
height: 16px;
font-size: 0;
margin-right: 6px;
img {
width: 100%;
height: 100%;
}
}
.date-text {
width: 20px;
flex: auto;
font-size: 18px;
line-height: 18px;
font-family: Source Han Sans CN;
color: var(--text-primary-80-color);
}
}
.cards-box {
margin: 20px 0 64px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 80px;
gap: 16px;
font-family: Microsoft YaHei;
.summary-item {
padding: 0 16px;
display: flex;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.item-left {
width: 48px;
height: 48px;
font-size: 0;
img {
width: 100%;
height: 100%;
}
}
.item-center {
width: 20px;
flex: auto;
margin: 0 16px;
cursor: pointer;
}
.item-name {
color: rgba(59, 65, 75, 1);
font-size: 20px;
font-weight: 700;
line-height: 20px;
&:hover {
color: var(--color-primary-100);
text-decoration: underline;
}
}
.item-sub-text {
margin-top: 6px;
color: var(--text-primary-50-color);
font-size: 14px;
line-height: 18px;
}
.item-total {
font-size: 20px;
margin-right: 2px;
white-space: nowrap;
font-weight: 700;
line-height: 20px;
color: var(--color-primary-100);
&:hover {
text-decoration: underline;
}
}
.item-more {
font-size: 16px;
margin-right: 12px;
white-space: nowrap;
font-weight: 700;
line-height: 16px;
color: var(--color-primary-100);
}
.item-dot {
position: absolute;
right: -13px;
top: -10px;
padding: 0 8px;
height: 26px;
background-color: #ff4d4f;
color: white;
font-size: 16px;
line-height: 26px;
font-family: Source Han Sans CN;
border-radius: 14px;
letter-spacing: 1px;
}
}
}
.cards-empty {
margin: 20px 0 64px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
}
}
</style>
......@@ -20,14 +20,37 @@
</div>
<div class="nav-right">
<div class="info-box" @click="handleClickToolBox">
<div class="mail">
<div class="info-box">
<div class="mail" @click="handleClickToolBox">
<img src="@/assets/icons/overview/mail.png" alt="" />
</div>
<div class="user">
<div class="user-trigger">
<div class="user" @click.stop="handleToggleUserPanel">
<img src="@/assets/icons/overview/user.png" alt="" />
</div>
<div class="name text-regular">{{ "管理员" }}</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>
</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>
</div>
</div>
</div>
<div class="menu-box" v-show="isShowMenu" @mouseenter="handleHoverMenu(true)"
......@@ -61,6 +84,7 @@ import { ref, computed, onMounted, watchEffect, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { useRoute } from "vue-router";
import { getPersonType } from "@/api/common/index";
import request, { removeToken } from "@/api/request.js";
import SearchBar from "@/components/layout/SearchBar.vue";
import Menu1 from "@/assets/icons/overview/menu1.png";
......@@ -87,6 +111,16 @@ const router = useRouter();
const route = useRoute()
const personTypeList = ref([]);
const ssoUserInfo = ref(null);
const isShowUserPanel = ref(false);
// 用户面板展示兜底:接口无数据时不显示 "-",改为默认文案
const userNickname = computed(() => ssoUserInfo.value?.userInfo?.userNickname || "管理员");
const organName = computed(() => ssoUserInfo.value?.organInfo?.organName || "开发部门");
const roleName = computed(() => {
const roles = ssoUserInfo.value?.roles;
return Array.isArray(roles) && roles.length ? (roles[0]?.roleName || "系统管理员") : "系统管理员";
});
// 获取人物类别
const handleGetPersonType = async () => {
......@@ -102,6 +136,21 @@ const handleGetPersonType = async () => {
} catch (error) { }
};
// 获取当前登录用户信息(不展示,仅缓存;Authorization 由全局 request 拦截器自动注入)
const handleGetSsoUserInfo = async () => {
try {
const res = await request({
method: "GET",
// 通过 Vite 代理:/api -> 172.19.21.228:28080(rewrite 去掉 /api,后端收到 /sso/user/info)
url: "/api/sso/user/info"
});
ssoUserInfo.value = res?.data ?? null;
console.log("用户信息", ssoUserInfo.value);
} catch {
ssoUserInfo.value = null;
}
};
// 概览页标题列表
const homeTitleList = ref([
......@@ -354,8 +403,93 @@ const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
const handleToggleUserPanel = () => {
isShowUserPanel.value = !isShowUserPanel.value;
};
const handleHideUserPanel = () => {
isShowUserPanel.value = false;
};
const handleUserCommand = async (command) => {
if (command === "logout") {
isShowUserPanel.value = false;
// ===== 退出登录(外部重定向版,切线上接口时先注释保留) =====
// const ssoOrigin = import.meta.env.VITE_SSO_GATEWAY_ORIGIN || "http://172.19.21.228:28080";
// try {
// // 走后端登出接口,返回统一登出页地址(能清 SSO Cookie,确保下次必须重新登录)
// const res = await request({
// method: "POST",
// // 通过 Vite 代理:/api -> 172.19.21.228:28080(rewrite 去掉 /api,后端收到 /sso/logout)
// url: "/api/sso/logout"
// });
//
// // 无论成功失败,都清掉本地 token(确保后续请求不再带 Authorization)
// removeToken();
// try {
// window.localStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.setItem("force_sso_login", "1");
// } catch {
// // ignore
// }
//
// const redirectUrl = res && typeof res.data === "string" ? res.data : "";
// window.location.replace(redirectUrl || `${ssoOrigin}/api/v2`);
// } catch (e) {
// // 兜底:清本地并跳网关
// removeToken();
// try {
// window.localStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.setItem("force_sso_login", "1");
// } catch {
// // ignore
// }
// window.location.replace(`${ssoOrigin}/api/v2`);
// }
// ===== 退出登录(站内 login 版:清 token 后进入 /login) =====
removeToken();
try {
window.localStorage.removeItem("auth_token");
} catch {
// ignore
}
// 退出后强制所有页面回到登录页(跨标签页/刷新生效)
try {
window.localStorage.setItem("force_login", "1");
} catch {
// ignore
}
try {
window.sessionStorage.removeItem("auth_token");
} catch {
// ignore
}
router.replace("/login");
}
};
onMounted(() => {
handleGetPersonType();
handleGetSsoUserInfo();
});
</script>
......@@ -469,6 +603,8 @@ onMounted(() => {
display: flex;
justify-content: flex-end;
align-items: center;
position: relative;
.mail {
width: 32px;
......@@ -499,6 +635,159 @@ onMounted(() => {
height: 30px;
color: var(--text-primary-80-color);
}
.user-trigger {
display: flex;
align-items: center;
cursor: pointer;
}
.user-panel {
position: absolute;
right: 0;
top: calc(32px + 21px);
width: 240px;
height: 141px;
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
display: flex;
flex-direction: column;
// 高度固定 141px,需要把内容控制在容器内,避免分割线被裁剪
padding: 16px 15px 12px 24px;
box-sizing: border-box;
overflow: hidden;
z-index: 999999;
}
.user-panel-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
gap: 12px;
position: relative;
padding-right: 3px;
}
.user-panel-row--single {
justify-content: flex-start;
}
.blue-solid {
position: absolute;
left: -24px;
width: 4px;
height: 48px;
background: rgb(5, 95, 194);
top: 0;
}
.solid {
width: 206px;
height: 1px;
background: rgb(234, 236, 238);
margin-top: 13px;
}
.user-panel-label {
color: rgba(59, 65, 75, 0.8);
font-size: 14px;
line-height: 20px;
flex-shrink: 0;
}
.user-panel-value {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-panel-value--nickname {
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
margin-left: 8px;
}
.role-box {
padding: 0px 8px;
background-color: rgb(231, 243, 255);
display: flex;
border-radius: 4px;
}
.user-panel-value--role {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: justify;
color: rgb(5, 95, 194);
}
.user-panel-value--organ {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
color: rgb(95, 101, 108);
margin-top: 4px;
margin-left: 8px;
}
.user-panel-logout {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
cursor: pointer;
height: 24px;
margin-top: 12px;
display: flex;
align-items: center;
margin-left: 8px;
.back-image {
width: 16px;
height: 16px;
margin-right: 12px;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
}
}
......
<template>
<p class="p-regular-rereg">
<span class="text-regular" v-for="(segment, index) in processedText" :key="index">
<span v-if="segment.isEntity" @click="$emit('onEntityClick', segment.entity)" class="entity-link">
<span
v-if="segment.isEntity"
@click="$emit('onEntityClick', segment.entity)"
:class="['entity-link', { 'keyword-highlight': segment.isKeywordHit }]"
>
{{ segment.entity?.text_span }}
<img :src="SearchIcon" :width="10" :height="10" alt="search" />
</span>
<span v-else>
{{ segment.text }}
<span :class="{ 'keyword-highlight': segment.isKeywordHit }">{{ segment.text }}</span>
</span>
</span>
</p>
......@@ -20,6 +24,7 @@ export interface ProcessedTextSegment {
text: string;
isEntity: boolean;
entity?: TextEntity;
isKeywordHit?: boolean;
}
const props = defineProps({
text: {
......@@ -29,15 +34,42 @@ const props = defineProps({
entities: {
type: Array<TextEntity>,
default: () => []
},
highlight: {
type: String,
default: ""
}
});
const emit = defineEmits(["onEntityClick"]);
// 处理后的文本段
const processedText = ref<ProcessedTextSegment[]>([]);
const normalizeKeyword = (value: unknown) => String(value ?? "").trim();
const getKeywordMatches = (text: string, keyword: string) => {
if (!keyword) return [{ text, isKeywordHit: false }];
const lowerText = text.toLowerCase();
const lowerKeyword = keyword.toLowerCase();
if (!lowerKeyword) return [{ text, isKeywordHit: false }];
const parts: Array<{ text: string; isKeywordHit: boolean }> = [];
let start = 0;
while (start < text.length) {
const index = lowerText.indexOf(lowerKeyword, start);
if (index === -1) {
parts.push({ text: text.slice(start), isKeywordHit: false });
break;
}
if (index > start) {
parts.push({ text: text.slice(start, index), isKeywordHit: false });
}
parts.push({ text: text.slice(index, index + keyword.length), isKeywordHit: true });
start = index + keyword.length;
}
return parts.filter(part => part.text);
};
// 处理文本,识别并替换实体
const processText = () => {
console.log("props.entities.length", props.entities.length);
if (!props.text || !props.entities) {
// console.log('props.text', props.entities.length)
processedText.value = [{ text: "", isEntity: false }];
......@@ -46,6 +78,7 @@ const processText = () => {
const result = [];
let currentPosition = 0;
const keyword = normalizeKeyword(props.highlight);
// 按实体文本长度排序,优先匹配长文本
const sortedEntities = [...props.entities].sort((a, b) => b.text_span.length - a.text_span.length);
......@@ -61,7 +94,8 @@ const processText = () => {
// 如果当前位置是实体,添加到结果
result.push({
isEntity: true,
entity: { ...entity }
entity: { ...entity },
isKeywordHit: keyword ? entityText.toLowerCase().includes(keyword.toLowerCase()) : false
});
currentPosition = endPosition;
matched = true;
......@@ -82,18 +116,26 @@ const processText = () => {
if (nextEntityStart > currentPosition) {
const plainText = props.text.substring(currentPosition, nextEntityStart);
const parts = getKeywordMatches(plainText, keyword);
parts.forEach(part => {
result.push({
text: plainText,
isEntity: false
text: part.text,
isEntity: false,
isKeywordHit: part.isKeywordHit
});
});
currentPosition = nextEntityStart;
} else {
// 没有更多实体,添加剩余文本
const remainingText = props.text.substring(currentPosition);
if (remainingText) {
const parts = getKeywordMatches(remainingText, keyword);
parts.forEach(part => {
result.push({
text: remainingText,
isEntity: false
text: part.text,
isEntity: false,
isKeywordHit: part.isKeywordHit
});
});
}
currentPosition = props.text.length;
......@@ -106,6 +148,7 @@ const processText = () => {
// 监听文本和实体变化
watch(() => props.text, processText);
watch(() => props.entities, processText, { deep: true });
watch(() => props.highlight, processText);
// 初始化处理
onMounted(processText);
......@@ -113,6 +156,11 @@ onMounted(processText);
<style lang="scss" scoped>
@use "@/styles/common.scss";
.keyword-highlight {
background: rgba(255, 199, 0, 0.35);
border-radius: 2px;
}
.entity-link {
color: var(--color-primary-100);
&:hover {
......
import "./bootstrapAuth.js";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
......
import { createRouter, createWebHistory } from "vue-router";
import { getIsLoggedIn } from "@/utils/auth";
import { setToken, removeToken, getToken } from "@/api/request.js";
/** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */
const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id";
/** 退出后强制回登录页(跨标签页/刷新生效) */
const FORCE_LOGIN_KEY = "force_login";
// ===== SSO/重定向相关(切线上接口时先注释保留) =====
// /** 强制走统一登录(仅前端控制):退出登录后置 1,直到 SSO 回跳带 ?token= 再清除 */
// const FORCE_SSO_LOGIN_KEY = "force_sso_login";
function getCurrentViteBootId() {
try {
// eslint-disable-next-line no-undef
return typeof __APP_BOOT_ID__ !== "undefined" ? String(__APP_BOOT_ID__) : "";
} catch {
return "";
}
}
/** 仅同步 bootId(SSO 回跳带 ?token= 时会提前 return,须单独写入,否则下次进入 /ZMOverView 会误判 boot 变更而清 token) */
function persistViteBootIdOnly() {
if (typeof window === "undefined") return;
const current = getCurrentViteBootId();
if (!current) return;
try {
window.localStorage.setItem(VITE_BOOT_STORAGE_KEY, current);
} catch {
// ignore
}
}
/**
* 每次重启 Vite/重新构建后 __APP_BOOT_ID__ 会变;仅当「曾记录过旧 bootId 且与当前不一致」时清空 token。
* 用 localStorage 而非 sessionStorage,避免新开标签页时误把有效 token 清掉导致请求不带 token、全员 401。
* 注意:须在处理完 URL ?token= 之后再执行,避免先清 token 再写入的竞态。
*/
function clearTokenIfNewDevBoot() {
if (typeof window === "undefined") return;
try {
const current = getCurrentViteBootId();
if (!current) return;
const saved = window.localStorage.getItem(VITE_BOOT_STORAGE_KEY);
if (saved && saved !== current) {
removeToken();
}
window.localStorage.setItem(VITE_BOOT_STORAGE_KEY, current);
} catch {
// ignore
}
}
// /** 统一认证网关:未登录时整页跳转此处,由后端再跳到登录页;登录成功后回跳到前端并带 ?token= */
// const SSO_GATEWAY_ORIGIN =
// import.meta.env.VITE_SSO_GATEWAY_ORIGIN || "http://172.19.21.228:28080";
//
// /** 网关注册的回跳地址常为 /callback;保存 token 后应进入业务首页,避免停在无路由的 /callback */
// const SSO_POST_LOGIN_PATH =
// import.meta.env.VITE_SSO_POST_LOGIN_PATH || "/ZMOverView";
/**
* 是否与「请求里 getToken()」一致:避免出现仅空白/脏数据时仍认为已登录 → 不走 SSO 且请求头无 token
*/
function hasStoredAuthToken() {
return Boolean(getToken());
}
const Home = () => import('@/views/home/index.vue')
const DataLibrary = () => import('@/views/dataLibrary/index.vue')
......@@ -33,6 +97,8 @@ const routes = [
{
path: "/",
name: "Home",
// 访问根路径时默认进入概览页(否则 Home 的 router-view 为空,看起来像白屏)
redirect: "/ZMOverView",
component: Home,
children: [
...fileRoutes,
......@@ -73,18 +139,88 @@ const router = createRouter({
routes
});
// 路由守卫 - 设置页面标题
// 路由守卫:SSO + 设置页面标题
// 1)首次/无本地 token:整页跳到网关 /api/v2 + 当前路径,走后端登录
// 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态)
// 3)已有本地 token:正常走前端路由
router.beforeEach((to, from, next) => {
// 登录态:同一次前端服务 BOOT_ID 内跨刷新/跨新标签有效;服务重启后自动失效
const isAuthed = getIsLoggedIn();
// ===== 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();
if (import.meta.env.DEV) {
clearTokenIfNewDevBoot();
}
// 启用站内登录页:无 token 时统一进入 /login(不做外部重定向)
// 退出登录后:无论当前 getToken() 返回什么,都强制先去 /login,直到再次登录清除此标记
let forceLoginActive = false;
try {
if (typeof window !== "undefined" && window.localStorage.getItem(FORCE_LOGIN_KEY) === "1") {
forceLoginActive = true;
if (to.path !== "/login") {
next({ path: "/login", replace: true });
return;
}
}
} catch {
// ignore
}
const isLoginRoute = to.name === "Login" || /^\/login\/?$/.test(String(to.path || ""));
const isAuthed = hasStoredAuthToken();
if (!isLoginRoute && !isAuthed) {
next({
path: "/login",
query: { redirect: to.fullPath }
});
// 已登录:不应停留在 /login(首页应为 /ZMOverView)
// 说明:force_login=1 时表示用户主动退出,仍允许进入 /login;否则一律跳回业务首页
if (isAuthed && to.path === "/login" && !forceLoginActive) {
const redirect = (to.query && to.query.redirect) ? String(to.query.redirect) : "";
next({ path: redirect || "/ZMOverView", replace: true });
return;
}
if (!isAuthed && to.path !== "/login") {
// 防止误入 /callback 空白页
next({ path: "/login", replace: true });
return;
}
......
......@@ -15,6 +15,7 @@ const BillProgressForecast = () => import('@/views/bill/influence/ProgressForeca
const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue')
const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue')
const BillVersionCompare = () => import('@/views/bill/versionCompare/index.vue')
const BillAllCommittee = () => import('@/views/bill/allCommittee/index.vue')
const billRoutes = [
......@@ -28,6 +29,19 @@ const billRoutes = [
isShowHeader: true
}
},
{
path: "/bill/allCommittee",
name: "BillAllCommittee",
component: BillAllCommittee,
meta: {
title: "法案委员会列表",
isShowHeader: true
}
},
{
path: "/billAllCommittee",
redirect: "/bill/allCommittee"
},
{
path: "/billLayout",
name: "BillLayoutContainer",
......
......@@ -511,6 +511,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
if (this.reportContent.includes('./out/img')) {
this.reportContent = this.reportContent.replaceAll('./out/img', outImgbaseUrl);
// console.log(reportContent)
}
};
......
<template>
<div class="view-box">
<div class="container-box">
<div class="hard-box">
<div class="hard-name text-title-0-show">美国国会委员会</div>
<div class="hard-num text-title-2-show">{{ committeeInfo.total }}</div>
<div style="width: 0; flex: auto"></div>
<div class="hard-input">
<el-input
v-model="committeeInfo.keyWord"
@keyup.enter="onAllCommittee()"
style="width: 100%; height: 100%"
:suffix-icon="Search"
placeholder="搜索委员会"
/>
</div>
</div>
<div class="date-box">
<div class="date-text">近期美国国会各委员会涉华提案数量汇总</div>
<TimeTabPane @time-click="handleDateChange" activeTime="近一年" />
</div>
<div class="committee-list" ref="refCommittee" v-loading="committeeInfo.loading">
<div class="committee-item" v-for="item in committeeInfo.list" :key="item.id" @click="handleToDataLibrary(item)">
<div class="item-left">
<img :src="iconCommit" alt="" />
</div>
<div class="item-right">
<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>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
</div>
</div>
<div class="pagination-box">
<el-pagination
@current-change="onAllCommittee"
:pageSize="committeeInfo.pageSize"
:current-page="committeeInfo.pageNum"
background
layout="prev, pager, next"
:total="committeeInfo.total"
/>
</div>
</div>
</div>
</template>
<script setup name="BillAllCommittee">
import { onMounted, reactive, ref } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ArrowRightBold } from "@element-plus/icons-vue";
import router from "@/router";
import TimeTabPane from "@/components/base/TimeTabPane/index.vue";
import { getStatisticsBillCountByCommittee } from "@/api/bill/billHome";
import iconCommit from "../billHome/assets/icons/icon-commit.png";
const committeeInfo = reactive({
loading: false,
pageNum: 1,
pageSize: 8,
total: 0,
keyWord: "",
dateDesc: "近一年",
list: []
});
const getChamberLabel = orgType => {
if (orgType === "Senate") return "参议院";
if (orgType === "House") return "众议院";
return orgType || "";
};
const onAllCommittee = async num => {
committeeInfo.pageNum = num || 1;
committeeInfo.loading = true;
try {
const res = await getStatisticsBillCountByCommittee({
dateDesc: committeeInfo.dateDesc
});
if (res.code === 200 && Array.isArray(res.data)) {
const source = res.data
.map(item => ({
id: `${item.orgType || ""}-${item.orgName || ""}`,
name: item.orgName,
chamber: getChamberLabel(item.orgType),
count: Number(item.count || 0)
}))
.filter(item => !committeeInfo.keyWord || item.name?.includes(committeeInfo.keyWord))
.sort((a, b) => (b.count || 0) - (a.count || 0));
committeeInfo.total = source.length;
const start = (committeeInfo.pageNum - 1) * committeeInfo.pageSize;
committeeInfo.list = source.slice(start, start + committeeInfo.pageSize);
} else {
committeeInfo.total = 0;
committeeInfo.list = [];
}
} catch (error) {
committeeInfo.total = 0;
committeeInfo.list = [];
}
committeeInfo.loading = false;
};
const handleDateChange = event => {
committeeInfo.dateDesc = event?.time || "近一年";
onAllCommittee();
};
const handleToDataLibrary = item => {
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: {
selectedOrg: item.name,
selectedCongress: item.chamber
}
});
window.open(route.href, "_blank");
};
const refCommittee = ref();
onMounted(() => {
let height = 2;
if (refCommittee.value) {
height = Math.floor(refCommittee.value?.clientHeight / 120);
}
committeeInfo.pageSize = height * 4;
onAllCommittee();
});
</script>
<style scoped lang="scss">
.view-box {
width: 100%;
height: 100%;
background: url("../billHome/assets/images/background.png"), linear-gradient(180deg, rgba(229, 241, 254, 1) 0%, rgba(246, 251, 255, 0) 30%);
background-size: 100% 100%;
display: flex;
justify-content: center;
.container-box {
width: 1600px;
padding: 50px 0 20px;
display: flex;
flex-direction: column;
.hard-box {
display: flex;
align-items: center;
width: 100%;
.hard-name {
color: var(--text-primary-90-color);
height: 62px;
line-height: 62px !important;
}
.hard-num {
height: 36px;
background-color: var(--color-primary-100);
color: var(--bg-white-100);
border-radius: 18px;
line-height: 36px !important;
padding: 0 16px;
margin-left: 16px;
}
.hard-input {
background-color: var(--el-fill-color-blank);
border-radius: var(--el-border-radius-base);
box-shadow: 0 0 0 1px var(--el-border-color) inset;
box-sizing: border-box;
margin-left: 20px;
width: 180px;
height: 32px;
}
}
.date-box {
margin-top: 6px;
display: flex;
align-items: center;
width: 100%;
.date-text {
width: 20px;
flex: auto;
font-size: 18px;
line-height: 18px;
font-family: Source Han Sans CN;
color: var(--text-primary-80-color);
}
}
.committee-list {
width: 100%;
height: 20px;
padding: 16px 0;
margin-top: 10px;
flex: auto;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 104px;
gap: 16px;
.committee-item {
padding: 0 16px;
display: flex;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
align-items: center;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.item-left {
width: 48px;
height: 48px;
margin-right: 12px;
img {
width: 100%;
height: 100%;
}
}
.item-right {
flex: auto;
min-width: 0;
.item-name {
font-size: 20px;
font-weight: 700;
line-height: 22px;
color: var(--text-primary-80-color);
}
.item-chamber {
margin-top: 6px;
font-size: 14px;
color: var(--text-primary-50-color);
}
}
.item-total {
font-size: 20px;
font-weight: 700;
line-height: 20px;
color: var(--color-primary-100);
margin-right: 6px;
}
}
}
.pagination-box {
display: flex;
justify-content: center;
}
}
}
</style>
......@@ -93,7 +93,7 @@
<div class="item">
<div class="item-left">法案进展:</div>
<div class="item-right2">
<div class="tag" v-for="(val, idx) in getReversedProgress(item.progress)" :key="`${item.billId}-${val}-${idx}`" :style="{ zIndex: item.progress.length - idx }">{{ val }}</div>
<div class="tag" v-for="(val, idx) in item.progress" :key="`${item.billId}-${val}-${idx}`" :style="{ zIndex: item.progress.length - idx }">{{ val }}</div>
</div>
</div>
</div>
......@@ -458,8 +458,6 @@ const handleClickAvatar = async member => {
} catch (error) {}
};
const getReversedProgress = progress => (Array.isArray(progress) ? [...progress].reverse() : []);
const handleClickCommitteeBill = bill => {
if (!bill?.billId) return;
props.onClickToDetail({
......
......@@ -9,34 +9,22 @@
:containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true" defaultBillSearchType="federal" />
</div>
<div class="committee-cards-section">
<div class="committee-cards-filter">
<span class="committee-cards-desc">近期美国国会各委员会涉华提案数量汇总</span>
<el-radio-group v-model="committeeTimeRange" class="committee-time-switch" size="default">
<el-radio-button v-for="item in committeeTimeOptions" :key="item.value" :value="item.value">
<span class="committee-time-switch-inner">
<el-icon v-if="committeeTimeRange === item.value" class="committee-time-switch-icon">
<Calendar />
</el-icon>
{{ item.label }}
</span>
</el-radio-button>
</el-radio-group>
</div>
<div class="committee-cards-row">
<div v-for="item in committeeCardList" :key="item.id" class="committee-card"
@click="handleToDataLibrary(item)">
<div class="committee-card-icon">
<img :src="iconCommit" alt="委员会头像" />
</div>
<div class="committee-card-content">
<div class="committee-card-name">{{ item.name }}</div>
<div class="committee-card-chamber">{{ item.chamber }}</div>
</div>
<div class="committee-card-count">{{ item.count }}&gt;</div>
</div>
</div>
</div>
<SummaryCardsPanel
descriptionText="近期美国国会各委员会涉华提案数量汇总"
:cards="committeeCards"
:totalCount="committeeTotalCount"
:tipIcon="box7HeaderIcon"
:defaultAvatar="iconCommit"
:loading="committeeLoading"
activeTime="近一年"
emptyText="暂无数据"
moreText="查看全部委员会"
:moreCardMinCount="7"
@time-click="handleCommitteeTimeClick"
@name-click="handleToDataLibrary"
@count-click="handleToDataLibrary"
@more-click="handleToCommitteeMore"
/>
<DivideHeader id="position1" class="divide1" :titleText="'最新动态'"></DivideHeader>
<div class="home-content-center">
......@@ -256,6 +244,7 @@
<script setup>
import RiskSignal from "@/components/base/riskSignal/index.vue";
import SummaryCardsPanel from "@/components/base/SummaryCardsPanel/index.vue";
import { onMounted, ref, onUnmounted, nextTick, watch, computed } from "vue";
import router from "@/router/index";
import setChart from "@/utils/setChart";
......@@ -275,7 +264,7 @@ import {
import { getPersonSummaryInfo } from "@/api/common/index";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import DivideHeader from "@/components/DivideHeader.vue";
import overviewMainBox from "@/components/base/boxBackground/overviewMainBox.vue";
import OverviewMainBox from "@/components/base/boxBackground/overviewMainBox.vue";
import OverviewCard from "./OverviewCard.vue";
import ResourceLibrarySection from "./ResourceLibrarySection.vue";
import { useContainerScroll } from "@/hooks/useScrollShow";
......@@ -294,7 +283,6 @@ import box7HeaderIcon from "./assets/images/box7-header-icon.png";
import iconCommit from "./assets/icons/icon-commit.png";
import { ElMessage } from "element-plus";
import { Calendar } from "@element-plus/icons-vue";
import { useGotoNewsDetail } from "@/router/modules/news";
// 跳转人物主页(MessageBubble 的 person-click 传入整条列表项,需取 personId)
......@@ -368,14 +356,11 @@ const handleClickToCharacter = async item => {
const containerRef = ref(null);
const { isShow } = useContainerScroll(containerRef);
// 委员会卡片占位数据
const committeeTimeRange = ref("近一月");
const committeeTimeOptions = [
{ label: "近一周", value: "近一周" },
{ label: "近一月", value: "近一月" },
{ label: "近一年", value: "近一年" }
];
// 委员会卡片数据
const committeeTimeRange = ref("近一年");
const committeeCardList = ref([]);
const committeeTotalCount = ref(0);
const committeeLoading = ref(false);
const getChamberLabel = orgType => {
if (orgType === "Senate") return "参议院";
......@@ -383,27 +368,51 @@ const getChamberLabel = orgType => {
return orgType || "";
};
const committeeCards = computed(() => {
return committeeCardList.value.map(item => ({
id: item.id,
name: item.name,
subText: item.chamber,
count: item.count
}));
});
const handleGetCommitteeBillCount = async () => {
committeeLoading.value = true;
try {
const res = await getStatisticsBillCountByCommittee({
dateDesc: committeeTimeRange.value
});
if (res.code === 200 && Array.isArray(res.data)) {
committeeCardList.value = res.data
const mappedList = res.data
.map(item => ({
id: `${item.orgType || ""}-${item.orgName || ""}`,
name: item.orgName,
chamber: getChamberLabel(item.orgType),
count: Number(item.count || 0)
}))
.sort((a, b) => (b.count || 0) - (a.count || 0))
.slice(0, 3);
.sort((a, b) => (b.count || 0) - (a.count || 0));
committeeTotalCount.value = mappedList.length;
committeeCardList.value = mappedList.slice(0, 3);
} else {
committeeTotalCount.value = 0;
committeeCardList.value = [];
}
} catch (error) {
committeeTotalCount.value = 0;
committeeCardList.value = [];
}
committeeLoading.value = false;
};
const handleCommitteeTimeClick = event => {
committeeTimeRange.value = event?.time || "近一年";
handleGetCommitteeBillCount();
};
const handleToCommitteeMore = () => {
const route = router.resolve({ path: "/bill/allCommittee" });
window.open(route.href, "_blank");
};
const hotBillList = ref([]); // 热门法案列表
......@@ -1029,14 +1038,15 @@ const handleBox6 = async () => {
// 涉华领域分布
const box9ChartData = ref([]);
const box9selectetedTime = ref("2025");
// 立法状态下拉:提出法案、众议院通过、参议院通过、解决分歧、完成立法
// v-model 存储的是接口需要的 status 值
const box9LegislativeStatus = ref("提案");
// 立法状态下拉:提出法案、众议院通过、参议院通过、解决分歧、呈交总统、完成立法
// v-model 存储的是接口需要的 status 值(直接作为接口参数)
const box9LegislativeStatus = ref("提出法案");
const box9LegislativeStatusList = ref([
{ label: "提出法案", value: "提案" },
{ label: "提出法案", value: "提出法案" },
{ label: "众议院通过", value: "众议院通过" },
{ label: "参议院通过", value: "参议院通过" },
{ label: "解决分歧", value: "分歧已解决" },
{ label: "解决分歧", value: "解决分歧" },
{ label: "呈交总统", value: "呈交总统" },
{ label: "完成立法", value: "完成立法" }
]);
const box9YearList = ref([
......@@ -1063,18 +1073,39 @@ const box9YearList = ref([
]);
const box9HasData = ref(true);
let box9ChartInstance = null;
const BOX9_MAX_DOMAIN_COUNT = 7;
const BOX9_OTHER_DOMAIN_NAME = "其他";
const formatBox9DomainData = (list = []) => {
if (!Array.isArray(list) || list.length <= BOX9_MAX_DOMAIN_COUNT) {
return list;
}
const topDomainList = list.slice(0, BOX9_MAX_DOMAIN_COUNT);
const otherDomainCount = list.slice(BOX9_MAX_DOMAIN_COUNT).reduce((sum, item) => {
return sum + Number(item?.countBill || 0);
}, 0);
if (!otherDomainCount) {
return topDomainList;
}
return [
...topDomainList,
{
industryName: BOX9_OTHER_DOMAIN_NAME,
countBill: otherDomainCount
}
];
};
const getBox9Data = async () => {
chartLoading.value = { ...chartLoading.value, box6: true };
const params = {
year: box9selectetedTime.value,
status: box9LegislativeStatus.value
stageName: box9LegislativeStatus.value
};
try {
const res = await getBillIndustry(params);
console.log("box9-涉华法案领域分布", res.data);
if (res.code === 200 && res.data && res.data.length > 0) {
box9HasData.value = true;
box9ChartData.value = res.data;
box9ChartData.value = formatBox9DomainData(res.data);
} else {
box9HasData.value = false;
box9ChartData.value = [];
......@@ -1104,16 +1135,9 @@ const handleBox9Data = async () => {
const selectedIndex = box9LegislativeStatusList.value.findIndex(
item => item.value === box9LegislativeStatus.value
);
const arr = [
{ label: "提出法案", value: "提案" },
{ label: "众议院通过", value: "众议院通过" },
{ label: "参议院通过", value: "参议院通过" },
{ label: "解决分歧", value: "分歧已解决" },
{ label: "完成立法", value: "完成立法" }
]
const status = arr.filter(item => {
return item.value === box9LegislativeStatus.value
})[0].label
// 当前选中的立法状态中文名(直接等于接口传参值)
const statusItem = box9LegislativeStatusList.value[selectedIndex];
const status = statusItem ? statusItem.label : "";
const selectParam = {
moduleType: '国会法案',
key: 2,
......@@ -1272,13 +1296,14 @@ const getBox8ChartOption = stageList => {
const handleBox8Data = async () => {
chartLoading.value = { ...chartLoading.value, box8: true };
// 进展分布显示顺序:提出法案(对应进度“提案”)、众议院通过、参议院通过、分歧已解决(解决分歧)、完成立法
const stageOrder = ["提案", "众议院通过", "参议院通过", "分歧已解决", "完成立法"];
// 进展分布显示顺序:提出法案(对应进度“提案”)、众议院通过、参议院通过、解决分歧(对应进度“分歧已解决”)、呈交总统、完成立法
const stageOrder = ["提案", "众议院通过", "参议院通过", "分歧已解决", "呈交总统", "完成立法"];
const stageNameMap = {
提案: "提出法案",
众议院通过: "众议院通过",
参议院通过: "参议院通过",
分歧已解决: "解决分歧",
呈交总统: "呈交总统",
完成立法: "完成立法"
};
......@@ -1347,13 +1372,9 @@ watch(box8selectetedTime, () => {
handleBox8Data();
});
watch(
committeeTimeRange,
() => {
onMounted(() => {
handleGetCommitteeBillCount();
},
{ immediate: true }
);
});
const handleToPosi = id => {
const element = document.getElementById(id);
......@@ -1389,16 +1410,25 @@ const handleResize = () => {
// 下钻至资源库
const handleToDataLibrary = (item) => {
// console.log('item', item);
const selectParam = {
selectedOrg: item.name,
selectedCongress: item.chamber
window.sessionStorage.setItem("curTabName", item.id);
const curRoute = router.resolve({
path: "/institution",
query: {
id: item.id
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
window.open(curRoute.href, "_blank");
// console.log('item', item);
// const selectParam = {
// selectedOrg: item.name,
// selectedCongress: item.chamber
// }
// const route = router.resolve({
// path: "/dataLibrary/countryBill",
// query: selectParam
// });
// window.open(route.href, "_blank");
}
......
......@@ -134,9 +134,10 @@ const handleAnalysisClick = analysisType => {
// 进展预测 -> 法案简介页(法案进展)
if (analysisType === "forsee") {
router.push({
const target = router.resolve({
path: `/billLayout/ProgressForecast/${billId}`,
});
window.open(target.href, "_blank");
return;
}
......
<template>
<BillPageShell class="wrap">
<BillPageShell>
<div class="wrap">
<div class="left">
<div class="box1">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">典型阶段耗时</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box1-main">
<div class="box1-main-center" id="chart1"></div>
<div class="box1-main-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
从立法耗时角度分析,大而美法案从提交到签署仅39天,远快于历史同类法案(通常需6个月以上),立法速度极快。
</div>
<div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div>
</div> -->
<AnalysisBox title="典型阶段耗时分析">
<div class="analysis-ai-wrapper analysis-ai-wrapper--box1">
<div class="box1-main" :class="{ 'box1-main--full': !timeFooterText }">
......@@ -48,7 +20,7 @@
</div>
</div>
<div v-if="!aiPaneVisible.box1" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" :text="'与历史同类法案的典型阶段耗时对比分析,数据来源:美国国会官网'"/>
<TipTab class="analysis-ai-tip" :text="'与历史同类法案的典型阶段耗时对比分析,数据来源:美国国会官网'" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box1')" />
</div>
<div v-if="aiPaneVisible.box1" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box1')">
......@@ -58,36 +30,6 @@
</AnalysisBox>
</div>
<div class="box2">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">修正案次数分析</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box2-main">
<div class="box2-main-center" id="chart2"></div>
<div class="box2-main-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
法案本质是共和党与资本集团的深度联盟,共和党获超
​80%利益集团献金,以减税、松监管、军工扩张为核心回报。
</div>
<div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div>
</div> -->
<AnalysisBox title="修正案次数分析">
<div class="analysis-ai-wrapper analysis-ai-wrapper--box2">
<div class="box2-main" :class="{ 'box2-main--full': !amendFooterText }">
......@@ -105,7 +47,7 @@
</div>
</div>
<div v-if="!aiPaneVisible.box2" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" :text="'与历史同类法案的修正案次数对比分析,数据来源:美国国会官网'"/>
<TipTab class="analysis-ai-tip" :text="'与历史同类法案的修正案次数对比分析,数据来源:美国国会官网'" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box2')" />
</div>
<div v-if="aiPaneVisible.box2" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box2')">
......@@ -117,272 +59,6 @@
</div>
<div class="right">
<div class="box3">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">投票分析</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box3-main">
<div class="box3-main-center">
<div class="box3-main-center-header">
<div class="box3-main-center-header-box1">立法阶段</div>
<div class="box3-main-center-header-box2">票数</div>
<div class="box3-main-center-header-box3">平均区间</div>
<div class="box3-main-center-header-box4">占比</div>
<div class="box3-main-center-header-box5">
倒戈人数<span style="font-weight: normal; display: inline-block">(平均区间)</span>
</div>
<div class="box3-main-center-header-box6">关键议员</div>
</div>
<div class="box3-main-center-content">
<div class="box3-main-center-content-box" v-for="item in voteAnalysisList" :key="item.actionId">
<div class="item">
<div class="item-box1">
<div class="box1-left">
<div style="width: 100%; display: flex; flex-direction: column; align-items: flex-end">
<div class="name nameBlod" :title="item.actionTitle" style="
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
">
{{ item.actionTitle }}
</div>
<div class="time">
{{ formatDate(item.actionDate) }}
</div>
</div>
</div>
<div class="box1-right">
<div class="box1-right-top">
<el-progress :percentage="Number(item.agreePercent)" :show-text="false" color="rgb(33, 129, 57)">
</el-progress>
</div>
<div class="box1-right-bottom">
<el-progress :percentage="Number(item.againstPercent)" :show-text="false" color="rgb(206, 79, 81)">
</el-progress>
</div>
</div>
</div>
<div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.agreeCount + "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.againstCount + "票" }}</div>
</div>
<div class="item-box3">
<div class="box3-1"></div>
<div class="box3-2"></div>
</div>
<div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.agreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.againstPercent + "%" }}</div>
</div>
<div class="item-box5"></div>
<div class="item-box6">
<el-icon size="20" color="#555">
<ArrowDownBold />
</el-icon>
</div>
</div>
<div class="item">
<div class="item-box1">
<div class="box1-left">
<div class="icon">
<img :src="MZD" alt="" />
</div>
<div class="name">民主党</div>
</div>
<div class="box1-right">
<div class="box1-right-top">
<el-progress :percentage="Number(item.dagreePercent)" :show-text="false" color="rgb(33, 129, 57)">
</el-progress>
</div>
<div class="box1-right-bottom">
<el-progress :percentage="Number(item.dagainstPercent)" :show-text="false" color="rgb(206, 79, 81)">
</el-progress>
</div>
</div>
</div>
<div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.dagreeCount + "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.dagainstCount + "票" }}</div>
</div>
<div class="item-box3"></div>
<div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.dagreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.dagainstPercent + "%" }}</div>
</div>
<div class="item-box5">
<div class="box5-1" style="color: #ce4f51">{{ item.dreverseCount + "人" }}</div>
</div>
<div class="item-box6">
<div class="img-box" v-if="item.dpersonImageUrl">
<img :src="item.dpersonImageUrl" alt="" />
</div>
</div>
</div>
<div class="item">
<div class="item-box1">
<div class="box1-left">
<div class="icon">
<img :src="GHD" alt="" />
</div>
<div class="name">共和党</div>
</div>
<div class="box1-right">
<div class="box1-right-top">
<el-progress :percentage="Number(item.ragreePercent)" :show-text="false" color="rgb(33, 129, 57)">
</el-progress>
</div>
<div class="box1-right-bottom">
<el-progress :percentage="Number(item.ragainstPercent)" :show-text="false" color="rgb(206, 79, 81)">
</el-progress>
</div>
</div>
</div>
<div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.ragreeCount + "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.ragainstCount + "票" }}</div>
</div>
<div class="item-box3"></div>
<div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.ragreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.ragainstPercent + "%" }}</div>
</div>
<div class="item-box5">
<div class="box5-1" style="color: #ce4f51">{{ item.rreverseCount + "人" }}</div>
</div>
<div class="item-box6">
<div class="img-box" v-if="item.rpersonImageUrl">
<img :src="item.rpersonImageUrl" alt="" />
</div>
</div>
</div>
</div>
<div class="item" v-for="(item, index) in voteAnalysisList3" :key="index">
<div class="item-box1">
<div class="box1-left">
<div class="icon" v-if="item.nameIcon">
<img :src="item.nameIcon" alt="" />
</div>
<div>
<div class="name" :class="{ nameBlod: item.nameBold }">
{{ item.name }}
</div>
<div class="time">
{{ item.time }}
</div>
</div>
</div>
<div class="box1-right">
<div class="box1-right-top">
<el-progress :percentage="item.supportRate" :show-text="false"> </el-progress>
</div>
<div class="box1-right-bottom">
<el-progress :percentage="item.againistRate" :show-text="false"> </el-progress>
</div>
</div>
</div>
<div class="item-box2">
<div class="box2-1">{{ item.support + "票" }}</div>
<div class="box2-2">{{ item.againist + "票" }}</div>
</div>
<div class="item-box3">
<div class="box3-1">{{ item.supportRank }}</div>
<div class="box3-2">{{ item.againistRank }}</div>
</div>
<div class="item-box4">
<div class="box4-1">{{ item.supportRate + "%" }}</div>
<div class="box4-2">{{ item.againistRate + "%" }}</div>
</div>
<div class="item-box5">
<div class="box5-1" v-if="item.people">{{ item.people+' 人' }}</div>
<div class="box5-2" v-if="item.peopleRank">{{ `( ${item.peopleRank} )` }}</div>
</div>
<div class="item-box6">
<div class="img-box" v-if="item.keyUser">
<img :src="item.keyUser" alt="" />
</div>
<div v-else>
<el-icon size="20" color="#555"><ArrowDownBold /></el-icon>
</div>
</div>
</div>
</div>
<div class="box3-main-center-content-box">
<div class="item" v-for="(item, index) in voteAnalysisList4" :key="index">
<div class="item-box1">
<div class="box1-left">
<div class="icon" v-if="item.nameIcon">
<img :src="item.nameIcon" alt="" />
</div>
<div>
<div class="name" :class="{ nameBlod: item.nameBold }">
{{ item.name }}
</div>
<div class="time">
{{ item.time }}
</div>
</div>
</div>
<div class="box1-right">
<div class="box1-right-top">
<el-progress :percentage="item.supportRate" :show-text="false"> </el-progress>
</div>
<div class="box1-right-bottom">
<el-progress :percentage="item.againistRate" :show-text="false"> </el-progress>
</div>
</div>
</div>
<div class="item-box2">
<div class="box2-1">{{ item.support + "票" }}</div>
<div class="box2-2">{{ item.againist + "票" }}</div>
</div>
<div class="item-box3">
<div class="box3-1">{{ item.supportRank }}</div>
<div class="box3-2">{{ item.againistRank }}</div>
</div>
<div class="item-box4">
<div class="box4-1">{{ item.supportRate + "%" }}</div>
<div class="box4-2">{{ item.againistRate + "%" }}</div>
</div>
<div class="item-box5">
<div class="box5-1" v-if="item.people">{{ item.people+' 人' }}</div>
<div class="box5-2" v-if="item.peopleRank">{{ `( ${item.peopleRank} )` }}</div>
</div>
<div class="item-box6">
<div class="img-box" v-if="item.keyUser">
<img :src="item.keyUser" alt="" />
</div>
<div v-else>
<el-icon size="20" color="#555"><ArrowDownBold /></el-icon>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="box3-main-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
法案以218:214​(众议院)和51:50​(副总统决胜票)微弱优势强行通过,暴露两党极端对立、党内倒戈频发的特点。
</div>
<div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div>
</div> -->
<AnalysisBox title="投票分析">
<div class="vote-legend">
<div class="vote-legend-item">
......@@ -716,6 +392,7 @@
</AnalysisBox>
</div>
</div>
</div>
</BillPageShell>
</template>
......@@ -1169,6 +846,10 @@ onMounted(async () => {
<style lang="scss" scoped>
.wrap {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
direction: ltr;
justify-content: flex-start;
margin-bottom: 30px;
......@@ -1254,6 +935,8 @@ onMounted(async () => {
.left {
margin-top: 16px;
width: 792px;
flex: 0 0 792px;
.box1 {
width: 792px;
......@@ -1651,6 +1334,7 @@ onMounted(async () => {
margin-left: 16px;
margin-top: 16px;
width: 792px;
flex: 0 0 792px;
height: 847px;
.box3 {
......
<template>
<div class="industry-wrap">
<BillPageShell>
<div class="wrap">
<div class="left">
<AnalysisBox title="涉及行业" :showAllBtn="false" width="100%" height="100%">
<div class="left-main">
<div class="left-center">
<el-select v-model="curHylyId" placeholder="请选择领域" class="left-center-select"
@change="handleIndustryChange">
<el-option v-for="item in industryList" :key="item.id" :label="item.name || item.hylymc"
:value="item.id" />
<el-select
v-model="curHylyId"
placeholder="请选择领域"
class="left-center-select"
@change="handleIndustryChange"
>
<el-option
v-for="item in industryList"
:key="item.id"
:label="item.name || item.hylymc"
:value="item.id"
/>
</el-select>
<el-input v-model="companySearchKeyword" placeholder="搜索实体" class="left-center-search"
:suffix-icon="Search" clearable />
<el-input
v-model="companySearchKeyword"
placeholder="搜索实体"
class="left-center-search"
:suffix-icon="Search"
clearable
/>
</div>
<div class="left-list">
<div class="left-list-title">实体名称</div>
<div class="left-list-content">
<el-empty v-if="!curCompanyList?.length" style="padding: 60px 0;" description="暂无数据"
:image-size="100" />
<el-empty
v-if="!curCompanyList?.length"
style="padding: 60px 0;"
description="暂无数据"
:image-size="100"
/>
<el-scrollbar v-else height="100%" always>
<div class="item-box">
<div class="item"
<div
v-for="(val, idx) in curCompanyList"
:key="val.id"
class="item"
:class="{ itemActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }"
@click="handleClickCompany(val, idx)" v-for="(val, idx) in curCompanyList"
:key="val.id">
@click="handleClickCompany(val, idx)"
>
<div class="item-icon">
<img :src="defaultIcon2" alt="" class="item-img" />
</div>
<div class="title"
:class="{ titleActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }">
<div
class="title"
:class="{ titleActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }"
>
{{ val.name }}
</div>
<div class="icon">
......@@ -42,28 +65,34 @@
<div class="left-pagination">
<div class="left-pagination-left">{{ `共 ${filteredCompanyList.length} 项` }}</div>
<div class="left-pagination-right">
<el-pagination @current-change="handleCurrentChange" :pageSize="pageSize"
:current-page="currentPage" size="small" background layout="prev, pager, next"
:total="filteredCompanyList.length" />
<el-pagination
@current-change="handleCurrentChange"
:pageSize="pageSize"
:current-page="currentPage"
size="small"
background
layout="prev, pager, next"
:total="filteredCompanyList.length"
/>
</div>
</div>
</div>
</AnalysisBox>
</div>
<div class="box2">
<div class="right">
<AnalysisBox :showAllBtn="false">
<template #custom-title>
<div class="custom-title">
<div class="title-left">
<div :class="['title-item', {'title-active': contentType==1}]" @click="headerContentType(1)">
<div :class="['title-item', { 'title-active': contentType==1 }]" @click="headerContentType(1)">
<div class="title-icon">
<img :src="contentType==1 ? icon1620 : icon1621" alt="">
<img :src="contentType==1 ? icon1620 : icon1621" alt="" />
</div>
<div>产业链</div>
</div>
<div :class="['title-item', {'title-active': contentType==2}]" @click="headerContentType(2)">
<div :class="['title-item', { 'title-active': contentType==2 }]" @click="headerContentType(2)">
<div class="title-icon">
<img :src="contentType==2 ? icon422 : icon423" alt="">
<img :src="contentType==2 ? icon422 : icon423" alt="" />
</div>
<div>实体关系</div>
</div>
......@@ -87,6 +116,7 @@
</AnalysisBox>
</div>
</div>
</BillPageShell>
</template>
<script setup>
......@@ -94,6 +124,8 @@ import { ref, onMounted, onBeforeUnmount, nextTick, computed, watch, reactive }
import { useRoute } from "vue-router";
import * as echarts from "echarts";
import BillPageShell from "@/views/bill/components/layout/BillPageShell.vue";
const route = useRoute();
import { getCompanyList, getHylyList, getCompanyDetail } from "@/api/influence";
import getLineChart from "./utils/lineChart";
......@@ -809,7 +841,7 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
.industry-wrap {
.wrap {
width: 100%;
height: 100%;
display: flex;
......@@ -1012,7 +1044,7 @@ onMounted(async () => {
.right {
margin-top: 16px;
margin-left: 16px;
width: 1247px;
width: 1104px;
height: 847px;
position: relative;
......@@ -1487,7 +1519,7 @@ onMounted(async () => {
}
.box-footer {
width: 1218px;
width: 100%;
height: 40px;
display: flex;
box-sizing: border-box;
......@@ -1510,13 +1542,17 @@ onMounted(async () => {
.box-footer-center {
margin-left: 13px;
margin-top: 8px;
width: 1119px;
flex: 1;
min-width: 0;
height: 24px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.box-footer-right {
......@@ -1539,10 +1575,10 @@ onMounted(async () => {
}
}
.box2 {
.right {
margin-top: 16px;
margin-left: 16px;
width: 1247px;
width: 1104px;
height: 847px;
position: relative;
......
......@@ -131,6 +131,7 @@
<IntelligentEntityText
:text="term?.fynr || ''"
:entities="termsHighlight ? getTermEntities(term, 'cn') : []"
:highlight="searchKeyword"
@on-entity-click="e => gotoSearchResults(e.text_span, '')"
/>
</div>
......@@ -141,6 +142,7 @@
<IntelligentEntityText
:text="term?.ywnr || ''"
:entities="termsHighlight ? getTermEntities(term, 'en') : []"
:highlight="searchKeyword"
@on-entity-click="e => gotoSearchResults(e.text_span, '')"
/>
</div>
......@@ -343,6 +345,27 @@ const chart1ColorList = ref([...MUTICHARTCOLORS]);
const chart2ColorList = ref([...MUTICHARTCOLORS]);
const chart2Data = ref([]);
const DOMAIN_MAX_DISPLAY_COUNT = 7;
const DOMAIN_OTHER_NAME = "其他";
const formatDomainChartData = (list = []) => {
if (!Array.isArray(list) || list.length <= DOMAIN_MAX_DISPLAY_COUNT) {
return list;
}
const topDomainList = list.slice(0, DOMAIN_MAX_DISPLAY_COUNT);
const otherCount = list.slice(DOMAIN_MAX_DISPLAY_COUNT).reduce((sum, item) => {
return sum + Number(item?.value || 0);
}, 0);
if (!otherCount) {
return topDomainList;
}
return [
...topDomainList,
{
name: DOMAIN_OTHER_NAME,
value: otherCount
}
];
};
const aiPaneVisible = ref({
domain: false,
......@@ -737,12 +760,13 @@ const handleGetBillHyly = async () => {
.map(name => {
return { label: name, value: name };
});
chart2Data.value = res.data.map(item => {
const domainChartData = res.data.map(item => {
return {
name: item.hylymc,
value: item.countTk
};
});
chart2Data.value = formatDomainChartData(domainChartData);
aiPaneFetched.value = { ...aiPaneFetched.value, domain: false };
let chart2 = getPieChart(chart2Data.value, chart2ColorList.value);
......
......@@ -212,7 +212,7 @@ const handleToRiskDetail = (item) => {
// 查看更多风险信号
const handleToMoreRiskSignal = () => {
const route = router.resolve("/vieRiskSignal");
const route = router.resolve("/viewRiskSignal");
window.open(route.href, "_blank");
};
......
......@@ -29,36 +29,22 @@
<div class="item-footer">分析报告</div>
</div>
</div> -->
<div class="date-box" v-if="keyOrganizationList.length">
<div class="date-icon">
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="onKeyOrganization" activeTime="近一年" />
</div>
<div class="home-main-header-item-box" v-if="keyOrganizationList.length">
<div class="organization-item" v-for="(item, index) in keyOrganizationList" :key="index">
<div class="item-left">
<img :src="item.imgUrl || DefaultIcon2" alt="" />
</div>
<div class="item-right one-line-ellipsis" @click="handleToInstitution(item)">{{ item.orgName }}</div>
<el-popover content="跳转至数据资源库" placement="top">
<template #reference>
<div class="item-total" @click="handleToDataLibrary(item)">{{ item.totalOrderNum }}</div>
</template>
</el-popover>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
<div class="item-dot" v-if="item.recentOrderNum">+{{ item.recentOrderNum }}</div>
</div>
<div class="organization-item" @click="onNavigateTo()">
<div class="item-more">查看全部机构 ({{ govInsList.length }}家)</div>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
</div>
</div>
<SummaryCardsPanel
descriptionText="近期美国各联邦政府机构发布涉华政令数量汇总"
:cards="keyOrganizationCards"
:totalCount="govInsList.length"
:tipIcon="tipsTcon"
:defaultAvatar="DefaultIcon2"
:loading="keyOrganizationLoading"
activeTime="近一年"
emptyText="暂无数据"
:moreCardMinCount="7"
moreText="查看全部机构"
@time-click="onKeyOrganization"
@name-click="handleToInstitution"
@count-click="handleToDataLibrary"
@more-click="onNavigateTo"
/>
</div>
<DivideHeader id="position1" class="divide" :titleText="'最新动态'"></DivideHeader>
<div class="home-main-center">
......@@ -421,11 +407,11 @@
</template>
<script setup>
import { onMounted, ref, watch, nextTick, reactive } from "vue";
import { onMounted, ref, watch, nextTick, reactive, computed } from "vue";
import router from "@/router";
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SimplePagination from "@/components/SimplePagination.vue";
import TimeTabPane from '@/components/base/TimeTabPane/index.vue';
import SummaryCardsPanel from "@/components/base/SummaryCardsPanel/index.vue";
import AiButton from '@/components/base/Ai/AiButton/index.vue';
import AiPane from '@/components/base/Ai/AiPane/index.vue';
import {
......@@ -1155,11 +1141,13 @@ const handleSwithCurDecree = name => {
// 关键机构
const keyOrganizationList = ref([]);
const keyOrganizationLoading = ref(false);
const onKeyOrganization = async (event) => {
let day = 365
if (event?.time === '近一周') day = 7
if (event?.time === '近一月') day = 30
if (event?.time === '近一年') day = 365
keyOrganizationLoading.value = true;
try {
const res = await getKeyOrganization({ day });
console.log("关键机构", res);
......@@ -1167,8 +1155,20 @@ const onKeyOrganization = async (event) => {
keyOrganizationList.value = res.data;
}
} catch (error) { }
keyOrganizationLoading.value = false;
}
const keyOrganizationCards = computed(() => {
return keyOrganizationList.value.map(item => ({
...item,
id: item.orgId,
name: item.orgName,
avatar: item.imgUrl,
count: item.totalOrderNum,
delta: item.recentOrderNum
}));
});
// 下钻至数据资源库
const handleToDataLibrary = (item) => {
const selectParam = {
......
......@@ -1141,7 +1141,7 @@ const radarOption = ref({
// 获取雷达图数据
const fetchRadarData = async checked => {
try {
const data = await getSanDomainCount(checked, "export");
const data = await getSanDomainCount(checked, allSanTypeIds.value.join(","));
if (data && Array.isArray(data) && data.length > 0) {
// 收集所有可能的领域名称
const allDomains = new Set();
......@@ -1785,15 +1785,6 @@ const handleMediaClick = item => {
</script>
<style lang="scss" scoped>
// * {
// margin: 0;
// padding: 0;
// }
:deep(.el-input__wrapper) {
// box-shadow: none;
}
.list-page {
padding-top: 0;
}
......@@ -3247,10 +3238,10 @@ const handleMediaClick = item => {
}
.text {
font-size: 20px;
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
font-family: "Source Han Sans CN";
line-height: 24px;
color: rgb(5, 95, 194);
}
}
......
......@@ -2,14 +2,14 @@
<div class="list-page">
<div class="search-box">
<div style="display: flex; justify-content: center">
<el-select v-model="currentCCLVersion" style="width: 388px; height: 32px; margin-right: 14px">
<el-select v-model="currentCCLVersion" style="width: 360px; height: 32px; margin-right: 14px">
<el-option v-for="item in cclVersionList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
<el-input v-model="searchKeyword" class="search-input" placeholder="搜索物项或ECCN编码" :suffix-icon="Search" />
</div>
<div class="filters">
<el-checkbox v-model="viewNew" label="查看最近更新内容" />
<el-select placeholder="全部类别" v-model="currentCCLType" style="width: 388px; height: 32px; margin-right: 14px">
<el-select placeholder="全部类别" v-model="currentCCLType" style="width: 360px; height: 32px; margin-right: 14px">
<el-option v-for="item in CCLTypeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
......@@ -381,14 +381,15 @@ onMounted(async () => {
align-items: center;
.search-input {
width: 388px;
width: 360px;
height: 32px;
:deep(.el-input__wrapper) {
padding: 0 11px;
border: 1.5px solid #dcdfe6;
// border: 1.5px solid #dcdfe6;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
}
:deep(.el-input__inner) {
......@@ -439,25 +440,26 @@ onMounted(async () => {
.left {
padding-bottom: 20px;
width: 388px;
width: 360px;
height: auto;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background-color: #fff;
.checkbox-group {
display: flex;
flex-wrap: wrap;
padding: 0 0 0 24px;
display: grid;
grid-template-columns: repeat(2, 160px);
gap: 8px 4px;
padding-left: 24px;
.el-checkbox {
width: 50%;
margin-right: 0;
margin-bottom: 4px;
// margin-bottom: 4px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
height: 24px;
color: rgb(95, 101, 108);
}
......@@ -504,7 +506,7 @@ onMounted(async () => {
}
.right {
width: 1196px;
width: 1223px;
height: auto;
.title {
width: 100%;
......
......@@ -514,11 +514,6 @@ watch(customDateRange, () => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.list-page {
width: 1601px;
padding-bottom: 50px;
......@@ -532,7 +527,7 @@ watch(customDateRange, () => {
align-items: center;
.search-input {
width: 388px;
width: 360px;
height: 32px;
:deep(.el-input__wrapper) {
......@@ -583,7 +578,7 @@ watch(customDateRange, () => {
.left {
padding-bottom: 20px;
width: 388px;
width: 360px;
height: auto;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
......@@ -654,7 +649,7 @@ watch(customDateRange, () => {
}
.right {
width: 1196px;
width: 1223px;
height: auto;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
......
<template>
<div class="main main-association">
<div class="left">
<!-- ... 左侧代码保持不变 ... -->
<AnalysisBox title="制裁历程">
<div class="left-main">
<div class="date-picker-box">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="--"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
:clearable="false"
@change="handleDateChange"
/>
</div>
<div class="list-header">
<el-checkbox v-model="isAllSelected" :indeterminate="indeterminate" @change="handleCheckAllChange">
全选
</el-checkbox>
<div class="count">{{ sanctionList.length }}次制裁</div>
</div>
<div class="list-content" v-loading="loading">
<div class="list-item" v-for="item in sanctionList" :key="item.id" @click="handleSanctionSelect(item.id)">
<el-checkbox v-model="item.checked" @change="val => handleCheckOneChange(val, item)" @click.stop>
<div class="item-label">
<div class="item-left">{{ item.date }}-{{ "SDN清单更新" }}</div>
<div class="item-right">{{ item.count }}{{ item.unit }}</div>
</div>
</el-checkbox>
</div>
</div>
<el-button type="primary" @click="handleAssociationClick" style="height: 36px">
多投融资限制举措关联分析
<el-icon><Right /></el-icon>
</el-button>
</div>
</AnalysisBox>
</div>
<div class="right">
<AnalysisBox title="投融资限制举措关系图">
<div class="right-main">
<div class="relation-empty" v-if="selectedSanctionIds.length == 0">
<el-empty :image="emptyImg" :image-size="200">
<template #description>
<div class="empty">请在左侧勾选多次投融资限制制裁后点击“开始分析”查看结果</div>
</template>
</el-empty>
</div>
<div class="relation-content" v-else>
<!-- 修改点:绑定转换后的 graphNodes 和 graphLinks -->
<GraphChart :nodes="graphNodes" :links="graphLinks" />
</div>
</div>
</AnalysisBox>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, onUnmounted, computed } from "vue";
import defaultTitle from "../../../assets/default-icon2.png";
import GraphChart from "@/components/base/GraphChart/index.vue";
import emptyImg from "../assets/empty.png";
import markIcon from "../assets/icon-mark.png";
import { getSanRecord, getRecordRelation } from "@/api/finance";
import { useRoute } from "vue-router";
const route = useRoute();
const colors = {
// 相似
similarity: {
fontColor: "rgba(5, 95, 194)",
color: "rgb(231, 243, 255)"
},
// 继承
inheritance: {
fontColor: "rgba(19, 168, 168)",
color: "rgba(230, 255, 251, 1)"
},
// 冲突
conflict: {
fontColor: "rgb(206, 79, 81)",
color: "rgba(5, 95, 194)"
}
};
// ... 其他原有变量保持不变 ...
const selectedSanctionIds = ref([]);
const isAllSelected = computed({
get() {
return sanctionList.value.length > 0 && sanctionList.value.every(item => item.checked);
},
set(val) {}
});
const handleCheckAllChange = val => {
sanctionList.value.forEach(item => {
item.checked = val;
});
updateSelectedIds();
};
const handleCheckOneChange = (val, item) => {
item.checked = val;
updateSelectedIds();
};
const updateSelectedIds = () => {
selectedSanctionIds.value = sanctionList.value.filter(item => item.checked).map(item => item.id);
if (selectedSanctionIds.value.length === 1) {
currentSanctionId.value = selectedSanctionIds.value[0];
}
};
const indeterminate = computed(() => {
const checkedCount = sanctionList.value.filter(item => item.checked).length;
return checkedCount > 0 && checkedCount < sanctionList.value.length;
});
const recordRelation = ref({ noRelationVertices: [], relationVoList: [] });
// 【新增】计算属性:处理图表数据
const graphNodes = computed(() => {
const nodesMap = new Map();
// 1. 处理无关联节点 (noRelationVertices)
if (recordRelation.value.noRelationVertices) {
recordRelation.value.noRelationVertices.forEach(node => {
if (!nodesMap.has(node.id)) {
nodesMap.set(node.id, {
id: node.id,
name: node.name,
// 可以根据需要添加 category 或其他样式属性
itemStyle: {
color: "#91cc75" // 例如:无关联节点用绿色区分
}
});
}
});
}
// 2. 处理关联节点 (relationVoList 中的 fromVertex 和 toVertex)
if (recordRelation.value.relationVoList) {
recordRelation.value.relationVoList.forEach(rel => {
const from = rel.fromVertex;
const to = rel.toVertex;
if (from && !nodesMap.has(from.id)) {
nodesMap.set(from.id, {
id: from.id,
name: from.name,
itemStyle: {
color: "#5470c6" // 例如:有关联节点用蓝色
}
});
}
if (to && !nodesMap.has(to.id)) {
nodesMap.set(to.id, {
id: to.id,
name: to.name,
itemStyle: {
color: "#5470c6"
}
});
}
});
}
return Array.from(nodesMap.values());
});
const graphLinks = computed(() => {
if (!recordRelation.value.relationVoList) return [];
return recordRelation.value.relationVoList.map(rel => {
return {
source: rel.fromVertex.id,
target: rel.toVertex.id,
// 将 edgeInfo 挂载到 data 上,以便在 formatter 中访问
label: {
formatter: rel.edgeInfo ? rel.edgeInfo.value : ""
}
};
});
});
const handleAssociationClick = () => {
console.log("handleAssociationClick", selectedSanctionIds.value);
fetchRecordRelation();
};
const fetchRecordRelation = async () => {
if (selectedSanctionIds.value.length === 0) {
return;
}
try {
const res = await getRecordRelation(selectedSanctionIds.value);
console.log("getRecordRelation", res);
if (!!res) {
recordRelation.value = res;
} else {
recordRelation.value = { noRelationVertices: [], relationVoList: [] };
}
} catch (error) {
console.error("获取制裁记录关联信息失败:", error);
recordRelation.value = { noRelationVertices: [], relationVoList: [] };
}
};
const loading = ref(false);
const currentPage = ref(1);
const fetchSanRecord = async () => {
loading.value = true;
const params = {
startDate: dateRange.value && dateRange.value[0] ? dateRange.value[0] : "",
endDate: dateRange.value && dateRange.value[1] ? dateRange.value[1] : "",
sanTypeId: sanTypeId.value || 1
};
try {
const res = await getSanRecord(params);
if (res && res.length > 0) {
sanctionList.value = res
.map(item => ({
id: item.sanRecordId,
date: item.sanRecordDate,
title: item.sanRecordName,
count: item.cnEntitiesNum,
unit: "家中国实体"
}))
.reverse();
if (sanctionList.value.length > 0) {
currentSanctionId.value = sanctionList.value[0].id;
}
} else {
sanctionList.value = [];
}
} catch (error) {
console.error("获取选择制裁数据失败:", error);
sanctionList.value = [];
} finally {
loading.value = false;
}
};
const handleDateChange = () => {
fetchSanRecord();
};
const handleSanctionSelect = id => {
currentSanctionId.value = id;
};
const dateRange = ref(["2025-01-01", "2025-12-31"]);
const sanctionList = ref([]);
const currentSanctionId = ref(5);
const sanTypeId = ref("");
onMounted(() => {
sanTypeId.value = route.query.sanTypeId || "";
fetchSanRecord();
});
</script>
<style lang="scss" scoped>
/* 样式保持不变 */
.main {
width: 100%;
padding-top: 12px;
padding-bottom: 50px;
display: flex;
justify-content: space-between;
.left {
width: 480px;
height: 828px;
.left-main {
margin-top: 11px;
padding: 0 22px 0 23px;
display: flex;
flex-direction: column;
height: calc(100% - 25px);
.date-picker-box {
margin-bottom: 16px;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
font-size: 14px;
color: #666;
.count {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
color: rgb(95, 101, 108);
}
}
.list-content {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.list-item {
border: 1px solid rgb(234, 236, 238);
border-radius: 4px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 16px;
cursor: pointer;
transition: all 0.3s;
position: relative;
background: #fff;
.item-label {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.item-left {
width: 260px;
font-weight: 700;
color: rgb(59, 65, 75);
font-size: 16px;
font-family: "Microsoft YaHei";
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 10px;
}
.item-right {
color: rgb(132, 136, 142);
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
flex-shrink: 0;
}
&:hover {
border-color: #055fc2;
}
&.active {
border-color: rgb(5, 95, 194);
background-color: rgba(246, 250, 255, 1);
.item-left,
.item-right {
color: rgb(5, 95, 194);
}
&::after {
content: "";
position: absolute;
right: 0;
top: 10px;
bottom: 10px;
width: 4px;
background-color: rgb(5, 95, 194);
}
}
}
}
}
}
.right {
width: 1105px;
height: 828px;
.right-main {
margin-top: 11px;
height: calc(100% - 10px);
padding: 0 16px 16px 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.relation-empty {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
width: 300px;
.empty {
font-size: 16px;
font-weight: 400;
font-family: Source Han Sans CN;
color: rgb(132, 136, 142);
line-height: 30px;
}
}
.relation-content {
width: 100%;
height: 100%;
}
}
}
}
.main-association {
justify-content: flex-start !important;
gap: 16px;
}
</style>
<template>
<div class="main main-association">
<div class="left">
<!-- ... 左侧代码保持不变 ... -->
<AnalysisBox title="制裁历程">
<div class="left-main">
<div class="date-picker-box">
......@@ -21,250 +22,154 @@
全选
</el-checkbox>
<div class="count">{{ sanctionList.length }}次制裁</div>
<!-- 暂时隐藏,说这里可能是轮播图的效果 -->
<!-- <div class="pagination">
<div class="page-btn prev" @click="handlePrevClick">
<el-icon><ArrowLeft /></el-icon>
</div>
<div class="page-btn next" @click="handleNextClick">
<el-icon><ArrowRight /></el-icon>
</div>
</div> -->
</div>
<div class="list-content" v-loading="loading">
<div
class="list-item"
v-for="item in sanctionList"
:key="item.id"
:class="{ active: currentSanctionId === item.id }"
@click="handleSanctionSelect(item.id)"
>
<div class="list-item" v-for="item in sanctionList" :key="item.id" @click="handleSanctionSelect(item.id)">
<el-checkbox v-model="item.checked" @change="val => handleCheckOneChange(val, item)" @click.stop>
<span class="item-label">
<span class="item-left">{{ item.date }}-{{ item.title }}</span>
<span class="item-right">{{ item.count }}{{ item.unit }}</span>
</span>
<div class="item-label">
<div class="item-left">
{{ dayjs(item.date).format("YYYY年MM月DD日") }}-{{ "SDN清单更新" }}
</div>
<div class="item-right">{{ item.count }}{{ item.unit }}</div>
</div>
</el-checkbox>
<!-- <div class="item-left">{{ item.date }}-{{ item.title }}</div>
<div class="item-right">{{ item.count }}{{ item.unit }}</div> -->
</div>
</div>
<el-button type="primary" @click="handleAssociationClick" style="height: 36px">
多投融资限制举措关联分析
<el-icon><Right /></el-icon>
</el-button>
</div>
</AnalysisBox>
</div>
<div class="right">
<AnalysisBox title="制裁产业链时序图">
<template #header-btn>
<el-select
v-model="selectedIndustryId"
placeholder="请选择"
class="industry-select"
@change="
() => {
getFishboneData();
getCnEntityOnChainData();
}
"
>
<el-option v-for="item in industryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<AnalysisBox title="投融资限制举措关系图">
<div class="right-main">
<div class="right-main-content">
<div class="hintWrap">
<div class="icon1"></div>
<div class="title">
2025年实体清单制裁范围扩大至芯片制造环节,为中国的芯片制造能力划定“技术天花板”,阻止其向更先进水平发展。制裁范围向上游设备和材料、下游先进封装以及关键工具(如EDA软件)延伸,意图瓦解中国构建自主可控产业链的努力。
</div>
<div class="icon2Wrap">
<div class="icon2"></div>
</div>
</div>
<div class="right-main-content-main">
<div class="fishbone-wrapper">
<div class="fishbone-scroll-container" ref="scrollContainerRef">
<div class="fishbone" ref="fishboneRef" v-if="fishboneDataList.length > 0">
<div class="main-line" :style="{ width: fishboneDataList.length * 200 + 300 + 'px' }">
<!-- 主轴上的标签 -->
<div
class="main-line-text"
v-for="(item, index) in mainLineLabels"
:key="'label-' + index"
:class="{
'blue-theme': index < 2,
'green-theme': index >= 2 && index < 4,
'purple-theme': index >= 4
}"
:style="{ left: index * 200 + 220 + 'px' }"
>
{{ item }}
</div>
</div>
<!-- 奇数索引的数据组放在上方 -->
<div
v-for="(causeGroup, groupIndex) in getOddGroups(fishboneDataList)"
:key="'top-' + groupIndex"
:class="getTopBoneClass(groupIndex)"
:style="{ left: groupIndex * 400 + 420 + 'px' }"
>
<div class="left-bone">
<div
class="left-bone-item"
v-for="(item, index) in getLeftItems(causeGroup.causes)"
:key="'left-' + index"
>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
<div class="line"></div>
</div>
</div>
<div class="right-bone">
<div
class="right-bone-item"
v-for="(item, index) in getRightItems(causeGroup.causes)"
:key="'right-' + index"
>
<div class="line"></div>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
</div>
</div>
</div>
<!-- 偶数索引的数据组放在下方 -->
<div
v-for="(causeGroup, groupIndex) in getEvenGroups(fishboneDataList)"
:key="'bottom-' + groupIndex"
:class="getBottomBoneClass(groupIndex)"
:style="{ left: groupIndex * 400 + 220 + 'px' }"
>
<div class="left-bone">
<div
class="left-bone-item"
v-for="(item, index) in getLeftItems(causeGroup.causes)"
:key="'left-bottom-' + index"
>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
<div class="line"></div>
</div>
</div>
<div class="right-bone">
<div
class="right-bone-item"
v-for="(item, index) in getRightItems(causeGroup.causes)"
:key="'right-bottom-' + index"
>
<div class="line"></div>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
</div>
</div>
</div>
</div>
<div
v-else
style="
display: flex;
justify-content: center;
align-items: center;
height: 200px;
width: 100%;
"
>
<el-empty description="暂无相关数据" />
</div>
</div>
</div>
<div class="relation-empty" v-if="selectedSanctionIds.length == 0">
<el-empty :image="emptyImg" :image-size="200">
<template #description>
<div class="empty">请在左侧勾选多次投融资限制制裁后点击“开始分析”查看结果</div>
</template>
</el-empty>
</div>
<div class="right-main-content-footer">
<div class="footer-item1">
<div class="footer-item1-bottom">
<div class="icon">
<img src="../../../../assets/images/warning.png" alt="" />
<div class="relation-content" v-else>
<!-- 绑定转换后的 graphNodes 和 graphLinks -->
<GraphChart :nodes="graphNodes" :links="graphLinks" @handleClickNode="handleClickNode" />
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.upstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.upstreamInternalRate
)}%),受制裁${cnEntityOnChainData.upstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.upstreamEntityRate
)}%)`
}}
</div>
</AnalysisBox>
</div>
<div class="footer-item1-top">{{ "上游" }}</div>
<el-dialog v-model="visible" :title="curNode.name" width="960">
<div class="dialog-content">
<div class="content-item">
<div class="item-label">制裁标题:</div>
<div class="item-desc item-label">
{{ vertexInfo.name }}
</div>
<div class="footer-item2">
<div class="footer-item2-bottom">
<div class="icon">
<img src="../../../../assets/images/warning.png" alt="" />
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.midstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.midstreamInternalRate
)}%),受制裁${cnEntityOnChainData.midstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.midstreamEntityRate
)}%)`
}}
<div class="content-item">
<div class="item-label">制裁领域:</div>
<div class="item-desc">
<AreaTag v-for="item in vertexInfo.domainList" :key="item" :tagName="item" />
</div>
</div>
<div class="footer-item2-top">{{ "中游" }}</div>
<div class="content-item">
<div class="item-label">依托文件:</div>
<div class="item-desc">
<div class="item-file" v-for="item in vertexInfo.relyFileList" :key="item.id">{{ item.name }}</div>
</div>
<div class="footer-item3">
<div class="footer-item3-bottom">
<div class="icon">
<img src="../../../../assets/images/warning.png" alt="" />
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.downstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.downstreamInternalRate
)}%),受制裁${cnEntityOnChainData.downstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.downstreamEntityRate
)}%)`
}}
<div class="content-item">
<div class="item-label">依托制裁:</div>
<div class="item-desc">
<div class="item-file" v-for="item in vertexInfo.relySanList" :key="item.id">{{ item.title }}</div>
</div>
</div>
<div class="footer-item3-top">{{ "下游" }}</div>
<div class="content-item">
<div class="item-label">制裁原因:</div>
<div class="item-desc">
<div class="item-file" v-for="item in vertexInfo.sanReasonList" :key="item">{{ item }}</div>
</div>
</div>
<div class="content-item">
<div class="item-label">制裁对象:</div>
<div class="item-desc">
<span class="item-table-desc" v-if="vertexInfo.addObjectList?.length || vertexInfo.delObjectList?.length">
{{ formatChangeSummary(vertexInfo.addObjectList, vertexInfo.delObjectList) }}
</span>
<el-table :data="vertexInfo.sanList" stripe>
<el-table-column property="entityNameZh" label="制裁对象" width="350" />
<el-table-column property="domainNames" label="所属领域" width="400">
<template #default="scope">
<AreaTag v-for="item in scope.row.domainNames" :key="item" :tagName="item" />
</template>
</el-table-column>
</el-table>
</div>
</div>
</AnalysisBox>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, onUnmounted, computed } from "vue";
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import defaultTitle from "../../../assets/default-icon2.png";
import {
getDeepMiningSelect,
getDeepMiningIndustry,
getDeepMiningIndustryFishbone,
getDeepMiningIndustryEntity
} from "@/api/exportControlV2.0";
import { ref, onMounted, computed } from "vue";
import GraphChart from "@/components/base/GraphChart/index.vue";
import emptyImg from "../assets/empty.png";
import markIcon from "../assets/icon-mark.png"; // 引入图标
import { getSanRecord, getRecordRelation, getVertexInfo } from "@/api/finance";
import { useRoute } from "vue-router";
import dayjs from "dayjs";
const route = useRoute();
// 存储选中的ID列表 (如果需要获取选中项,可以使用这个,或者直接遍历 sanctionList 找 checked=true 的)
const selectedSanctionIds = ref([]);
// 定义颜色映射
const colors = {
// 相似
similarity: {
fontColor: "rgba(5, 95, 194)",
color: "rgb(231, 243, 255)", // 连线颜色 & 标签背景
lineColor: "rgba(5, 95, 194)" // 专门用于连线的颜色,如果需要和背景色区分
},
// 继承
inheritance: {
fontColor: "rgba(19, 168, 168)",
color: "rgba(230, 255, 251, 1)",
lineColor: "rgba(19, 168, 168)"
},
// 冲突
conflict: {
fontColor: "rgb(206, 79, 81)",
color: "rgba(255, 241, 240, 1)", // 修正:冲突背景通常偏红/浅红,这里沿用你提供的蓝色背景可能不太符合直觉,但我保留你的配置或微调
lineColor: "rgb(206, 79, 81)"
}
};
// 计算属性:是否全选
// 辅助函数:获取关系类型对应的颜色配置
const getRelationStyle = relationType => {
switch (relationType) {
case "相似":
return colors.similarity;
case "继承":
return colors.inheritance;
case "冲突":
return colors.conflict;
default:
return {
fontColor: "#666",
color: "#f0f2f5",
lineColor: "#AED6FF"
};
}
};
const selectedSanctionIds = ref([]);
const isAllSelected = computed({
get() {
return sanctionList.value.length > 0 && sanctionList.value.every(item => item.checked);
},
set(val) {
// set 方法由 handleCheckAllChange 处理,这里主要是为了配合 v-model 的读取
}
});
// 全选/取消全选
const handleCheckAllChange = val => {
sanctionList.value.forEach(item => {
item.checked = val;
......@@ -272,167 +177,235 @@ const handleCheckAllChange = val => {
updateSelectedIds();
};
// 单个选中变化
const handleCheckOneChange = (val, item) => {
item.checked = val;
updateSelectedIds();
};
// 点击行触发复选框切换
const handleItemClick = item => {
item.checked = !item.checked;
updateSelectedIds();
};
// 更新选中ID数组(供外部业务使用,如获取选中数据进行查询等)
const updateSelectedIds = () => {
selectedSanctionIds.value = sanctionList.value.filter(item => item.checked).map(item => item.id);
// 如果业务需要:当选中项变化时,可能需要触发右侧图表更新?
// 原逻辑是点击选中一个就更新右侧。现在如果是多选,右侧图表如何展示?
// 假设:如果只选中了一个,更新右侧;如果选中多个或没选中,保持现状或显示提示?
// 这里暂时保留原逻辑的触发点,但建议根据实际需求调整。
// 例如:只有当选中项为1个时,才调用 getFishboneData
if (selectedSanctionIds.value.length === 1) {
currentSanctionId.value = selectedSanctionIds.value[0];
getFishboneData();
getCnEntityOnChainData();
} else if (selectedSanctionIds.value.length === 0) {
// 可选:清空右侧或显示默认状态
}
};
// 计算属性:是否半选(不确定状态)
const indeterminate = computed(() => {
const checkedCount = sanctionList.value.filter(item => item.checked).length;
return checkedCount > 0 && checkedCount < sanctionList.value.length;
});
// 实体清单-深度挖掘-产业链中国企业实体信息查询
const getCnEntityOnChainData = async () => {
const currentSanction = sanctionList.value.find(item => item.id === currentSanctionId.value);
const date = currentSanction ? currentSanction.date : "";
// 确保 date 格式正确
const formattedDate = date && date.includes("年") ? date.replace("年", "-").replace("月", "-").replace("日", "") : date;
const params = {
date: formattedDate
};
if (selectedIndustryId.value) {
params.chainId = selectedIndustryId.value;
const recordRelation = ref({ noRelationVertices: [], relationVoList: [] });
// 【修改】处理图表节点数据
const graphNodes = computed(() => {
const nodesMap = new Map();
// 1. 处理无关联节点
if (recordRelation.value.noRelationVertices) {
recordRelation.value.noRelationVertices.forEach(node => {
if (!nodesMap.has(node.id)) {
nodesMap.set(node.id, {
id: node.id,
// name: node.name,
name: dayjs(node.date).format("YYYY年MM月DD日") + " " + "SDN清单更新",
symbol: `image://${markIcon}`, // 设置自定义图标
symbolSize: [50, 50], // 根据图标实际大小调整
itemStyle: {
color: "#91cc75"
}
try {
const res = await getDeepMiningIndustryEntity(params);
if (res.code === 200 && res.data) {
cnEntityOnChainData.value = res.data;
} else {
cnEntityOnChainData.value = {};
});
}
} catch (error) {
console.error("获取产业链中国企业实体信息失败:", error);
cnEntityOnChainData.value = {};
});
}
};
// 实体清单-深度挖掘-产业链鱼骨图信息
const fishboneDataList = ref([]);
const getFishboneData = async () => {
const currentSanction = sanctionList.value.find(item => item.id === currentSanctionId.value);
const date = currentSanction ? currentSanction.date : "";
// 确保 date 格式正确
const formattedDate = date && date.includes("年") ? date.replace("年", "-").replace("月", "-").replace("日", "") : date;
// 2. 处理关联节点
if (recordRelation.value.relationVoList) {
recordRelation.value.relationVoList.forEach(rel => {
const from = rel.fromVertex;
const to = rel.toVertex;
const params = {
date: formattedDate
};
if (selectedIndustryId.value) {
params.chainId = selectedIndustryId.value;
if (from && !nodesMap.has(from.id)) {
nodesMap.set(from.id, {
id: from.id,
// name: from.name,
name: dayjs(from.date).format("YYYY年MM月DD日") + " " + "SDN清单更新",
symbol: `image://${markIcon}`, // 设置自定义图标
symbolSize: [50, 50],
itemStyle: {
color: "#5470c6"
}
try {
const res = await getDeepMiningIndustryFishbone(params);
if (res.code === 200 && res.data && res.data.causes && res.data.causes.length > 0) {
const rootCauses = res.data.causes;
if (rootCauses.length > 0 && rootCauses[0].causes) {
fishboneDataList.value = rootCauses.map(group => {
});
}
if (to && !nodesMap.has(to.id)) {
nodesMap.set(to.id, {
id: to.id,
// name: to.name,
name: dayjs(to.date).format("YYYY年MM月DD日") + " " + "SDN清单更新",
symbol: `image://${markIcon}`, // 设置自定义图标
symbolSize: [50, 50],
itemStyle: {
color: "#5470c6"
}
});
}
});
}
return Array.from(nodesMap.values());
});
// 【修改】处理图表连线数据
const graphLinks = computed(() => {
if (!recordRelation.value.relationVoList) return [];
return recordRelation.value.relationVoList.map(rel => {
const relationType = rel.edgeInfo ? rel.edgeInfo.value : "";
const style = getRelationStyle(relationType);
return {
causes: group.causes || []
source: rel.fromVertex.id,
target: rel.toVertex.id,
// 将样式信息挂载到 label 或 data 上,供 formatter 和 lineStyle 使用
label: {
formatter: relationType,
show: true,
color: style.fontColor, // 字体颜色
backgroundColor: style.color, // 标签背景色
borderColor: style.color, // 标签边框色
padding: [4, 8],
borderRadius: 4,
fontSize: 12
},
lineStyle: {
color: style.lineColor, // 连线颜色
width: 2
// curveness: 0.1 // 稍微有点弧度可能更好看
},
// 额外存储原始数据,以备后用
relationType: relationType
};
});
});
mainLineLabels.value = rootCauses.map(group => group.text || "");
} else {
fishboneDataList.value = [];
mainLineLabels.value = [];
const handleAssociationClick = () => {
console.log("handleAssociationClick", selectedSanctionIds.value);
fetchRecordRelation();
};
const fetchRecordRelation = async () => {
if (selectedSanctionIds.value.length === 0) {
return;
}
try {
const res = await getRecordRelation(selectedSanctionIds.value);
console.log("getRecordRelation", res);
if (!!res) {
recordRelation.value = res;
} else {
fishboneDataList.value = [];
mainLineLabels.value = [];
recordRelation.value = { noRelationVertices: [], relationVoList: [] };
}
} catch (error) {
console.error("获取产业链鱼骨图数据失败:", error);
fishboneDataList.value = [];
console.error("获取制裁记录关联信息失败:", error);
recordRelation.value = { noRelationVertices: [], relationVoList: [] };
}
};
// 实体清单-深度挖掘-产业链列表信息
const industryList = ref([]);
const selectedIndustryId = ref(null);
const getIndustryList = async () => {
try {
const res = await getDeepMiningIndustry();
if (res.code === 200 && res.data && res.data.length > 0) {
industryList.value = res.data;
selectedIndustryId.value = res.data[0].id;
getFishboneData();
getCnEntityOnChainData();
const vertexInfo = ref({});
const curNode = ref({});
const visible = ref(false);
const handleClickNode = node => {
console.log("节点点击", node);
curNode.value = node.data;
getVertexInfo(node.data.id).then(res => {
console.log("getVertexInfo", res);
if (!!res) {
vertexInfo.value = res;
visible.value = true;
} else {
industryList.value = [];
selectedIndustryId.value = null;
vertexInfo.value = {};
}
} catch (error) {
console.error("获取产业链列表数据失败:", error);
industryList.value = [];
selectedIndustryId.value = null;
});
};
// 【新增/修改】格式化变动 summary 的函数
const formatChangeSummary = (addList, delList) => {
const parts = [];
// 处理新增列表
if (addList && addList.length > 0) {
// 将每个对象转换为 "value个实体" 或 "value名个人" 的形式
const addItems = addList.map(item => {
let unit = "个";
let noun = "实体";
if (item.key === "人物") {
unit = "名";
noun = "个人";
} else if (item.key === "机构") {
// 默认机构对应实体,也可以根据需求调整
unit = "个";
noun = "实体";
}
return `${item.value}${unit}${noun}`;
});
// 拼接:新增 + item1 + , + item2 ...
parts.push(`新增${addItems.join(",")}`);
}
// 处理移除列表
if (delList && delList.length > 0) {
// 将每个对象转换为 "value个实体" 或 "value名个人" 的形式
const delItems = delList.map(item => {
let unit = "个";
let noun = "实体";
if (item.key === "人物") {
unit = "名";
noun = "个人";
} else if (item.key === "机构") {
unit = "个";
noun = "实体";
}
return `${item.value}${unit}${noun}`;
});
// 拼接:移除 + item1 + , + item2 ...
// 注意:题目要求“删除”,但之前代码用的是“移除”,这里统一使用“移除”或“删除”。
// 根据题目描述“展示样本为:新增12个实体,3名个人,移除1个实体”,这里使用“移除”更贴切上下文,
// 如果必须用“删除”,请将下面的 '移除' 改为 '删除'。
parts.push(`移除${delItems.join(",")}`);
}
return parts.length > 0 ? parts.join(",") : "无变动";
};
// 获取选择制裁
const loading = ref(false);
const currentPage = ref(1);
const pageSize = ref(100);
const total = ref(0);
const totalPage = ref(0);
const sanctionList = ref([]);
const currentSanctionId = ref(5);
const dateRange = ref(["2025-01-01", "2025-12-31"]);
const sanTypeId = ref("");
const getDeepMiningSelectData = async () => {
const fetchSanRecord = async () => {
loading.value = true;
const params = {
startDate: dateRange.value && dateRange.value[0] ? dateRange.value[0] : "",
endDate: dateRange.value && dateRange.value[1] ? dateRange.value[1] : "",
// typeName: "实体清单",
isCn: false,
pageNum: currentPage.value,
pageSize: pageSize.value,
sanTypeIds: [Number(sanTypeId.value)] || 1 // 实体清单固定1
sanTypeId: sanTypeId.value || 1
};
try {
const res = await getDeepMiningSelect(params);
if (res.code === 200 && res.data && res.data.content) {
sanctionList.value = res.data.content
const res = await getSanRecord(params);
if (res && res.length > 0) {
sanctionList.value = res
.map(item => ({
id: item.id,
date: item.postDate,
title: item.name,
count: item.cnEntityCount,
unit: "家中国实体", // 接口未返回单位,暂时固定
summary: item.summary, // 保留额外信息备用
techDomainList: item.techDomainList // 保留额外信息备用
id: item.sanRecordId,
date: item.sanRecordDate,
title: item.sanRecordName,
count: item.cnEntitiesNum,
unit: "家中国实体"
}))
.reverse();
// 默认选中第一条
if (sanctionList.value.length > 0) {
currentSanctionId.value = sanctionList.value[0].id;
// getFishboneData(); // 这里不需要调用,因为getIndustryList会调用
}
} else {
sanctionList.value = [];
......@@ -445,102 +418,25 @@ const getDeepMiningSelectData = async () => {
}
};
// 日期选择变化
const handleDateChange = () => {
currentPage.value = 1;
getDeepMiningSelectData();
};
// ✅ 自动轮播定时器
const autoPlayTimer = ref(null);
// ✅ 自动下一个(支持循环)
const handleNextClickAuto = () => {
const currentIndex = sanctionList.value.findIndex(item => item.id === currentSanctionId.value);
let nextItem;
if (currentIndex < sanctionList.value.length - 1) {
nextItem = sanctionList.value[currentIndex + 1];
} else {
nextItem = sanctionList.value[0]; // 循环到第一个
}
if (nextItem) {
handleSanctionSelect(nextItem.id);
}
fetchSanRecord();
};
const handleSanctionSelect = id => {
currentSanctionId.value = id;
getFishboneData();
getCnEntityOnChainData();
};
const activeTab = ref(["制裁时序分析", "限制关联分析"]);
const activeIndex = ref(0);
const dateRange = ref(["2025-01-01", "2025-12-31"]);
const sanctionList = ref([]);
const currentSanctionId = ref(5);
const cnEntityOnChainData = ref({});
const mainLineLabels = ref(["关键原材料", "电池材料", "电子元器件", "动力电池", "电子控制系统", "动力电池"]);
// 获取奇数索引的数据组(放在上方)
const getOddGroups = data => {
return data.filter((_, index) => index % 2 !== 0);
};
// 获取偶数索引的数据组(放在下方)
const getEvenGroups = data => {
return data.filter((_, index) => index % 2 === 0);
};
// 获取上方鱼骨图位置类名
const getTopBoneClass = index => {
const positions = ["top-bone", "top-bone1", "top-bone2"];
return positions[index % 3] || "top-bone";
};
// 获取下方鱼骨图位置类名
const getBottomBoneClass = index => {
const positions = ["bottom-bone", "bottom-bone1", "bottom-bone2"];
return positions[index % 3] || "bottom-bone";
};
// 获取左侧显示的项目(前半部分)
const getLeftItems = items => {
const midpoint = Math.ceil(items.length / 2);
return items.slice(0, midpoint);
};
// 获取右侧显示的项目(后半部分)
const getRightItems = items => {
const midpoint = Math.ceil(items.length / 2);
return items.slice(midpoint);
};
// 格式化比率
const formatRate = rate => {
if (rate === undefined || rate === null) return "0.00";
return (rate * 100).toFixed(2);
};
const sanTypeId = ref("");
onMounted(() => {
// 获取路由参数中的sanTypeId
sanTypeId.value = route.query.sanTypeId || "";
// 获取选择制裁
getDeepMiningSelectData();
// 获取产业链信息
getIndustryList();
fetchSanRecord();
});
</script>
<style lang="scss" scoped>
/* 样式保持不变 */
.main {
width: 100%;
padding-top: 16px;
padding-top: 12px;
padding-bottom: 50px;
display: flex;
justify-content: space-between;
......@@ -554,7 +450,7 @@ onMounted(() => {
padding: 0 22px 0 23px;
display: flex;
flex-direction: column;
height: calc(100% - 56px);
height: calc(100% - 25px);
.date-picker-box {
margin-bottom: 16px;
......@@ -574,34 +470,6 @@ onMounted(() => {
font-family: "Microsoft YaHei";
color: rgb(95, 101, 108);
}
.pagination {
display: flex;
gap: 12px;
.page-btn {
width: 28px;
height: 28px;
background: rgba(231, 243, 255, 1);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: rgb(5, 95, 194);
font-size: 16px;
&.disabled {
cursor: not-allowed;
background: #f5f7fa;
color: #c0c4cc;
}
&:not(.disabled):hover {
background: #e1eeff;
}
}
}
}
.list-content {
......@@ -619,7 +487,6 @@ onMounted(() => {
}
.list-item {
// height: 60px;
border: 1px solid rgb(234, 236, 238);
border-radius: 4px;
margin-bottom: 8px;
......@@ -631,6 +498,12 @@ onMounted(() => {
transition: all 0.3s;
position: relative;
background: #fff;
.item-label {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.item-left {
width: 260px;
......@@ -638,6 +511,10 @@ onMounted(() => {
color: rgb(59, 65, 75);
font-size: 16px;
font-family: "Microsoft YaHei";
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 10px;
}
.item-right {
......@@ -645,6 +522,7 @@ onMounted(() => {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
flex-shrink: 0;
}
&:hover {
......@@ -668,7 +546,6 @@ onMounted(() => {
bottom: 10px;
width: 4px;
background-color: rgb(5, 95, 194);
// border-radius: 4px 0 0 4px;
}
}
}
......@@ -682,508 +559,61 @@ onMounted(() => {
.right-main {
margin-top: 11px;
height: calc(100% - 56px);
height: calc(100% - 10px);
padding: 0 16px 16px 16px;
.right-main-content {
height: 100%;
display: flex;
flex-direction: column;
.hintWrap {
display: flex;
align-items: center;
padding: 7px 12px;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
margin-bottom: 9px;
.icon1 {
width: 19px;
height: 20px;
background-image: url("../assets/ai.png");
background-size: 100% 100%;
flex-shrink: 0;
}
.title {
color: rgb(5, 95, 194);
font-size: 16px;
font-weight: 400;
line-height: 24px;
margin-left: 13px;
flex: 1;
}
.icon2Wrap {
width: 24px;
height: 24px;
background-color: rgba(231, 243, 255, 1);
display: flex;
justify-content: center;
align-items: center;
border-radius: 12px;
margin-left: 20px;
flex-shrink: 0;
.icon2 {
width: 24px;
height: 24px;
background-image: url("../assets/right.png");
background-size: 100% 100%;
}
}
}
.right-main-content-main {
flex: 1;
// border: 1px solid #eaecee;
// border-radius: 4px;
// background: #f7f8f9;
position: relative;
overflow: hidden;
.fishbone-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.fishbone-scroll-container {
.relation-empty {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(144, 202, 249, 0.5) transparent;
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(144, 202, 249, 0.5);
border-radius: 3px;
height: 300px;
width: 300px;
.empty {
font-size: 16px;
font-weight: 400;
font-family: Source Han Sans CN;
color: rgb(132, 136, 142);
line-height: 30px;
}
}
.fishbone {
position: relative;
width: fit-content;
height: 100%;
margin-top: 40px;
min-width: 100%;
padding-left: 275px;
margin-left: 40px;
.main-line {
margin-top: 280px;
width: 1888px;
height: 3px;
background: rgb(230, 231, 232);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 100px;
// 虚线
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
.relation-content {
width: 100%;
height: 100%;
// background: repeating-linear-gradient(to right, rgba(174, 208, 255, 1) 0, rgba(174, 208, 255, 1) 10px, transparent 10px, transparent 20px);
}
// 添加中间的文字块
.main-line-text {
position: absolute;
// top: -14px;
font-size: 16px;
color: #055fc2;
font-weight: bold;
background-color: #f7f8f9;
padding: 0 10px;
z-index: 2;
// 箭头背景
height: 32px;
line-height: 32px;
width: 160px;
text-align: center;
background: rgba(231, 243, 255, 1);
clip-path: polygon(0% 0%, 90% 0%, 100% 50%, 90% 100%, 0% 100%, 10% 50%);
&.blue-theme {
background: rgba(231, 243, 255, 1);
color: rgba(22, 119, 255, 1);
}
&.green-theme {
background: rgba(225, 255, 251, 1);
color: rgba(19, 168, 168, 1);
}
&.purple-theme {
background: rgba(246, 235, 255, 1);
color: rgba(146, 84, 222, 1);
}
}
}
}
.company-icon {
width: 16px;
height: 16px;
margin: 0 4px;
object-fit: contain;
}
.top-bone {
position: absolute;
top: 20px;
right: 200px;
width: 3px;
height: 260px;
background: rgb(230, 231, 232);
transform: skew(30deg);
z-index: 1;
.left-bone {
color: #777;
position: absolute;
top: 0;
left: -150px;
width: 150px;
height: 50px;
// overflow: hidden;
.left-bone-item {
transform: skew(-30deg);
height: 45px;
margin-bottom: 2px;
margin-top: 2px;
display: flex;
justify-content: flex-end;
align-items: center;
.text {
margin-left: 4px;
height: 25px;
line-height: 25px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line {
margin-left: 7px;
width: 40px;
height: 2px;
background: rgb(230, 231, 232);
}
}
}
.right-bone {
color: #777;
position: absolute;
top: 0;
right: -150px;
width: 150px;
height: 210px;
overflow: hidden;
.right-bone-item {
transform: skew(-30deg);
height: 39px;
margin-bottom: 2px;
margin-top: 2px;
display: flex;
justify-content: flex-start;
align-items: center;
.line {
margin-right: 7px;
width: 30px;
height: 2px;
background: rgb(230, 231, 232);
}
.text {
max-width: 100px;
margin-right: 4px;
height: 25px;
line-height: 25px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.top-bone1 {
@extend .top-bone;
right: 500px;
}
.top-bone2 {
@extend .top-bone;
right: 800px;
}
.bottom-bone {
position: absolute;
top: 280px;
right: 360px;
width: 3px;
height: 260px;
background: rgb(230, 231, 232);
transform: skew(-30deg);
z-index: 1;
.left-bone {
color: #777;
position: absolute;
top: 50px;
left: -150px;
width: 150px;
height: 260px;
.left-bone-item {
transform: skew(30deg);
height: 39px;
margin-bottom: 2px;
margin-top: 2px;
}
.main-association {
padding-top: 12px !important;
justify-content: flex-start !important;
gap: 16px;
}
.dialog-content {
padding: 20px;
display: flex;
justify-content: flex-end;
align-items: center;
.text {
margin-left: 4px;
height: 25px;
max-width: 130px;
line-height: 25px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line {
margin-left: 7px;
width: 40px;
height: 2px;
background: rgb(230, 231, 232);
}
}
}
.right-bone {
color: #777;
position: absolute;
top: 50px;
right: -150px;
width: 150px;
height: 260px;
.right-bone-item {
transform: skew(30deg);
height: 35px;
margin-bottom: 2px;
margin-top: 2px;
flex-direction: column;
gap: 8px;
border-top: 1px solid rgb(238, 238, 238);
.content-item {
display: flex;
justify-content: flex-start;
align-items: center;
.line {
margin-right: 7px;
width: 30px;
height: 2px;
background: rgb(230, 231, 232);
}
.text {
max-width: 100px;
margin-right: 4px;
height: 25px;
line-height: 25px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.bottom-bone1 {
@extend .bottom-bone;
right: 660px;
}
.bottom-bone2 {
@extend .bottom-bone;
right: 960px;
}
}
.right-main-content-footer {
height: 84px;
margin-top: 16px;
display: flex;
justify-content: space-between;
.footer-item1,
.footer-item2,
.footer-item3 {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.footer-item1 {
.footer-item1-top {
height: 28px;
text-align: center;
line-height: 28px;
background: url("../../../../assets/images/bg3.png");
background-size: 100% 100%;
color: rgba(22, 119, 255, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
margin-top: 15px;
margin-right: -10px; // Negative margin to overlap/connect
position: relative; // Ensure z-index works if needed
z-index: 1;
}
.footer-item1-bottom {
display: flex;
justify-content: center;
.icon {
margin-top: 9px;
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-top: 7px;
margin-left: 8px;
height: 22px;
color: rgba(206, 79, 81, 1);
font-size: 14px;
}
}
}
.footer-item2 {
.footer-item2-top {
height: 28px;
text-align: center;
line-height: 28px;
background: url("../../../../assets/images/bg2.png");
background-size: 100% 100%;
color: rgba(19, 168, 168, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
margin-top: 15px;
margin-right: -10px; // Negative margin to connect with next item
margin-left: -10px; // Negative margin to connect with prev item
position: relative;
z-index: 1;
}
.footer-item2-bottom {
display: flex;
justify-content: center;
.icon {
margin-top: 9px;
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-top: 7px;
margin-left: 8px;
height: 22px;
color: rgba(206, 79, 81, 1);
font-size: 14px;
}
}
}
.footer-item3 {
.footer-item3-top {
height: 28px;
text-align: center;
line-height: 28px;
background: url("../../../../assets/images/bg1.png");
background-size: 100% 100%;
color: rgba(146, 84, 222, 1);
font-family: Microsoft YaHei;
font-size: 16px;
gap: 8px;
.item-label {
font-size: 146x;
font-weight: 700;
margin-top: 15px;
margin-left: -10px; // Negative margin to connect
position: relative;
z-index: 1;
}
.footer-item3-bottom {
display: flex;
justify-content: center;
.icon {
margin-top: 9px;
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
font-family: Source Han Sans CN;
color: rgba(59, 65, 75, 1);
line-height: 24px;
}
.text {
margin-top: 7px;
margin-left: 8px;
height: 22px;
color: rgba(206, 79, 81, 1);
.item-desc {
font-size: 14px;
font-family: Source Han Sans CN;
margin-top: 3px;
}
}
}
}
}
}
}
}
.main-association {
justify-content: flex-start !important;
gap: 16px;
}
</style>
{
"id": 2140,
"name": "美国以伊朗石油非法贸易为由实施制裁,多家中国企业被列入制裁名单",
"domainList": [
"海洋"
],
"relyFileList": [
{
"id": null,
"name": "第13382号行政命令"
},
{
"id": null,
"name": "第14530号行政命令"
}
],
"relySanList": [
{
"sanTypeId": 2,
"id": 2145,
"title": "OFAC将38个实体及4名个人列入SDN清单,涉及中国关联主体",
"postDate": "2025-10-14"
}
],
"sanReasonList": [
"参与了从伊朗购买、收购、销售、运输或营销石油化工产品",
"参与了与采购、获取、销售、运输或销售伊朗石油及石油制品相关的重大交易"
],
"addObjectList": [
{
"key": "机构",
"value": 3
}
],
"delObjectList": [
{
"key": "人物",
"value": 1
}
],
"sanList": [
{
"entityId": "91310115MA1HBB8PXH",
"entityName": "SHANGHAI QIZHANG SHIP MANAGEMENT CO., LTD.",
"entityNameZh": "上海启章船舶管理有限公司",
"entityTypeId": 2,
"entityTypeName": "机构",
"domainNames": [
"海洋"
]
},
{
"entityId": "71180883",
"entityName": "ALL WIN SHIPPING MANAGEMENT LIMITED",
"entityNameZh": "誠安船舶管理有限公司",
"entityTypeId": 2,
"entityTypeName": "机构",
"domainNames": [
"海洋"
]
},
{
"entityId": "91370211MAEBUA7E2Q",
"entityName": "QINGDAO OCEAN KIMO SHIP MANAGEMENT CO LTD",
"entityNameZh": "青岛明洋凯茂船舶管理有限公司",
"entityTypeId": 2,
"entityTypeName": "机构",
"domainNames": [
"海洋"
]
}
]
}
\ No newline at end of file
......@@ -13,7 +13,7 @@
</div>
</div>
<div class="main">
<div v-if="activeIndex == 0">
<div class="sanctionTime" v-if="activeIndex == 0">
<div class="left">
<AnalysisBox title="选择制裁">
<div class="left-main">
......@@ -76,7 +76,7 @@
</template>
<div class="right-main">
<div class="right-main-content">
<div class="hintWrap">
<!-- <div class="hintWrap">
<div class="icon1"></div>
<div class="title">
2025年实体清单制裁范围扩大至芯片制造环节,为中国的芯片制造能力划定“技术天花板”,阻止其向更先进水平发展。制裁范围向上游设备和材料、下游先进封装以及关键工具(如EDA软件)延伸,意图瓦解中国构建自主可控产业链的努力。
......@@ -84,7 +84,7 @@
<div class="icon2Wrap">
<div class="icon2"></div>
</div>
</div>
</div> -->
<div class="right-main-content-main">
<div class="fishbone-wrapper">
<div class="fishbone-scroll-container" ref="scrollContainerRef">
......@@ -557,8 +557,6 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
.deep-mining {
width: 1601px;
margin: 0 auto;
......@@ -610,6 +608,11 @@ onUnmounted(() => {
padding-bottom: 50px;
display: flex;
justify-content: space-between;
.sanctionTime {
display: flex;
padding-top: 12px;
gap: 10px;
}
.left {
width: 480px;
......
......@@ -433,11 +433,6 @@ onMounted(() => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.introduction-page {
width: 1601px;
margin: 0 auto;
......
......@@ -532,12 +532,12 @@ watch(customDateRange, () => {
align-items: center;
.search-input {
width: 388px;
width: 360px;
height: 32px;
:deep(.el-input__wrapper) {
padding: 0 11px;
border: 1px solid rgba(170, 173, 177, 1);
border: 1px solid rgba(170, 173, 177, 0.5);
background-color: #fff;
border-radius: 3px;
}
......@@ -579,7 +579,7 @@ watch(customDateRange, () => {
.left {
padding-bottom: 20px;
width: 388px;
width: 360px;
height: auto;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
......@@ -598,11 +598,12 @@ watch(customDateRange, () => {
.el-checkbox {
width: 50%;
margin-right: 0;
margin-bottom: 4px;
// margin-bottom: 4px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
height: 24px;
color: rgb(95, 101, 108);
}
......@@ -649,7 +650,7 @@ watch(customDateRange, () => {
}
.right {
width: 1196px;
width: 1223px;
height: auto;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
......
......@@ -724,7 +724,7 @@ import {
getCountDomainByYear,
getEntitiesList,
getSanctionProcess,
// getSanDomainCount,
getSanDomainCount,
// getRiskSignal,
// getSocialMediaInfo,
// getNewsInfo,
......@@ -738,7 +738,7 @@ import {
getNewsInfo,
getSocialMediaInfo,
getReleaseCount,
getSanDomainCount,
// getSanDomainCount,
getAnnualSanDomain
// getSanctionProcess
} from "@/api/finance";
......@@ -1143,7 +1143,7 @@ const radarOption = ref({
// 获取雷达图数据
const fetchRadarData = async checked => {
try {
const data = await getSanDomainCount(checked, "export");
const data = await getSanDomainCount(checked, allSanTypeIds.value.join(","));
if (data && Array.isArray(data) && data.length > 0) {
// 收集所有可能的领域名称
const allDomains = new Set();
......
......@@ -516,7 +516,7 @@ const handleClickToDetail = university => {
// 查看更多风险信号
const handleToMoreRiskSignal = () => {
const route = router.resolve("/riskSignal");
const route = router.resolve("/viewRiskSignal");
window.open(route.href, "_blank");
};
......
......@@ -64,7 +64,7 @@
</template>
<script setup>
import { ref, watch, onMounted } from "vue";
import { ref, watch, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { setIsLoggedIn } from "@/utils/auth";
......@@ -118,6 +118,22 @@ const loadLoginRemember = () => {
onMounted(() => {
loadLoginRemember();
// 登录页禁止出现任何滚动条(仅在本页生效)
try {
document.documentElement.classList.add("login-no-scroll");
document.body.classList.add("login-no-scroll");
} catch (_) {
// ignore
}
});
onUnmounted(() => {
try {
document.documentElement.classList.remove("login-no-scroll");
document.body.classList.remove("login-no-scroll");
} catch (_) {
// ignore
}
});
/** 取消记住我:清空本地存储与输入框,刷新/再次进入也为空 */
......@@ -161,6 +177,12 @@ const handleLogin = () => {
}
setIsLoggedIn(true);
// 登录成功:清除“强制回登录页”标记(否则跨页/新开标签页会一直被路由守卫拦截到 /login)
try {
window.localStorage.removeItem("force_login");
} catch (_) {
// ignore
}
if (checked.value) {
saveLoginRemember(u, p);
......@@ -180,12 +202,16 @@ const handleLogin = () => {
</script>
<style scoped lang="scss">
:global(html.login-no-scroll),
:global(body.login-no-scroll) {
overflow: hidden !important;
}
.login-page {
/* 允许纵向滚动,禁止横向滚动(放大时避免横向滚动条) */
/* 登录页不允许出现任何滚动条 */
width: 100vw;
min-height: 100vh;
overflow-x: hidden;
overflow-y: auto;
height: 100vh;
overflow: hidden;
/* 背景色(图片加载不出来时兜底) */
background: rgb(5, 95, 194);
......
......@@ -804,7 +804,7 @@ const handleClickCate = cate => {
// 查看更多风险信号
const handleToMoreRiskSignal = () => {
const route = router.resolve("/riskSignal");
const route = router.resolve("/viewRiskSignal");
window.open(route.href, "_blank");
};
......
......@@ -62,129 +62,104 @@
<div class="home-main-footer">
<div class="home-main-footer-main">
<div class="left">
<div class="left-box1">
<div class="left-box1-header">
<div class="select-box">
<div class="header">
<div class="icon"></div>
<div class="title">{{ "风险类型" }}</div>
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="left-box1-main">
<el-checkbox
v-model="isRiskTypeCheckedAll"
:indeterminate="isRiskTypeIndeterminate"
@change="handleRiskTypeCheckAllChange"
class="checkbox-all"
>
全部类型
<div class="select-main">
<el-checkbox-group class="checkbox-group" :model-value="selectedAreaModel"
@change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RISK_FILTER_ALL_AREA">
{{ RISK_FILTER_ALL_AREA }}
</el-checkbox>
<el-checkbox-group
class="checkbox-group"
v-model="checkedRiskType"
@change="handleRiskTypeChange"
>
<el-checkbox
v-for="(item, index) in riskType"
:key="index"
:label="item"
class="filter-checkbox"
>
{{ item }}
<el-checkbox v-for="item in area" :key="item.id" class="filter-checkbox" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="left-box1">
<div class="left-box1-header">
<div class="select-box">
<div class="header">
<div class="icon"></div>
<div class="title">{{ "风险来源" }}</div>
<div class="title">{{ "发布时间" }}</div>
</div>
<div class="left-box1-main">
<el-checkbox
v-model="isRiskSourceCheckedAll"
:indeterminate="isRiskSourceIndeterminate"
@change="handleRiskSourceCheckAllChange"
class="checkbox-all"
>
全部来源
<div class="select-main">
<el-checkbox-group class="checkbox-group" :model-value="selectedTimeModel"
@change="handleTimeGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="Time_FILTER_ALL_SOURCE">
{{ Time_FILTER_ALL_SOURCE }}
</el-checkbox>
<el-checkbox-group
class="checkbox-group"
v-model="checkedRiskSource"
@change="handleRiskSourceChange"
>
<el-checkbox
v-for="item in riskSource"
:key="item.id"
:label="item.id"
class="filter-checkbox"
>
<el-checkbox v-for="item in timeSource" :key="item.id" class="filter-checkbox" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="left-box1">
<div class="left-box1-header">
<div class="select-box">
<div class="header">
<div class="icon"></div>
<div class="title">{{ "风险等级" }}</div>
<div class="title">{{ "风险来源" }}</div>
</div>
<div class="left-box1-main">
<el-checkbox
v-model="isRiskDegreeCheckedAll"
:indeterminate="isRiskDegreeIndeterminate"
@change="handleRiskDegreeCheckAllChange"
class="checkbox-all"
>
全部等级
<div class="select-main">
<el-checkbox-group class="checkbox-group" :model-value="selectedRiskSourceModel"
@change="handleRiskSourceGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RISK_FILTER_ALL_SOURCE">
{{ RISK_FILTER_ALL_SOURCE }}
</el-checkbox>
<el-checkbox-group
class="checkbox-group"
v-model="checkedRiskDegree"
@change="handleRiskDegreeChange"
>
<el-checkbox
v-for="item in riskDegree"
:key="item.id"
:label="item.name"
class="filter-checkbox"
>
<el-checkbox v-for="item in riskSource" :key="item.id" class="filter-checkbox" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="left-box1">
<div class="left-box1-header">
<div class="select-box">
<div class="header">
<div class="icon"></div>
<div class="title">{{ "风险类型" }}</div>
</div>
<div class="select-main">
<el-checkbox-group class="checkbox-group" :model-value="selectedRiskTypeModel"
@change="handleRiskTypeGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RISK_FILTER_ALL_TYPE">
{{ RISK_FILTER_ALL_TYPE }}
</el-checkbox>
<el-checkbox v-for="item in riskType" :key="item" class="filter-checkbox" :label="item">
{{ item }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="select-box">
<div class="header">
<div class="icon"></div>
<div class="title">{{ "涉及领域" }}</div>
<div class="title">{{ "风险等级" }}</div>
</div>
<div class="left-box1-main">
<el-checkbox
v-model="isAreaCheckedAll"
:indeterminate="isAreaIndeterminate"
@change="handleAreaCheckAllChange"
class="checkbox-all"
>
全部领域
<div class="select-main">
<el-checkbox-group class="checkbox-group" :model-value="selectedRiskDegreeModel"
@change="handleRiskDegreeGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RISK_FILTER_ALL_LEVEL">
{{ RISK_FILTER_ALL_LEVEL }}
</el-checkbox>
<el-checkbox-group class="checkbox-group" v-model="checkedArea" @change="handleAreaChange">
<el-checkbox v-for="item in area" :key="item.id" :label="item.name" class="filter-checkbox">
<el-checkbox v-for="item in riskDegree" :key="item.id" class="filter-checkbox" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
<div class="right">
<div class="right-header">
<div class="header-left">
<div
class="btn-left"
:class="{ btnleftactive: activeProcessStatusId === item.id }"
@click="handleClickBtn(item)"
v-for="item in processStatusList"
:key="item.id"
>
<div class="btn-left" :class="{ btnleftactive: activeProcessStatusId === item.id }"
@click="handleClickBtn(item)" v-for="item in processStatusList" :key="item.id">
<div class="btn-text" :class="{ btntextactive: activeProcessStatusId === item.id }">
{{ item.name }}
</div>
......@@ -192,26 +167,24 @@
</div>
<div class="header-right">
<div class="searchbox">
<el-input
@keyup.enter="handleSearch"
v-model="kewword"
style="width: 268px; height: 100%"
placeholder="搜索"
/>
<el-input @keyup.enter="handleSearch" v-model="kewword" style="width: 268px; height: 100%"
placeholder="搜索" />
<div class="search-btn" @click="handleSearch"></div>
</div>
<div class="select-box">
<div class="paixu-btn" @click="handleSwithSort">
<div class="icon1">
<img v-if="isSort" src="@/assets/icons/shengxu1.png" alt="" />
<img v-else src="@/assets/icons/jiangxu1.png" alt="" />
</div>
<div class="text">{{ "发布时间" }}</div>
<div class="icon2">
<img v-if="isSort" src="@/assets/icons/shengxu2.png" alt="" />
<img v-else src="@/assets/icons/jiangxu2.png" alt="" />
</div>
</div>
<el-select v-model="sortModel" class="resource-library-sort-select" placeholder="发布时间"
style="width: 120px" :teleported="true" placement="bottom-start"
:popper-options="resourceLibrarySortPopperOptions" @change="handleSortChange">
<template #prefix>
<img v-if="sortModel !== true"
src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png"
class="resource-library-sort-prefix-img" alt="" @click.stop="toggleSortPrefix" />
<img v-else src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image up.png"
class="resource-library-sort-prefix-img" alt="" @click.stop="toggleSortPrefix" />
</template>
<el-option :key="'risk-sort-asc'" label="正序" :value="true" />
<el-option :key="'risk-sort-desc'" label="倒序" :value="false" />
</el-select>
</div>
</div>
</div>
......@@ -220,49 +193,40 @@
<div class="itemlist" v-for="(val, idx) in riskList" :key="idx">
<div class="box-title">
<div class="risktitle">{{ val.title }}</div>
<div
class="risktype"
:class="{
risk1: val.risktype === '特别重大风险' || val.risktype === '特别重大',
<div class="risktype" :class="{
risk1: val.risktype === '特别重大风险' || val.risktype === '特别重大风险',
risk2: val.risktype === '重大风险',
risk3: val.risktype === '一般风险'
}"
>
<div
class="icon"
:class="{
icon1: val.risktype === '特别重大风险' || val.risktype === '特别重大',
}">
<div class="icon" :class="{
icon1: val.risktype === '特别重大风险' || val.risktype === '特别重大风险',
icon2: val.risktype === '重大风险',
icon3: val.risktype === '一般风险'
}"
></div>
}"></div>
<div class="text">{{ val.risktype }}</div>
</div>
</div>
<div class="box-source">
<img class="source-pic" :src="val.pic ? val.pic : DefaultIcon2" alt="" />
<div class="source-text">{{ val.origin + " · " + val.fileType }}</div>
<img class="source-pic" :src="DefaultIcon2" alt="" />
<div class="source-text">{{ val.origin }}</div>
<div class="source-text">{{ val.time }}</div>
</div>
<div class="desc-box">{{ val.dsc }}</div>
<div class="tag-box" v-if="val.tag.length">
<div class="tag" v-for="(tag, index) in val.tag" :key="index">{{ tag }}</div>
<AreaTag v-for="(tag, index) in val.tag" :key="index" :tagName="tag">{{ tag }}</AreaTag>
</div>
</div>
</div>
<div class="right-footer">
<div class="footer-left">
{{ `共 ${totalNum} 项` }}
{{ `共 ${totalNum} 项调查` }}
</div>
<div class="footer-right">
<el-pagination
@current-change="handleCurrentChange"
:pageSize="pageSize"
:current-page="currentPage"
:total="totalNum"
background
layout="prev, pager, next"
/>
<el-pagination @current-change="handleCurrentChange" :pageSize="pageSize" :current-page="currentPage"
:total="totalNum" background layout="prev, pager, next" />
</div>
</div>
</div>
......@@ -275,26 +239,38 @@
<script setup>
import { onMounted, ref } from "vue";
import { getCountInfo, getDailyCount, getPageQuery } from "@/api/riskSignal/index";
import { getHylyList } from "@/api/thinkTank/overview";
import setChart from "@/utils/setChart";
import getCalendarHeatChart from "./utils/cleandarHeat";
import DefaultIcon2 from "@/assets/icons/default-icon2.png";
import { normalizeExclusiveAllOption } from "@/views/thinkTank/utils/resourceLibraryFilters";
const riskType = ref(["科技法案", "行政令", "智库报告", "出口管制", "投融资限制", "市场准入限制", "规则限制"]);
const isRiskTypeCheckedAll = ref(false);
const isRiskTypeIndeterminate = ref(true);
const checkedRiskType = ref([]);
const handleRiskTypeCheckAllChange = val => {
checkedRiskType.value = val ? riskType.value : [];
isRiskTypeIndeterminate.value = false;
const RISK_FILTER_ALL_TYPE = "全部类型";
const selectedRiskTypeModel = ref([RISK_FILTER_ALL_TYPE]);
const handleRiskTypeGroupChange = (val) => {
selectedRiskTypeModel.value = normalizeExclusiveAllOption(val, RISK_FILTER_ALL_TYPE);
handleGetPageQuery();
};
const handleRiskTypeChange = val => {
const checkedCount = val.length;
isRiskTypeCheckedAll.value = checkedCount === riskType.value.length;
isRiskTypeIndeterminate.value = checkedCount > 0 && checkedCount < riskType.value.length;
// 发布时间(与「全部时间」互斥)
const Time_FILTER_ALL_SOURCE = "全部时间";
const TIME_FILTER_1M = "TIME_FILTER_1M";
const TIME_FILTER_3M = "TIME_FILTER_3M";
const TIME_FILTER_6M = "TIME_FILTER_6M";
const TIME_FILTER_1Y = "TIME_FILTER_1Y";
const timeSource = ref([
{ id: TIME_FILTER_1M, name: "近一个月" },
{ id: TIME_FILTER_3M, name: "近三个月" },
{ id: TIME_FILTER_6M, name: "近半年" },
{ id: TIME_FILTER_1Y, name: "近一年" }
]);
const selectedTimeModel = ref([Time_FILTER_ALL_SOURCE]);
const handleTimeGroupChange = (val) => {
selectedTimeModel.value = normalizeExclusiveAllOption(val, Time_FILTER_ALL_SOURCE);
handleGetPageQuery();
};
......@@ -328,30 +304,17 @@ const riskSource = ref([
id: "0401"
}
]);
const isRiskSourceCheckedAll = ref(false);
const isRiskSourceIndeterminate = ref(true);
const checkedRiskSource = ref([]);
const handleRiskSourceCheckAllChange = val => {
checkedRiskSource.value = val
? riskSource.value.map(item => {
return item.id;
})
: [];
isRiskSourceIndeterminate.value = false;
handleGetPageQuery();
};
const handleRiskSourceChange = val => {
const checkedCount = val.length;
isRiskSourceCheckedAll.value = checkedCount === riskSource.value.length;
isRiskSourceIndeterminate.value = checkedCount > 0 && checkedCount < riskSource.value.length;
const RISK_FILTER_ALL_SOURCE = "全部国家";
const selectedRiskSourceModel = ref([RISK_FILTER_ALL_SOURCE]);
const handleRiskSourceGroupChange = (val) => {
selectedRiskSourceModel.value = normalizeExclusiveAllOption(val, RISK_FILTER_ALL_SOURCE);
handleGetPageQuery();
};
const riskDegree = ref([
{
name: "特别重大",
id: "特别重大"
name: "特别重大风险",
id: "特别重大风险"
},
{
name: "重大风险",
......@@ -366,110 +329,40 @@ const riskDegree = ref([
id: "一般风险"
},
{
name: "风险",
id: "风险"
name: "风险",
id: "风险"
}
]);
const isRiskDegreeCheckedAll = ref(false);
const isRiskDegreeIndeterminate = ref(true);
const checkedRiskDegree = ref([]);
const handleRiskDegreeCheckAllChange = val => {
checkedRiskDegree.value = val
? riskDegree.value.map(item => {
return item.id;
})
: [];
isRiskDegreeIndeterminate.value = false;
handleGetPageQuery();
};
const handleRiskDegreeChange = val => {
const checkedCount = val.length;
isRiskDegreeCheckedAll.value = checkedCount === riskDegree.value.length;
isRiskDegreeIndeterminate.value = checkedCount > 0 && checkedCount < riskDegree.value.length;
const RISK_FILTER_ALL_LEVEL = "全部等级";
const selectedRiskDegreeModel = ref([RISK_FILTER_ALL_LEVEL]);
const handleRiskDegreeGroupChange = (val) => {
selectedRiskDegreeModel.value = normalizeExclusiveAllOption(val, RISK_FILTER_ALL_LEVEL);
handleGetPageQuery();
};
const area = ref([
{
name: "人工智能",
id: 1
},
{
name: "生物科技",
id: 2
},
{
name: "新一代通信技术",
id: 3
},
{
name: "量子科技",
id: 4
},
{
name: "新能源",
id: 5
},
{
name: "集成电路",
id: 6
},
{
name: "海洋",
id: 7
},
{
name: "先进制造",
id: 8
},
{
name: "新材料",
id: 9
},
{
name: "航空航天",
id: 10
},
{
name: "深海",
id: 11
},
{
name: "极地",
id: 12
},
{
name: "太空",
id: 13
},
{
name: "核",
id: 14
},
{
name: "其他",
id: 99
}
]);
const isAreaCheckedAll = ref(false);
const isAreaIndeterminate = ref(true);
const checkedArea = ref([]);
const handleAreaCheckAllChange = val => {
checkedArea.value = val
? area.value.map(item => {
return item.id;
})
: [];
isAreaIndeterminate.value = false;
handleGetPageQuery();
// 科技领域字典:与智库资源库一致
const handleGetHylyList = async () => {
try {
const res = await getHylyList();
if (res.code === 200 && Array.isArray(res.data)) {
area.value = res.data;
} else {
area.value = [];
}
} catch (e) {
console.error("获取科技领域字典失败", e);
area.value = [];
}
};
const handleAreaChange = val => {
const checkedCount = val.length;
isAreaCheckedAll.value = checkedCount === area.value.length;
isAreaIndeterminate.value = checkedCount > 0 && checkedCount < area.value.length;
const RISK_FILTER_ALL_AREA = "全部领域";
const selectedAreaModel = ref([RISK_FILTER_ALL_AREA]);
const handleAreaGroupChange = (val) => {
selectedAreaModel.value = normalizeExclusiveAllOption(val, RISK_FILTER_ALL_AREA);
handleGetPageQuery();
};
......@@ -479,16 +372,13 @@ const processStatusList = ref([
id: -1
},
{
name: "未处理",
name: "未",
id: 0
},
{
name: "处理中",
name: "已读",
id: 1
},
{
name: "已处理",
id: 2
}
]);
const activeProcessStatusId = ref(-1);
......@@ -499,113 +389,75 @@ const handleClickBtn = item => {
};
const riskList = ref([
// {
// title: "扩大实体清单制裁范围,对中企子公司实施同等管制",
// origin: "美国商务部",
// fileType: "实体清单",
// time: "2025年11月10日 16:14",
// dsc: "任何被列入美国出口管制“实体清单”或“军事最终用户清单”的企业,如果其直接或间接持有另一家公司 ​50%或以上的股权,那么这家被控股的公司也将自动受到与清单上母公司同等的出口管制限制",
// tag: ["生物科技", "人工智能"],
// risktype: "特别重大风险",
// pic: "src/views/riskSignal/assets/images/origin1.png"
// }
// {
// title: "大而美法案通过国会众议院投票,将提交至总统签署",
// origin: "美国国会 · 科技法案",
// time: "2025年11月10日 16:14",
// dsc: "",
// tag: ["能源", "人工智能"],
// risktype: "重大风险",
// pic: "src/views/riskSignal/assets/images/origin2.png",
// textcolor: "rgba(255, 149, 77, 1)",
// bgcolor: "rgba(255, 149, 77, 0.1)"
// },
// {
// title: "兰德公司发布智库报告《中美经济竞争:复杂经济和地缘政治关系中的收益和风险》",
// origin: "兰德公司 · 科技智库",
// time: "2025年11月10日 16:14",
// dsc: "包括经济竞争在内的美中竞争自2017年以来一直在定义美国外交政策。这两个经济体是世界上第一和第二大国家经济体,并且深深交织在一起。改变关系,无论多么必要,可能是昂贵的。因此,美国面临着一项挑战,确保其经济在耦合的战略竞争条件下满足国家的需求。",
// tag: ["生物科技", "人工智能", "集成电路"],
// risktype: "一般风险",
// pic: "src/views/riskSignal/assets/images/origin3.png",
// textcolor: "rgba(5, 95, 194, 1)",
// bgcolor: "rgba(5, 95, 194, 0.1)"
// },
// {
// title: "美国白宫发布总统政令《关于进一步延长TikTok执法宽限期的行政令》",
// origin: "美国白宫 · 总统政令",
// time: "2025年11月10日 16:14",
// dsc: "再次推迟(第四次)对TikTok禁令的执法,新的宽限期截止日为2025年12月16日​。在宽限期内及对于宽限期前的行为,司法部不得强制执行​《保护美国人免受外国对手控制应用程序法》或因此处罚相关实体​(如TikTok及其分发平台)。",
// tag: ["人工智能"],
// risktype: "一般风险",
// pic: "src/views/riskSignal/assets/images/origin2.png",
// textcolor: "rgba(5, 95, 194, 1)",
// bgcolor: "rgba(5, 95, 194, 0.1)"
// },
// {
// title: "美国财政部更新《特别指定国民清单》",
// origin: "美国财政部 · 特别指定国民清单",
// time: "2025年11月10日 16:14",
// dsc: "",
// tag: ["生物科技"],
// risktype: "特别重大风险",
// pic: "src/views/riskSignal/assets/images/origin4.png",
// textcolor: "rgba(206, 79, 81, 1)",
// bgcolor: "rgba(206, 79, 81, 0.1)"
// },
// {
// title: "美国FDA针对两家中国第三方检测机构的数据完整性问题采取行动",
// origin: "美国食品药品监督管理局 · 规则限制",
// time: "2025年11月10日 16:14",
// dsc: "FDA因发现数据伪造或无效问题,向两家中国第三方检测公司(天津中联科技检测有限公司和苏州大学卫生与环境技术研究所)正式发出“一般信函”。",
// tag: ["生物科技", "人工智能"],
// risktype: "特别重大风险",
// pic: "src/views/riskSignal/assets/images/origin5.png",
// textcolor: "rgba(206, 79, 81, 1)",
// bgcolor: "rgba(206, 79, 81, 0.1)"
// }
{
title: "扩大实体清单制裁范围,对中企子公司实施同等管制",
origin: "美国商务部",
fileType: "实体清单",
time: "2025年11月10日 16:14",
dsc: "任何被列入美国出口管制“实体清单”或“军事最终用户清单”的企业,如果其直接或间接持有另一家公司 ​50%或以上的股权,那么这家被控股的公司也将自动受到与清单上母公司同等的出口管制限制",
tag: ["生物科技", "人工智能"],
risktype: "特别重大风险",
pic: "src/views/riskSignal/assets/images/origin1.png"
},
{
title: "大而美法案通过国会众议院投票,将提交至总统签署",
origin: "美国国会 · 科技法案",
time: "2025年11月10日 16:14",
dsc: "",
tag: ["能源", "人工智能"],
risktype: "重大风险",
pic: "src/views/riskSignal/assets/images/origin2.png",
textcolor: "rgba(255, 149, 77, 1)",
bgcolor: "rgba(255, 149, 77, 0.1)"
},
{
title: "兰德公司发布智库报告《中美经济竞争:复杂经济和地缘政治关系中的收益和风险》",
origin: "兰德公司 · 科技智库",
time: "2025年11月10日 16:14",
dsc: "包括经济竞争在内的美中竞争自2017年以来一直在定义美国外交政策。这两个经济体是世界上第一和第二大国家经济体,并且深深交织在一起。改变关系,无论多么必要,可能是昂贵的。因此,美国面临着一项挑战,确保其经济在耦合的战略竞争条件下满足国家的需求。",
tag: ["生物科技", "人工智能", "集成电路"],
risktype: "一般风险",
pic: "src/views/riskSignal/assets/images/origin3.png",
textcolor: "rgba(5, 95, 194, 1)",
bgcolor: "rgba(5, 95, 194, 0.1)"
},
{
title: "美国白宫发布总统政令《关于进一步延长TikTok执法宽限期的行政令》",
origin: "美国白宫 · 总统政令",
time: "2025年11月10日 16:14",
dsc: "再次推迟(第四次)对TikTok禁令的执法,新的宽限期截止日为2025年12月16日​。在宽限期内及对于宽限期前的行为,司法部不得强制执行​《保护美国人免受外国对手控制应用程序法》或因此处罚相关实体​(如TikTok及其分发平台)。",
tag: ["人工智能"],
risktype: "一般风险",
pic: "src/views/riskSignal/assets/images/origin2.png",
textcolor: "rgba(5, 95, 194, 1)",
bgcolor: "rgba(5, 95, 194, 0.1)"
},
{
title: "美国财政部更新《特别指定国民清单》",
origin: "美国财政部 · 特别指定国民清单",
time: "2025年11月10日 16:14",
dsc: "",
tag: ["生物科技"],
risktype: "特别重大风险",
pic: "src/views/riskSignal/assets/images/origin4.png",
textcolor: "rgba(206, 79, 81, 1)",
bgcolor: "rgba(206, 79, 81, 0.1)"
},
{
title: "美国FDA针对两家中国第三方检测机构的数据完整性问题采取行动",
origin: "美国食品药品监督管理局 · 规则限制",
time: "2025年11月10日 16:14",
dsc: "FDA因发现数据伪造或无效问题,向两家中国第三方检测公司(天津中联科技检测有限公司和苏州大学卫生与环境技术研究所)正式发出“一般信函”。",
tag: ["生物科技", "人工智能"],
risktype: "特别重大风险",
pic: "src/views/riskSignal/assets/images/origin5.png",
textcolor: "rgba(206, 79, 81, 1)",
bgcolor: "rgba(206, 79, 81, 0.1)"
}
]);
const calendarData = ref([
// ["2025-01-01", 20],
// ["2025-01-05", 120],
// ["2025-01-09", 220],
// ["2025-01-15", 320],
// ["2025-01-20", 120],
// ["2025-01-24", 420],
// ["2025-02-05", 80],
// ["2025-02-08", 280],
// ["2025-02-18", 480],
// ["2025-02-11", 420],
// ["2025-02-21", 320],
// ["2025-03-05", 160],
// ["2025-03-09", 260],
// ["2025-03-19", 460],
// ["2025-03-26", 430],
// ["2025-04-01", 70],
// ["2025-04-05", 170],
// ["2025-04-11", 270],
// ["2025-04-18", 370],
// ["2025-05-05", 210],
// ["2025-05-09", 210],
// ["2025-05-15", 410],
// ["2025-05-22", 480],
// ["2025-06-06", 45],
// ["2025-06-09", 415],
// ["2025-06-16", 245],
// ["2025-06-19", 332],
// ["2025-07-04", 127],
// ["2025-07-09", 327],
// ["2025-07-24", 427],
// ["2025-08-08", 150],
// ["2025-08-11", 250],
// ["2025-08-15", 350],
// ["2025-08-22", 460],
// ["2025-09-10", 480],
// ["2025-09-18", 312],
// ["2025-10-15", 60],
// ["2025-10-19", 80],
// ["2025-10-21", 190]
]);
// 基本统计信息
......@@ -622,7 +474,7 @@ const handleGetCountInfo = async () => {
if (res.code === 200 && res.data) {
basicInfo.value = res.data;
}
} catch (error) {}
} catch (error) { }
};
// 每日统计信息
......@@ -635,7 +487,7 @@ const handleGetDailyCount = async () => {
return [item.day, item.count];
});
}
} catch (error) {}
} catch (error) { }
};
const handleCleandarChart = async () => {
......@@ -644,10 +496,22 @@ const handleCleandarChart = async () => {
setChart(chartCalendar, "chartCalendar");
};
//
const isSort = ref(true); // true 升序 false 倒序
const handleSwithSort = () => {
isSort.value = !isSort.value;
/** null:占位「发布时间」且默认倒序;true 正序;false 倒序(显式选中),与智库概览资源库一致 */
const sortModel = ref(null);
const resourceLibrarySortPopperOptions = {
modifiers: [
{ name: "preventOverflow", options: { mainAxis: false, altAxis: false } },
{ name: "flip", enabled: false }
]
};
const toggleSortPrefix = () => {
sortModel.value = sortModel.value === true ? false : true;
handleSortChange();
};
const handleSortChange = () => {
handleGetPageQuery();
};
......@@ -658,7 +522,7 @@ const handleSearch = async () => {
};
// 风险信号总数
const totalNum = ref(0);
const totalNum = ref(6);
const currentPage = ref(1);
const pageSize = ref(10);
// 处理页码改变事件
......@@ -669,31 +533,61 @@ const handleCurrentChange = page => {
// 按条件分页查询风险信号信息
const handleGetPageQuery = async () => {
const stripAll = (list, allLabel) => (Array.isArray(list) ? list.filter((x) => x !== allLabel) : []);
// 选中「全部xxx」时,传空数组表示不按该条件过滤(与之前未勾选时语义一致)
const riskTypes = stripAll(selectedRiskTypeModel.value, RISK_FILTER_ALL_TYPE);
const srcCountryList = stripAll(selectedRiskSourceModel.value, RISK_FILTER_ALL_SOURCE);
const riskLevels = stripAll(selectedRiskDegreeModel.value, RISK_FILTER_ALL_LEVEL);
const techDomainIds = stripAll(selectedAreaModel.value, RISK_FILTER_ALL_AREA);
const timeFilters = stripAll(selectedTimeModel.value, Time_FILTER_ALL_SOURCE);
const pad2 = (n) => String(n).padStart(2, "0");
const formatYmd = (d) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
const buildStartDateFromTimeFilter = () => {
if (!timeFilters.length) return "";
// 单选时间窗:取第一个即可(normalizeExclusiveAllOption 已保证互斥)
const id = timeFilters[0];
const now = new Date();
const start = new Date(now);
if (id === TIME_FILTER_1M) start.setMonth(start.getMonth() - 1);
else if (id === TIME_FILTER_3M) start.setMonth(start.getMonth() - 3);
else if (id === TIME_FILTER_6M) start.setMonth(start.getMonth() - 6);
else if (id === TIME_FILTER_1Y) start.setFullYear(start.getFullYear() - 1);
else return "";
return formatYmd(start);
};
const startDate = buildStartDateFromTimeFilter();
const endDate = timeFilters.length ? formatYmd(new Date()) : "";
let params;
if (activeProcessStatusId.value === -1) {
params = {
riskTypes: checkedRiskType.value,
srcCountryList: checkedRiskSource.value,
riskLevels: checkedRiskDegree.value,
techDomainIds: checkedArea.value,
riskTypes,
srcCountryList,
riskLevels,
techDomainIds,
startDate,
endDate,
keywords: kewword.value,
pageNum: currentPage.value,
pageSize: pageSize.value,
sortField: "time",
sortOrder: isSort ? "asc" : "desc"
sortOrder: sortModel.value === true ? "asc" : "desc"
};
} else {
params = {
riskTypes: checkedRiskType.value,
srcCountryList: checkedRiskSource.value,
riskLevels: checkedRiskDegree.value,
techDomainIds: checkedArea.value,
riskTypes,
srcCountryList,
riskLevels,
techDomainIds,
dealStatus: activeProcessStatusId.value,
startDate,
endDate,
keywords: kewword.value,
pageNum: 1,
pageSize: 10,
sortField: "time",
sortOrder: isSort ? "asc" : "desc"
sortOrder: sortModel.value === true ? "asc" : "desc"
};
}
......@@ -723,6 +617,7 @@ const handleGetPageQuery = async () => {
onMounted(async () => {
handleGetCountInfo();
handleCleandarChart();
await handleGetHylyList();
handleGetPageQuery();
});
</script>
......@@ -738,13 +633,16 @@ onMounted(async () => {
overflow: hidden;
overflow-y: auto;
background: rgba(248, 249, 250, 1);
.home-main {
width: 100%;
height: 100%;
margin: 0 auto;
background-size: 100% 100%;
.home-main-center {
margin-top: 34px;
.center-center {
margin: 0 auto;
margin-top: 24px;
......@@ -753,18 +651,22 @@ onMounted(async () => {
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
.center-header {
display: flex;
justify-content: space-between;
.center-header-left {
margin-left: 30px;
display: flex;
margin-top: 15px;
.iconstyle {
width: 27px;
height: 24px;
margin-top: 3px;
}
.center-header-title {
margin-left: 10px;
font-size: 24px;
......@@ -772,9 +674,10 @@ onMounted(async () => {
line-height: 32px;
}
}
.center-header-right {
margin-top: 16px;
margin-right: 59px;
margin-top: 20px;
margin-right: 36px;
width: 118px;
height: 36px;
border-radius: 6px;
......@@ -784,10 +687,12 @@ onMounted(async () => {
justify-content: center;
align-items: center;
cursor: pointer;
.img {
width: 16px;
height: 15px;
}
.text {
margin-left: 8px;
font-size: 16px;
......@@ -797,25 +702,33 @@ onMounted(async () => {
}
}
}
.center-middle {
display: flex;
.center-middle-left {
display: flex;
flex-direction: column;
.lineitem {
display: flex;
justify-content: space-between;
.item {
margin-left: 61px;
margin-top: 33px;
margin-top: 28px;
margin-bottom: 3px;
width: 111px;
height: 66px;
display: flex;
flex-direction: column;
justify-content: flex-end;
.top {
display: flex;
justify-content: flex-end;
padding-right: 2px;
.dot {
width: 6px;
height: 6px;
......@@ -823,6 +736,7 @@ onMounted(async () => {
margin-right: 7px;
border-radius: 3px;
}
.text1 {
font-size: 16px;
font-weight: 400;
......@@ -831,130 +745,145 @@ onMounted(async () => {
text-align: right;
}
}
.text2 {
font-size: 32px;
font-weight: 700;
line-height: 42px;
text-align: right;
/* 强制不换行 */
white-space: nowrap;
}
}
}
}
.center-middle-right {
width: 1159px;
height: 189px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 4px;
margin: 30px auto 0;
margin-top: 16px;
margin-left: 61px;
}
}
}
}
.home-main-footer {
// width: 100%;
// height: 1059px;
.home-main-footer-header {
width: 1600px;
height: 42px;
margin: 36px auto;
// background: orange;
display: flex;
justify-content: space-between;
.btn-box {
width: 1000px;
display: flex;
.btn {
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 42px;
padding: 0 24px;
border-radius: 21px;
background: rgba(20, 89, 187, 0);
margin-right: 20px;
cursor: pointer;
&:hover {
background: rgba(20, 89, 187, 0.1);
}
}
.btnActive {
padding: 0 24px;
border-radius: 21px;
background: rgba(20, 89, 187, 1);
color: #fff;
&:hover {
color: #fff;
background: rgba(20, 89, 187, 1);
}
}
}
}
.home-main-footer-main {
width: 1600px;
margin-bottom: 30px;
// box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
// background: rgba(255, 255, 255, 1);
margin: 30px auto;
/* 意思:上30px / 左右auto / 下300px */
box-sizing: border-box;
// padding: 20px;
display: flex;
.left {
width: 389px;
height: 950px;
width: 388px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
padding-bottom: 24px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
background: rgba(255, 255, 255, 1);
.left-box1 {
margin-top: 17px;
height: 100%;
.select-box {
margin-top: 16px;
.left-box1-header {
.header {
display: flex;
gap: 17px;
.icon {
margin-top: 4px;
width: 8px;
height: 16px;
margin-top: 4px;
border-radius: 0px 4px 4px 0;
background: var(--color-main-active);
border-radius: 0 4px 4px 0;
}
.title {
height: 2px;
margin-left: 17px;
height: 24px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-family: "Source Han Sans CN";
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
}
}
.select-main {
margin-left: 25px;
margin-top: 12px;
.checkbox-group {
display: grid;
grid-template-columns: repeat(2, 160px);
gap: 8px 4px;
:deep(.all-checkbox) {
width: 160px;
height: 24px;
margin: 0;
}
:deep(.filter-checkbox) {
width: 160px;
height: 24px;
margin-right: 0 !important;
}
.left-box1-main {
margin-top: 10px;
:deep(.el-checkbox__label) {
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
color: rgb(95, 101, 108);
}
}
}
}
}
.right {
margin-left: 16px;
margin-bottom: 24px;
width: 1196px;
height: 1821px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
.right-header {
height: 60px;
display: flex;
justify-content: space-between;
border-bottom: 1px solid rgba(234, 236, 238, 1);
.header-left {
display: flex;
margin-left: 44px;
gap: 36px;
.btn-left {
display: flex;
height: 60px;
cursor: pointer;
align-items: center;
.btn-text {
font-size: 18px;
font-weight: 400;
......@@ -962,20 +891,24 @@ onMounted(async () => {
color: rgba(59, 65, 75, 1);
text-align: center;
}
.btntextactive {
font-weight: 700;
color: rgba(5, 95, 194, 1);
}
}
.btnleftactive {
border-bottom: 2px solid rgba(5, 95, 194, 1);
}
}
.header-right {
display: flex;
margin-right: 42px;
gap: 12px;
align-items: center;
.searchbox {
display: flex;
......@@ -985,6 +918,7 @@ onMounted(async () => {
border: 1px solid rgba(230, 231, 232, 1);
background: rgba(255, 255, 255, 1);
box-sizing: border-box;
.search-btn {
width: 16px;
height: 16px;
......@@ -993,68 +927,41 @@ onMounted(async () => {
background: url("./assets/images/search-icon.png");
}
}
.select-box {
width: 120px;
height: 32px;
box-sizing: border-box;
.paixu-btn {
display: flex;
width: 120px;
.resource-library-sort-select {
:deep(.el-select__wrapper) {
height: 32px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
min-height: 32px;
border-radius: 4px;
background: rgba(255, 255, 255, 1);
&:hover {
background: var(--color-bg-hover);
}
cursor: pointer;
.icon1 {
width: 11px;
height: 14px;
margin-top: 10px;
margin-left: 9px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 19px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: 0px;
text-align: left;
margin-top: 7px;
margin-left: 9px;
}
.icon2 {
width: 10px;
height: 5px;
margin-top: 5px;
margin-left: 13px;
img {
width: 100%;
height: 100%;
}
}
.resource-library-sort-prefix-img {
width: 6.72px;
height: 14px;
}
}
}
}
.right-main {
width: 1196px;
min-height: 790px;
min-height: 1667px;
padding-left: 18px;
padding-top: 6px;
.itemlist {
padding-left: 25px;
padding-top: 18px;
padding-top: 16px;
padding-bottom: 16px;
width: 1138px;
display: flex;
margin-left: 18px;
flex-direction: column;
border-bottom: 1px solid rgba(234, 236, 238, 1);
......@@ -1062,12 +969,14 @@ onMounted(async () => {
height: 28px;
display: flex;
justify-content: space-between;
.risktitle {
font-size: 18px;
font-weight: 700;
line-height: 24px;
color: rgba(59, 65, 75, 1);
}
.risktype {
height: 28px;
padding: 0 8px;
......@@ -1079,34 +988,42 @@ onMounted(async () => {
justify-content: center;
align-items: center;
gap: 6px;
.icon {
width: 4px;
height: 4px;
border-radius: 2px;
}
.icon1 {
background: rgba(206, 79, 81, 1);
}
.icon2 {
background: rgba(255, 149, 77, 1);
}
.icon3 {
background: var(--color-main-active);
}
}
.risk1 {
background: rgba(206, 79, 81, 0.1);
color: rgba(206, 79, 81, 1);
}
.risk2 {
background: rgba(255, 149, 77, 0.1);
color: rgba(255, 149, 77, 1);
}
.risk3 {
background: rgba(231, 243, 255, 1);
color: var(--color-main-active);
}
}
.box-source {
margin-top: 6px;
display: flex;
......@@ -1114,10 +1031,12 @@ onMounted(async () => {
align-items: center;
height: 24px;
gap: 10px;
.source-pic {
width: 16px;
height: 16px;
}
.source-text {
font-size: 16px;
font-weight: 400;
......@@ -1125,6 +1044,7 @@ onMounted(async () => {
color: rgba(95, 101, 108, 1);
}
}
.desc-box {
margin-top: 9px;
font-size: 16px;
......@@ -1133,44 +1053,37 @@ onMounted(async () => {
color: rgba(59, 65, 75, 1);
text-align: justify;
}
.tag-box {
height: 28px;
margin-top: 11px;
display: flex;
flex-direction: row;
gap: 8px;
.tag {
height: 28px;
padding: 0 8px;
line-height: 28px;
border-radius: 4px;
background: rgba(231, 243, 255, 1);
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 14px;
font-weight: 400;
letter-spacing: 0px;
}
}
}
}
.right-footer {
display: flex;
height: 90px;
padding-top: 30px;
padding-top: 28px;
justify-content: space-between;
.footer-left {
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-family: "Source Han Sans CN";
font-size: 14px;
font-weight: 400;
line-height: 18px;
margin-left: 24px;
margin-top: 6px;
margin-left: 26px;
margin-top: 4px;
}
.footer-right {
margin-right: 24px;
margin-right: 58px;
}
}
}
......@@ -1178,12 +1091,6 @@ onMounted(async () => {
}
}
}
.checkbox-all {
margin-left: 20px;
width: 260px;
}
.filter-checkbox {
width: 130px;
margin-left: 20px;
}
/* 复选框尺寸由 .checkbox-group 内统一控制,避免重复覆盖 */
</style>
\ No newline at end of file
......@@ -19,13 +19,13 @@ const getCalendarHeatChart = (data) => {
visualMap: {
show: false,
min: 0,
max: 500,
max: 20,
calculable: true,
orient: 'horizontal',
left: 'center',
top: 65,
inRange: {
color: ['rgba(231, 243, 255, 1)', 'rgba(138, 196, 255, 1)', 'rgba(5, 95, 194, 1)']
color: ['rgb(231, 243, 255)', 'rgb(137, 193, 255)', 'rgb(5, 95, 194)']
},
textStyle: {
color: 'rgba(95, 101, 108, 1)'
......@@ -36,7 +36,7 @@ const getCalendarHeatChart = (data) => {
left: 30,
right: 30,
cellSize: ['auto', 20],
range: '2025',
range: '2026',
splitLine: {
show:false
},
......
......@@ -61,6 +61,7 @@ export default defineConfig({
target: 'http://8.140.26.4:9085/',
// target: 'http://192.168.0.4:28080/',
changeOrigin: true,
// 前端仍请求 /api/xxx,实际转发到后端 /api/v2/xxx
rewrite: (path) => path.replace(/^\/api/, '')
// '/api': {
......
......@@ -730,8 +730,6 @@ public class RemarksVO {
}
```
# 字典
## 领域类别(id:name)
......@@ -764,7 +762,7 @@ public class RemarksVO {
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw
```
输出结果:ApiResult<List<SanctionTypeBean>>
输出结果:ApiResult
## 最新出口管制政策(4条)
......@@ -782,7 +780,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw
```
输出结果:ApiResult<LatestExportControlInfo>
输出结果:ApiResult
## 发布(更新)频度
......@@ -802,7 +800,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw
```
输出结果:ApiResult<List<AnnualCount>>
输出结果:ApiResult
## **制裁领域分析**(20251215)
......@@ -820,7 +818,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw
```
输出结果:ApiResult<List<DomainCount>>
输出结果:ApiResult
## **历次制裁过程**
......@@ -838,7 +836,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw
```
输出结果:ApiResult<Page<SanctionProcess>>
输出结果:ApiResult
## **制裁实体清单**列表(20251215)
......@@ -860,7 +858,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw
```
输出结果:ApiResult<Page<SanctionListBean>>
输出结果:ApiResult
## **发布机构与重点人物**
......@@ -876,7 +874,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<OrgInfo>
输出结果:ApiResult
## **领域分布查询**
......@@ -890,7 +888,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **类型分布查询**
......@@ -904,7 +902,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **区域分布查询**
......@@ -918,7 +916,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<RegionCount>>
输出结果:ApiResult
## **制裁理由查询**
......@@ -932,7 +930,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<String>>
输出结果:ApiResult
## **深度挖掘-制裁信息变化统计**
......@@ -946,7 +944,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<SanCountInfo>
输出结果:ApiResult
## **年度实体数统计**
......@@ -962,7 +960,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<SanCountInfo>
输出结果:ApiResult
## **重点实体列表查询**
......@@ -976,7 +974,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<OrgInfo>>
输出结果:ApiResult
## **上市企业制裁强度**
......@@ -990,7 +988,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **上市企业融资变化情况**
......@@ -1004,7 +1002,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **上市企业市值变化情况**
......@@ -1018,7 +1016,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **重点上市企业列表**
......@@ -1032,7 +1030,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<OrgInfo>>
输出结果:ApiResult
## **历次制裁涉及领域数查询**
......@@ -1046,7 +1044,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **具体领域的制裁实体数统计**
......@@ -1060,7 +1058,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<AnnualCount>>
输出结果:ApiResult
## **具体实体类型的制裁实体数统计**
......@@ -1074,7 +1072,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<AnnualCount>>
输出结果:ApiResult
## **产业链结构查询**
......@@ -1088,7 +1086,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<Chain>>
输出结果:ApiResult
## **根据领域获取产业链信息**
......@@ -1102,7 +1100,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<Chain>>
输出结果:ApiResult
## **产业链鱼骨图信息查询**
......@@ -1116,7 +1114,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<FishboneResp>
输出结果:ApiResult
## **产业链中国企业实体信息查询**
......@@ -1130,7 +1128,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<AreasStreamResp>
输出结果:ApiResult
## **实体列表查询**
......@@ -1144,7 +1142,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<OrgInfo>>
输出结果:ApiResult
## **历年制裁领域统计**
......@@ -1158,7 +1156,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<AnnualDomainCount>>
输出结果:ApiResult
## **新增实体数量增长趋势**
......@@ -1172,7 +1170,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<AnnualCount>>
输出结果:ApiResult
## **获取机构的详情信息**
......@@ -1186,7 +1184,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<Organization>
输出结果:ApiResult
## **获取美国前序事件**
......@@ -1200,7 +1198,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<Page<EventInfo>>
输出结果:ApiResult
## **新增科研机构列表**
......@@ -1214,7 +1212,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **各类别仪器对美依赖情况**
......@@ -1224,11 +1222,11 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
输入参数:
​ 参数:List<String> orgIds 机构公司ID列表
​ 参数:List orgIds 机构公司ID列表
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **仪器对美依赖度升高风险分析**
......@@ -1242,7 +1240,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **仪器进口国可替代性分析**
......@@ -1252,11 +1250,11 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
输入参数:
​ 参数:List<String> orgIds 机构公司ID列表
​ 参数:List orgIds 机构公司ID列表
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **仪器国产化降低风险分析**
......@@ -1270,7 +1268,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<BaseCount>>
输出结果:ApiResult
## **制裁实体清单50%规则实体数**
......@@ -1284,7 +1282,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<RuleEntityCount>
输出结果:ApiResult
## **科研院所类实体历史制裁情况**
......@@ -1298,7 +1296,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<AnnualCount>>
输出结果:ApiResult
## **企业类实体历史制裁情况**
......@@ -1312,7 +1310,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<AnnualCount>>
输出结果:ApiResult
## **风险信号**
......@@ -1326,7 +1324,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<RiskSignalVO>>
输出结果:ApiResult
## **新闻资讯**
......@@ -1340,7 +1338,7 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<NewsVO>>
输出结果:ApiResult
## **社交媒体**
......@@ -1354,4 +1352,4 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiI
​ 请求头:携带token
输出结果:ApiResult<List<RemarksVO>>
\ No newline at end of file
输出结果:ApiResult
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论