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

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

流水线 #598 已通过 于阶段
in 1 分 33 秒
<template> <template>
<el-dialog <el-dialog v-model="visible" class="risk-signal-detail-dialog" modal-class="risk-signal-detail-modal" width="1280px"
v-model="visible" align-center :z-index="zIndex" :show-close="true" destroy-on-close @closed="handleClosed">
class="risk-signal-detail-dialog"
modal-class="risk-signal-detail-modal"
width="1280px"
align-center
:z-index="zIndex"
:show-close="true"
destroy-on-close
@closed="handleClosed"
>
<template #header> <template #header>
<img class="header-icon" src="@/views/viewRiskSignal/assets/images/risk-icon.png" alt="" /> <img class="header-icon" :src="headerIconSrc" alt="" />
<span <span v-if="listLevelText" class="risk-signal-detail-dialog__level" :class="listLevelModifierClass">{{
v-if="listLevelText" listLevelText }}</span>
class="risk-signal-detail-dialog__level"
:class="listLevelModifierClass"
>{{ listLevelText }}</span>
<div v-if="bodyFromApi" class="risk-signal-detail-dialog__read-indicator"> <div v-if="bodyFromApi" class="risk-signal-detail-dialog__read-indicator">
<el-icon v-if="riskDetailStatus === false" class="risk-signal-detail-dialog__header-badge-close"> <el-icon v-if="riskDetailStatus === false" class="risk-signal-detail-dialog__header-badge-close">
<Close /> <Close />
</el-icon> </el-icon>
<img <img v-else-if="riskDetailStatus === true" class="risk-signal-detail-dialog__header-badge-read"
v-else-if="riskDetailStatus === true" :src="greenRightImg" alt="" />
class="risk-signal-detail-dialog__header-badge-read"
:src="greenRightImg"
alt=""
/>
<span v-if="riskDetailStatus != null" class="read">{{ riskDetailStatus ? "已读" : "未读" }}</span> <span v-if="riskDetailStatus != null" class="read">{{ riskDetailStatus ? "已读" : "未读" }}</span>
</div> </div>
</template> </template>
...@@ -35,28 +19,19 @@ ...@@ -35,28 +19,19 @@
<div v-if="riskDetailItem.title" class="risk-signal-detail-dialog__body"> <div v-if="riskDetailItem.title" class="risk-signal-detail-dialog__body">
<span class="risk-signal-detail-dialog__title">{{ riskDetailItem.title }}</span> <span class="risk-signal-detail-dialog__title">{{ riskDetailItem.title }}</span>
<div v-if="riskDetailItem.directionLabels.length" class="risk-signal-detail-dialog__directions"> <div v-if="riskDetailItem.directionLabels.length" class="risk-signal-detail-dialog__directions">
<div <div v-for="(dirLabel, dirIndex) in riskDetailItem.directionLabels"
v-for="(dirLabel, dirIndex) in riskDetailItem.directionLabels"
:key="'overview-risk-detail-direction-' + dirIndex + '-' + dirLabel" :key="'overview-risk-detail-direction-' + dirIndex + '-' + dirLabel"
class="risk-signal-detail-dialog__origin" class="risk-signal-detail-dialog__origin">{{ dirLabel }}</div>
>{{ dirLabel }}</div>
</div> </div>
<div class="risk-signal-detail-dialog__meta"> <div class="risk-signal-detail-dialog__meta">
<span>{{ metaLine }}</span> <span>{{ metaLine }}</span>
<div v-if="riskDetailItem.tag.length" class="risk-signal-detail-dialog__tags"> <div v-if="riskDetailItem.tag.length" class="risk-signal-detail-dialog__tags">
<AreaTag <AreaTag v-for="(tag, index) in riskDetailItem.tag" :key="'overview-risk-detail-tag-' + index + '-' + tag"
v-for="(tag, index) in riskDetailItem.tag" :tag-name="tag">{{ tag }}</AreaTag>
:key="'overview-risk-detail-tag-' + index + '-' + tag"
:tag-name="tag"
>{{ tag }}</AreaTag>
</div> </div>
</div> </div>
</div> </div>
<div <div v-if="showRelationBar" class="risk-signal-detail-dialog_relation" @click="handleRelationClick">
v-if="showRelationBar"
class="risk-signal-detail-dialog_relation"
@click="handleRelationClick"
>
<div class="relation"> <div class="relation">
<div class="logo"> <div class="logo">
<img src="@/views/viewRiskSignal/assets/images/logo.png" alt="" /> <img src="@/views/viewRiskSignal/assets/images/logo.png" alt="" />
...@@ -72,12 +47,8 @@ ...@@ -72,12 +47,8 @@
</div> </div>
</div> </div>
<template #footer> <template #footer>
<el-button <el-button type="primary" class="risk-signal-detail-dialog__action-btn" :loading="confirmLoading"
type="primary" @click="handleConfirm">
class="risk-signal-detail-dialog__action-btn"
:loading="confirmLoading"
@click="handleConfirm"
>
确定 确定
</el-button> </el-button>
</template> </template>
...@@ -91,6 +62,11 @@ import AreaTag from "@/components/base/AreaTag/index.vue"; ...@@ -91,6 +62,11 @@ import AreaTag from "@/components/base/AreaTag/index.vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { Close } from "@element-plus/icons-vue"; import { Close } from "@element-plus/icons-vue";
import greenRightImg from "@/views/viewRiskSignal/assets/images/green-right.png"; import greenRightImg from "@/views/viewRiskSignal/assets/images/green-right.png";
import riskIconRed from "@/views/viewRiskSignal/assets/images/risk-icon-red.png";
import riskIconOrange from "@/views/viewRiskSignal/assets/images/risk-icon-orange.png";
import riskIconYellow from "@/views/viewRiskSignal/assets/images/risk-icon-yellow.png";
import riskIconGreen from "@/views/viewRiskSignal/assets/images/risk-icon-green.png";
import riskIconBlue from "@/views/viewRiskSignal/assets/images/risk-icon-blue.png";
import { getRiskSignalInfoById, updateRiskSignalStatus } from "@/api/riskSignal/index.js"; import { getRiskSignalInfoById, updateRiskSignalStatus } from "@/api/riskSignal/index.js";
import { import {
buildListRowFallbackFromRawRow, buildListRowFallbackFromRawRow,
...@@ -180,6 +156,15 @@ const listLevelModifierClass = computed( ...@@ -180,6 +156,15 @@ const listLevelModifierClass = computed(
() => `risk-signal-detail-dialog__level--${getRiskDetailLevelModifier(listLevelText.value)}` () => `risk-signal-detail-dialog__level--${getRiskDetailLevelModifier(listLevelText.value)}`
); );
const headerIconSrc = computed(() => {
const lv = getRiskDetailLevelModifier(listLevelText.value);
if (lv === "lv1") return riskIconRed;
if (lv === "lv2") return riskIconOrange;
if (lv === "lv3") return riskIconYellow;
if (lv === "lv4") return riskIconGreen;
return riskIconBlue;
});
const fieldMap = computed(() => ({ const fieldMap = computed(() => ({
nameField: props.nameField, nameField: props.nameField,
postDateField: props.postDateField, postDateField: props.postDateField,
......
...@@ -147,7 +147,7 @@ const router = createRouter({ ...@@ -147,7 +147,7 @@ const router = createRouter({
// 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态) // 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态)
// 3)已有本地 token:正常走前端路由 // 3)已有本地 token:正常走前端路由
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
// 【新增】在每次路由跳转开始前,取消上一个页面所有未完成的请求 // 在每次路由跳转开始前,取消上一个页面所有未完成的请求
// 这能防止旧页面的数据回来覆盖新页面,也能减少服务器压力 // 这能防止旧页面的数据回来覆盖新页面,也能减少服务器压力
cancelAllRequests(); cancelAllRequests();
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
......
<template> <template>
<div class="com-title"> <div class="com-title">
<div class="cl1"></div> <div class="cl1"></div>
<div class="cl2"></div> <div class="cl2"></div>
<div class="title">{{ title }}</div> <div class="title">{{ title }}</div>
<div class="cl3"></div> <div class="cl3"></div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
// 传入的title数据 // 传入的title数据
const props = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: "" default: ""
} }
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.com-title { .com-title {
width: 1575px; width: 1600px;
height: 42px; height: 42px;
display: flex; display: flex;
align-items: center; align-items: center;
.cl1 { .cl1 {
width: 24px; width: 24px;
height: 30px; height: 30px;
background-color: rgba(174, 214, 255, 1); background-color: rgba(174, 214, 255, 1);
margin-right: 8px; margin-right: 8px;
} }
.cl2 { .cl2 {
width: 8px; width: 8px;
height: 30px; height: 30px;
background-color: rgba(174, 214, 255, 1); background-color: rgba(174, 214, 255, 1);
margin-right: 8px; margin-right: 8px;
} }
.title { .title {
width: 152px;
height: 42px; height: 42px;
text-align: center; text-align: center;
font-size: 32px; font-size: 32px;
...@@ -50,13 +54,16 @@ const props = defineProps({ ...@@ -50,13 +54,16 @@ const props = defineProps({
font-family: 'Microsoft YaHei'; font-family: 'Microsoft YaHei';
line-height: 42px; line-height: 42px;
margin-right: 8px; margin-right: 8px;
width: fit-content;
/* 核心:强制不换行 */
white-space: nowrap;
} }
.cl3 { .cl3 {
width: 1367px; width: 100%;
height: 1px; height: 1px;
background-color: rgba(174, 214, 255, 1); background-color: rgba(174, 214, 255, 1);
box-sizing: border-box; box-sizing: border-box;
} }
} }
</style> </style>
...@@ -33,12 +33,9 @@ ...@@ -33,12 +33,9 @@
</li> </li>
<li> <li>
<span class="ul-title">涉及领域:</span> <span class="ul-title">涉及领域:</span>
<div class="ul-tags" v-if="item.AREA"> <div class="ul-tags" v-if="getAreaTagList(item).length">
<span v-for="(field, fIndex) in typeof item.AREA === 'string' <AreaTag v-for="(field, fIndex) in getAreaTagList(item)" :key="`${field}-${fIndex}`"
? item.AREA.split(',') :tagName="field" />
: item.AREA" :key="fIndex" class="ul-pie" :class="'cl' + ((fIndex % 3) + 1)">
{{ field }}
</span>
</div> </div>
<span v-else class="ul-content">未知</span> <span v-else class="ul-content">未知</span>
</li> </li>
...@@ -113,13 +110,8 @@ ...@@ -113,13 +110,8 @@
<RiskSignal :list="riskSignals" @more-click="handleToMoreRiskSignal" postDate="time" name="content" <RiskSignal :list="riskSignals" @more-click="handleToMoreRiskSignal" postDate="time" name="content"
riskLevel="title" @item-click="handleRiskSignalItemToManage" /> riskLevel="title" @item-click="handleRiskSignalItemToManage" />
<RiskSignalOverviewDetailDialog <RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" :row="riskOverviewDetailRow" name-field="content"
v-model="isRiskOverviewDetailOpen" post-date-field="time" risk-level-field="title" />
:row="riskOverviewDetailRow"
name-field="content"
post-date-field="time"
risk-level-field="title"
/>
</div> </div>
</template> </template>
...@@ -132,6 +124,7 @@ import { navigateToViewRiskSignal } from "@/utils/riskSignalOverviewNavigate"; ...@@ -132,6 +124,7 @@ import { navigateToViewRiskSignal } from "@/utils/riskSignalOverviewNavigate";
import { getCoopRestrictionTrends, getCoopRestrictionSignals } from "@/api/coopRestriction/coopRestriction.js"; import { getCoopRestrictionTrends, getCoopRestrictionSignals } from "@/api/coopRestriction/coopRestriction.js";
import defaultImg from "./assets/usImg.png"; import defaultImg from "./assets/usImg.png";
import CommonPrompt from "../../commonPrompt/index.vue"; import CommonPrompt from "../../commonPrompt/index.vue";
import AreaTag from "@/components/base/AreaTag/index.vue";
// 合作限制-查询风险信号数据 // 合作限制-查询风险信号数据
const getCoopRestrictionSignalsData = async () => { const getCoopRestrictionSignalsData = async () => {
...@@ -168,6 +161,20 @@ const getCoopRestrictionTrendsData = async () => { ...@@ -168,6 +161,20 @@ const getCoopRestrictionTrendsData = async () => {
} }
}; };
const getAreaTagList = (item) => {
const raw = item?.AREA;
if (Array.isArray(raw)) {
return raw.map(v => String(v || "").trim()).filter(Boolean);
}
if (typeof raw === "string") {
return raw
.split(",")
.map(v => String(v || "").trim())
.filter(Boolean);
}
return [];
};
// 轮播图手动切换 // 轮播图手动切换
const handlePrev = () => { const handlePrev = () => {
if (carouselRef.value) { if (carouselRef.value) {
...@@ -350,7 +357,7 @@ onMounted(() => { ...@@ -350,7 +357,7 @@ onMounted(() => {
display: flex; display: flex;
.left-center-main { .left-center-main {
width: 439px; width: 1000dvb;
height: 175px; height: 175px;
position: relative; position: relative;
...@@ -364,7 +371,7 @@ onMounted(() => { ...@@ -364,7 +371,7 @@ onMounted(() => {
} }
.left-center-main-ul { .left-center-main-ul {
width: 439px; width: 1000px;
height: 132px; height: 132px;
ul { ul {
...@@ -410,6 +417,7 @@ onMounted(() => { ...@@ -410,6 +417,7 @@ onMounted(() => {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
width: 600px;
} }
.ul-pie { .ul-pie {
......
...@@ -74,8 +74,8 @@ import TipTab from "@/views/thinkTank/TipTab/index.vue"; ...@@ -74,8 +74,8 @@ import TipTab from "@/views/thinkTank/TipTab/index.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue"; import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue"; import AiPane from "@/components/base/Ai/AiPane/index.vue";
const COOP_LEFT_TIP_TEXT = "各类型合作限制政策对比,数据来源:美对华科技合作限制信息平台"; const COOP_LEFT_TIP_TEXT = "数据来源:美对华科技合作限制信息平台";
const COOP_RIGHT_TIP_TEXT = "各领域规则分布情况,数据来源:美对华科技合作限制信息平台"; const COOP_RIGHT_TIP_TEXT = "数据来源:美对华科技合作限制信息平台";
// 临时展示 mock(不改样式):右侧“各领域规则分布情况” // 临时展示 mock(不改样式):右侧“各领域规则分布情况”
// 用完把这个开关改回 false 即可恢复走接口 // 用完把这个开关改回 false 即可恢复走接口
......
...@@ -2,21 +2,14 @@ ...@@ -2,21 +2,14 @@
<div class="reslib-page" ref="reslibContainer"> <div class="reslib-page" ref="reslibContainer">
<div class="nav"> <div class="nav">
<div v-for="item in navList" :key="item.id" class="nav-item" :class="{ active: item.id === activeItem }" <div v-for="item in navList" :key="item.id" class="nav-item" :class="{ active: item.id === activeItem }"
@click="activeItem = item.id"> @click="handleNavItemClick(item.id)">
{{ item.name }} {{ item.name }}
</div> </div>
</div> </div>
<el-select v-model="sortModel" placeholder="发布时间" class="select" popper-class="coop-select-dropdown" <div class="select">
:teleported="true" placement="bottom-start" :popper-options="sortPopperOptions" @change="handleSortChange"> <TimeSortSelectBox :key="`coop-reslib-sort-${activeItem}`" :sort-demension="1"
<template #prefix> @handle-px-change="handleCoopReslibPxChange" />
<img v-if="sortModel !== true" src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png" </div>
class="select-prefix-img" alt="" @click.stop="toggleSortPrefix" />
<img v-else src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image up.png" class="select-prefix-img"
alt="" @click.stop="toggleSortPrefix" />
</template>
<el-option :key="true" label="正序" :value="true" />
<el-option :key="false" label="倒序" :value="false" />
</el-select>
<div class="main"> <div class="main">
<div class="left"> <div class="left">
<div class="left-ti1"></div> <div class="left-ti1"></div>
...@@ -82,6 +75,7 @@ ...@@ -82,6 +75,7 @@
import { ref, onMounted, watch, computed } from "vue"; import { ref, onMounted, watch, computed } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getCoopRestrictionList } from "@/api/coopRestriction/coopRestriction"; import { getCoopRestrictionList } from "@/api/coopRestriction/coopRestriction";
import TimeSortSelectBox from "@/components/base/TimeSortSelectBox/index.vue";
import defaultImg from "../../assets/images/default-icon2.png"; import defaultImg from "../../assets/images/default-icon2.png";
...@@ -172,21 +166,15 @@ const activeItem = ref("0"); ...@@ -172,21 +166,15 @@ const activeItem = ref("0");
/** null:占位「发布时间」且默认倒序;true 正序;false 倒序(显式),与智库概览一致 */ /** null:占位「发布时间」且默认倒序;true 正序;false 倒序(显式),与智库概览一致 */
const sort = ref(null); const sort = ref(null);
const sortPopperOptions = { const handleNavItemClick = (id) => {
modifiers: [ if (activeItem.value === id) {
{ name: "preventOverflow", options: { mainAxis: false, altAxis: false } }, return;
{ name: "flip", enabled: false }
]
};
const sortModel = computed({
get() {
return sort.value;
},
set(v) {
sort.value = v;
} }
}); // 切换 tab:默认回到倒序,并从第一页开始
sort.value = null;
currentPage.value = 1;
activeItem.value = id;
};
const handleSortChange = () => { const handleSortChange = () => {
// 改变排序后从第一页开始 // 改变排序后从第一页开始
...@@ -197,14 +185,10 @@ const handleSortChange = () => { ...@@ -197,14 +185,10 @@ const handleSortChange = () => {
} }
}; };
const toggleSortPrefix = () => { /** 合作限制数据库排序公共组件回调:1=时间倒序,2=时间正序(映射到现有 sort(true/false/null)) */
sort.value = sort.value === true ? false : true; const handleCoopReslibPxChange = (val) => {
// 切换排序后从第一页开始 sort.value = Number(val) === 2 ? true : false;
if (currentPage.value === 1) { handleSortChange();
getMainDataList();
} else {
currentPage.value = 1;
}
}; };
const dataList = ref([ const dataList = ref([
{ {
......
...@@ -432,9 +432,6 @@ const dataList3 = ref([ ...@@ -432,9 +432,6 @@ const dataList3 = ref([
padding: 19px 0 20px; padding: 19px 0 20px;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
position: sticky;
top: 0;
z-index: 9;
.nav-main { .nav-main {
width: 1600px; width: 1600px;
...@@ -442,6 +439,7 @@ const dataList3 = ref([ ...@@ -442,6 +439,7 @@ const dataList3 = ref([
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative;
img { img {
width: 72px; width: 72px;
...@@ -488,8 +486,9 @@ const dataList3 = ref([ ...@@ -488,8 +486,9 @@ const dataList3 = ref([
display: flex; display: flex;
justify-content: right; justify-content: right;
position: absolute; position: absolute;
bottom: 0; right: 0;
margin-left: 1224px; bottom: -20px;
margin-left: 0;
.btn1 { .btn1 {
......
...@@ -34,14 +34,14 @@ ...@@ -34,14 +34,14 @@
</div> </div>
<!-- 数据总览 --> <!-- 数据总览 -->
<div class="datasub" id="position3"> <div class="datasub" id="position3">
<com-title title="数据总览" /> <com-title title="全景概览" />
<div class="datasub-main"> <div class="datasub-main">
<dataSub /> <dataSub />
</div> </div>
</div> </div>
<!-- 资源库 --> <!-- 资源库 -->
<div class="reslib" id="position4"> <div class="reslib" id="position4">
<com-title title="资源库" /> <com-title title="合作限制数据库" />
<div class="reslib-main"> <div class="reslib-main">
<resLib /> <resLib />
</div> </div>
......
<template>
<div class="chart-ai-analysis" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<!-- 触发按钮 -->
<div class="ai-button-wrapper">
<AiButton />
</div>
<!-- AI 内容面板 -->
<transition name="ai-fade">
<div v-if="isVisible" class="ai-pane-wrapper">
<div class="ai-pane-content">
<!-- 1. Loading 状态 -->
<div v-if="hookInstance.loading" class="ai-loading-text">智能总结生成中...</div>
<!-- 2. 错误状态 -->
<div v-else-if="hookInstance.error" class="ai-error-text">
{{ hookInstance.error }}
</div>
<!-- 3. 成功状态 -->
<div v-else-if="hookInstance.interpretation" class="ai-success-content">
<AiPane :aiContent="hookInstance.interpretation" />
</div>
<!-- 4. 初始空状态 (可选,防止闪烁) -->
<div v-else class="ai-empty-text">暂无数据</div>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { ref } from "vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
const props = defineProps({
// 接收由 useChartInterpretation() 创建的实例对象
hookInstance: {
type: Object,
required: true
},
// 图表数据 Payload,用于首次请求
payload: {
type: Object,
required: true
}
});
const isVisible = ref(false);
const hasRequested = ref(false);
const handleMouseEnter = () => {
isVisible.value = true;
// 如果还没有请求过数据,则发起请求
if (!hasRequested.value && !props.hookInstance.loading) {
hasRequested.value = true;
// 调用 Hook 中的 interpret 方法
props.hookInstance.interpret(props.payload);
}
};
const handleMouseLeave = () => {
isVisible.value = false;
};
</script>
<style lang="scss" scoped>
.chart-ai-analysis {
position: absolute;
right: 0px;
bottom: 15px;
z-index: 999;
width: auto;
height: auto;
.ai-button-wrapper {
display: flex;
justify-content: flex-end;
}
.ai-pane-wrapper {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
// 确保面板出现在按钮上方或覆盖区域,根据原有 CSS 调整
// 原有 .ai-pane:hover 逻辑是宽度变宽,这里我们直接显示完整面板
background: #fff;
border-radius: 4px;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
padding: 10px;
box-sizing: border-box;
.ai-pane-content {
min-height: 40px;
.ai-loading-text,
.ai-error-text,
.ai-empty-text {
font-size: 14px;
color: var(--text-primary-50-color);
display: flex;
align-items: center;
justify-content: center;
min-height: 40px;
}
.ai-error-text {
color: var(--color-red-100);
}
}
}
}
// 复用原有的淡入淡出动画
.ai-fade-enter-active,
.ai-fade-leave-active {
transition: opacity 0.3s ease;
}
.ai-fade-enter-from,
.ai-fade-leave-to {
opacity: 0;
}
</style>
...@@ -43,8 +43,8 @@ defineProps({ ...@@ -43,8 +43,8 @@ defineProps({
.title-text { .title-text {
color: rgba(10, 18, 30, 1); color: rgba(10, 18, 30, 1);
font-size: 32px; font-size: 32px;
font-family: $base-font-family; font-family: "Microsoft YaHei";
font-weight: bold; font-weight: 700;
margin-left: 20px; margin-left: 20px;
white-space: nowrap; white-space: nowrap;
} }
......
...@@ -3,12 +3,25 @@ ...@@ -3,12 +3,25 @@
<div class="home-main" ref="homeMainRef"> <div class="home-main" ref="homeMainRef">
<div class="home-top-bg"></div> <div class="home-top-bg"></div>
<div class="home-main-header"> <div class="home-main-header">
<SearchContainer style="margin-bottom: 0; margin-top: 48px; height: fit-content" v-if="homeMainRef" <SearchContainer
placeholder="搜索出口管制" :containerRef="homeMainRef" areaName="实体清单" /> style="margin-bottom: 0; margin-top: 48px; height: fit-content"
v-if="homeMainRef"
placeholder="搜索出口管制"
:containerRef="homeMainRef"
areaName="实体清单"
/>
<div class="home-main-header-footer-info"> <div class="home-main-header-footer-info">
<InfoCard v-for="(item, index) in infoList" :key="item.id" :title="item.nameZh" :subtitle="item.nameAbbr" <InfoCard
:description="item.description" :quantity="item.postCount" :unit="item.unit" :color="infoListColor[index]" v-for="(item, index) in infoList"
@click="handleToEntityListNoId(item)" /> :key="item.id"
:title="item.nameZh"
:subtitle="item.nameAbbr"
:description="item.description"
:quantity="item.postCount"
:unit="item.unit"
:color="infoListColor[index]"
@click="handleToEntityListNoId(item)"
/>
</div> </div>
</div> </div>
...@@ -71,10 +84,18 @@ ...@@ -71,10 +84,18 @@
<div class="box1-bottom-sanTypeId" v-if="item.sanEntities?.length"> <div class="box1-bottom-sanTypeId" v-if="item.sanEntities?.length">
<div class="box1-bottom-title">· 涉及主要实体:</div> <div class="box1-bottom-title">· 涉及主要实体:</div>
<div class="box1-bottom-content"> <div class="box1-bottom-content">
<div class="box1-bottom-content-item" v-for="(ett, index) in item.sanEntities" :key="index" <div
@click="handleEntityClick(ett)"> class="box1-bottom-content-item"
<el-image v-if="ett.img" class="box1-bottom-content-item-img" :src="ett.img" v-for="(ett, index) in item.sanEntities"
alt=""></el-image> :key="index"
@click="handleEntityClick(ett)"
>
<el-image
v-if="ett.img"
class="box1-bottom-content-item-img"
:src="ett.img"
alt=""
></el-image>
<div v-else class="box1-bottom-content-item-imgUndefined"> <div v-else class="box1-bottom-content-item-imgUndefined">
{{ {{
(ett.orgName || ett.orgNameZh)?.match( (ett.orgName || ett.orgNameZh)?.match(
...@@ -91,8 +112,12 @@ ...@@ -91,8 +112,12 @@
<div class="box1-bottom-sanTypeId" v-if="item.sanItems?.length > 0"> <div class="box1-bottom-sanTypeId" v-if="item.sanItems?.length > 0">
<div class="box1-bottom-title">· 涉及管制物项:</div> <div class="box1-bottom-title">· 涉及管制物项:</div>
<div class="box1-bottom-content__wx"> <div class="box1-bottom-content__wx">
<div class="box1-bottom-content__wx-item" v-for="(ett, index) in item.sanItems" :key="index" <div
@click="handleWxClick(item)"> class="box1-bottom-content__wx-item"
v-for="(ett, index) in item.sanItems"
:key="index"
@click="handleWxClick(item)"
>
<div class="box1-bottom-content__wx-item-id"> <div class="box1-bottom-content__wx-item-id">
{{ ett.id }} {{ ett.id }}
</div> </div>
...@@ -124,86 +149,35 @@ ...@@ -124,86 +149,35 @@
</custom-container> </custom-container>
</el-col> </el-col>
<el-col :span="8" style="padding-right: 0px"> <el-col :span="8" style="padding-right: 0px">
<!-- <custom-container <RiskSignal
titleType="danger" :list="warningList"
title="风险信号" @item-click="handleToRiskSignalDetail"
:headerNum="warningList.length" @more-click="handleToMoreRiskSignal"
:titleIcon="dangerIcon" riskLevel="signalLevel"
height="450px" postDate="signalTime"
> name="signalTitle"
<template #default> />
<div class="box2-main">
<div
class="box2-main-item"
v-for="(item, index) in warningList"
:key="index"
@click="handleToRiskSignalDetail"
>
<div
class="item-left"
:class="{
itemLeftStatus1: item.status === '一般风险',
itemLeftStatus2: item.status === '重大风险'
}"
>
{{ item.status }}
</div>
<div class="item-right">
<div class="text">
<CommonPrompt :content="item.title">
{{ item.title }}
</CommonPrompt>
</div>
<div class="time">{{ item.time }}</div>
</div>
</div>
<div class="box2-footer" @click="handleToMoreRiskSignal">
<div class="icon">
<img src="./assets/images/box2-footer-icon.png" alt="" />
</div>
<div class="text">{{ "查看更多" }}</div>
</div>
</div>
</template>
</custom-container> -->
<RiskSignal :list="warningList" @item-click="handleToRiskSignalDetail" @more-click="handleToMoreRiskSignal"
riskLevel="signalLevel" postDate="signalTime" name="signalTitle" />
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="16" style="width: 1600px; margin: 0 auto; height: 50px; margin-top: 64px"> <el-row :gutter="16" style="width: 1600px; margin: 0 auto; height: 50px; margin-top: 64px">
<CustomTitle id="position2" title="资讯要闻" /> <CustomTitle id="position2" title="资讯要闻" />
</el-row> </el-row>
<!-- <el-col :span="12">
<custom-container title="新闻资讯" :titleIcon="newsIcon" height="450px">
<template #header-right>
<el-button type="primary" link @click="handleToMoreNews">
{{ "更多 +" }}
</el-button>
</template>
<template #default>
<div class="news-list">
<NewsList :list-data="newsList" @item-click="item => handleNewsInfoClick(item)" />
</div>
</template>
</custom-container>
</el-col> -->
<div class="center-center"> <div class="center-center">
<NewsList :newsList="newsList" @item-click="handleNewsInfoClick" @more-click="handleToMoreNews" <NewsList
content="newsContent" /> :newsList="newsList"
@item-click="handleNewsInfoClick"
<MessageBubble :messageList="socialMediaList" @person-click="handlePerClick" imageUrl="avatar" @more-click="handleToMoreNews"
@more-click="handleToSocialDetail" /> content="newsContent"
<!-- <custom-container title="社交媒体" :titleIcon="dialogIcon" height="450px"> />
<template #default>
<div class="dialog-list"> <MessageBubble
<MessageBubble v-for="(item, index) in socialMediaList" @click="handlePerClick(item)" :messageList="socialMediaList"
@info-click="handleMediaClick(item)" :key="index" :avatar="item.avatar" :name="item.name" @person-click="handlePerClick"
:time="item.time" :source="item.source" :content="item.content" /> imageUrl="avatar"
</div> @more-click="handleToSocialDetail"
</template> />
</custom-container> -->
</div> </div>
<el-row :gutter="16" style="width: 1600px; margin: 0 auto; height: 510px; margin-top: 64px"> <el-row :gutter="16" style="width: 1600px; margin: 0 auto; height: 510px; margin-top: 64px">
...@@ -214,24 +188,31 @@ ...@@ -214,24 +188,31 @@
<div class="box3"> <div class="box3">
<div class="box3-content"> <div class="box3-content">
<div class="box3-content-title">实体清单发布频次统计</div> <div class="box3-content-title">实体清单发布频次统计</div>
<el-table :data="entityListReleaseFreq" stripe style="width: 100%" @row-click="handleEntityRowClick"> <el-table
:data="entityListReleaseFreq"
stripe
style="width: 100%"
@row-click="handleEntityRowClick"
>
<el-table-column prop="year" label="年份" width="200" /> <el-table-column prop="year" label="年份" width="200" />
<el-table-column label="发布次数" width="300"> <el-table-column label="发布次数" width="300">
<template #default="scope"> <template #default="scope">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span> <span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress :percentage="scope.row.percent * 100" :show-text="false" <el-progress
:status="getStatus(scope.row.percent)" /> :percentage="scope.row.percent * 100"
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="重点领域" width="220" align="center"> <el-table-column label="重点领域" width="220" align="center">
<template #default="scope"> <template #default="scope">
<div style="display: flex; justify-content: center; align-items: center; gap: 5px"> <div
style="display: flex; justify-content: center; align-items: center; gap: 5px"
>
<AreaTag v-for="tag in scope.row.tags" :key="tag" :tagName="tag" /> <AreaTag v-for="tag in scope.row.tags" :key="tag" :tagName="tag" />
<!-- <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)">{{
tag
}}</el-tag> -->
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
...@@ -240,34 +221,44 @@ ...@@ -240,34 +221,44 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text">美国商务部发布实体清单的频次,数据来源:美国商务部官网</div> <div class="data-origin-text">数据来源:美国商务部官网</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <AiButton @mouseenter="handleShowAiPane('entityListReleaseFreqChart')" />
<AiPane :aiContent="entityListReleaseFreqChart.interpretation" /> <AiPane
v-if="aiPaneVisible?.entityListReleaseFreqChart"
:aiContent="overviewAiContent.entityListReleaseFreqChart"
@mouseleave="handleHideAiPane('entityListReleaseFreqChart')"
/>
</div> </div>
</div> </div>
<div class="box3-content"> <div class="box3-content">
<div class="box3-content-title">商业管制清单发布频次统计</div> <div class="box3-content-title">商业管制清单发布频次统计</div>
<el-table :data="commerceControlListReleaseFreq" stripe style="width: 100%" <el-table
@row-click="handleCommercialRowClick"> :data="commerceControlListReleaseFreq"
stripe
style="width: 100%"
@row-click="handleCommercialRowClick"
>
<el-table-column prop="year" label="年份" width="200" /> <el-table-column prop="year" label="年份" width="200" />
<el-table-column label="发布次数" width="300"> <el-table-column label="发布次数" width="300">
<template #default="scope"> <template #default="scope">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span> <span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress :percentage="scope.row.percent * 100" :show-text="false" <el-progress
:status="getStatus(scope.row.percent)" /> :percentage="scope.row.percent * 100"
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="重点领域" width="220" align="center"> <el-table-column label="重点领域" width="220" align="center">
<template #default="scope"> <template #default="scope">
<div style="display: flex; justify-content: center; align-items: center; gap: 5px"> <div
style="display: flex; justify-content: center; align-items: center; gap: 5px"
>
<AreaTag v-for="tag in scope.row.tags" :key="tag" :tagName="tag" /> <AreaTag v-for="tag in scope.row.tags" :key="tag" :tagName="tag" />
<!-- <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)">{{
tag
}}</el-tag> -->
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
...@@ -276,13 +267,15 @@ ...@@ -276,13 +267,15 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text"> <div class="data-origin-text">数据来源:美国商务部官网</div>
美国商务部发布商业管制清单的频次,数据来源:美国商务部官网
</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <AiButton @mouseenter="handleShowAiPane('commerceControlListReleaseFreqChart')" />
<AiPane :aiContent="commerceControlListReleaseFreqChart.interpretation" /> <AiPane
v-if="aiPaneVisible?.commerceControlListReleaseFreqChart"
:aiContent="overviewAiContent.commerceControlListReleaseFreqChart"
@mouseleave="handleHideAiPane('commerceControlListReleaseFreqChart')"
/>
</div> </div>
</div> </div>
<div class="box3-content" style="display: none"> <div class="box3-content" style="display: none">
...@@ -293,8 +286,11 @@ ...@@ -293,8 +286,11 @@
<template #default="scope"> <template #default="scope">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span> <span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress :percentage="scope.row.percent * 100" :show-text="false" <el-progress
:status="getStatus(scope.row.percent)" /> :percentage="scope.row.percent * 100"
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
...@@ -322,17 +318,21 @@ ...@@ -322,17 +318,21 @@
<el-checkbox v-model="domainChecked" label="50%规则" size="large" /> <el-checkbox v-model="domainChecked" label="50%规则" size="large" />
</template> </template>
<template #default> <template #default>
<EChart :option="radarOption" autoresize :style="{ height: '420px' }" <EChart
@chart-click="handleRadarChartClick" /> :option="radarOption"
autoresize
:style="{ height: '420px' }"
@chart-click="handleRadarChartClick"
/>
<div class="data-origin-box"> <div class="data-origin-box">
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text">进入实体清单的中国实体领域分布情况,数据来源:美国商务部官网</div> <div class="data-origin-text">数据来源:美国商务部官网</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <AiButton @mouseenter="handleShowAiPane('radarChart')" />
<AiPane :aiContent="radarChart.interpretation" /> <AiPane :aiContent="overviewAiContent.radarChart" @mouseleave="handleHideAiPane('radarChart')" />
</div> </div>
</template> </template>
</custom-container> </custom-container>
...@@ -341,24 +341,37 @@ ...@@ -341,24 +341,37 @@
<custom-container title="制裁清单数量增长趋势" :titleIcon="qushiIcon" height="540px"> <custom-container title="制裁清单数量增长趋势" :titleIcon="qushiIcon" height="540px">
<template #header-right> <template #header-right>
<div style="display: flex; align-items: center; gap: 16px"> <div style="display: flex; align-items: center; gap: 16px">
<el-checkbox v-if="selectedEntityId != '13'" v-model="trendChecked" label="50%规则" size="large" /> <el-checkbox
v-if="selectedEntityId != '13'"
v-model="trendChecked"
label="50%规则"
size="large"
/>
<el-select v-model="selectedEntityId" placeholder="请选择清单类型" style="width: 160px"> <el-select v-model="selectedEntityId" placeholder="请选择清单类型" style="width: 160px">
<el-option v-for="item in infoList" :key="item.id" :label="item.nameZh" :value="item.id" /> <el-option v-for="item in infoList" :key="item.id" :label="item.nameZh" :value="item.id" />
</el-select> </el-select>
</div> </div>
</template> </template>
<template #default> <template #default>
<EChart :option="trendOption" autoresize :style="{ height: '420px' }" <EChart
@chart-click="handleMultiBarChartClick" /> :option="trendOption"
autoresize
:style="{ height: '420px' }"
@chart-click="handleMultiBarChartClick"
/>
<div class="data-origin-box"> <div class="data-origin-box">
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text">进入实体清单的中国实体数量变化趋势,数据来源:美国商务部官网</div> <div class="data-origin-text">数据来源:美国商务部官网</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <AiButton @mouseenter="handleShowAiPane('trendChart')" />
<AiPane :aiContent="trendChart.interpretation" /> <AiPane
v-if="aiPaneVisible?.trendChart"
:aiContent="overviewAiContent.trendChart"
@mouseleave="handleHideAiPane('trendChart')"
/>
</div> </div>
</template> </template>
</custom-container> </custom-container>
...@@ -368,9 +381,13 @@ ...@@ -368,9 +381,13 @@
<el-row :gutter="16" style="width: 1600px; margin: 0 auto; margin-top: 39px; padding-bottom: 60px"> <el-row :gutter="16" style="width: 1600px; margin: 0 auto; margin-top: 39px; padding-bottom: 60px">
<CustomTitle id="position4" title="资源库" style="margin-top: 0px" /> <CustomTitle id="position4" title="资源库" style="margin-top: 0px" />
<div class="resource-tabs"> <div class="resource-tabs">
<div v-for="tab in resourceTabs" :key="tab.value" class="resource-tab-item" <div
v-for="tab in resourceTabs"
:key="tab.value"
class="resource-tab-item"
:class="{ active: activeResourceTab == tab.value, disabled: tab.disabled }" :class="{ active: activeResourceTab == tab.value, disabled: tab.disabled }"
@click="handleResourceTabClick(tab)"> @click="handleResourceTabClick(tab)"
>
{{ tab.label }} {{ tab.label }}
</div> </div>
</div> </div>
...@@ -383,15 +400,25 @@ ...@@ -383,15 +400,25 @@
<div class="box4-item" v-for="(item, idx) in sanctionProcessList" :key="item.title"> <div class="box4-item" v-for="(item, idx) in sanctionProcessList" :key="item.title">
<div class="box4-item-left"> <div class="box4-item-left">
<el-image :src="dotIcon" alt="图片" class="box4-item-left-icon" /> <el-image :src="dotIcon" alt="图片" class="box4-item-left-icon" />
<div class="box4-item-left-line" v-if="idx + 1 != sanctionProcessList.length"></div> <div
class="box4-item-left-line"
v-if="idx + 1 != sanctionProcessList.length"
></div>
</div> </div>
<div class="box4-item-right"> <div class="box4-item-right">
<div class="box4-item-right-header" @click="handleSanc(item)"> <div class="box4-item-right-header" @click="handleSanc(item)">
<span class="box4-item-right-header-title">{{ item.postDate }}{{ item.title }}</span> <span class="box4-item-right-header-title"
>{{ item.postDate }}{{ item.title }}</span
>
<span class="box4-item-right-header-desc">{{ item.desc }}</span> <span class="box4-item-right-header-desc">{{ item.desc }}</span>
</div> </div>
<el-tooltip effect="dark" :content="item.content" popper-class="common-prompt-popper" <el-tooltip
placement="top" :show-after="500"> effect="dark"
:content="item.content"
popper-class="common-prompt-popper"
placement="top"
:show-after="500"
>
<div class="box4-item-right-content"> <div class="box4-item-right-content">
{{ item.content }} {{ item.content }}
</div> </div>
...@@ -399,8 +426,12 @@ ...@@ -399,8 +426,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box4-footer" :style="{ marginTop: sanctionProcessList.length > 0 ? '0px' : 'auto' }"> <div
<el-button type="primary" link @click="handleGetMore">查看更多 class="box4-footer"
:style="{ marginTop: sanctionProcessList.length > 0 ? '0px' : 'auto' }"
>
<el-button type="primary" link @click="handleGetMore"
>查看更多
<el-icon> <el-icon>
<DArrowRight /> <DArrowRight />
</el-icon> </el-icon>
...@@ -417,13 +448,24 @@ ...@@ -417,13 +448,24 @@
</template> </template>
<template #default> <template #default>
<div class="box5"> <div class="box5">
<el-table :data="entitiesList" class="sanction-table" stripe empty-text="暂无数据" height="700px" <el-table
header-row-class-name="table-header" row-class-name="table-row"> :data="entitiesList"
class="sanction-table"
stripe
empty-text="暂无数据"
height="700px"
header-row-class-name="table-header"
row-class-name="table-row"
>
<el-table-column prop="name" label="实体名称" min-width="200"> <el-table-column prop="name" label="实体名称" min-width="200">
<template #default="scope"> <template #default="scope">
<div class="tableName" @click="handleCompClick(scope.row)"> <div class="tableName" @click="handleCompClick(scope.row)">
<el-image v-if="scope.row.img" class="box1-bottom-content-item-img" :src="scope.row.img" <el-image
alt=""></el-image> v-if="scope.row.img"
class="box1-bottom-content-item-img"
:src="scope.row.img"
alt=""
></el-image>
<div v-else class="box1-bottom-content-item-imgUndefined"> <div v-else class="box1-bottom-content-item-imgUndefined">
{{ {{
(scope.row.name || scope.row.enName)?.match( (scope.row.name || scope.row.enName)?.match(
...@@ -455,35 +497,22 @@ ...@@ -455,35 +497,22 @@
</template> </template>
</el-table-column> </el-table-column>
<!-- <el-table-column prop="strength" label="制裁强度" width="120" align="center">
<template #default="scope">
<div class="sanction-strength">
<div :class="['strength-bar', `strength-${scope.row.strength}`]"></div>
<span>{{ strengthLabels[scope.row.strength] }}</span>
</div>
</template>
</el-table-column> -->
<!-- <el-table-column prop="revenue" label="营收(亿元)" width="140" align="right">
<template #default="scope">
<span
:class="['revenue-cell', scope.row.revenue === '无营收数据' ? 'no-revenue' : '']"
>
{{ scope.row.revenue }}
</span>
</template>
</el-table-column> -->
<el-table-column prop="revenue" label="50%规则子企业" width="280" align="right"> <el-table-column prop="revenue" label="50%规则子企业" width="280" align="right">
<template #default="scope"> <template #default="scope">
<div class="num-item" v-if="scope.row.ruleOrgCount > 0"> <div class="num-item" v-if="scope.row.ruleOrgCount > 0">
<div class="name-item" :class="[ <div
'revenue-cell', class="name-item"
scope.row.revenue === '无营收数据' ? 'no-revenue' : '' :class="[
]"> 'revenue-cell',
scope.row.revenue === '无营收数据' ? 'no-revenue' : ''
]"
>
{{ scope.row.ruleOrgList[0].orgName }}...等 {{ scope.row.ruleOrgList[0].orgName }}...等
</div> </div>
<div style="width: 50px; color: #409eff; cursor: pointer" @click="handleOrgClick(scope.row)"> <div
style="width: 50px; color: #409eff; cursor: pointer"
@click="handleOrgClick(scope.row)"
>
{{ scope.row.ruleOrgCount }}家> {{ scope.row.ruleOrgCount }}家>
</div> </div>
</div> </div>
...@@ -495,8 +524,15 @@ ...@@ -495,8 +524,15 @@
<!-- <div class="pagination-info"> <!-- <div class="pagination-info">
第{{ currentPage }}页,共{{ totalPages }}页 第{{ currentPage }}页,共{{ totalPages }}页
</div> --> </div> -->
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total" <el-pagination
:pager-count="5" layout="prev, pager, next" background @current-change="handlePageChange" /> v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
:pager-count="5"
layout="prev, pager, next"
background
@current-change="handlePageChange"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -562,8 +598,14 @@ ...@@ -562,8 +598,14 @@
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="total-count">{{ totalAll }}</div> <div class="total-count">{{ totalAll }}</div>
<el-pagination v-model:current-page="currentPageAll" :page-size="pageSizeAll" :total="totalAll" <el-pagination
layout="prev, pager, next" background @current-change="handlePageChangeAll" /> v-model:current-page="currentPageAll"
:page-size="pageSizeAll"
:total="totalAll"
layout="prev, pager, next"
background
@current-change="handlePageChangeAll"
/>
</div> </div>
</div> </div>
</div> </div>
...@@ -576,8 +618,12 @@ ...@@ -576,8 +618,12 @@
</template> </template>
</el-row> </el-row>
</div> </div>
<RuleSubsidiaryDialog v-model="dialogVisible" :company-name="currentRuleCompany" :total-count="currentRuleCount" <RuleSubsidiaryDialog
:data-list="currentOrgList" /> v-model="dialogVisible"
:company-name="currentRuleCompany"
:total-count="currentRuleCount"
:data-list="currentOrgList"
/>
</div> </div>
<el-dialog v-model="mediaVisible" title="社交媒体信息" width="500" :before-close="handleMediaClose"> <el-dialog v-model="mediaVisible" title="社交媒体信息" width="500" :before-close="handleMediaClose">
<div class="dialog-content"> <div class="dialog-content">
...@@ -590,32 +636,34 @@ ...@@ -590,32 +636,34 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" :row="riskOverviewDetailRow" <RiskSignalOverviewDetailDialog
name-field="signalTitle" post-date-field="signalTime" risk-level-field="signalLevel" /> v-model="isRiskOverviewDetailOpen"
:row="riskOverviewDetailRow"
name-field="signalTitle"
post-date-field="signalTime"
risk-level-field="signalLevel"
/>
</template> </template>
<script setup> <script setup>
//这是一个备注 //这是一个备注
import NewsList from "@/components/base/newsList/index.vue"; import NewsList from "@/components/base/newsList/index.vue";
import RiskSignal from "@/components/base/riskSignal/index.vue"; import RiskSignal from "@/components/base/riskSignal/index.vue";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import RiskSignalOverviewDetailDialog from "@/components/base/RiskSignalOverviewDetailDialog/index.vue"; import RiskSignalOverviewDetailDialog from "@/components/base/RiskSignalOverviewDetailDialog/index.vue";
import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue"; import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue";
import { useContainerScroll } from "@/hooks/useScrollShow"; import { useContainerScroll } from "@/hooks/useScrollShow";
const homeMainRef = ref(null); const homeMainRef = ref(null);
const { isShow } = useContainerScroll(homeMainRef); const { isShow } = useContainerScroll(homeMainRef);
import * as echarts from "echarts";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import { ElMessage, ElMessageBox } from "element-plus";
import listPage from "./v2.0CommercialControlList/components/sanctionsOverview/components/listPage/index.vue"; import listPage from "./v2.0CommercialControlList/components/sanctionsOverview/components/listPage/index.vue";
import { DArrowRight, Warning, Search, Message } from "@element-plus/icons-vue";
import EChart from "@/components/Chart/index.vue"; import EChart from "@/components/Chart/index.vue";
import tipsIcon from "./assets/icons/info-icon.png"; import tipsIcon from "./assets/icons/info-icon.png";
import AiButton from "@/components/base/Ai/AiButton/index.vue"; import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue"; import AiPane from "@/components/base/Ai/AiPane/index.vue";
import AreaTag from "@/components/base/AreaTag/index.vue"; import AreaTag from "@/components/base/AreaTag/index.vue";
import { useChartInterpretation } from "@/views/exportControl/utils/common"; import { useChartInterpretation } from "@/views/exportControl/utils/common";
const sanctionCountChart = useChartInterpretation();
import { TAGTYPE } from "@/public/constant"; import { TAGTYPE } from "@/public/constant";
import { useGotoCompanyPages } from "@/router/modules/company"; import { useGotoCompanyPages } from "@/router/modules/company";
import { useGotoNewsDetail } from "@/router/modules/news"; import { useGotoNewsDetail } from "@/router/modules/news";
...@@ -886,11 +934,12 @@ onMounted(async () => { ...@@ -886,11 +934,12 @@ onMounted(async () => {
tags: item.domain tags: item.domain
}; };
}); });
entityListReleaseFreqChart.interpret({ entityListReleaseFreqChartData.value = entityListReleaseFreq.value;
type: "柱状图", // entityListReleaseFreqChart.interpret({
name: "美国商务部发布实体清单的频次", // type: "柱状图",
data: entityListReleaseFreq.value // name: "美国商务部发布实体清单的频次",
}); // data: entityListReleaseFreq.value
// });
commerceControlListReleaseFreq.value = _.map(cclList1, item => { commerceControlListReleaseFreq.value = _.map(cclList1, item => {
return { return {
year: item.year, year: item.year,
...@@ -899,11 +948,12 @@ onMounted(async () => { ...@@ -899,11 +948,12 @@ onMounted(async () => {
tags: item.domain tags: item.domain
}; };
}); });
commerceControlListReleaseFreqChart.interpret({ commerceControlListReleaseFreqChartData.value = commerceControlListReleaseFreq.value;
type: "柱状图", // commerceControlListReleaseFreqChart.interpret({
name: "美国商务部发布商业管制清单的频次", // type: "柱状图",
data: commerceControlListReleaseFreq.value // name: "美国商务部发布商业管制清单的频次",
}); // data: commerceControlListReleaseFreq.value
// });
// 获取趋势图数据 // 获取趋势图数据
await fetchTrendData(); await fetchTrendData();
...@@ -944,13 +994,136 @@ const fetchTrendData = async () => { ...@@ -944,13 +994,136 @@ const fetchTrendData = async () => {
}); });
if (res && res[0] && res[0].yearDomainCount) { if (res && res[0] && res[0].yearDomainCount) {
trendOption.value = processYearDomainCountData(res[0].yearDomainCount); trendOption.value = processYearDomainCountData(res[0].yearDomainCount);
trendChart.interpret({ type: "柱状图", name: "制裁清单数量增长趋势", data: res[0].yearDomainCount }); trendChartData.value = res[0].yearDomainCount;
// trendChart.interpret({ type: "柱状图", name: "制裁清单数量增长趋势", data: res[0].yearDomainCount });
} }
} catch (error) { } catch (error) {
console.error("获取趋势图数据失败:", error); console.error("获取趋势图数据失败:", error);
} }
}; };
const requestAiPaneContent = async key => {
if (!key || aiPaneLoading.value[key] || aiPaneFetched.value[key]) return;
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: true };
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成中..." };
try {
const payload = buildAiChartPayload(key);
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
overviewAiContent.value = {
...overviewAiContent.value,
[key]: base + chunk
};
}
}
);
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
const interpretation = first?.解读 || first?.["解读"];
// 流式已渲染过内容,最终用解析出的解读覆盖(保证显示格式统一)
if (interpretation) {
overviewAiContent.value = {
...overviewAiContent.value,
[key]: interpretation
};
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: false };
}
};
const trendChartData = ref([]);
const radarChartData = ref([]);
const entityListReleaseFreqChartData = ref([]);
const commerceControlListReleaseFreqChartData = ref([]);
const aiPaneVisible = ref({
trendChart: false,
radarChart: false,
entityListReleaseFreqChart: false,
commerceControlListReleaseFreqChart: false
});
const overviewAiContent = ref({
trendChart: "智能总结生成中...",
radarChart: "智能总结生成中...",
entityListReleaseFreqChart: "智能总结生成中...",
commerceControlListReleaseFreqChart: "智能总结生成中..."
});
const aiPaneFetched = ref({
trendChart: false,
radarChart: false,
entityListReleaseFreqChart: false,
commerceControlListReleaseFreqChart: false
});
const aiPaneLoading = ref({
trendChart: false,
radarChart: false,
entityListReleaseFreqChart: false,
commerceControlListReleaseFreqChart: false
});
const chartLoading = ref({
trendChart: false,
radarChart: false,
entityListReleaseFreqChart: false,
commerceControlListReleaseFreqChart: false
});
const buildAiChartPayload = key => {
if (key === "trendChart") {
return { type: "柱状图", name: "制裁清单数量增长趋势", data: trendChartData.value };
}
if (key === "radarChart") {
return { type: "雷达图", name: "实体清单领域分布情况", data: radarChartData.value };
}
if (key === "entityListReleaseFreqChart") {
return {
type: "柱状图",
name: "美国商务部发布实体清单的频次",
data: entityListReleaseFreqChartData.value
};
}
if (key === "commerceControlListReleaseFreqChart") {
return {
type: "柱状图",
name: "美国商务部发布商业管制清单的频次",
data: commerceControlListReleaseFreqChartData.value
};
}
return { type: "", name: "", data: [] };
};
const handleShowAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: true
};
requestAiPaneContent(key);
};
const handleHideAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: false
};
};
watch( watch(
() => [trendChecked.value, selectedEntityId.value], () => [trendChecked.value, selectedEntityId.value],
() => { () => {
...@@ -965,6 +1138,7 @@ const processYearDomainCountData = yearDomainCountData => { ...@@ -965,6 +1138,7 @@ const processYearDomainCountData = yearDomainCountData => {
// 提取所有领域名称 // 提取所有领域名称
const allDomains = [...new Set(yearDomainCountData.flatMap(item => item.domainCountInfo.map(domain => domain.name)))]; const allDomains = [...new Set(yearDomainCountData.flatMap(item => item.domainCountInfo.map(domain => domain.name)))];
console.log("不同领域的数据 =>", allDomains);
// 构造 getMultipleBarChart_m 所需的数据结构 // 构造 getMultipleBarChart_m 所需的数据结构
const chartData = { const chartData = {
...@@ -991,7 +1165,7 @@ const processYearDomainCountData = yearDomainCountData => { ...@@ -991,7 +1165,7 @@ const processYearDomainCountData = yearDomainCountData => {
}; };
}) })
}; };
console.log("不同领域的数据 chartData", chartData);
// 使用 getMultipleBarChart_m 生成图表配置 // 使用 getMultipleBarChart_m 生成图表配置
return getMultipleBarChart_m(chartData); return getMultipleBarChart_m(chartData);
}; };
...@@ -1218,7 +1392,8 @@ const fetchRadarData = async checked => { ...@@ -1218,7 +1392,8 @@ const fetchRadarData = async checked => {
} }
}; };
}); });
radarChart.interpret({ type: "雷达图", name: "实体清单领域分布情况", data: data }); radarChartData.value = data;
// radarChart.interpret({ type: "雷达图", name: "实体清单领域分布情况", data: data });
} }
} catch (error) { } catch (error) {
console.error("获取雷达图数据失败:", error); console.error("获取雷达图数据失败:", error);
...@@ -1318,7 +1493,7 @@ const fetchSanctionList = async () => { ...@@ -1318,7 +1493,7 @@ const fetchSanctionList = async () => {
}); });
totalAll.value = res.totalElements; totalAll.value = res.totalElements;
} }
} catch (error) { } } catch (error) {}
}; };
const handlePageChangeAll = val => { const handlePageChangeAll = val => {
...@@ -1612,7 +1787,7 @@ const handleGetHylyList = async () => { ...@@ -1612,7 +1787,7 @@ const handleGetHylyList = async () => {
hylymc: "全部分类" hylymc: "全部分类"
}; };
categoryList.value = [obj, ...categoryList.value]; categoryList.value = [obj, ...categoryList.value];
} catch (error) { } } catch (error) {}
}; };
const chart1Data = ref({ const chart1Data = ref({
...@@ -2218,7 +2393,6 @@ const handleMediaClick = item => { ...@@ -2218,7 +2393,6 @@ const handleMediaClick = item => {
} }
.box3-content { .box3-content {
// flex: 1; // flex: 1;
.el-progress--line { .el-progress--line {
width: 82px; width: 82px;
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="nav-item" @click="handleToDataLibrary2"> <div class="nav-item" @click="handleToDataLibrary2">
<div class="item-position"></div> <div class="item-position"></div>
<div class="content"> <div class="content">
<div class="info"> <div class="info">
...@@ -54,107 +54,94 @@ ...@@ -54,107 +54,94 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<EChart :option="domainChartOption" autoresize :style="{ height: '300px', padding: '0 20px' }" <EChart
@chart-click="handleToDataLibrary3" /> :option="domainChartOption"
autoresize
:style="{ height: '300px', padding: '0 20px' }"
@chart-click="handleToDataLibrary3"
/>
<div class="data-origin-box"> <div class="data-origin-box">
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text">进入本次实体清单的中国实体领域分布情况,数据来源:美国商务部官网</div> <div class="data-origin-text">数据来源:美国商务部官网</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <!-- <AiButton />
<AiPane :aiContent="domainChart.interpretation" /> <AiPane :aiContent="domainChart.interpretation" /> -->
<AiButton @mouseenter="handleShowAiPane('domainChart')" />
<AiPane
v-if="aiPaneVisible?.domainChart"
:aiContent="overviewAiContent.domainChart"
@mouseleave="handleHideAiPane('domainChart')"
/>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="main-item"> <div class="main-item">
<!-- <div class="title-com">
<div class="box"></div>
<div class="text">制裁实体类型分布情况</div>
<div class="right-group">
<div class="btn">
<img src="../../assets/数据库按钮.png" alt="" />
<img src="../../assets/下载按钮.png" alt="" />
<img src="../../assets/收藏按钮.png" alt="" />
</div>
</div>
</div>
<div class="echarts" ref="typeChartRef"></div>
<div class="bottom">
<div class="ai">
<div class="left">
<img :src="ai" alt="" class="icon1" />
<div class="text">我国被制裁实体以企业、科研院所和高校为主。</div>
</div>
<div class="right">
<img :src="right" alt="" class="icon2" />
</div>
</div>
</div> -->
<AnalysisBox title="制裁实体类型分布情况"> <AnalysisBox title="制裁实体类型分布情况">
<!-- <div class="echarts" ref="typeChartRef"></div> <EChart
<div class="bottom"> :option="typeChartOption"
<div class="ai"> autoresize
<div class="left"> :style="{ height: '300px', padding: '0 20px' }"
<img :src="ai" alt="" class="icon1" /> @chart-click="handleToDataLibrary4"
<div class="text">我国被制裁实体以企业、科研院所和高校为主。</div> />
</div>
<div class="right">
<img :src="right" alt="" class="icon2" />
</div>
</div>
</div> -->
<EChart :option="typeChartOption" autoresize :style="{ height: '300px', padding: '0 20px' }"
@chart-click="handleToDataLibrary4" />
<div class="data-origin-box"> <div class="data-origin-box">
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text">进入本次实体清单的中国实体类型分布情况,数据来源:美国商务部官网</div> <div class="data-origin-text">数据来源:美国商务部官网</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <!-- <AiButton />
<AiPane :aiContent="typeChart.interpretation" /> <AiPane :aiContent="typeChart.interpretation" /> -->
<AiButton @mouseenter="handleShowAiPane('typeChart')" />
<AiPane
v-if="aiPaneVisible?.typeChart"
:aiContent="overviewAiContent.typeChart"
@mouseleave="handleHideAiPane('typeChart')"
/>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="main-item"> <div class="main-item">
<AnalysisBox title="制裁实体国家地区分布情况"> <AnalysisBox title="制裁实体国家地区分布情况">
<div class="country-list"> <div class="country-list">
<div class="list-item" v-for="(item, index) in countryDistribution" :key="index" <div
@click="handleToDataLibrary5(item)"> class="list-item"
v-for="(item, index) in countryDistribution"
:key="index"
@click="handleToDataLibrary5(item)"
>
<img :src="flag" alt="" class="flag" /> <img :src="flag" alt="" class="flag" />
<div class="country-name">{{ item.name }}</div> <div class="country-name">{{ item.name }}</div>
<div class="progress-bar-container"> <div class="progress-bar-container">
<div class="progress-bar" :style="{ <div
width: item.width, class="progress-bar"
background: item.gradient :style="{
}"></div> width: item.width,
background: item.gradient
}"
></div>
</div> </div>
<div class="count" :class="{ highlight: index === 0 }">{{ item.count }}</div> <div class="count" :class="{ highlight: index === 0 }">{{ item.count }}</div>
</div> </div>
</div> </div>
<!-- <div class="bottom">
<div class="ai">
<div class="left">
<img :src="ai" alt="" class="icon1" />
<div class="text">美国对中国的制裁近年来呈现显著增长趋势。</div>
</div>
<div class="right">
<img :src="right" alt="" class="icon2" />
</div>
</div>
</div> -->
<div class="data-origin-box"> <div class="data-origin-box">
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text">进入本次实体清单的实体国家地区分布情况,数据来源:美国商务部官网</div> <div class="data-origin-text">数据来源:美国商务部官网</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <!-- <AiButton />
<AiPane :aiContent="countryDistributionChart.interpretation" /> <AiPane :aiContent="countryDistributionChart.interpretation" /> -->
<AiButton @mouseenter="handleShowAiPane('countryDistributionChart')" />
<AiPane
v-if="aiPaneVisible?.countryDistributionChart"
:aiContent="overviewAiContent.countryDistributionChart"
@mouseleave="handleHideAiPane('countryDistributionChart')"
/>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -163,40 +150,42 @@ ...@@ -163,40 +150,42 @@
<div class="map-wrapper"> <div class="map-wrapper">
<div class="map-chart" ref="mapChartRef"></div> <div class="map-chart" ref="mapChartRef"></div>
<div class="rank-list"> <div class="rank-list">
<div class="rank-item" v-for="(item, index) in regionDistribution" :key="index" <div
@click="handleToDataLibrary6(item)"> class="rank-item"
v-for="(item, index) in regionDistribution"
:key="index"
@click="handleToDataLibrary6(item)"
>
<div class="rank-index" :class="'rank-' + (index + 1)">{{ index + 1 }}</div> <div class="rank-index" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
<div class="rank-name">{{ item.name }}</div> <div class="rank-name">{{ item.name }}</div>
<div class="rank-bar-bg"> <div class="rank-bar-bg">
<div class="rank-bar-fill" :style="{ <div
width: (maxRegionCount > 0 ? (item.count / maxRegionCount) * 100 : 0) + '%', class="rank-bar-fill"
background: getBarColor(index) :style="{
}"></div> width: (maxRegionCount > 0 ? (item.count / maxRegionCount) * 100 : 0) + '%',
background: getBarColor(index)
}"
></div>
</div> </div>
<div class="rank-value">{{ item.count }}</div> <div class="rank-value">{{ item.count }}</div>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="bottom">
<div class="ai">
<div class="left">
<img :src="ai" alt="" class="icon1" />
<div class="text">我国被制裁实体多分布于沿海经济活跃省份。</div>
</div>
<div class="right">
<img :src="right" alt="" class="icon2" />
</div>
</div>
</div> -->
<div class="data-origin-box"> <div class="data-origin-box">
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text">进入本次实体清单的中国实体各省分布情况,数据来源:美国商务部官网</div> <div class="data-origin-text">数据来源:美国商务部官网</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <!-- <AiButton />
<AiPane :aiContent="regionDistributionChart.interpretation" /> <AiPane :aiContent="regionDistributionChart.interpretation" /> -->
<AiButton @mouseenter="handleShowAiPane('regionDistributionChart')" />
<AiPane
v-if="aiPaneVisible?.regionDistributionChart"
:aiContent="overviewAiContent.regionDistributionChart"
@mouseleave="handleHideAiPane('regionDistributionChart')"
/>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -223,9 +212,9 @@ import { ...@@ -223,9 +212,9 @@ import {
getSingleSanctionEntityCountryCount, getSingleSanctionEntityCountryCount,
getSingleSanctionEntityRegionCount getSingleSanctionEntityRegionCount
} from "@/api/exportControlV2.0"; } from "@/api/exportControlV2.0";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import { useChartInterpretation } from "@/views/exportControl/utils/common"; import { useChartInterpretation } from "@/views/exportControl/utils/common";
const sanctionCountChart = useChartInterpretation();
const domainChart = useChartInterpretation(); const domainChart = useChartInterpretation();
const typeChart = useChartInterpretation(); const typeChart = useChartInterpretation();
const countryDistributionChart = useChartInterpretation(); const countryDistributionChart = useChartInterpretation();
...@@ -248,8 +237,9 @@ const getRegionData = async () => { ...@@ -248,8 +237,9 @@ const getRegionData = async () => {
if (res.code === 200) { if (res.code === 200) {
regionDistribution.value = res.data || []; regionDistribution.value = res.data || [];
maxRegionCount.value = Math.max(...regionDistribution.value.map(item => item.count), 0); maxRegionCount.value = Math.max(...regionDistribution.value.map(item => item.count), 0);
regionDistributionChartData.value = res.data || [];
initMapChart(); initMapChart();
regionDistributionChart.interpret({ type: "柱状图", name: "进入本次实体清单的中国实体各省分布情况", data: res.data }); // regionDistributionChart.interpret({ type: "柱状图", name: "进入本次实体清单的中国实体各省分布情况", data: res.data });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -289,11 +279,12 @@ const getCountryCount = async () => { ...@@ -289,11 +279,12 @@ const getCountryCount = async () => {
gradient gradient
}; };
}); });
countryDistributionChart.interpret({ countryDistributionChartData.value = res.data || [];
type: "柱状图", // countryDistributionChart.interpret({
name: "进入本次实体清单的实体国家地区分布情况", // type: "柱状图",
data: res.data // name: "进入本次实体清单的实体国家地区分布情况",
}); // data: res.data
// });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -314,7 +305,7 @@ const getEntityTypeCount = async () => { ...@@ -314,7 +305,7 @@ const getEntityTypeCount = async () => {
const res = await getSingleSanctionEntityTypeCount(params); const res = await getSingleSanctionEntityTypeCount(params);
if (res.code === 200) { if (res.code === 200) {
entityTypeCount.value = res.data || []; entityTypeCount.value = res.data || [];
typeChart.interpret({ type: "饼图", name: "进入本次实体清单的中国实体类型分布情况", data: entityTypeCount.value }); // typeChart.interpret({ type: "饼图", name: "进入本次实体清单的中国实体类型分布情况", data: entityTypeCount.value });
initTypeChart(); initTypeChart();
} }
} catch (error) { } catch (error) {
...@@ -337,7 +328,7 @@ const getDomainCount = async () => { ...@@ -337,7 +328,7 @@ const getDomainCount = async () => {
if (res.code === 200) { if (res.code === 200) {
domainCount.value = res.data || []; domainCount.value = res.data || [];
initDomainChart(); initDomainChart();
domainChart.interpret({ type: "饼图", name: "进入本次实体清单的中国实体领域分布情况", data: domainCount.value }); // domainChart.interpret({ type: "饼图", name: "进入本次实体清单的中国实体领域分布情况", data: domainCount.value });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -351,7 +342,7 @@ const getTotalCount = async () => { ...@@ -351,7 +342,7 @@ const getTotalCount = async () => {
if (!sanRecordId.value) return; if (!sanRecordId.value) return;
try { try {
const res = await getSingleSanctionTotalCount(route.query.sanTypeId, sanRecordId.value); const res = await getSingleSanctionTotalCount(route.query.sanTypeId, sanRecordId.value);
console.log('统计', res); console.log("统计", res);
if (res.code === 200) { if (res.code === 200) {
totalCount.value = res.data || {}; totalCount.value = res.data || {};
...@@ -389,14 +380,6 @@ for (let i = 2025; i >= 2000; i--) { ...@@ -389,14 +380,6 @@ for (let i = 2025; i >= 2000; i--) {
timeOptions.push({ label: `${i}年`, value: `${i}` }); timeOptions.push({ label: `${i}年`, value: `${i}` });
} }
// const countryDistribution = [
// { name: "中国", count: 24, width: "80%", gradient: "linear-gradient(90deg, rgba(205, 66, 70, 0) 0%, rgba(205, 66, 70, 1) 100%)" },
// { name: "沙特阿拉伯", count: 2, width: "60%", gradient: "linear-gradient(90deg, rgba(255, 172, 77, 0) 0%, rgba(255, 172, 77, 1) 100%)" },
// { name: "伊朗", count: 2, width: "60%", gradient: "linear-gradient(90deg, rgba(255, 172, 77, 0) 0%, rgba(255, 172, 77, 1) 100%)" },
// { name: "俄罗斯", count: 2, width: "55%", gradient: "linear-gradient(90deg, rgba(255, 172, 77, 0) 0%, rgba(255, 172, 77, 1) 100%)" },
// { name: "中国香港", count: 1, width: "40%", gradient: "linear-gradient(90deg, rgba(5, 95, 194, 0) 0%, rgba(5, 95, 194, 1) 100%)" }
// ];
const mapChartRef = ref(null); const mapChartRef = ref(null);
const domainChartRef = ref(null); const domainChartRef = ref(null);
const typeChartRef = ref(null); const typeChartRef = ref(null);
...@@ -474,13 +457,13 @@ const initMapChart = () => { ...@@ -474,13 +457,13 @@ const initMapChart = () => {
chart.setOption(option); chart.setOption(option);
chart.on('click', function (params) { chart.on("click", function (params) {
const param = { const param = {
selectedProvince: params.name, selectedProvince: params.name,
selectedDate: JSON.stringify([route.query.date, route.query.date]) selectedDate: JSON.stringify([route.query.date, route.query.date])
} };
const curRoute = router.resolve({ const curRoute = router.resolve({
path: '/dataLibrary/dataEntityList', path: "/dataLibrary/dataEntityList",
query: param query: param
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
...@@ -855,67 +838,67 @@ const initTypeChart = () => { ...@@ -855,67 +838,67 @@ const initTypeChart = () => {
const sanTypeId = ref(""); const sanTypeId = ref("");
// 制裁实体领域分布情况 // 制裁实体领域分布情况
const handleToDataLibrary3 = (val) => { const handleToDataLibrary3 = val => {
// console.log('val', val); // console.log('val', val);
const params = { const params = {
domains: val.name, domains: val.name,
isCnEntityOnly: true, isCnEntityOnly: true,
selectedDate: JSON.stringify([route.query.date, route.query.date]) selectedDate: JSON.stringify([route.query.date, route.query.date])
} };
const curRoute = router.resolve({ const curRoute = router.resolve({
path: '/dataLibrary/dataEntityList', path: "/dataLibrary/dataEntityList",
query: params query: params
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
} };
// 制裁实体类型分布情况 // 制裁实体类型分布情况
const handleToDataLibrary4 = (val) => { const handleToDataLibrary4 = val => {
// console.log('val', val); // console.log('val', val);
const params = { const params = {
selectedEntityType: val.name, selectedEntityType: val.name,
isCnEntityOnly: true, isCnEntityOnly: true,
selectedDate: JSON.stringify([route.query.date, route.query.date]) selectedDate: JSON.stringify([route.query.date, route.query.date])
} };
const curRoute = router.resolve({ const curRoute = router.resolve({
path: '/dataLibrary/dataEntityList', path: "/dataLibrary/dataEntityList",
query: params query: params
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
} };
// 制裁实体国家地区分布情况 // 制裁实体国家地区分布情况
const handleToDataLibrary5 = (item) => { const handleToDataLibrary5 = item => {
const params = { const params = {
selectedCountryId: item.id, selectedCountryId: item.id,
isCnEntityOnly: true, isCnEntityOnly: true,
selectedDate: JSON.stringify([route.query.date, route.query.date]) selectedDate: JSON.stringify([route.query.date, route.query.date])
} };
const curRoute = router.resolve({ const curRoute = router.resolve({
path: '/dataLibrary/dataEntityList', path: "/dataLibrary/dataEntityList",
query: params query: params
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
} };
// 制裁实体各省分布情况 // 制裁实体各省分布情况
const handleToDataLibrary6 = (item) => { const handleToDataLibrary6 = item => {
console.log('item', item); console.log("item", item);
const params = { const params = {
selectedProvince: item.name, selectedProvince: item.name,
isCnEntityOnly: true, isCnEntityOnly: true,
selectedDate: JSON.stringify([route.query.date, route.query.date]) selectedDate: JSON.stringify([route.query.date, route.query.date])
} };
const curRoute = router.resolve({ const curRoute = router.resolve({
path: '/dataLibrary/dataEntityList', path: "/dataLibrary/dataEntityList",
query: params query: params
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
} };
// 跳转到数据资源库 // 跳转到数据资源库
const handleToDataLibrary = () => { const handleToDataLibrary = () => {
const dateStr = route.query.date ? route.query.date : '' const dateStr = route.query.date ? route.query.date : "";
const curRoute = router.resolve({ const curRoute = router.resolve({
path: "/dataLibrary/dataEntityList", path: "/dataLibrary/dataEntityList",
query: { query: {
...@@ -924,10 +907,10 @@ const handleToDataLibrary = () => { ...@@ -924,10 +907,10 @@ const handleToDataLibrary = () => {
} }
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
} };
const handleToDataLibrary1 = () => { const handleToDataLibrary1 = () => {
const dateStr = route.query.date ? route.query.date : '' const dateStr = route.query.date ? route.query.date : "";
const curRoute = router.resolve({ const curRoute = router.resolve({
path: "/dataLibrary/dataEntityList", path: "/dataLibrary/dataEntityList",
query: { query: {
...@@ -937,10 +920,10 @@ const handleToDataLibrary1 = () => { ...@@ -937,10 +920,10 @@ const handleToDataLibrary1 = () => {
} }
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
} };
const handleToDataLibrary2 = () => { const handleToDataLibrary2 = () => {
const dateStr = route.query.date ? route.query.date : '' const dateStr = route.query.date ? route.query.date : "";
const curRoute = router.resolve({ const curRoute = router.resolve({
path: "/dataLibrary/dataEntityList", path: "/dataLibrary/dataEntityList",
query: { query: {
...@@ -949,8 +932,129 @@ const handleToDataLibrary2 = () => { ...@@ -949,8 +932,129 @@ const handleToDataLibrary2 = () => {
} }
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
} };
const requestAiPaneContent = async key => {
if (!key || aiPaneLoading.value[key] || aiPaneFetched.value[key]) return;
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: true };
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成中..." };
try {
const payload = buildAiChartPayload(key);
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
overviewAiContent.value = {
...overviewAiContent.value,
[key]: base + chunk
};
}
}
);
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
const interpretation = first?.解读 || first?.["解读"];
// 流式已渲染过内容,最终用解析出的解读覆盖(保证显示格式统一)
if (interpretation) {
overviewAiContent.value = {
...overviewAiContent.value,
[key]: interpretation
};
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: false };
}
};
const domainChartData = ref([]);
const typeChartData = ref([]);
const countryDistributionChartData = ref([]);
const regionDistributionChartData = ref([]);
const aiPaneVisible = ref({
domainChart: false,
typeChart: false,
countryDistributionChart: false,
regionDistributionChart: false
});
const overviewAiContent = ref({
domainChart: "智能总结生成中...",
typeChart: "智能总结生成中...",
countryDistributionChart: "智能总结生成中...",
regionDistributionChart: "智能总结生成中..."
});
const aiPaneFetched = ref({
domainChart: false,
typeChart: false,
countryDistributionChart: false,
regionDistributionChart: false
});
const aiPaneLoading = ref({
domainChart: false,
typeChart: false,
countryDistributionChart: false,
regionDistributionChart: false
});
const chartLoading = ref({
domainChart: false,
typeChart: false,
countryDistributionChart: false,
regionDistributionChart: false
});
const buildAiChartPayload = key => {
if (key === "domainChart") {
return { type: "饼图", name: "进入本次实体清单的中国实体领域分布情况", data: domainCount.value };
}
if (key === "typeChart") {
return { type: "饼图", name: "进入本次实体清单的中国实体类型分布情况", data: entityTypeCount.value };
}
if (key === "countryDistributionChart") {
return {
type: "柱状图",
name: "进入本次实体清单的实体国家地区分布情况",
data: countryDistributionChartData.value
};
}
if (key === "regionDistributionChart") {
return {
type: "柱状图",
name: "进入本次实体清单的中国实体各省分布情况",
data: regionDistributionChartData.value
};
}
return { type: "", name: "", data: [] };
};
const handleShowAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: true
};
requestAiPaneContent(key);
};
const handleHideAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: false
};
};
onMounted(() => { onMounted(() => {
// 获取路由参数id // 获取路由参数id
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="info-row"> <div class="info-row">
<div class="label">发布机构:</div> <div class="label">发布机构:</div>
<div class="value link"> <div class="value link">
<img :src="title" alt="" class="icon" /> <img :src="formattedData.postOrgLogoUrl || title" alt="" class="icon" />
<span @click="handleClickDp">{{ formattedData.postOrgName }} ></span> <span @click="handleClickDp">{{ formattedData.postOrgName }} ></span>
</div> </div>
</div> </div>
...@@ -45,15 +45,22 @@ ...@@ -45,15 +45,22 @@
<div class="left-top-content"> <div class="left-top-content">
<div class="content-title">制裁实体分布:</div> <div class="content-title">制裁实体分布:</div>
<div class="distribution-list"> <div class="distribution-list">
<div class="list-item" v-for="(item, index) in entityDistribution" :key="index" <div
@click="handleToDataLibrary(item)"> class="list-item"
v-for="(item, index) in entityDistribution"
:key="index"
@click="handleToDataLibrary(item)"
>
<img :src="item.imageUrl || flag" alt="" class="flag" /> <img :src="item.imageUrl || flag" alt="" class="flag" />
<div class="country-name">{{ item.name }}</div> <div class="country-name">{{ item.name }}</div>
<div class="progress-bar-container"> <div class="progress-bar-container">
<div class="progress-bar" :style="{ <div
width: item.width, class="progress-bar"
background: item.gradient :style="{
}"></div> width: item.width,
background: item.gradient
}"
></div>
</div> </div>
<div class="count" :class="{ highlight: index === 0 }">{{ item.count }}</div> <div class="count" :class="{ highlight: index === 0 }">{{ item.count }}</div>
</div> </div>
...@@ -96,13 +103,25 @@ ...@@ -96,13 +103,25 @@
</div> </div>
<div class="filter-right"> <div class="filter-right">
<el-checkbox v-model="onlyChina" label="只看中国实体" /> <el-checkbox v-model="onlyChina" label="只看中国实体" />
<el-select v-model="filterField" placeholder="选择领域" style="width: 150px; margin: 0 12px 0 16px"> <el-select
v-model="filterField"
placeholder="选择领域"
style="width: 150px; margin: 0 12px 0 16px"
>
<!-- <el-option label="全部领域" value="" /> --> <!-- <el-option label="全部领域" value="" /> -->
<el-option v-for="item in domainOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option
v-for="item in domainOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
<el-input v-model="searchKeyword" placeholder="搜索实体" <el-input
v-model="searchKeyword"
placeholder="搜索实体"
style="width: 150px; border: 1px solid rgba(170, 173, 177, 0.4); border-radius: 5px" style="width: 150px; border: 1px solid rgba(170, 173, 177, 0.4); border-radius: 5px"
:suffix-icon="Search" /> :suffix-icon="Search"
/>
</div> </div>
</div> </div>
<div class="stats-row"> <div class="stats-row">
...@@ -117,14 +136,21 @@ ...@@ -117,14 +136,21 @@
<div class="stats-info"> <div class="stats-info">
<div class="stat-item"> <div class="stat-item">
<span class="dot red"></span> <span class="dot red"></span>
<span class="text">新增 <span class="num red">{{ addCount }}</span> 家 (50%规则涉及<span class="num red">{{ <span class="text"
addRuleCount >新增 <span class="num red">{{ addCount }}</span> 家 (50%规则涉及<span class="num red">{{
}}</span>家)</span> addRuleCount
}}</span
>家)</span
>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span class="dot green"></span> <span class="dot green"></span>
<span class="text">移除 <span class="num green">{{ removeCount }}</span> 家 (50%规则涉及<span <span class="text"
class="num green">{{ removeRuleCount }}</span>家)</span> >移除 <span class="num green">{{ removeCount }}</span> 家 (50%规则涉及<span
class="num green"
>{{ removeRuleCount }}</span
>家)</span
>
</div> </div>
</div> </div>
</div> </div>
...@@ -156,7 +182,11 @@ ...@@ -156,7 +182,11 @@
>{{ item }}</span >{{ item }}</span
> --> > -->
<div class="domain-box"> <div class="domain-box">
<AreaTag v-for="(domain, index) in scope.row.fields" :key="index" :tagName="domain" /> <AreaTag
v-for="(domain, index) in scope.row.fields"
:key="index"
:tagName="domain"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
...@@ -165,8 +195,11 @@ ...@@ -165,8 +195,11 @@
<el-table-column prop="revenue" label="营收(亿元)" width="110" align="center" /> <el-table-column prop="revenue" label="营收(亿元)" width="110" align="center" />
<el-table-column label="50%规则子企业" width="180" align="center"> <el-table-column label="50%规则子企业" width="180" align="center">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.subsidiaryCount" class="subsidiary-link" <span
@click="handleSubsidiaryClick(scope.row)"> v-if="scope.row.subsidiaryCount"
class="subsidiary-link"
@click="handleSubsidiaryClick(scope.row)"
>
{{ scope.row.subsidiaryText }} {{ scope.row.subsidiaryText }}
<span class="blue-text">{{ scope.row.subsidiaryCount }}家 ></span> <span class="blue-text">{{ scope.row.subsidiaryCount }}家 ></span>
</span> </span>
...@@ -184,8 +217,12 @@ ...@@ -184,8 +217,12 @@
</div> </div>
</div> </div>
<!-- 50%规则子企业弹框 --> <!-- 50%规则子企业弹框 -->
<RuleSubsidiaryDialog v-model="subsidiaryDialogVisible" :company-name="currentSubsidiaryCompanyName" <RuleSubsidiaryDialog
:total-count="currentSubsidiaryCount" :data-list="currentSubsidiaryList" /> v-model="subsidiaryDialogVisible"
:company-name="currentSubsidiaryCompanyName"
:total-count="currentSubsidiaryCount"
:data-list="currentSubsidiaryList"
/>
</div> </div>
</template> </template>
...@@ -310,8 +347,8 @@ const getSanctionOverviewList = async () => { ...@@ -310,8 +347,8 @@ const getSanctionOverviewList = async () => {
subsidiaryText: subsidiaryText:
org.ruleOrgList && org.ruleOrgList.length > 0 org.ruleOrgList && org.ruleOrgList.length > 0
? (org.ruleOrgList[0].orgName.length > 10 ? (org.ruleOrgList[0].orgName.length > 10
? org.ruleOrgList[0].orgName.slice(0, 10) + "..." ? org.ruleOrgList[0].orgName.slice(0, 10) + "..."
: org.ruleOrgList[0].orgName) + "...等" : org.ruleOrgList[0].orgName) + "...等"
: "" : ""
})) }))
})); }));
...@@ -445,7 +482,8 @@ const formattedData = computed(() => { ...@@ -445,7 +482,8 @@ const formattedData = computed(() => {
administrativeOrderId: info.administrativeOrderId ? `No. ${info.administrativeOrderId}` : "", administrativeOrderId: info.administrativeOrderId ? `No. ${info.administrativeOrderId}` : "",
postPersonName: info.postPersonName, postPersonName: info.postPersonName,
domains: info.domainNames, domains: info.domainNames,
avartar: info.postPersonAvatarUrl avartar: info.postPersonAvatarUrl,
postOrgLogoUrl: info.postOrgLogoUrl
}; };
}); });
...@@ -525,23 +563,22 @@ const entityDistribution = ref([ ...@@ -525,23 +563,22 @@ const entityDistribution = ref([
const sanTypeId = ref(""); const sanTypeId = ref("");
// 跳转到数据资源库 // 跳转到数据资源库
const handleToDataLibrary = (item) => { const handleToDataLibrary = item => {
console.log('item', item); console.log("item", item);
const dateStr = formattedData.value.postDate.replace(/(\d{4})(\d{1,2})(\d{1,2})日/, (_, y, m, d) => const dateStr = formattedData.value.postDate.replace(
`${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}` /(\d{4})(\d{1,2})(\d{1,2})日/,
(_, y, m, d) => `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`
); );
const route = router.resolve({ const route = router.resolve({
path: "/dataLibrary/dataEntityList", path: "/dataLibrary/dataEntityList",
query:{ query: {
selectedDate: JSON.stringify([dateStr,dateStr]), selectedDate: JSON.stringify([dateStr, dateStr]),
selectedCountryId: item.id selectedCountryId: item.id
} }
}); });
window.open(route.href, "_blank"); window.open(route.href, "_blank");
};
}
onMounted(() => { onMounted(() => {
// 获取路由参数中的sanTypeId // 获取路由参数中的sanTypeId
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
object-fit: contain; object-fit: contain;
" /> " />
</div> </div>
<div class="translate-text">{{ "显示文" }}</div> <div class="translate-text">{{ "显示文" }}</div>
</div> </div>
<div class="btn" @click="handleDownload"> <div class="btn" @click="handleDownload">
<div class="icon"> <div class="icon">
...@@ -62,13 +62,13 @@ ...@@ -62,13 +62,13 @@
</div> </div>
</div> </div>
<div class="report-box"> <div class="report-box">
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div>
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage"> <div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage" <pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
class="pdf-pane-inner" /> class="pdf-pane-inner" />
</div> </div>
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -43,8 +43,8 @@ defineProps({ ...@@ -43,8 +43,8 @@ defineProps({
.title-text { .title-text {
color: rgba(10, 18, 30, 1); color: rgba(10, 18, 30, 1);
font-size: 32px; font-size: 32px;
font-family: $base-font-family; font-family: "Microsoft YaHei";
font-weight: bold; font-weight: 700;
margin-left: 20px; margin-left: 20px;
white-space: nowrap; white-space: nowrap;
} }
......
...@@ -234,13 +234,17 @@ ...@@ -234,13 +234,17 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text"> <div class="data-origin-text">数据来源:美国财政部海外资产管理办公室官网</div>
美国商务部发布实体清单的频次,数据来源:美国财政部海外资产管理办公室官网
</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <!-- <AiButton />
<AiPane :aiContent="entityListReleaseFreqChart.interpretation" /> <AiPane :aiContent="entityListReleaseFreqChart.interpretation" /> -->
<AiButton @mouseenter="handleShowAiPane('entityListReleaseFreqChart')" />
<AiPane
v-if="aiPaneVisible?.entityListReleaseFreqChart"
:aiContent="overviewAiContent.entityListReleaseFreqChart"
@mouseleave="handleHideAiPane('entityListReleaseFreqChart')"
/>
</div> </div>
</div> </div>
<div class="box3-content"> <div class="box3-content">
...@@ -280,13 +284,17 @@ ...@@ -280,13 +284,17 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text"> <div class="data-origin-text">数据来源:美国财政部海外资产管理办公室官网</div>
美国商务部发布商业管制清单的频次,数据来源:美国财政部海外资产管理办公室官网
</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <!-- <AiButton />
<AiPane :aiContent="commerceControlListReleaseFreqChart.interpretation" /> <AiPane :aiContent="commerceControlListReleaseFreqChart.interpretation" /> -->
<AiButton @mouseenter="handleShowAiPane('commerceControlListReleaseFreqChart')" />
<AiPane
v-if="aiPaneVisible?.commerceControlListReleaseFreqChart"
:aiContent="overviewAiContent.commerceControlListReleaseFreqChart"
@mouseleave="handleHideAiPane('commerceControlListReleaseFreqChart')"
/>
</div> </div>
</div> </div>
<div class="box3-content" style="display: none"> <div class="box3-content" style="display: none">
...@@ -339,13 +347,13 @@ ...@@ -339,13 +347,13 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text"> <div class="data-origin-text">数据来源:美国财政部海外资产管理办公室官网</div>
进入SDN清单的中国实体领域分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <!-- <AiButton />
<AiPane :aiContent="radarChart.interpretation" /> <AiPane :aiContent="radarChart.interpretation" /> -->
<AiButton @mouseenter="handleShowAiPane('radarChart')" />
<AiPane :aiContent="overviewAiContent.radarChart" @mouseleave="handleHideAiPane('radarChart')" />
</div> </div>
</template> </template>
</custom-container> </custom-container>
...@@ -371,13 +379,17 @@ ...@@ -371,13 +379,17 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
</div> </div>
<div class="data-origin-text"> <div class="data-origin-text">数据来源:美国财政部海外资产管理办公室官网</div>
进入SDN清单的中国实体数量变化趋势,数据来源:美国财政部海外资产管理办公室官网
</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <!-- <AiButton />
<AiPane :aiContent="trendChart.interpretation" /> <AiPane :aiContent="trendChart.interpretation" /> -->
<AiButton @mouseenter="handleShowAiPane('trendChart')" />
<AiPane
v-if="aiPaneVisible?.trendChart"
:aiContent="overviewAiContent.trendChart"
@mouseleave="handleHideAiPane('trendChart')"
/>
</div> </div>
</template> </template>
</custom-container> </custom-container>
...@@ -705,6 +717,7 @@ import RiskSignal from "@/components/base/riskSignal/index.vue"; ...@@ -705,6 +717,7 @@ import RiskSignal from "@/components/base/riskSignal/index.vue";
import RiskSignalOverviewDetailDialog from "@/components/base/RiskSignalOverviewDetailDialog/index.vue"; import RiskSignalOverviewDetailDialog from "@/components/base/RiskSignalOverviewDetailDialog/index.vue";
import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue"; import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue";
import { useContainerScroll } from "@/hooks/useScrollShow"; import { useContainerScroll } from "@/hooks/useScrollShow";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
const homeMainRef = ref(null); const homeMainRef = ref(null);
const { isShow } = useContainerScroll(homeMainRef); const { isShow } = useContainerScroll(homeMainRef);
import * as echarts from "echarts"; import * as echarts from "echarts";
...@@ -1265,7 +1278,8 @@ const fetchRadarData = async checked => { ...@@ -1265,7 +1278,8 @@ const fetchRadarData = async checked => {
}; };
}); });
console.log("图例 =>", radarOption.value); console.log("图例 =>", radarOption.value);
radarChart.interpret({ type: "雷达图", name: "实体清单领域分布情况", data: data }); radarChartData.value = data;
// radarChart.interpret({ type: "雷达图", name: "实体清单领域分布情况", data: data });
} }
} catch (error) { } catch (error) {
console.error("获取雷达图数据失败:", error); console.error("获取雷达图数据失败:", error);
...@@ -1824,6 +1838,128 @@ const handleToDataLibrary = item => { ...@@ -1824,6 +1838,128 @@ const handleToDataLibrary = item => {
window.open(route.href, "_blank"); window.open(route.href, "_blank");
}; };
const requestAiPaneContent = async key => {
if (!key || aiPaneLoading.value[key] || aiPaneFetched.value[key]) return;
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: true };
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成中..." };
try {
const payload = buildAiChartPayload(key);
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
overviewAiContent.value = {
...overviewAiContent.value,
[key]: base + chunk
};
}
}
);
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
const interpretation = first?.解读 || first?.["解读"];
// 流式已渲染过内容,最终用解析出的解读覆盖(保证显示格式统一)
if (interpretation) {
overviewAiContent.value = {
...overviewAiContent.value,
[key]: interpretation
};
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: false };
}
};
const trendChartData = ref([]);
const radarChartData = ref([]);
const entityListReleaseFreqChartData = ref([]);
const commerceControlListReleaseFreqChartData = ref([]);
const aiPaneVisible = ref({
trendChart: false,
radarChart: false,
entityListReleaseFreqChart: false,
commerceControlListReleaseFreqChart: false
});
const overviewAiContent = ref({
trendChart: "智能总结生成中...",
radarChart: "智能总结生成中...",
entityListReleaseFreqChart: "智能总结生成中...",
commerceControlListReleaseFreqChart: "智能总结生成中..."
});
const aiPaneFetched = ref({
trendChart: false,
radarChart: false,
entityListReleaseFreqChart: false,
commerceControlListReleaseFreqChart: false
});
const aiPaneLoading = ref({
trendChart: false,
radarChart: false,
entityListReleaseFreqChart: false,
commerceControlListReleaseFreqChart: false
});
const chartLoading = ref({
trendChart: false,
radarChart: false,
entityListReleaseFreqChart: false,
commerceControlListReleaseFreqChart: false
});
const buildAiChartPayload = key => {
if (key === "trendChart") {
return { type: "柱状图", name: "制裁清单数量增长趋势", data: trendChartData.value };
}
if (key === "radarChart") {
return { type: "雷达图", name: "实体清单领域分布情况", data: radarChartData.value };
}
if (key === "entityListReleaseFreqChart") {
return {
type: "柱状图",
name: "美国商务部发布实体清单的频次",
data: entityListReleaseFreqChartData.value
};
}
if (key === "commerceControlListReleaseFreqChart") {
return {
type: "柱状图",
name: "美国商务部发布商业管制清单的频次",
data: commerceControlListReleaseFreqChartData.value
};
}
return { type: "", name: "", data: [] };
};
const handleShowAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: true
};
requestAiPaneContent(key);
};
const handleHideAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: false
};
};
onMounted(async () => { onMounted(async () => {
console.log("finance 页面 mounted"); console.log("finance 页面 mounted");
try { try {
...@@ -1886,11 +2022,12 @@ onMounted(async () => { ...@@ -1886,11 +2022,12 @@ onMounted(async () => {
await fetchRadarData(domainChecked.value); await fetchRadarData(domainChecked.value);
// 获取出口管制制裁措施 // 获取出口管制制裁措施
await fetchSanctionList(); await fetchSanctionList();
entityListReleaseFreqChart.interpret({ entityListReleaseFreqChartData.value = entityListReleaseFreq.value;
type: "柱状图", // entityListReleaseFreqChart.interpret({
name: "美国商务部发布实体清单的频次", // type: "柱状图",
data: entityListReleaseFreq.value // name: "美国商务部发布实体清单的频次",
}); // data: entityListReleaseFreq.value
// });
commerceControlListReleaseFreq.value = _.map(cclList1, item => { commerceControlListReleaseFreq.value = _.map(cclList1, item => {
return { return {
year: item.year, year: item.year,
...@@ -1899,11 +2036,12 @@ onMounted(async () => { ...@@ -1899,11 +2036,12 @@ onMounted(async () => {
tags: item.domain tags: item.domain
}; };
}); });
commerceControlListReleaseFreqChart.interpret({ commerceControlListReleaseFreqChartData.value = commerceControlListReleaseFreq.value;
type: "柱状图", // commerceControlListReleaseFreqChart.interpret({
name: "美国商务部发布商业管制清单的频次", // type: "柱状图",
data: commerceControlListReleaseFreq.value // name: "美国商务部发布商业管制清单的频次",
}); // data: commerceControlListReleaseFreq.value
// });
} catch (err) { } catch (err) {
console.log("此处报错?"); console.log("此处报错?");
console.log(err); console.log(err);
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="info-row"> <div class="info-row">
<div class="label">发布机构:</div> <div class="label">发布机构:</div>
<div class="value link"> <div class="value link">
<img :src="title" alt="" class="icon" /> <img :src="formattedData.postOrgLogoUrl || title" alt="" class="icon" />
<span @click="handleClickDp">{{ formattedData.postOrgName }} ></span> <span @click="handleClickDp">{{ formattedData.postOrgName }} ></span>
</div> </div>
</div> </div>
...@@ -46,17 +46,24 @@ ...@@ -46,17 +46,24 @@
<div class="left-top-content"> <div class="left-top-content">
<div class="content-title">制裁实体分布:</div> <div class="content-title">制裁实体分布:</div>
<div class="distribution-list"> <div class="distribution-list">
<div class="list-item" v-for="(item, index) in entityDistribution" :key="index" <div
@click="handleToDataLibrary(item)"> class="list-item"
v-for="(item, index) in entityDistribution"
:key="index"
@click="handleToDataLibrary(item)"
>
<img :src="item.imageUrl || flag" alt="" class="flag" /> <img :src="item.imageUrl || flag" alt="" class="flag" />
<div class="country-name">{{ item.name }}</div> <div class="country-name">{{ item.name }}</div>
<div class="progress-bar-container"> <div class="progress-bar-container">
<div class="progress-bar" :style="{ <div
width: item.width, class="progress-bar"
background: item.gradient :style="{
}"></div> width: item.width,
background: item.gradient
}"
></div>
</div> </div>
<div class="count" :class="{ highlight: index === 0 }">{{ item.count }}</div> <div class="count" :class="{ highlight: item.name === '中国' }">{{ item.count }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -97,13 +104,25 @@ ...@@ -97,13 +104,25 @@
</div> </div>
<div class="filter-right"> <div class="filter-right">
<el-checkbox v-model="onlyChina" label="只看中国实体" /> <el-checkbox v-model="onlyChina" label="只看中国实体" />
<el-select v-model="filterField" placeholder="全部领域" style="width: 150px; margin: 0 12px 0 16px"> <el-select
v-model="filterField"
placeholder="全部领域"
style="width: 150px; margin: 0 12px 0 16px"
>
<el-option label="全部领域" value="" /> <el-option label="全部领域" value="" />
<el-option v-for="item in domainOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option
v-for="item in domainOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
<el-input v-model="searchKeyword" placeholder="搜索实体" <el-input
v-model="searchKeyword"
placeholder="搜索实体"
style="width: 150px; border: 1px solid rgba(170, 173, 177, 0.4); border-radius: 5px" style="width: 150px; border: 1px solid rgba(170, 173, 177, 0.4); border-radius: 5px"
:suffix-icon="Search" /> :suffix-icon="Search"
/>
</div> </div>
</div> </div>
<div class="stats-row"> <div class="stats-row">
...@@ -118,14 +137,21 @@ ...@@ -118,14 +137,21 @@
<div class="stats-info"> <div class="stats-info">
<div class="stat-item"> <div class="stat-item">
<span class="dot red"></span> <span class="dot red"></span>
<span class="text">新增 <span class="num red">{{ addCount }}</span> 家 (50%规则涉及<span class="num red">{{ <span class="text"
addRuleCount >新增 <span class="num red">{{ addCount }}</span> 家 (50%规则涉及<span class="num red">{{
}}</span>家)</span> addRuleCount
}}</span
>家)</span
>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span class="dot green"></span> <span class="dot green"></span>
<span class="text">移除 <span class="num green">{{ removeCount }}</span> 家 (50%规则涉及<span <span class="text"
class="num green">{{ removeRuleCount }}</span>家)</span> >移除 <span class="num green">{{ removeCount }}</span> 家 (50%规则涉及<span
class="num green"
>{{ removeRuleCount }}</span
>家)</span
>
</div> </div>
</div> </div>
</div> </div>
...@@ -157,7 +183,11 @@ ...@@ -157,7 +183,11 @@
>{{ item }}</span >{{ item }}</span
> --> > -->
<div class="domain-box"> <div class="domain-box">
<AreaTag v-for="(domain, index) in scope.row.fields" :key="index" :tagName="domain" /> <AreaTag
v-for="(domain, index) in scope.row.fields"
:key="index"
:tagName="domain"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
...@@ -166,20 +196,26 @@ ...@@ -166,20 +196,26 @@
<el-table-column prop="entityTypeId" label="类型" width="120" align="center"> <el-table-column prop="entityTypeId" label="类型" width="120" align="center">
<template #default="scope"> <template #default="scope">
<div style="display: flex; gap: 4px; justify-content: center"> <div style="display: flex; gap: 4px; justify-content: center">
<AreaTag :tagName="scope.row.entityType === 1 <AreaTag
? '个人' :tagName="
: scope.row.entityType === 2 scope.row.entityType === 1
? '实体' ? '个人'
: '公司' : scope.row.entityType === 2
" /> ? '实体'
: '公司'
"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<!-- <el-table-column prop="revenue" label="营收(亿元)" width="110" align="center" /> --> <!-- <el-table-column prop="revenue" label="营收(亿元)" width="110" align="center" /> -->
<el-table-column label="50%规则子企业" width="180" align="center"> <el-table-column label="50%规则子企业" width="180" align="center">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.subsidiaryCount" class="subsidiary-link" <span
@click="handleSubsidiaryClick(scope.row)"> v-if="scope.row.subsidiaryCount"
class="subsidiary-link"
@click="handleSubsidiaryClick(scope.row)"
>
{{ scope.row.subsidiaryText }} {{ scope.row.subsidiaryText }}
<span class="blue-text">{{ scope.row.subsidiaryCount }}家 ></span> <span class="blue-text">{{ scope.row.subsidiaryCount }}家 ></span>
</span> </span>
...@@ -223,8 +259,12 @@ ...@@ -223,8 +259,12 @@
</div> </div>
</div> </div>
<!-- 50%规则子企业弹框 --> <!-- 50%规则子企业弹框 -->
<RuleSubsidiaryDialog v-model="subsidiaryDialogVisible" :company-name="currentSubsidiaryCompanyName" <RuleSubsidiaryDialog
:total-count="currentSubsidiaryCount" :data-list="currentSubsidiaryList" /> v-model="subsidiaryDialogVisible"
:company-name="currentSubsidiaryCompanyName"
:total-count="currentSubsidiaryCount"
:data-list="currentSubsidiaryList"
/>
</div> </div>
</template> </template>
...@@ -333,8 +373,8 @@ const getSanctionOverviewList = async () => { ...@@ -333,8 +373,8 @@ const getSanctionOverviewList = async () => {
subsidiaryText: subsidiaryText:
org.ruleOrgList && org.ruleOrgList.length > 0 org.ruleOrgList && org.ruleOrgList.length > 0
? (org.ruleOrgList[0].orgName.length > 10 ? (org.ruleOrgList[0].orgName.length > 10
? org.ruleOrgList[0].orgName.slice(0, 10) + "..." ? org.ruleOrgList[0].orgName.slice(0, 10) + "..."
: org.ruleOrgList[0].orgName) + "...等" : org.ruleOrgList[0].orgName) + "...等"
: "" : ""
})) }))
})); }));
...@@ -481,7 +521,8 @@ const formattedData = computed(() => { ...@@ -481,7 +521,8 @@ const formattedData = computed(() => {
administrativeOrderId: info.administrativeOrderId ? `No. ${info.administrativeOrderId}` : "", administrativeOrderId: info.administrativeOrderId ? `No. ${info.administrativeOrderId}` : "",
postPersonName: info.postPersonName, postPersonName: info.postPersonName,
domains: info.domainNames, domains: info.domainNames,
avartar: info.postPersonAvatarUrl avartar: info.postPersonAvatarUrl,
postOrgLogoUrl: info.postOrgLogoUrl
}; };
}); });
...@@ -590,10 +631,11 @@ const getReasonHistoryList = async () => { ...@@ -590,10 +631,11 @@ const getReasonHistoryList = async () => {
const sanTypeId = ref(""); const sanTypeId = ref("");
// 跳转到数据资源库 // 跳转到数据资源库
const handleToDataLibrary = (item) => { const handleToDataLibrary = item => {
console.log('item', item); console.log("item", item);
const dateStr = formattedData.value.postDate.replace(/(\d{4})(\d{1,2})(\d{1,2})日/, (_, y, m, d) => const dateStr = formattedData.value.postDate.replace(
`${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}` /(\d{4})(\d{1,2})(\d{1,2})日/,
(_, y, m, d) => `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`
); );
const route = router.resolve({ const route = router.resolve({
...@@ -604,7 +646,7 @@ const handleToDataLibrary = (item) => { ...@@ -604,7 +646,7 @@ const handleToDataLibrary = (item) => {
} }
}); });
window.open(route.href, "_blank"); window.open(route.href, "_blank");
} };
onMounted(() => { onMounted(() => {
// 获取路由参数中的sanTypeId // 获取路由参数中的sanTypeId
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
</div> </div>
<div class="original-text-btn" @click="handleClickOriginalText"> <div class="original-text-btn" @click="handleClickOriginalText">
<img :src="icon1" alt="" /> <img :src="icon1" alt="" />
<span>实体清单原文</span> <span>SDN清单原文</span>
</div> </div>
<div class="btn3" @click="handleAnalysisClick"> <div class="btn3" @click="handleAnalysisClick">
<div class="icon"> <div class="icon">
......
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
" "
/> />
</div> </div>
<div class="translate-text">{{ "显示文" }}</div> <div class="translate-text">{{ "显示文" }}</div>
</div> </div>
<div class="btn" @click="handleDownload"> <div class="btn" @click="handleDownload">
<div class="icon"> <div class="icon">
...@@ -79,9 +79,6 @@ ...@@ -79,9 +79,6 @@
</div> </div>
</div> </div>
<div class="report-box"> <div class="report-box">
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div>
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage"> <div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf <pdf
:key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`"
...@@ -90,6 +87,9 @@ ...@@ -90,6 +87,9 @@
class="pdf-pane-inner" class="pdf-pane-inner"
/> />
</div> </div>
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div>
</div> </div>
</div> </div>
</div> </div>
......
<template> <template>
<div class="wrap"> <div class="wrap">
<div class="scroll-inner"> <div class="scroll-inner" ref="pageScrollRef">
<div class="header"> <div class="header">
<div class="header-top"> <div class="header-top">
<div class="header-top-left"> <div class="header-top-left">
...@@ -231,7 +231,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png' ...@@ -231,7 +231,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue" import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.vue" import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SearchContainer from "@/components/SearchContainer.vue"; import SearchContainer from "@/components/SearchContainer.vue";
import { ref, onMounted, computed, defineProps } from "vue"; import { ref, onMounted, computed, defineProps, nextTick } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { import {
getThinkTankReportAbstract, getThinkTankReportAbstract,
...@@ -304,7 +304,7 @@ const handleGetThinkTankHearingInfo = async () => { ...@@ -304,7 +304,7 @@ const handleGetThinkTankHearingInfo = async () => {
} }
}; };
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"国会听证会关键词云,数据来源:美国兰德公司官网"; "数据来源:美国兰德公司官网";
// 默认仅展示 AiButton,悬停后再请求 AI // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
...@@ -592,8 +592,30 @@ const switchTab = name => { ...@@ -592,8 +592,30 @@ const switchTab = name => {
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
const total = ref(0); const total = ref(0);
const pageScrollRef = ref(null);
const getScrollableParent = (el) => {
let cur = el;
while (cur && cur !== document.body && cur !== document.documentElement) {
const style = window.getComputedStyle(cur);
const overflowY = style?.overflowY;
const isScrollable = overflowY === "auto" || overflowY === "scroll";
if (isScrollable && cur.scrollHeight > cur.clientHeight + 1) {
return cur;
}
cur = cur.parentElement;
}
return null;
};
const scrollToTop = async () => {
await nextTick();
const anchor = pageScrollRef.value;
if (!anchor) return;
const scrollEl = getScrollableParent(anchor) || anchor;
scrollEl.scrollTop = 0;
};
const handleCurrentChange = page => { const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
scrollToTop();
handleGetThinkTankReportViewpoint(); handleGetThinkTankReportViewpoint();
}; };
...@@ -749,9 +771,6 @@ onMounted(() => { ...@@ -749,9 +771,6 @@ onMounted(() => {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden; overflow: hidden;
.header-top { .header-top {
......
...@@ -232,9 +232,6 @@ const handleDownloadDocument = async () => { ...@@ -232,9 +232,6 @@ const handleDownloadDocument = async () => {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden; overflow: hidden;
.header-top { .header-top {
......
<template> <template>
<div class="wrap"> <div class="wrap" ref="pageScrollRef">
<div class="top"> <div class="top">
<WarningPane :warnningLevel="riskSignal?.level" :warnningContent="riskSignal?.content" v-if="riskSignal?.level"> <WarningPane :warnningLevel="riskSignal?.level" :warnningContent="riskSignal?.content" v-if="riskSignal?.level">
</WarningPane> </WarningPane>
...@@ -217,7 +217,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png' ...@@ -217,7 +217,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue" import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.vue" import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SearchContainer from "@/components/SearchContainer.vue"; import SearchContainer from "@/components/SearchContainer.vue";
import { ref, onMounted, computed, defineProps } from "vue"; import { ref, onMounted, computed, defineProps, nextTick } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { import {
getThinkTankReportAbstract, getThinkTankReportAbstract,
...@@ -258,7 +258,7 @@ const props = defineProps({ ...@@ -258,7 +258,7 @@ const props = defineProps({
} }
}); });
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网"; "数据来源:美国兰德公司官网";
// 默认仅展示 AiButton,悬停后再请求 AI // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
...@@ -545,8 +545,30 @@ const switchTab = name => { ...@@ -545,8 +545,30 @@ const switchTab = name => {
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
const total = ref(0); const total = ref(0);
const pageScrollRef = ref(null);
const getScrollableParent = (el) => {
let cur = el;
while (cur && cur !== document.body && cur !== document.documentElement) {
const style = window.getComputedStyle(cur);
const overflowY = style?.overflowY;
const isScrollable = overflowY === "auto" || overflowY === "scroll";
if (isScrollable && cur.scrollHeight > cur.clientHeight + 1) {
return cur;
}
cur = cur.parentElement;
}
return null;
};
const scrollToTop = async () => {
await nextTick();
const anchor = pageScrollRef.value;
if (!anchor) return;
const scrollEl = getScrollableParent(anchor) || anchor;
scrollEl.scrollTop = 0;
};
const handleCurrentChange = page => { const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
scrollToTop();
handleGetThinkTankReportViewpoint(); handleGetThinkTankReportViewpoint();
}; };
......
...@@ -184,7 +184,7 @@ const applySurveyProjectDocumentTitle = (title) => { ...@@ -184,7 +184,7 @@ const applySurveyProjectDocumentTitle = (title) => {
document.title = text; document.title = text;
}; };
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"调查项目关键词云,数据来源:美国兰德公司官网"; "数据来源:美国兰德公司官网";
// 默认仅展示 AiButton,悬停后再请求 AI // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
...@@ -636,9 +636,6 @@ onMounted(() => { ...@@ -636,9 +636,6 @@ onMounted(() => {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden; overflow: hidden;
.header-top { .header-top {
......
...@@ -149,27 +149,8 @@ ...@@ -149,27 +149,8 @@
</div> </div>
<div class="select-box"> <div class="select-box">
<TimeSortSelectBox :key="`policy-tracking-sort-${router.currentRoute.value?.params?.id || ''}`"
<el-select class="select-box-sort" v-model="sort" placeholder="发布时间" style="width: 120px" :teleported="true" :sort-demension="1" @handle-px-change="handlePolicyTrackingPxChange" />
:placement="'bottom-start'" :popper-options="{
modifiers: [
{
name: 'preventOverflow',
options: { mainAxis: false, altAxis: false }
},
{
name: 'flip',
enabled: false
}
]
}">
<template #prefix>
<img v-if="sort !== true" src="../thinkDynamics/images/image down.png" class="select-prefix-img" alt="" />
<img v-else src="../thinkDynamics/images/image up.png" class="select-prefix-img" alt="" />
</template>
<el-option @click="handleGetThinkPolicy()" :key="true" label="正序" :value="true" />
<el-option @click="handleGetThinkPolicy()" :key="false" label="倒序" :value="false" />
</el-select>
</div> </div>
</div> </div>
<div class="bottom-main"> <div class="bottom-main">
...@@ -279,8 +260,8 @@ ...@@ -279,8 +260,8 @@
<div class="right-footer"> <div class="right-footer">
<div class="info">共{{ total }}条政策建议</div> <div class="info">共{{ total }}条政策建议</div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="10" :page-count="pageCount" background layout="prev, pager, next" :total="total" <el-pagination :page-size="10" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" /> :total="total" @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
</div> </div>
...@@ -306,6 +287,7 @@ import { ...@@ -306,6 +287,7 @@ import {
import { getChartAnalysis } from "@/api/aiAnalysis/index"; import { getChartAnalysis } from "@/api/aiAnalysis/index";
import AiButton from "@/components/base/Ai/AiButton/index.vue"; import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue"; import AiPane from "@/components/base/Ai/AiPane/index.vue";
import TimeSortSelectBox from "@/components/base/TimeSortSelectBox/index.vue";
import TipTab from "@/views/thinkTank/TipTab/index.vue"; import TipTab from "@/views/thinkTank/TipTab/index.vue";
import defaultNewsIcon from "@/assets/icons/default-icon-news.png"; import defaultNewsIcon from "@/assets/icons/default-icon-news.png";
import AreaTag from "@/components/base/AreaTag/index.vue"; import AreaTag from "@/components/base/AreaTag/index.vue";
...@@ -353,11 +335,11 @@ const getAreaTagColor = (name, idx = 0) => ...@@ -353,11 +335,11 @@ const getAreaTagColor = (name, idx = 0) =>
/** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */ /** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */
const POLICY_TRACKING_TIP_BOX1 = const POLICY_TRACKING_TIP_BOX1 =
"智库报告中政策建议的领域分布情况,数据来源:美国兰德公司官网"; "数据来源:美国兰德公司官网";
const POLICY_TRACKING_TIP_BOX2 = const POLICY_TRACKING_TIP_BOX2 =
"智库报告中政策建议部门分布情况,数据来源:美国兰德公司官网"; "数据来源:美国兰德公司官网";
const POLICY_TRACKING_TIP_BOX3 = const POLICY_TRACKING_TIP_BOX3 =
"智库报告热门研究领域变化趋势,数据来源:美国兰德公司官网"; "数据来源:美国兰德公司官网";
/** 筛选「全部」项文案,与市场准入概览-资源库复选逻辑一致 */ /** 筛选「全部」项文案,与市场准入概览-资源库复选逻辑一致 */
const POLICY_FILTER_ALL_AREA = "全部领域"; const POLICY_FILTER_ALL_AREA = "全部领域";
...@@ -1319,6 +1301,12 @@ const handleSwithSort = () => { ...@@ -1319,6 +1301,12 @@ const handleSwithSort = () => {
handleGetThinkPolicy(); handleGetThinkPolicy();
}; };
/** 政策追踪排序公共组件回调:1=时间倒序,2=时间正序(映射到现有 sort(true/false/null)) */
const handlePolicyTrackingPxChange = (val) => {
sort.value = Number(val) === 2 ? true : false;
handleGetThinkPolicy();
};
const currentPage = ref(1); const currentPage = ref(1);
const pageCount = computed(() => { const pageCount = computed(() => {
const size = 10; const size = 10;
...@@ -1610,6 +1598,8 @@ onMounted(async () => { ...@@ -1610,6 +1598,8 @@ onMounted(async () => {
width: 420px; width: 420px;
height: 22px; height: 22px;
display: flex; display: flex;
align-items: center;
justify-content: flex-start;
} }
.chart-box { .chart-box {
...@@ -1669,10 +1659,13 @@ onMounted(async () => { ...@@ -1669,10 +1659,13 @@ onMounted(async () => {
.source { .source {
position: absolute; position: absolute;
bottom: 24px; bottom: 24px;
left: 24px;
width: 420px; width: 420px;
height: 22px; height: 22px;
display: flex; display: flex;
align-items: center;
justify-content: flex-start;
} }
.chart-box { .chart-box {
...@@ -2150,7 +2143,7 @@ onMounted(async () => { ...@@ -2150,7 +2143,7 @@ onMounted(async () => {
.right { .right {
width: 1224px; width: 1224px;
min-height: 1670px;
margin-bottom: 20px; margin-bottom: 20px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1); border: 1px solid rgba(234, 236, 238, 1);
...@@ -2167,7 +2160,7 @@ onMounted(async () => { ...@@ -2167,7 +2160,7 @@ onMounted(async () => {
box-sizing: border-box; box-sizing: border-box;
padding-left: 37px; padding-left: 37px;
padding-right: 0; padding-right: 0;
max-height: 1540px;
flex: 1; flex: 1;
.right-empty { .right-empty {
...@@ -2187,7 +2180,7 @@ onMounted(async () => { ...@@ -2187,7 +2180,7 @@ onMounted(async () => {
padding-left: 37px; padding-left: 37px;
padding-right: 36px; padding-right: 36px;
width: calc(100% + 37px - 36px); width: calc(100% + 37px - 36px);
height: 153px;
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
display: flex; display: flex;
......
...@@ -123,9 +123,6 @@ onMounted(async () => { ...@@ -123,9 +123,6 @@ onMounted(async () => {
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1); box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: relative; position: relative;
position: sticky;
top: 0;
z-index: 99999;
overflow: visible; overflow: visible;
.header-top { .header-top {
......
<template> <template>
<div class="wrap"> <div class="wrap" ref="pageScrollRef">
<div class="main-header"> <div class="main-header">
<div class="search-box"> <div class="search-box">
...@@ -19,33 +19,8 @@ ...@@ -19,33 +19,8 @@
<div class="select-box"> <div class="select-box">
<div class="select-box-sort"> <div class="select-box-sort">
<el-select v-model="sort" placeholder="发布时间" style="width: 120px" :teleported="true" <TimeSortSelectBox :key="`dynamics-sort-${tabResetKey}`" :sort-demension="1"
:placement="'bottom-start'" :popper-options="{ @handle-px-change="handleDynamicsPxChange" />
modifiers: [
{
name: 'preventOverflow', // 禁用自动翻转逻辑
options: {
mainAxis: false, // 禁用垂直方向的自动调整
altAxis: false, // 禁用水平方向的自动调整
}
},
{
name: 'flip', // 完全禁用翻转功能
enabled: false
}
]
}">
<template #prefix>
<img v-if="sort !== true" src="./images/image down.png" class="select-prefix-img" alt=""
@click.stop="toggleSortAndFetch()" />
<img v-else src="./images/image up.png" class="select-prefix-img" alt=""
@click.stop="toggleSortAndFetch()" />
</template>
<el-option :key="'think-dynamics-sort-asc'" label="正序" :value="true"
@click="handleGetThinkDynamicsReport()" />
<el-option :key="'think-dynamics-sort-desc'" label="倒序" :value="false"
@click="handleGetThinkDynamicsReport()" />
</el-select>
</div> </div>
</div> </div>
</div> </div>
...@@ -53,8 +28,7 @@ ...@@ -53,8 +28,7 @@
<ThinkTankReport v-if="isThinkTankReport" :research-type-list="researchTypeList" <ThinkTankReport v-if="isThinkTankReport" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters" :research-time-list="researchTimeList" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters"
:cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :search-keyword="searchReport" :cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :search-keyword="searchReport"
:loading="isThinkTankReportLoading" :loading="isThinkTankReportLoading" @update:selected-filters="handleSelectedFiltersUpdate"
@update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange" @filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToReportDetail" /> @report-click="handleToReportDetail" />
<CongressHearing v-else-if="isCongressHearing" :research-type-list="researchTypeList" <CongressHearing v-else-if="isCongressHearing" :research-type-list="researchTypeList"
...@@ -76,6 +50,7 @@ ...@@ -76,6 +50,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted, nextTick } from "vue"; import { ref, reactive, onMounted, nextTick } from "vue";
import SurveyForm from "./SurveyForm/index.vue" import SurveyForm from "./SurveyForm/index.vue"
import TimeSortSelectBox from "@/components/base/TimeSortSelectBox/index.vue";
// import Img1 from "./images/img1.png"; // import Img1 from "./images/img1.png";
// import Img2 from "./images/img2.png"; // import Img2 from "./images/img2.png";
// import Img3 from "./images/img3.png"; // import Img3 from "./images/img3.png";
...@@ -330,14 +305,37 @@ const handleGetHylyList = async () => { ...@@ -330,14 +305,37 @@ const handleGetHylyList = async () => {
} }
}; };
const toggleSortAndFetch = async () => { /** 智库动态排序公共组件回调:1=时间倒序,2=时间正序(映射到现有 sort(true/false/null)) */
sort.value = sort.value === true ? false : true; const handleDynamicsPxChange = async (val) => {
sort.value = Number(val) === 2 ? true : false;
await handleGetThinkDynamicsReport(); await handleGetThinkDynamicsReport();
}; };
const currentPage = ref(1); const currentPage = ref(1);
const pageScrollRef = ref(null);
const getScrollableParent = (el) => {
let cur = el;
while (cur && cur !== document.body && cur !== document.documentElement) {
const style = window.getComputedStyle(cur);
const overflowY = style?.overflowY;
const isScrollable = overflowY === "auto" || overflowY === "scroll";
if (isScrollable && cur.scrollHeight > cur.clientHeight + 1) {
return cur;
}
cur = cur.parentElement;
}
return null;
};
const scrollToTop = async () => {
await nextTick();
const anchor = pageScrollRef.value;
if (!anchor) return;
const scrollEl = getScrollableParent(anchor) || anchor;
scrollEl.scrollTop = 0;
};
// 处理页码改变事件 // 处理页码改变事件
const handleCurrentChange = page => { const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
scrollToTop();
handleGetThinkDynamicsReport() handleGetThinkDynamicsReport()
}; };
...@@ -615,14 +613,6 @@ onMounted(async () => { ...@@ -615,14 +613,6 @@ onMounted(async () => {
margin-top: 16px; margin-top: 16px;
display: flex; display: flex;
.select-box-time,
.select-box-sort {
background: rgb(255, 255, 255);
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
border: 1px solid rgb(230, 231, 232);
border-radius: 4px;
height: 32px;
}
.select-prefix-img { .select-prefix-img {
width: 8px; width: 8px;
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
</div> </div>
<div class="source"> <div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div> <div class="info"><img src="./images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> <div class="text"> 数据来源:美国国会官网</div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -222,7 +222,7 @@ ...@@ -222,7 +222,7 @@
</div> </div>
<div class="source"> <div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div> <div class="info"><img src="./images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> <div class="text"> 数据来源:美国国会官网</div>
</div> </div>
<div class="middle"> <div class="middle">
<div class="middle-text">{{ "共" }}{{ personTotal }}{{ "名核心研究人员" }}</div> <div class="middle-text">{{ "共" }}{{ personTotal }}{{ "名核心研究人员" }}</div>
......
...@@ -32,7 +32,7 @@ const props = defineProps({ ...@@ -32,7 +32,7 @@ const props = defineProps({
width: 100%; width: 100%;
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: center; justify-content: flex-start;
align-items: center; align-items: center;
height: 22px; height: 22px;
......
...@@ -319,7 +319,8 @@ const handleYearGroupChange = (val) => { ...@@ -319,7 +319,8 @@ const handleYearGroupChange = (val) => {
.card-box { .card-box {
width: 100%; width: 100%;
height: 1134px; padding-bottom: 32px;
;
display: flex; display: flex;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
box-sizing: border-box; box-sizing: border-box;
...@@ -339,7 +340,7 @@ const handleYearGroupChange = (val) => { ...@@ -339,7 +340,7 @@ const handleYearGroupChange = (val) => {
.card-content { .card-content {
width: 1211px; width: 1211px;
height: 1067px;
margin-top: 33px; margin-top: 33px;
margin-left: 37px; margin-left: 37px;
......
...@@ -221,7 +221,7 @@ ...@@ -221,7 +221,7 @@
<MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail" <MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail"
@person-click="handleClickPerson" name="personName" content="remarks" source="orgName" /> @person-click="handleClickPerson" name="personName" content="remarks" source="orgName" />
</div> </div>
<DivideHeader id="position3" class="divide-header" :titleText="'数据总览'"></DivideHeader> <DivideHeader id="position3" class="divide-header" :titleText="'全景概览'"></DivideHeader>
<div class="center-footer"> <div class="center-footer">
<div class="box5"> <div class="box5">
<div class="box5-header"> <div class="box5-header">
...@@ -253,7 +253,7 @@ ...@@ -253,7 +253,7 @@
<div id="box5Chart" class="box5-chart-canvas"></div> <div id="box5Chart" class="box5-chart-canvas"></div>
</div> </div>
<div class="source"> <div class="source">
<TipTab :text="'智库报告数量变化趋势,数据来源:美国各智库官网'" /> <TipTab :text="'数据来源:美国各智库官网'" />
</div> </div>
<div class="chart-box"> <div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="btn-box" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
...@@ -293,7 +293,7 @@ ...@@ -293,7 +293,7 @@
<template v-else> <template v-else>
<div id="box6Chart"></div> <div id="box6Chart"></div>
<div class="source"> <div class="source">
<TipTab :text="'智库报告领域分布情况,数据来源:美国各智库官网'" /> <TipTab :text="'数据来源:美国各智库官网'" />
</div> </div>
<div class="chart-box"> <div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentBox6" @mouseenter="handleSwitchAiContentShowBox6(true)"> <div class="btn-box" v-if="!isShowAiContentBox6" @mouseenter="handleSwitchAiContentShowBox6(true)">
...@@ -325,7 +325,7 @@ ...@@ -325,7 +325,7 @@
<template v-else> <template v-else>
<div id="box7Chart"></div> <div id="box7Chart"></div>
<div class="source"> <div class="source">
<TipTab :text="'美国科技智库与主要政府机构之间的资金往来,数据来源:美国各智库官网'" /> <TipTab :text="'数据来源:美国各智库官网'" />
</div> </div>
<div class="chart-box"> <div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentBox7" @mouseenter="handleSwitchAiContentShowBox7(true)"> <div class="btn-box" v-if="!isShowAiContentBox7" @mouseenter="handleSwitchAiContentShowBox7(true)">
...@@ -375,7 +375,7 @@ ...@@ -375,7 +375,7 @@
</div> </div>
<div class="home-main-footer"> <div class="home-main-footer">
<DivideHeader id="position4" class="divide-header" :titleText="'资源库'"></DivideHeader> <DivideHeader id="position4" class="divide-header" :titleText="'美国科技智库数据库'"></DivideHeader>
<div class="home-main-footer-header"> <div class="home-main-footer-header">
<div class="btn-box"> <div class="btn-box">
...@@ -385,19 +385,8 @@ ...@@ -385,19 +385,8 @@
</div> </div>
</div> </div>
<div class="select-box"> <div class="select-box">
<el-select v-model="resourceLibrarySortModel" class="resource-library-sort-select" placeholder="发布时间" <TimeSortSelectBox :key="`reslib-sort-${resourceTabResetKey}`" :sort-demension="1"
style="width: 120px" :teleported="true" placement="bottom-start" @handle-px-change="handleResourceLibraryPxChange" />
:popper-options="resourceLibrarySortPopperOptions" @change="handleResourceLibrarySortChange">
<template #prefix>
<img v-if="resourceLibrarySortModel !== true"
src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png"
class="resource-library-sort-prefix-img" alt="" @click.stop="toggleResourceLibrarySortPrefix" />
<img v-else src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image up.png"
class="resource-library-sort-prefix-img" alt="" @click.stop="toggleResourceLibrarySortPrefix" />
</template>
<el-option :key="'resource-lib-sort-asc'" label="正序" :value="true" />
<el-option :key="'resource-lib-sort-desc'" label="倒序" :value="false" />
</el-select>
</div> </div>
<!-- <el-select v-model="sort" placeholder="发布时间" style="width: 120px; margin-left: 8px"> <!-- <el-select v-model="sort" placeholder="发布时间" style="width: 120px; margin-left: 8px">
<el-option @click="handleGetetThinkTankReport()" :key="true" label="正序" :value="true" /> <el-option @click="handleGetetThinkTankReport()" :key="true" label="正序" :value="true" />
...@@ -409,29 +398,28 @@ ...@@ -409,29 +398,28 @@
v-model:selectedAreaList="selectedAreaList" :pub-time-list="pubTimeList" v-model:selectedAreaList="selectedAreaList" :pub-time-list="pubTimeList"
v-model:selectedPubTimeList="selectedPubTimeList" @filter-change="handleThinkTankReportFilterChange" v-model:selectedPubTimeList="selectedPubTimeList" @filter-change="handleThinkTankReportFilterChange"
:cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :cur-footer-list="curFooterList" :total="total" :current-page="currentPage"
:loading="isResourceReportLoading" :loading="isResourceReportLoading" @report-click="handleToReportDetail"
@report-click="handleToReportDetail" @page-change="handleCurrentChange" /> @page-change="handleCurrentChange" />
<HomeMainFooterSurvey v-else-if="activeCate === '调查项目'" :area-list="areaList" <HomeMainFooterSurvey v-else-if="activeCate === '调查项目'" :area-list="areaList"
v-model:selectedAreaList="surveySelectedAreaList" :pub-time-list="pubTimeList" v-model:selectedAreaList="surveySelectedAreaList" :pub-time-list="pubTimeList"
v-model:selectedPubTimeList="surveySelectedPubTimeList" @filter-change="handleSurveyFilterChange" v-model:selectedPubTimeList="surveySelectedPubTimeList" @filter-change="handleSurveyFilterChange"
:cur-footer-list="surveyFooterList" :total="surveyTotal" :current-page="surveyCurrentPage" :cur-footer-list="surveyFooterList" :total="surveyTotal" :current-page="surveyCurrentPage"
:loading="isResourceSurveyLoading" :loading="isResourceSurveyLoading" @report-click="handleToSurveyProjectView"
@report-click="handleToSurveyProjectView" @page-change="handleSurveyCurrentChange" /> @page-change="handleSurveyCurrentChange" />
<ThinkTankCongressHearingOverview v-else-if="activeCate === '国会听证会'" :key="`congress-${resourceTabResetKey}`" <ThinkTankCongressHearingOverview v-else-if="activeCate === '国会听证会'" :key="`congress-${resourceTabResetKey}`"
:hearing-data="hearingData" :research-type-list="areaList" :research-time-list="pubTimeList" :hearing-data="hearingData" :research-type-list="areaList" :research-time-list="pubTimeList"
v-model:selectedAreaList="congressSelectedAreaList" v-model:selectedAreaList="congressSelectedAreaList"
v-model:selectedPubTimeList="congressSelectedPubTimeList" :total="congressTotal" v-model:selectedPubTimeList="congressSelectedPubTimeList" :total="congressTotal"
:current-page="congressCurrentPage" @filter-change="handleCongressFilterChange" :current-page="congressCurrentPage" @filter-change="handleCongressFilterChange"
:loading="isResourceHearingLoading" :loading="isResourceHearingLoading" @page-change="handleCongressCurrentChange"
@page-change="handleCongressCurrentChange" @report-click="handleToHearingDetail" /> @report-click="handleToHearingDetail" />
<ThinkTankPolicyAdviceOverview v-else :key="`policy-${resourceTabResetKey}`" :research-type-list="areaList" <ThinkTankPolicyAdviceOverview v-else :key="`policy-${resourceTabResetKey}`" :research-type-list="areaList"
:research-time-list="pubTimeList" :list="policyFooterList" :total="policyTotal" :research-time-list="pubTimeList" :list="policyFooterList" :total="policyTotal"
:current-page="policyCurrentPage" :page-size="7" @filter-change="handlePolicyFilterChange" :current-page="policyCurrentPage" :page-size="7" @filter-change="handlePolicyFilterChange"
:loading="isResourcePolicyLoading" :loading="isResourcePolicyLoading" @page-change="handlePolicyCurrentChange" />
@page-change="handlePolicyCurrentChange" />
</div> </div>
</div> </div>
...@@ -477,6 +465,7 @@ import getPieChart from "./utils/piechart"; ...@@ -477,6 +465,7 @@ import getPieChart from "./utils/piechart";
import { MUTICHARTCOLORS } from "@/common/constant.js"; import { MUTICHARTCOLORS } from "@/common/constant.js";
import getSankeyChart from "./utils/sankey"; import getSankeyChart from "./utils/sankey";
import { getChartAnalysis } from "@/api/aiAnalysis/index"; import { getChartAnalysis } from "@/api/aiAnalysis/index";
import TimeSortSelectBox from "@/components/base/TimeSortSelectBox/index.vue";
import defaultNewsIcon from "@/assets/icons/default-icon-news.png"; import defaultNewsIcon from "@/assets/icons/default-icon-news.png";
import defaultHeaderIcin from "@/assets/icons/default-icon1.png"; import defaultHeaderIcin from "@/assets/icons/default-icon1.png";
import News1 from "./assets/images/news1.png"; import News1 from "./assets/images/news1.png";
...@@ -1998,6 +1987,14 @@ const toggleResourceLibrarySortPrefix = () => { ...@@ -1998,6 +1987,14 @@ const toggleResourceLibrarySortPrefix = () => {
} }
}; };
/** 资源库排序公共组件回调:1=时间倒序,2=时间正序(与现有 sort(true/false/null) 映射) */
const handleResourceLibraryPxChange = (val) => {
// 组件默认值为 1(时间倒序),这里保持与旧逻辑一致:倒序用 false,正序用 true
const mapped = Number(val) === 2 ? true : false;
resourceLibrarySortModel.value = mapped;
handleResourceLibrarySortChange();
};
/** 与调查项目 surveyFooterList 一致:初始空列表,由接口填充;失败或非 200 时清空 */ /** 与调查项目 surveyFooterList 一致:初始空列表,由接口填充;失败或非 200 时清空 */
const curFooterList = ref([]); const curFooterList = ref([]);
const currentPage = ref(1); const currentPage = ref(1);
......
...@@ -309,8 +309,9 @@ onMounted(async () => { ...@@ -309,8 +309,9 @@ onMounted(async () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.wrap { .wrap {
overflow-y: auto;
height: 100vh; height: 100vh;
overflow-y: auto;
overflow-x: hidden;
.header { .header {
...@@ -320,9 +321,6 @@ onMounted(async () => { ...@@ -320,9 +321,6 @@ onMounted(async () => {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden; overflow: hidden;
.header-top { .header-top {
...@@ -703,7 +701,6 @@ onMounted(async () => { ...@@ -703,7 +701,6 @@ onMounted(async () => {
height: 881px; height: 881px;
display: flex; display: flex;
overflow-y: auto; overflow-y: auto;
/* 右侧统一滚动条,控制两侧原文+译文一起滚动 */
overflow-x: hidden; overflow-x: hidden;
} }
......
...@@ -326,7 +326,79 @@ export default { ...@@ -326,7 +326,79 @@ export default {
} }
} }
if (matchList.value.length > 0) jumpTo(0); if (matchList.value.length > 0) {
// 先把所有命中都标黄,再把“当前命中”改成蓝底
renderAllHighlights();
jumpTo(0);
}
};
const setActiveHighlight = (idx) => {
const all = document.querySelectorAll('.highlight-rect[data-match-idx]');
all.forEach((el) => {
const isActive = Number(el.getAttribute('data-match-idx')) === Number(idx);
if (isActive) {
el.classList.add('highlight-rect--active');
} else {
el.classList.remove('highlight-rect--active');
}
});
};
const renderAllHighlights = () => {
clearHighlights();
const list = Array.isArray(matchList.value) ? matchList.value : [];
list.forEach((m, idx) => {
if (!m || m.fallback) return;
const layer = overlayMap[m.pageNum];
if (!layer) return;
const pageWrap = layer.closest('.page-wrap');
const container = (pageWrap || layer);
const containerRect = container.getBoundingClientRect();
const segs = Array.isArray(m?.segments) ? m.segments : [];
for (const seg of segs) {
const segEl = seg?.el;
if (!segEl) continue;
const textNode = segEl.firstChild;
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) continue;
try {
const range = document.createRange();
range.setStart(textNode, Math.max(0, seg.startIdx ?? 0));
range.setEnd(textNode, Math.max(0, seg.endIdx ?? 0));
const rectList = Array.from(range.getClientRects());
if (rectList.length) {
rectList.forEach(r => {
const mark = document.createElement('div');
mark.className = 'highlight-rect';
mark.setAttribute('data-match-idx', String(idx));
mark.style.zIndex = '5';
mark.style.left = (r.left - containerRect.left) + 'px';
mark.style.top = (r.top - containerRect.top) + 'px';
mark.style.width = r.width + 'px';
mark.style.height = r.height + 'px';
container.appendChild(mark);
});
} else {
const r = segEl.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
const mark = document.createElement('div');
mark.className = 'highlight-rect';
mark.setAttribute('data-match-idx', String(idx));
mark.style.zIndex = '5';
mark.style.left = (r.left - containerRect.left) + 'px';
mark.style.top = (r.top - containerRect.top) + 'px';
mark.style.width = r.width + 'px';
mark.style.height = r.height + 'px';
container.appendChild(mark);
}
}
range.detach?.();
} catch (e) {
// ignore
}
}
});
setActiveHighlight(matchIdx.value);
}; };
// 跳转到第 N 个匹配项 // 跳转到第 N 个匹配项
...@@ -342,55 +414,7 @@ export default { ...@@ -342,55 +414,7 @@ export default {
const firstSeg = m?.segments?.[0]; const firstSeg = m?.segments?.[0];
const el = firstSeg?.el; const el = firstSeg?.el;
if (!el) return; if (!el) return;
clearHighlights(); setActiveHighlight(idx);
const layer = overlayMap[m.pageNum];
if (!layer) return;
const pageWrap = layer.closest('.page-wrap');
// 用 Range 精确计算“子串”在页面上的矩形位置,再画黄色块(支持跨 span)
const containerRect = (pageWrap || layer).getBoundingClientRect();
const segs = Array.isArray(m?.segments) ? m.segments : [];
for (const seg of segs) {
const segEl = seg?.el;
if (!segEl) continue;
const textNode = segEl.firstChild;
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) continue;
try {
const range = document.createRange();
range.setStart(textNode, Math.max(0, seg.startIdx ?? 0));
range.setEnd(textNode, Math.max(0, seg.endIdx ?? 0));
const rectList = Array.from(range.getClientRects());
if (rectList.length) {
rectList.forEach(r => {
const mark = document.createElement('div');
mark.className = 'highlight-rect';
mark.style.zIndex = '5';
mark.style.left = (r.left - containerRect.left) + 'px';
mark.style.top = (r.top - containerRect.top) + 'px';
mark.style.width = r.width + 'px';
mark.style.height = r.height + 'px';
(pageWrap || layer).appendChild(mark);
});
} else {
// Range 兜底为空时:用 span 自身的矩形画块(精度低,但尽量可见)
const r = segEl.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
const mark = document.createElement('div');
mark.className = 'highlight-rect';
mark.style.zIndex = '5';
mark.style.left = (r.left - containerRect.left) + 'px';
mark.style.top = (r.top - containerRect.top) + 'px';
mark.style.width = r.width + 'px';
mark.style.height = r.height + 'px';
(pageWrap || layer).appendChild(mark);
}
}
range.detach?.();
} catch (e) {
// ignore
}
}
// 优先只滚动右侧 report-box,避免触发整页滚动导致 header 遮挡 // 优先只滚动右侧 report-box,避免触发整页滚动导致 header 遮挡
const container = el.closest('.report-box'); const container = el.closest('.report-box');
...@@ -510,6 +534,10 @@ canvas { ...@@ -510,6 +534,10 @@ canvas {
z-index: 5; z-index: 5;
} }
.textLayer :deep(.highlight-rect--active) {
background: rgb(184, 222, 254);
}
.page-wrap :deep(.highlight-rect) { .page-wrap :deep(.highlight-rect) {
position: absolute; position: absolute;
background: #ff0; background: #ff0;
...@@ -519,6 +547,10 @@ canvas { ...@@ -519,6 +547,10 @@ canvas {
z-index: 3; z-index: 3;
} }
.page-wrap :deep(.highlight-rect--active) {
background: rgb(184, 222, 254);
}
.loading { .loading {
position: absolute; position: absolute;
top: 50%; top: 50%;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<div class="center-center"> <div class="center-center">
<div class="center-header"> <div class="center-header">
<div class="center-header-left"> <div class="center-header-left">
<img class="iconstyle" src="./assets/images/warning.svg" /> <img class="iconstyle" src="./assets/images/warning-red.svg" />
<div class="center-header-title">风险信号管理</div> <div class="center-header-title">风险信号管理</div>
</div> </div>
<div class="center-header-right"> <div class="center-header-right">
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<div class="center-middle"> <div class="center-middle">
<div class="center-middle-left"> <div class="center-middle-left">
<div class="lineitem"> <div class="lineitem">
<div class="item"> <div class="item item--shift-left">
<div class="top"> <div class="top">
<div class="dot" style="background-color: rgba(95, 101, 108, 1)"></div> <div class="dot" style="background-color: rgba(95, 101, 108, 1)"></div>
<div class="text1">本年新增风险</div> <div class="text1">本年新增风险</div>
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
</div> </div>
</div> </div>
<div class="lineitem"> <div class="lineitem">
<div class="item"> <div class="item item--shift-left">
<div class="top"> <div class="top">
<div class="dot" style="background-color: rgba(5, 95, 194, 1)"></div> <div class="dot" style="background-color: rgba(5, 95, 194, 1)"></div>
<div class="text1">已处理风险</div> <div class="text1">已处理风险</div>
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
<div class="text1">待处理风险</div> <div class="text1">待处理风险</div>
</div> </div>
<div class="text2" style="color: rgba(206, 79, 81, 1)"> <div class="text2" style="color: rgba(206, 79, 81, 1)">
<span class="text2-inner">{{ basicInfo.pendingCount + " 项" }}</span> <span class="text2-inner">{{ formatThousands(basicInfo.pendingCount) + " 项" }}</span>
</div> </div>
</div> </div>
</div> </div>
...@@ -213,7 +213,7 @@ ...@@ -213,7 +213,7 @@
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="footer-left"> <div class="footer-left">
{{ `共 ${totalNum} 项调查` }} {{ `共 ${formatThousands(totalNum)} 项调查` }}
</div> </div>
<div class="footer-right"> <div class="footer-right">
<el-pagination @current-change="handleCurrentChange" :pageSize="pageSize" :current-page="currentPage" <el-pagination @current-change="handleCurrentChange" :pageSize="pageSize" :current-page="currentPage"
...@@ -233,7 +233,7 @@ ...@@ -233,7 +233,7 @@
<el-dialog v-model="isRiskDetailVisible" class="risk-signal-detail-dialog" modal-class="risk-signal-detail-modal" <el-dialog v-model="isRiskDetailVisible" class="risk-signal-detail-dialog" modal-class="risk-signal-detail-modal"
width="1280px" align-center :show-close="true" destroy-on-close @closed="handleCloseRiskDetail"> width="1280px" align-center :show-close="true" destroy-on-close @closed="handleCloseRiskDetail">
<template #header> <template #header>
<img class="header-icon" src="./assets/images/risk-icon.png" alt="" /> <img class="header-icon" :src="riskDetailHeaderIconSrc" alt="" />
<span v-if="riskDetailListLevelText" class="risk-signal-detail-dialog__level" <span v-if="riskDetailListLevelText" class="risk-signal-detail-dialog__level"
:class="riskDetailListLevelModifierClass">{{ riskDetailListLevelText }}</span> :class="riskDetailListLevelModifierClass">{{ riskDetailListLevelText }}</span>
<div v-if="riskDetailBodyFromApi" class="risk-signal-detail-dialog__read-indicator"> <div v-if="riskDetailBodyFromApi" class="risk-signal-detail-dialog__read-indicator">
...@@ -597,6 +597,15 @@ const riskDetailListLevelModifierClass = computed( ...@@ -597,6 +597,15 @@ const riskDetailListLevelModifierClass = computed(
() => `risk-signal-detail-dialog__level--${getRiskDetailLevelModifier(riskDetailListLevelText.value)}` () => `risk-signal-detail-dialog__level--${getRiskDetailLevelModifier(riskDetailListLevelText.value)}`
); );
const riskDetailHeaderIconSrc = computed(() => {
const key = getRiskListItemLevelKey(riskDetailListLevelText.value);
if (key === "lv1") return new URL("./assets/images/risk-icon-red.png", import.meta.url).href;
if (key === "lv2") return new URL("./assets/images/risk-icon-orange.png", import.meta.url).href;
if (key === "lv3") return new URL("./assets/images/risk-icon-yellow.png", import.meta.url).href;
if (key === "lv4") return new URL("./assets/images/risk-icon-green.png", import.meta.url).href;
return new URL("./assets/images/risk-icon-blue.png", import.meta.url).href;
});
/** 列表项风险等级样式键:与 `@/components/base/riskSignal` itemLeftStatus1~5 一致 */ /** 列表项风险等级样式键:与 `@/components/base/riskSignal` itemLeftStatus1~5 一致 */
const getRiskListItemLevelKey = (level) => { const getRiskListItemLevelKey = (level) => {
const t = String(level ?? "").trim(); const t = String(level ?? "").trim();
...@@ -623,6 +632,12 @@ const isRiskLevelNoData = (level) => { ...@@ -623,6 +632,12 @@ const isRiskLevelNoData = (level) => {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const formatThousands = (val) => {
const n = Number(val ?? 0);
if (!Number.isFinite(n)) return "0";
return new Intl.NumberFormat("en-US").format(n);
};
const consumeOpenFirstDetailFromQuery = async () => { const consumeOpenFirstDetailFromQuery = async () => {
if (route.query[OPEN_FIRST_RISK_DETAIL_QUERY_KEY] !== "1") { if (route.query[OPEN_FIRST_RISK_DETAIL_QUERY_KEY] !== "1") {
return; return;
...@@ -1095,6 +1110,10 @@ onMounted(async () => { ...@@ -1095,6 +1110,10 @@ onMounted(async () => {
unicode-bidi: isolate; unicode-bidi: isolate;
} }
} }
.item--shift-left {
transform: translateX(-20px);
}
} }
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论