提交 7cc43bd9 authored 作者: 朱政's avatar 朱政

Merge branch 'pre' into zz-dev

流水线 #116 已取消 于阶段
<template> <template>
<div class="analysis-box-wrapper" :style="{ width: width ? width : '100%', height: height ? height : '100%' }"> <div class="analysis-box-wrapper" :style="{ width: width ? width : '100%', height: height ? height : '100%' }">
<div class="wrapper-header"> <div class="wrapper-header">
<div class="header-icon"></div> <div class="header-icon"></div>
<div class="header-title"> <div class="header-title">
<div v-if="title">{{ title }}</div> <div v-if="title">{{ title }}</div>
<slot v-else name="custom-title"></slot> <slot v-else name="custom-title"></slot>
</div> </div>
<div class="header-btn" v-if="!showAllBtn"> <div class="header-btn" v-if="!showAllBtn">
<slot name="header-btn"></slot> <slot name="header-btn"></slot>
</div> </div>
<div class="header-btn1" v-else> <div class="header-btn1" v-else>
<slot name="header-btn"></slot> <slot name="header-btn"></slot>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="header-right-btn" @click="handleSave" v-if="showAllBtn"> <div class="header-right-btn" @click="handleSave" v-if="showAllBtn">
<img src="@/assets/icons/box-header-icon1.png" alt=""> <img src="@/assets/icons/box-header-icon1.png" alt="" />
</div> </div>
<div class="header-right-btn" @click="handleDownload"> <div class="header-right-btn" @click="handleDownload">
<img src="@/assets/icons/box-header-icon2.png" alt=""> <img src="@/assets/icons/box-header-icon2.png" alt="" />
</div> </div>
<div class="header-right-btn" @click="handleCollect"> <div class="header-right-btn" @click="handleCollect">
<img src="@/assets/icons/box-header-icon3.png" alt=""> <img src="@/assets/icons/box-header-icon3.png" alt="" />
</div> </div>
</div> </div>
</div> </div>
<div class="wrapper-main"> <div class="wrapper-main">
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ElMessage } from 'element-plus' import { ElMessage } from "element-plus";
import { ref, computed } from 'vue' import { ref, computed } from "vue";
const props = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: '' default: ""
}, },
width: { width: {
type: String, type: String,
default: '' default: ""
}, },
height: { height: {
type: String, type: String,
default: '' default: ""
}, },
showAllBtn: { showAllBtn: {
type: Boolean, type: Boolean,
default: true default: true
}, },
// 当业务功能尚未实现时,点击右上角图标仅弹出统一提示 // 当业务功能尚未实现时,点击右上角图标仅弹出统一提示
devTip: { devTip: {
type: Boolean, type: Boolean,
default: false default: false
} }
}) });
const handleSave = () => { const handleSave = () => {
if (props.devTip) { if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!') ElMessage.warning("当前功能正在开发中,敬请期待!");
return return;
} }
ElMessage.success('保存当前内容') ElMessage.success("保存当前内容");
// emit('save') // emit('save')
} };
const handleDownload = () => { const handleDownload = () => {
if (props.devTip) { if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!') ElMessage.warning("当前功能正在开发中,敬请期待!");
return return;
} }
ElMessage.success('下载当前内容') ElMessage.success("下载当前内容");
// emit('download') // emit('download')
};
}
const handleCollect = () => { const handleCollect = () => {
if (props.devTip) { if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!') ElMessage.warning("当前功能正在开发中,敬请期待!");
return return;
} }
ElMessage.success('收藏当前内容') ElMessage.success("收藏当前内容");
// emit('collect') // emit('collect')
};
}
const emit = defineEmits(["save", "download", "collect"]);
const emit = defineEmits(['save', 'download', 'collect'])
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.analysis-box-wrapper { .analysis-box-wrapper {
border: 1px solid rgba(234, 236, 238, 1); border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px; border-radius: 10px;
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: relative;
.wrapper-header { .wrapper-header {
height: 45px; height: 45px;
display: flex; display: flex;
padding-right: 14px; padding-right: 14px;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
.header-icon { .header-icon {
width: 8px; width: 8px;
height: 20px; height: 20px;
background: var(--color-main-active); background: var(--color-main-active);
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
margin-right: 14px; margin-right: 14px;
} }
.header-title { .header-title {
flex: auto; flex: auto;
width: 20px; width: 20px;
// color: var(--color-main-active); // color: var(--color-main-active);
// font-family: Source Han Sans CN; // font-family: Source Han Sans CN;
// font-size: 20px; // font-size: 20px;
// font-weight: 700; // font-weight: 700;
// line-height: 26px; // line-height: 26px;
// letter-spacing: 0px; // letter-spacing: 0px;
height: 100%; height: 100%;
&>div { & > div {
height: 100%; height: 100%;
color: var(--color-main-active); color: var(--color-main-active);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
font-size: 20px; font-size: 20px;
line-height: 45px; line-height: 45px;
font-weight: 700; font-weight: 700;
} }
} }
.header-btn { .header-btn {
// display: flex; // display: flex;
// justify-content: flex-end; // justify-content: flex-end;
// gap: 8px; // gap: 8px;
margin-right: 10px; margin-right: 10px;
} }
// .header-btn1 { // .header-btn1 {
// position: absolute; // position: absolute;
// top: 14px; // top: 14px;
// right: 116px; // right: 116px;
// } // }
.header-right { .header-right {
height: 28px; height: 28px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 4px; gap: 4px;
.header-right-btn { .header-right-btn {
width: 28px; width: 28px;
height: 28px; height: 28px;
cursor: pointer; cursor: pointer;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
} }
} }
.wrapper-main { .wrapper-main {
height: calc(100% - 45px); height: calc(100% - 45px);
overflow: hidden; overflow: hidden;
// overflow-y: auto; // overflow-y: auto;
padding: 5px auto; padding: 5px auto;
} }
} }
</style> </style>
...@@ -368,19 +368,29 @@ ...@@ -368,19 +368,29 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 505px; margin-top: 16px"> <el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 540px; margin-top: 16px">
<el-col :span="8"> <el-col :span="8">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="480px"> <custom-container title="实体领域分布" :titleIcon="radarIcon" height="540px">
<template #header-right> <template #header-right>
<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: '460px' }" /> <EChart :option="radarOption" autoresize :style="{ height: '420px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">实体领域分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="radarChart.loading ? '解读生成中...' : radarChart.interpretation" />
</div>
</template> </template>
</custom-container> </custom-container>
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="480px"> <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-model="trendChecked" label="50%规则" size="large" /> <el-checkbox v-model="trendChecked" label="50%规则" size="large" />
...@@ -390,13 +400,23 @@ ...@@ -390,13 +400,23 @@
</div> </div>
</template> </template>
<template #default> <template #default>
<EChart :option="trendOption" autoresize :style="{ height: '400px' }" /> <EChart :option="trendOption" autoresize :style="{ height: '420px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">实体清单数量增长趋势情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="trendChart.loading ? '解读生成中...' : trendChart.interpretation" />
</div>
</template> </template>
</custom-container> </custom-container>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" style="width: 1600px; margin: 0 auto; margin-top: 39px"> <el-row :gutter="20" 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 <div
...@@ -714,11 +734,18 @@ import { ElMessage, ElMessageBox } from "element-plus"; ...@@ -714,11 +734,18 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { DArrowRight, Warning, Search } from "@element-plus/icons-vue"; import { DArrowRight, Warning, Search } 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 AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
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";
const gotoCompanyPages = useGotoCompanyPages(); const gotoCompanyPages = useGotoCompanyPages();
const gotoNewsDetail = useGotoNewsDetail(); const gotoNewsDetail = useGotoNewsDetail();
const trendChart = useChartInterpretation();
const radarChart = useChartInterpretation();
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
...@@ -1026,6 +1053,7 @@ const fetchTrendData = async () => { ...@@ -1026,6 +1053,7 @@ 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 });
} }
} catch (error) { } catch (error) {
console.error("获取趋势图数据失败:", error); console.error("获取趋势图数据失败:", error);
...@@ -1297,6 +1325,7 @@ const fetchRadarData = async checked => { ...@@ -1297,6 +1325,7 @@ const fetchRadarData = async checked => {
} }
}; };
}); });
radarChart.interpret({ type: "雷达图", name: "实体领域分布", data: data });
} }
} catch (error) { } catch (error) {
console.error("获取雷达图数据失败:", error); console.error("获取雷达图数据失败:", error);
...@@ -3573,7 +3602,7 @@ const handleMediaClick = item => { ...@@ -3573,7 +3602,7 @@ const handleMediaClick = item => {
} }
.scroll-main { .scroll-main {
height: calc(100% - 144px) !important; // height: calc(100% - 144px) !important;
} }
.center-center { .center-center {
...@@ -3596,4 +3625,50 @@ const handleMediaClick = item => { ...@@ -3596,4 +3625,50 @@ const handleMediaClick = item => {
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
} }
} }
.data-origin-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 22px 0;
.data-origin-icon {
width: 16px;
height: 16px;
font-size: 0px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
}
}
.data-origin-text {
font-family: Source Han Sans CN;
font-size: 14px;
color: var(--text-primary-50-color);
}
}
.ai-pane {
position: absolute;
right: 0px;
bottom: 15px;
z-index: 2;
:deep(.ai-pane-wrapper) {
display: none;
}
:deep(.ai-button-wrapper) {
display: flex;
}
&:hover {
width: 100%;
bottom: 0px;
:deep(.ai-pane-wrapper) {
display: block;
}
:deep(.ai-button-wrapper) {
display: none;
}
}
}
</style> </style>
import { ref } from "vue";
export const useChartInterpretation = () => {
const loading = ref(false);
const interpretation = ref("");
const error = ref(null);
const interpret = async text => {
loading.value = true;
error.value = null;
interpretation.value = "";
try {
const response = await fetch("/aiAnalysis/chart_interpretation", {
method: "POST",
headers: {
"X-API-Key": "aircasKEY19491001",
"Content-Type": "application/json"
},
body: JSON.stringify({ text })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const content = line.substring(6);
const textMatch = content.match(/"解读":\s*"([^"]*)"/);
if (textMatch && textMatch[1]) {
interpretation.value = textMatch[1];
}
}
}
}
} catch (err) {
error.value = err.message || "AI 解读失败";
console.error("AI Chart Interpretation Error:", err);
} finally {
loading.value = false;
}
};
return {
loading,
interpretation,
error,
interpret
};
};
...@@ -104,8 +104,9 @@ ...@@ -104,8 +104,9 @@
</div> </div>
</div> </div>
</template> </template>
<div class="echarts" ref="sanctionCountChartRef"></div> <!-- <div class="echarts" ref="sanctionCountChartRef"></div> -->
<div class="bottom"> <EChart :option="sanctionCountChartOption" autoresize :style="{ height: '300px' }" />
<!-- <div class="bottom">
<div class="ai"> <div class="ai">
<div class="left"> <div class="left">
<img :src="ai" alt="" class="icon1" /> <img :src="ai" alt="" class="icon1" />
...@@ -115,56 +116,20 @@ ...@@ -115,56 +116,20 @@
<img :src="right" alt="" class="icon2" /> <img :src="right" alt="" class="icon2" />
</div> </div>
</div> </div>
</div> -->
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体数量变化情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="sanctionCountChart.interpretation" />
</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">
<el-select
v-model="regionTime"
class="time-select"
placeholder="请选择"
@change="getRegionCountData"
>
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<div class="btn">
<img src="../../assets/数据库按钮.png" alt="" />
<img src="../../assets/下载按钮.png" alt="" />
<img src="../../assets/收藏按钮.png" alt="" />
</div>
</div>
</div>
<div class="map-wrapper">
<div class="map-chart" ref="mapChartRef"></div>
<div class="rank-list">
<div class="rank-item" v-for="(item, index) in rankData" :key="index">
<div class="rank-index" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
<div class="rank-name">{{ item.name }}</div>
<div class="rank-bar-bg">
<div
class="rank-bar-fill"
:style="{ width: (item.value / maxRankValue) * 100 + '%', background: getBarColor(index) }"
></div>
</div>
<div class="rank-value">{{ item.value }}家</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> -->
<AnalysisBox title="制裁实体地域分布情况"> <AnalysisBox title="制裁实体地域分布情况">
<template #header-btn> <template #header-btn>
<el-select v-model="regionTime" class="time-select" placeholder="请选择" @change="getRegionCountData"> <el-select v-model="regionTime" class="time-select" placeholder="请选择" @change="getRegionCountData">
...@@ -190,7 +155,7 @@ ...@@ -190,7 +155,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bottom"> <!-- <div class="bottom">
<div class="ai"> <div class="ai">
<div class="left"> <div class="left">
<img :src="ai" alt="" class="icon1" /> <img :src="ai" alt="" class="icon1" />
...@@ -200,44 +165,29 @@ ...@@ -200,44 +165,29 @@
<img :src="right" alt="" class="icon2" /> <img :src="right" alt="" class="icon2" />
</div> </div>
</div> </div>
</div> -->
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体地域分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="rankChart.interpretation" />
</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">
<el-select v-model="domainTime" class="time-select" placeholder="请选择" @change="getDomainCountData">
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<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="domainChartRef"></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="制裁实体领域分布情况">
<template #header-btn> <template #header-btn>
<el-select v-model="domainTime" class="time-select" placeholder="请选择" @change="getDomainCountData"> <el-select v-model="domainTime" class="time-select" placeholder="请选择" @change="getDomainCountData">
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</template> </template>
<div class="echarts" ref="domainChartRef"></div> <!-- <div class="echarts" ref="domainChartRef"></div> -->
<div class="bottom"> <EChart :option="domainChartOption" autoresize :style="{ height: '300px' }" />
<!-- <div class="bottom">
<div class="ai"> <div class="ai">
<div class="left"> <div class="left">
<img :src="ai" alt="" class="icon1" /> <img :src="ai" alt="" class="icon1" />
...@@ -247,44 +197,29 @@ ...@@ -247,44 +197,29 @@
<img :src="right" alt="" class="icon2" /> <img :src="right" alt="" class="icon2" />
</div> </div>
</div> </div>
</div> -->
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体领域分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="domainChart.interpretation" />
</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">
<el-select v-model="typeTime" class="time-select" placeholder="请选择" @change="getTypeCountData">
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<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="制裁实体类型分布情况">
<template #header-btn> <template #header-btn>
<el-select v-model="typeTime" class="time-select" placeholder="请选择" @change="getTypeCountData"> <el-select v-model="typeTime" class="time-select" placeholder="请选择" @change="getTypeCountData">
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</template> </template>
<div class="echarts" ref="typeChartRef"></div> <!-- <div class="echarts" ref="typeChartRef"></div> -->
<div class="bottom"> <EChart :option="typeChartOption" autoresize :style="{ height: '300px' }" />
<!-- <div class="bottom">
<div class="ai"> <div class="ai">
<div class="left"> <div class="left">
<img :src="ai" alt="" class="icon1" /> <img :src="ai" alt="" class="icon1" />
...@@ -294,6 +229,16 @@ ...@@ -294,6 +229,16 @@
<img :src="right" alt="" class="icon2" /> <img :src="right" alt="" class="icon2" />
</div> </div>
</div> </div>
</div> -->
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体类型分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="typeChart.interpretation" />
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -314,7 +259,16 @@ import { ...@@ -314,7 +259,16 @@ import {
getTechDomainCount, getTechDomainCount,
getEntityTypeCount getEntityTypeCount
} from "@/api/exportControlV2.0"; } from "@/api/exportControlV2.0";
import EChart from "@/components/Chart/index.vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import tipsIcon from "../../../assets/icons/info-icon.png";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import { useChartInterpretation } from "@/views/exportControl/utils/common";
const sanctionCountChart = useChartInterpretation();
const domainChart = useChartInterpretation();
const typeChart = useChartInterpretation();
const rankChart = useChartInterpretation();
const route = useRoute(); const route = useRoute();
// 实体清单-数据统计-制裁实体类型分布情况 // 实体清单-数据统计-制裁实体类型分布情况
...@@ -337,6 +291,7 @@ const getTypeCountData = async () => { ...@@ -337,6 +291,7 @@ const getTypeCountData = async () => {
name: item.name, name: item.name,
value: item.count || item.value value: item.count || item.value
})); }));
typeChart.interpret({ type: "饼图", name: "制裁实体类型分布情况", data: data });
updateTypeChart(); updateTypeChart();
} }
} catch (error) { } catch (error) {
...@@ -365,6 +320,7 @@ const getDomainCountData = async () => { ...@@ -365,6 +320,7 @@ const getDomainCountData = async () => {
value: item.count || item.value value: item.count || item.value
})); }));
updateDomainChart(); updateDomainChart();
domainChart.interpret({ type: "饼图", name: "制裁实体领域分布情况", data: data });
} }
} catch (error) { } catch (error) {
console.error("获取实体清单-数据统计-制裁实体领域分布情况失败:", error); console.error("获取实体清单-数据统计-制裁实体领域分布情况失败:", error);
...@@ -401,6 +357,7 @@ const getRegionCountData = async () => { ...@@ -401,6 +357,7 @@ const getRegionCountData = async () => {
})); }));
// Sort by value descending // Sort by value descending
rankData.value.sort((a, b) => b.value - a.value); rankData.value.sort((a, b) => b.value - a.value);
rankChart.interpret({ type: "柱状图", name: "制裁实体地域分布情况", data: data });
updateMapChart(); updateMapChart();
} }
} catch (error) { } catch (error) {
...@@ -421,6 +378,7 @@ const getSanctionCountChangeData = async () => { ...@@ -421,6 +378,7 @@ const getSanctionCountChangeData = async () => {
const res = await getSanctionCountChange(param); const res = await getSanctionCountChange(param);
sanctionCountChange.value = res.data || []; sanctionCountChange.value = res.data || [];
updateSanctionCountChart(); updateSanctionCountChart();
sanctionCountChart.interpret({ type: "饼图", name: "制裁实体数量变化情况", data: res.data });
} catch (error) { } catch (error) {
console.error("获取实体清单-数据统计-制裁实体数量变化情况失败:", error); console.error("获取实体清单-数据统计-制裁实体数量变化情况失败:", error);
} }
...@@ -467,13 +425,73 @@ const getBarColor = index => { ...@@ -467,13 +425,73 @@ const getBarColor = index => {
if (index === 1 || index === 2) return "linear-gradient(90deg, rgba(255, 172, 77, 0) 0%, rgba(255, 172, 77, 1) 100%)"; if (index === 1 || index === 2) return "linear-gradient(90deg, rgba(255, 172, 77, 0) 0%, rgba(255, 172, 77, 1) 100%)";
return "linear-gradient(90deg, rgba(5, 95, 194, 0) 0%, rgba(5, 95, 194, 1) 100%)"; return "linear-gradient(90deg, rgba(5, 95, 194, 0) 0%, rgba(5, 95, 194, 1) 100%)";
}; };
const sanctionCountChartOption = ref({
grid: {
top: "15%",
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow"
}
},
xAxis: {
type: "category",
data: [],
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
color: "#909399",
fontFamily: "Microsoft YaHei",
interval: 0,
rotate: 0 // 如果数据较多,旋转标签
}
},
yAxis: {
type: "value",
name: "数量",
nameLocation: "end",
nameGap: 5,
nameTextStyle: {
padding: [0, 40, 10, 0] // [上, 右, 下, 左] —— 减少右侧 padding 相当于左移
},
splitLine: {
lineStyle: {
type: "dashed",
color: "#E6EBF5"
}
},
axisLabel: {
color: "#909399",
fontFamily: "Microsoft YaHei"
}
},
series: [
{
data: [],
type: "bar",
barWidth: 16,
itemStyle: {
borderRadius: [20, 20, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgb(5, 95, 194)" },
{ offset: 1, color: "rgba(5, 95, 194, 0)" }
])
}
}
]
});
const updateSanctionCountChart = () => { const updateSanctionCountChart = () => {
if (!sanctionCountChartRef.value) return; // if (!sanctionCountChartRef.value) return;
let chart = echarts.getInstanceByDom(sanctionCountChartRef.value); // let chart = echarts.getInstanceByDom(sanctionCountChartRef.value);
if (!chart) { // if (!chart) {
chart = echarts.init(sanctionCountChartRef.value); // chart = echarts.init(sanctionCountChartRef.value);
} // }
const data = sanctionCountChange.value || []; const data = sanctionCountChange.value || [];
// 按日期/年份排序 (从小到大) // 按日期/年份排序 (从小到大)
...@@ -488,65 +506,68 @@ const updateSanctionCountChart = () => { ...@@ -488,65 +506,68 @@ const updateSanctionCountChart = () => {
const xData = data.map(item => item.year || item.name || item.date || ""); const xData = data.map(item => item.year || item.name || item.date || "");
const yData = data.map(item => item.count || item.value || item.num || 0); const yData = data.map(item => item.count || item.value || item.num || 0);
const option = { sanctionCountChartOption.value.xAxis.data = xData;
grid: { sanctionCountChartOption.value.series[0].data = yData;
top: "15%", sanctionCountChartOption.value.xAxis.axisLabel.rotate = xData.length > 10 ? 30 : 0; // 如果数据较多,旋转标签
left: "3%", // const option = {
right: "4%", // grid: {
bottom: "3%", // top: "15%",
containLabel: true // left: "3%",
}, // right: "4%",
tooltip: { // bottom: "3%",
trigger: "axis", // containLabel: true
axisPointer: { // },
type: "shadow" // tooltip: {
} // trigger: "axis",
}, // axisPointer: {
xAxis: { // type: "shadow"
type: "category", // }
data: xData.length ? xData : ["2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025"], // },
axisTick: { show: false }, // xAxis: {
axisLine: { show: false }, // type: "category",
axisLabel: { // data: xData.length ? xData : ["2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025"],
color: "#909399", // axisTick: { show: false },
fontFamily: "Microsoft YaHei", // axisLine: { show: false },
interval: 0, // axisLabel: {
rotate: xData.length > 10 ? 30 : 0 // 如果数据较多,旋转标签 // color: "#909399",
} // fontFamily: "Microsoft YaHei",
}, // interval: 0,
yAxis: { // rotate: xData.length > 10 ? 30 : 0 // 如果数据较多,旋转标签
type: "value", // }
splitLine: { // },
lineStyle: { // yAxis: {
type: "dashed", // type: "value",
color: "#E6EBF5" // splitLine: {
} // lineStyle: {
}, // type: "dashed",
axisLabel: { // color: "#E6EBF5"
color: "#909399", // }
fontFamily: "Microsoft YaHei" // },
} // axisLabel: {
}, // color: "#909399",
series: [ // fontFamily: "Microsoft YaHei"
{ // }
data: yData.length ? yData : [200, 250, 300, 400, 500, 600, 610, 650, 750, 850], // },
type: "bar", // series: [
barWidth: 16, // {
itemStyle: { // data: yData.length ? yData : [200, 250, 300, 400, 500, 600, 610, 650, 750, 850],
borderRadius: [20, 20, 0, 0], // type: "bar",
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ // barWidth: 16,
{ offset: 0, color: "rgb(5, 95, 194)" }, // itemStyle: {
{ offset: 1, color: "rgba(5, 95, 194, 0)" } // borderRadius: [20, 20, 0, 0],
]) // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
} // { offset: 0, color: "rgb(5, 95, 194)" },
} // { offset: 1, color: "rgba(5, 95, 194, 0)" }
] // ])
}; // }
chart.setOption(option); // }
// ]
window.addEventListener("resize", () => { // };
chart.resize(); // chart.setOption(option);
});
// window.addEventListener("resize", () => {
// chart.resize();
// });
}; };
const initSanctionCountChart = () => { const initSanctionCountChart = () => {
...@@ -622,12 +643,95 @@ const initMapChart = () => { ...@@ -622,12 +643,95 @@ const initMapChart = () => {
updateMapChart(); updateMapChart();
}; };
const domainChartOption = ref({
tooltip: {
trigger: "item",
formatter: params => {
// 如果是“其他”项,展示详细列表
if (params.name === "其他" && params.data.extra) {
let listStr = params.data.extra
.map(item => {
const percent = totalValue ? ((item.value / totalValue) * 100).toFixed(2) : 0;
return `<div style="display:flex;justify-content:space-between;gap:10px;"><span>${item.name}</span><span style="font-weight:bold">${item.value}家 (${percent}%)</span></div>`;
})
.join("");
return `<div style="text-align:left;">
<div style="font-weight:bold;margin-bottom:5px;">其他 (${params.value}${params.percent}%)</div>
${listStr}
</div>`;
}
// 默认展示
return `${params.name}: ${params.value} (${params.percent}%)`;
}
},
color: [
"#60acfc", // 集成电路 - 蓝色
"#feb64d", // 人工智能 - 橙色
"#5bc49f", // 通信网络 - 青色
"#959cf3", // 量子科技 - 淡蓝紫
"#ff7c7c", // 先进制造 - 红色
"#b689ea", // 新材料 - 紫色
"#32d3eb" // 航空航天 - 浅蓝
],
series: [
{
name: "制裁实体领域分布",
type: "pie",
radius: [73.5, 89.5],
center: ["50%", "50%"],
startAngle: 0,
data: [],
label: {
show: true,
alignTo: "edge",
minMargin: 5,
edgeDistance: 10,
formatter: params => {
return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}";
},
rich: {
name: {
fontSize: 18,
fontWeight: 700,
color: "rgb(59, 65, 75)",
padding: [0, 0, 5, 0],
fontFamily: "Microsoft YaHei",
lineHeight: 26
},
value: {
fontSize: 16,
fontWeight: 400,
color: "rgb(95, 101, 108)",
fontFamily: "Microsoft YaHei",
lineHeight: 24,
padding: [5, 0, 0, 0]
}
}
},
labelLine: {
show: true,
length: 15,
length2: 0,
maxSurfaceAngle: 80,
lineStyle: {
width: 1.1
}
},
labelLayout: {
hideOverlap: true
},
itemStyle: {
borderWidth: 0
}
}
]
});
const updateDomainChart = () => { const updateDomainChart = () => {
if (!domainChartRef.value) return; // if (!domainChartRef.value) return;
let chart = echarts.getInstanceByDom(domainChartRef.value); // let chart = echarts.getInstanceByDom(domainChartRef.value);
if (!chart) { // if (!chart) {
chart = echarts.init(domainChartRef.value); // chart = echarts.init(domainChartRef.value);
} // }
let data = domainData.value.length ? [...domainData.value] : []; let data = domainData.value.length ? [...domainData.value] : [];
...@@ -656,108 +760,192 @@ const updateDomainChart = () => { ...@@ -656,108 +760,192 @@ const updateDomainChart = () => {
} }
data = topData; data = topData;
} }
domainChartOption.value.series[0].data = data;
// const option = {
// tooltip: {
// trigger: "item",
// formatter: params => {
// // 如果是“其他”项,展示详细列表
// if (params.name === "其他" && params.data.extra) {
// let listStr = params.data.extra
// .map(item => {
// const percent = totalValue ? ((item.value / totalValue) * 100).toFixed(2) : 0;
// return `<div style="display:flex;justify-content:space-between;gap:10px;"><span>${item.name}</span><span style="font-weight:bold">${item.value}家 (${percent}%)</span></div>`;
// })
// .join("");
// return `<div style="text-align:left;">
// <div style="font-weight:bold;margin-bottom:5px;">其他 (${params.value} ${params.percent}%)</div>
// ${listStr}
// </div>`;
// }
// // 默认展示
// return `${params.name}: ${params.value} (${params.percent}%)`;
// }
// },
// color: [
// "#60acfc", // 集成电路 - 蓝色
// "#feb64d", // 人工智能 - 橙色
// "#5bc49f", // 通信网络 - 青色
// "#959cf3", // 量子科技 - 淡蓝紫
// "#ff7c7c", // 先进制造 - 红色
// "#b689ea", // 新材料 - 紫色
// "#32d3eb" // 航空航天 - 浅蓝
// ],
// series: [
// {
// name: "制裁实体领域分布",
// type: "pie",
// radius: [73.5, 89.5],
// center: ["50%", "50%"],
// startAngle: 0,
// data: data,
// label: {
// show: true,
// alignTo: "edge",
// minMargin: 5,
// edgeDistance: 10,
// formatter: params => {
// return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}";
// },
// rich: {
// name: {
// fontSize: 18,
// fontWeight: 700,
// color: "rgb(59, 65, 75)",
// padding: [0, 0, 5, 0],
// fontFamily: "Microsoft YaHei",
// lineHeight: 26
// },
// value: {
// fontSize: 16,
// fontWeight: 400,
// color: "rgb(95, 101, 108)",
// fontFamily: "Microsoft YaHei",
// lineHeight: 24,
// padding: [5, 0, 0, 0]
// }
// }
// },
// labelLine: {
// show: true,
// length: 15,
// length2: 0,
// maxSurfaceAngle: 80,
// lineStyle: {
// width: 1.1
// }
// },
// labelLayout: {
// hideOverlap: true
// },
// itemStyle: {
// borderWidth: 0
// }
// }
// ]
// };
// chart.setOption(option);
// window.addEventListener("resize", () => {
// chart.resize();
// });
};
const option = { const initDomainChart = () => {
tooltip: { updateDomainChart();
trigger: "item", };
formatter: params => {
// 如果是“其他”项,展示详细列表 const typeChartOption = ref({
if (params.name === "其他" && params.data.extra) { tooltip: {
let listStr = params.data.extra trigger: "item",
.map(item => { formatter: params => {
const percent = totalValue ? ((item.value / totalValue) * 100).toFixed(2) : 0; if (params.name === "其他" && params.data.extra) {
return `<div style="display:flex;justify-content:space-between;gap:10px;"><span>${item.name}</span><span style="font-weight:bold">${item.value}家 (${percent}%)</span></div>`; let listStr = params.data.extra
}) .map(item => {
.join(""); const percent = totalValue ? ((item.value / totalValue) * 100).toFixed(2) : 0;
return `<div style="text-align:left;"> return `<div style="display:flex;justify-content:space-between;gap:10px;"><span>${item.name}</span><span style="font-weight:bold">${item.value}家 (${percent}%)</span></div>`;
})
.join("");
return `<div style="text-align:left;">
<div style="font-weight:bold;margin-bottom:5px;">其他 (${params.value}${params.percent}%)</div> <div style="font-weight:bold;margin-bottom:5px;">其他 (${params.value}${params.percent}%)</div>
${listStr} ${listStr}
</div>`; </div>`;
}
// 默认展示
return `${params.name}: ${params.value} (${params.percent}%)`;
} }
}, return `${params.name}: ${params.value} (${params.percent}%)`;
color: [ }
"#60acfc", // 集成电路 - 蓝色 },
"#feb64d", // 人工智能 - 橙色 color: [
"#5bc49f", // 通信网络 - 青色 "#3B82F6", // 企业 - 蓝色
"#959cf3", // 量子科技 - 淡蓝紫 "#feb64d", // 高校 - 橙色
"#ff7c7c", // 先进制造 - 红色 "#ff9f9f" // 科研院所 - 粉红
"#b689ea", // 新材料 - 紫色 ],
"#32d3eb" // 航空航天 - 浅蓝 series: [
], {
series: [ name: "制裁实体类型分布",
{ type: "pie",
name: "制裁实体领域分布", radius: [73.5, 89.5],
type: "pie", center: ["50%", "50%"],
radius: [73.5, 89.5], startAngle: -90,
center: ["50%", "50%"], data: [],
startAngle: 0, label: {
data: data, show: true,
label: { alignTo: "edge",
show: true, minMargin: 5,
alignTo: "edge", edgeDistance: 10,
minMargin: 5, formatter: params => {
edgeDistance: 10, return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}";
formatter: params => {
return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}";
},
rich: {
name: {
fontSize: 18,
fontWeight: 700,
color: "rgb(59, 65, 75)",
padding: [0, 0, 5, 0],
fontFamily: "Microsoft YaHei",
lineHeight: 26
},
value: {
fontSize: 16,
fontWeight: 400,
color: "rgb(95, 101, 108)",
fontFamily: "Microsoft YaHei",
lineHeight: 24,
padding: [5, 0, 0, 0]
}
}
}, },
labelLine: { rich: {
show: true, name: {
length: 15, fontSize: 18,
length2: 0, fontWeight: 700,
maxSurfaceAngle: 80, color: "rgb(59, 65, 75)",
lineStyle: { padding: [0, 0, 5, 0],
width: 1.1 fontFamily: "Microsoft YaHei",
lineHeight: 26
},
value: {
fontSize: 16,
fontWeight: 400,
color: "rgb(95, 101, 108)",
fontFamily: "Microsoft YaHei",
lineHeight: 24,
padding: [5, 0, 0, 0]
} }
},
labelLayout: {
hideOverlap: true
},
itemStyle: {
borderWidth: 0
} }
},
labelLine: {
show: true,
length: 15,
length2: 0,
maxSurfaceAngle: 80,
lineStyle: {
width: 1
}
},
labelLayout: function (params) {
const isLeft = params.labelRect.x < chart.getWidth() / 2;
const points = params.labelLinePoints;
// Update the end point.
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
},
itemStyle: {
borderWidth: 0
} }
] }
}; ]
});
chart.setOption(option);
window.addEventListener("resize", () => {
chart.resize();
});
};
const initDomainChart = () => {
updateDomainChart();
};
const updateTypeChart = () => { const updateTypeChart = () => {
if (!typeChartRef.value) return; // if (!typeChartRef.value) return;
let chart = echarts.getInstanceByDom(typeChartRef.value); // let chart = echarts.getInstanceByDom(typeChartRef.value);
if (!chart) { // if (!chart) {
chart = echarts.init(typeChartRef.value); // chart = echarts.init(typeChartRef.value);
} // }
let data = typeData.value.length let data = typeData.value.length
? [...typeData.value] ? [...typeData.value]
...@@ -765,7 +953,7 @@ const updateTypeChart = () => { ...@@ -765,7 +953,7 @@ const updateTypeChart = () => {
{ value: 50, name: "企业" }, { value: 50, name: "企业" },
{ value: 32, name: "高校" }, { value: 32, name: "高校" },
{ value: 32, name: "科研院所" } { value: 32, name: "科研院所" }
]; ];
// 2. 聚合逻辑:保留前5项,其余合并为“其他” // 2. 聚合逻辑:保留前5项,其余合并为“其他”
data.sort((a, b) => b.value - a.value); data.sort((a, b) => b.value - a.value);
...@@ -780,105 +968,105 @@ const updateTypeChart = () => { ...@@ -780,105 +968,105 @@ const updateTypeChart = () => {
const otherData = data.slice(TOP_N); const otherData = data.slice(TOP_N);
const otherValue = otherData.reduce((sum, item) => sum + (item.value || 0), 0); const otherValue = otherData.reduce((sum, item) => sum + (item.value || 0), 0);
if (otherValue > 0) { // if (otherValue > 0) {
topData.push({ // topData.push({
name: "其他", // name: "其他",
value: otherValue, // value: otherValue,
extra: otherData // extra: otherData
}); // });
} // }
data = topData; typeChartOption.value.series[0].data = topData;
} }
const option = { // const option = {
tooltip: { // tooltip: {
trigger: "item", // trigger: "item",
formatter: params => { // formatter: params => {
if (params.name === "其他" && params.data.extra) { // if (params.name === "其他" && params.data.extra) {
let listStr = params.data.extra // let listStr = params.data.extra
.map(item => { // .map(item => {
const percent = totalValue ? ((item.value / totalValue) * 100).toFixed(2) : 0; // const percent = totalValue ? ((item.value / totalValue) * 100).toFixed(2) : 0;
return `<div style="display:flex;justify-content:space-between;gap:10px;"><span>${item.name}</span><span style="font-weight:bold">${item.value}家 (${percent}%)</span></div>`; // return `<div style="display:flex;justify-content:space-between;gap:10px;"><span>${item.name}</span><span style="font-weight:bold">${item.value}家 (${percent}%)</span></div>`;
}) // })
.join(""); // .join("");
return `<div style="text-align:left;"> // return `<div style="text-align:left;">
<div style="font-weight:bold;margin-bottom:5px;">其他 (${params.value}${params.percent}%)</div> // <div style="font-weight:bold;margin-bottom:5px;">其他 (${params.value} ${params.percent}%)</div>
${listStr} // ${listStr}
</div>`; // </div>`;
} // }
return `${params.name}: ${params.value} (${params.percent}%)`; // return `${params.name}: ${params.value} (${params.percent}%)`;
} // }
}, // },
color: [ // color: [
"#3B82F6", // 企业 - 蓝色 // "#3B82F6", // 企业 - 蓝色
"#feb64d", // 高校 - 橙色 // "#feb64d", // 高校 - 橙色
"#ff9f9f" // 科研院所 - 粉红 // "#ff9f9f" // 科研院所 - 粉红
], // ],
series: [ // series: [
{ // {
name: "制裁实体类型分布", // name: "制裁实体类型分布",
type: "pie", // type: "pie",
radius: [73.5, 89.5], // radius: [73.5, 89.5],
center: ["50%", "50%"], // center: ["50%", "50%"],
startAngle: -90, // startAngle: -90,
data: data, // data: data,
label: { // label: {
show: true, // show: true,
alignTo: "edge", // alignTo: "edge",
minMargin: 5, // minMargin: 5,
edgeDistance: 10, // edgeDistance: 10,
formatter: params => { // formatter: params => {
return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}"; // return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}";
}, // },
rich: { // rich: {
name: { // name: {
fontSize: 18, // fontSize: 18,
fontWeight: 700, // fontWeight: 700,
color: "rgb(59, 65, 75)", // color: "rgb(59, 65, 75)",
padding: [0, 0, 5, 0], // padding: [0, 0, 5, 0],
fontFamily: "Microsoft YaHei", // fontFamily: "Microsoft YaHei",
lineHeight: 26 // lineHeight: 26
}, // },
value: { // value: {
fontSize: 16, // fontSize: 16,
fontWeight: 400, // fontWeight: 400,
color: "rgb(95, 101, 108)", // color: "rgb(95, 101, 108)",
fontFamily: "Microsoft YaHei", // fontFamily: "Microsoft YaHei",
lineHeight: 24, // lineHeight: 24,
padding: [5, 0, 0, 0] // padding: [5, 0, 0, 0]
} // }
} // }
}, // },
labelLine: { // labelLine: {
show: true, // show: true,
length: 15, // length: 15,
length2: 0, // length2: 0,
maxSurfaceAngle: 80, // maxSurfaceAngle: 80,
lineStyle: { // lineStyle: {
width: 1 // width: 1
} // }
}, // },
labelLayout: function (params) { // labelLayout: function (params) {
const isLeft = params.labelRect.x < chart.getWidth() / 2; // const isLeft = params.labelRect.x < chart.getWidth() / 2;
const points = params.labelLinePoints; // const points = params.labelLinePoints;
// Update the end point. // // Update the end point.
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width; // points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
return { // return {
labelLinePoints: points // labelLinePoints: points
}; // };
}, // },
itemStyle: { // itemStyle: {
borderWidth: 0 // borderWidth: 0
} // }
} // }
] // ]
}; // };
chart.setOption(option); // chart.setOption(option);
window.addEventListener("resize", () => { // window.addEventListener("resize", () => {
chart.resize(); // chart.resize();
}); // });
}; };
const initTypeChart = () => { const initTypeChart = () => {
...@@ -1315,4 +1503,50 @@ onMounted(() => { ...@@ -1315,4 +1503,50 @@ onMounted(() => {
font-size: 14px; font-size: 14px;
} }
} }
.data-origin-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 22px 0;
.data-origin-icon {
width: 16px;
height: 16px;
font-size: 0px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
}
}
.data-origin-text {
font-family: Source Han Sans CN;
font-size: 14px;
color: var(--text-primary-50-color);
}
}
.ai-pane {
position: absolute;
right: 0px;
bottom: 15px;
z-index: 2;
:deep(.ai-pane-wrapper) {
display: none;
}
:deep(.ai-button-wrapper) {
display: flex;
}
&:hover {
width: 100%;
bottom: 0px;
:deep(.ai-pane-wrapper) {
display: block;
}
:deep(.ai-button-wrapper) {
display: none;
}
}
}
</style> </style>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
{{ headerTitle.department }} {{ headerTitle.department }}
</div> </div>
</div> </div>
<div class="btn"><img :src="icon01" alt="" />切换</div> <!-- <div class="btn"><img :src="icon01" alt="" />切换</div> -->
</div> </div>
<div class="header-nav"> <div class="header-nav">
<div <div
......
...@@ -42,8 +42,8 @@ ...@@ -42,8 +42,8 @@
<div class="main"> <div class="main">
<div class="main-item"> <div class="main-item">
<AnalysisBox title="制裁实体领域分布情况"> <AnalysisBox title="制裁实体领域分布情况">
<div class="echarts" ref="domainChartRef"></div> <!-- <div class="echarts" ref="domainChartRef"></div> -->
<div class="bottom"> <!-- <div class="bottom">
<div class="ai"> <div class="ai">
<div class="left"> <div class="left">
<img :src="ai" alt="" class="icon1" /> <img :src="ai" alt="" class="icon1" />
...@@ -53,6 +53,17 @@ ...@@ -53,6 +53,17 @@
<img :src="right" alt="" class="icon2" /> <img :src="right" alt="" class="icon2" />
</div> </div>
</div> </div>
</div> -->
<EChart :option="domainChartOption" autoresize :style="{ height: '300px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体领域分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="domainChart.interpretation" />
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -81,7 +92,7 @@ ...@@ -81,7 +92,7 @@
</div> </div>
</div> --> </div> -->
<AnalysisBox title="制裁实体类型分布情况"> <AnalysisBox title="制裁实体类型分布情况">
<div class="echarts" ref="typeChartRef"></div> <!-- <div class="echarts" ref="typeChartRef"></div>
<div class="bottom"> <div class="bottom">
<div class="ai"> <div class="ai">
<div class="left"> <div class="left">
...@@ -92,48 +103,21 @@ ...@@ -92,48 +103,21 @@
<img :src="right" alt="" class="icon2" /> <img :src="right" alt="" class="icon2" />
</div> </div>
</div> </div>
</div> -->
<EChart :option="typeChartOption" autoresize :style="{ height: '300px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体类型分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="typeChart.interpretation" />
</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="country-list">
<div class="list-item" v-for="(item, index) in countryDistribution" :key="index">
<img :src="flag" alt="" class="flag">
<div class="country-name">{{ item.name }}</div>
<div class="progress-bar-container">
<div
class="progress-bar"
:style="{
width: item.width,
background: item.gradient
}"
></div>
</div>
<div class="count" :class="{ 'highlight': index === 0 }">{{ item.count }}</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> -->
<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 class="list-item" v-for="(item, index) in countryDistribution" :key="index">
...@@ -151,7 +135,7 @@ ...@@ -151,7 +135,7 @@
<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="bottom">
<div class="ai"> <div class="ai">
<div class="left"> <div class="left">
<img :src="ai" alt="" class="icon1" /> <img :src="ai" alt="" class="icon1" />
...@@ -161,6 +145,16 @@ ...@@ -161,6 +145,16 @@
<img :src="right" alt="" class="icon2" /> <img :src="right" alt="" class="icon2" />
</div> </div>
</div> </div>
</div> -->
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体国家分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="countryDistributionChart.interpretation" />
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -185,7 +179,7 @@ ...@@ -185,7 +179,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bottom"> <!-- <div class="bottom">
<div class="ai"> <div class="ai">
<div class="left"> <div class="left">
<img :src="ai" alt="" class="icon1" /> <img :src="ai" alt="" class="icon1" />
...@@ -195,6 +189,16 @@ ...@@ -195,6 +189,16 @@
<img :src="right" alt="" class="icon2" /> <img :src="right" alt="" class="icon2" />
</div> </div>
</div> </div>
</div> -->
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体地域分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="regionDistributionChart.interpretation" />
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -206,10 +210,14 @@ ...@@ -206,10 +210,14 @@
import { ref, onMounted, watch } from "vue"; import { ref, onMounted, watch } from "vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
import chinaJson from "../../../utils/China.json"; import chinaJson from "../../../utils/China.json";
import tipsIcon from "../../../assets/icons/info-icon.png";
import ai from "./assets/ai.png"; import ai from "./assets/ai.png";
import right from "./assets/right.png"; import right from "./assets/right.png";
import flag from "../../assets/default-icon2.png"; import flag from "../../assets/default-icon2.png";
import { useRouter } from "vue-router"; import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import EChart from "@/components/Chart/index.vue";
import { useRouter, useRoute } from "vue-router";
import { import {
getSingleSanctionTotalCount, getSingleSanctionTotalCount,
getSingleSanctionDomainCount, getSingleSanctionDomainCount,
...@@ -217,7 +225,13 @@ import { ...@@ -217,7 +225,13 @@ import {
getSingleSanctionEntityCountryCount, getSingleSanctionEntityCountryCount,
getSingleSanctionEntityRegionCount getSingleSanctionEntityRegionCount
} from "@/api/exportControlV2.0"; } from "@/api/exportControlV2.0";
import { useRoute } from "vue-router";
import { useChartInterpretation } from "@/views/exportControl/utils/common";
const sanctionCountChart = useChartInterpretation();
const domainChart = useChartInterpretation();
const typeChart = useChartInterpretation();
const countryDistributionChart = useChartInterpretation();
const regionDistributionChart = useChartInterpretation();
const route = useRoute(); const route = useRoute();
// 单次制裁-数据统计-制裁实体地域分布情况 // 单次制裁-数据统计-制裁实体地域分布情况
...@@ -237,6 +251,7 @@ const getRegionData = async () => { ...@@ -237,6 +251,7 @@ const getRegionData = async () => {
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);
initMapChart(); initMapChart();
regionDistributionChart.interpret({ type: "柱状图", name: "制裁实体地域分布情况", data: res.data });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -276,6 +291,7 @@ const getCountryCount = async () => { ...@@ -276,6 +291,7 @@ const getCountryCount = async () => {
gradient gradient
}; };
}); });
countryDistributionChart.interpret({ type: "柱状图", name: "制裁实体国家分布情况", data: res.data });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -296,6 +312,7 @@ const getEntityTypeCount = async () => { ...@@ -296,6 +312,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 });
initTypeChart(); initTypeChart();
} }
} catch (error) { } catch (error) {
...@@ -318,6 +335,7 @@ const getDomainCount = async () => { ...@@ -318,6 +335,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 });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -457,190 +475,346 @@ const initMapChart = () => { ...@@ -457,190 +475,346 @@ const initMapChart = () => {
}); });
}; };
const domainChartOption = ref({
tooltip: {
trigger: "item",
formatter: "{b}: {c} ({d}%)"
},
color: [
"#60acfc", // 集成电路 - 蓝色
"#feb64d", // 人工智能 - 橙色
"#5bc49f", // 通信网络 - 青色
"#959cf3", // 量子科技 - 淡蓝紫
"#ff7c7c", // 先进制造 - 红色
"#b689ea", // 新材料 - 紫色
"#32d3eb" // 航空航天 - 浅蓝
],
series: [
{
name: "制裁实体领域分布",
type: "pie",
radius: [65, 81],
center: ["50%", "50%"],
startAngle: 90,
data: [],
label: {
show: true,
alignTo: "edge",
minMargin: 5,
edgeDistance: 10,
formatter: params => {
return "{name|" + params.name + "} \n {value|" + params.value + "家 " + params.percent + "%}";
},
rich: {
name: {
fontSize: 18,
fontWeight: 700,
color: "rgb(59, 65, 75)",
padding: [10, 0, 10, 0],
fontFamily: "Microsoft YaHei",
lineHeight: 22
},
value: {
fontSize: 16,
fontWeight: 400,
color: "rgb(95, 101, 108)",
fontFamily: "Microsoft YaHei",
lineHeight: 22,
padding: [10, 0, 10, 0]
}
}
},
labelLine: {
show: true,
length: 15,
length2: 0,
maxSurfaceAngle: 80,
lineStyle: {
width: 1.1
}
},
labelLayout: function (params) {
const isLeft = params.labelRect.x < params.viewWidth / 2;
const points = params.labelLinePoints;
// Update the end point.
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
// if (isLeft) {
// // 左侧:终点对齐到标签左边界
// points[2][0] = 0;
// } else {
// // 右侧:终点对齐到标签右边界
// points[2][0] = params.labelRect.x + params.labelRect.width;
// }
// return {
// labelLinePoints: points
// };
},
itemStyle: {
borderWidth: 0
}
}
]
});
const initDomainChart = () => { const initDomainChart = () => {
if (!domainChartRef.value) return; // if (!domainChartRef.value) return;
// 销毁旧实例,防止内存泄漏或状态残留 // // 销毁旧实例,防止内存泄漏或状态残留
const oldChart = echarts.getInstanceByDom(domainChartRef.value); // const oldChart = echarts.getInstanceByDom(domainChartRef.value);
if (oldChart) { // if (oldChart) {
oldChart.dispose(); // oldChart.dispose();
} // }
const chart = echarts.init(domainChartRef.value); // const chart = echarts.init(domainChartRef.value);
const data = domainCount.value.map(item => ({ const data = domainCount.value.map(item => ({
value: item.count, value: item.count,
name: item.name name: item.name
})); }));
domainChartOption.value.series[0].data = data;
// const option = {
// tooltip: {
// trigger: "item",
// formatter: "{b}: {c} ({d}%)"
// },
// color: [
// "#60acfc", // 集成电路 - 蓝色
// "#feb64d", // 人工智能 - 橙色
// "#5bc49f", // 通信网络 - 青色
// "#959cf3", // 量子科技 - 淡蓝紫
// "#ff7c7c", // 先进制造 - 红色
// "#b689ea", // 新材料 - 紫色
// "#32d3eb" // 航空航天 - 浅蓝
// ],
// series: [
// {
// name: "制裁实体领域分布",
// type: "pie",
// radius: [65, 81],
// center: ["50%", "50%"],
// startAngle: 90,
// data: data,
// label: {
// show: true,
// alignTo: "edge",
// minMargin: 5,
// edgeDistance: 10,
// formatter: params => {
// return "{name|" + params.name + "} \n {value|" + params.value + "家 " + params.percent + "%}";
// },
// rich: {
// name: {
// fontSize: 18,
// fontWeight: 700,
// color: "rgb(59, 65, 75)",
// padding: [10, 0, 10, 0],
// fontFamily: "Microsoft YaHei",
// lineHeight: 22
// },
// value: {
// fontSize: 16,
// fontWeight: 400,
// color: "rgb(95, 101, 108)",
// fontFamily: "Microsoft YaHei",
// lineHeight: 22,
// padding: [10, 0, 10, 0]
// }
// }
// },
// labelLine: {
// show: true,
// length: 15,
// length2: 0,
// maxSurfaceAngle: 80,
// lineStyle: {
// width: 1.1
// }
// },
// labelLayout: function (params) {
// const isLeft = params.labelRect.x < chart.getWidth() / 2;
// const points = params.labelLinePoints;
// // Update the end point.
// points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
// return {
// labelLinePoints: points
// };
// },
// itemStyle: {
// borderWidth: 0
// }
// }
// ]
// };
// chart.setOption(option);
// window.addEventListener("resize", () => {
// chart.resize();
// });
};
const option = { const typeChartOption = ref({
tooltip: { tooltip: {
trigger: "item", trigger: "item",
formatter: "{b}: {c} ({d}%)" formatter: "{b}: {c} ({d}%)"
}, },
color: [ color: [
"#60acfc", // 集成电路 - 蓝色 "#3B82F6", // 企业 - 蓝色
"#feb64d", // 人工智能 - 橙色 "#feb64d", // 高校 - 橙色
"#5bc49f", // 通信网络 - 青色 "#ff9f9f" // 科研院所 - 粉红
"#959cf3", // 量子科技 - 淡蓝紫 ],
"#ff7c7c", // 先进制造 - 红色 series: [
"#b689ea", // 新材料 - 紫色 {
"#32d3eb" // 航空航天 - 浅蓝 name: "制裁实体类型分布",
], type: "pie",
series: [ radius: [73.5, 89.5],
{ center: ["50%", "50%"],
name: "制裁实体领域分布", startAngle: -90,
type: "pie", data: [],
radius: [65, 81], label: {
center: ["50%", "50%"], show: true,
startAngle: 90, alignTo: "edge",
data: data, minMargin: 5,
label: { edgeDistance: 10,
show: true, formatter: params => {
alignTo: "edge", return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}";
minMargin: 5,
edgeDistance: 10,
formatter: params => {
return "{name|" + params.name + "} \n {value|" + params.value + "家 " + params.percent + "%}";
},
rich: {
name: {
fontSize: 18,
fontWeight: 700,
color: "rgb(59, 65, 75)",
padding: [10, 0, 10, 0],
fontFamily: "Microsoft YaHei",
lineHeight: 22
},
value: {
fontSize: 16,
fontWeight: 400,
color: "rgb(95, 101, 108)",
fontFamily: "Microsoft YaHei",
lineHeight: 22,
padding: [10, 0, 10, 0]
}
}
}, },
labelLine: { rich: {
show: true, name: {
length: 15, fontSize: 18,
length2: 0, fontWeight: 700,
maxSurfaceAngle: 80, color: "rgb(59, 65, 75)",
lineStyle: { padding: [0, 0, 5, 0],
width: 1.1 fontFamily: "Microsoft YaHei",
lineHeight: 26
},
value: {
fontSize: 16,
fontWeight: 400,
color: "rgb(95, 101, 108)",
fontFamily: "Microsoft YaHei",
lineHeight: 24,
padding: [5, 0, 0, 0]
} }
},
labelLayout: function (params) {
const isLeft = params.labelRect.x < chart.getWidth() / 2;
const points = params.labelLinePoints;
// Update the end point.
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
},
itemStyle: {
borderWidth: 0
} }
},
labelLine: {
show: true,
length: 15,
length2: 0,
maxSurfaceAngle: 80,
lineStyle: {
width: 1
}
},
labelLayout: function (params) {
const isLeft = params.labelRect.x < params.viewWidth / 2;
const points = params.labelLinePoints;
// Update the end point.
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
},
itemStyle: {
borderWidth: 0
} }
] }
}; ]
});
chart.setOption(option);
window.addEventListener("resize", () => {
chart.resize();
});
};
const initTypeChart = () => { const initTypeChart = () => {
if (!typeChartRef.value) return; // if (!typeChartRef.value) return;
// 销毁旧实例,防止内存泄漏或状态残留 // // 销毁旧实例,防止内存泄漏或状态残留
const oldChart = echarts.getInstanceByDom(typeChartRef.value); // const oldChart = echarts.getInstanceByDom(typeChartRef.value);
if (oldChart) { // if (oldChart) {
oldChart.dispose(); // oldChart.dispose();
} // }
const chart = echarts.init(typeChartRef.value); // const chart = echarts.init(typeChartRef.value);
const data = entityTypeCount.value.map(item => ({ const data = entityTypeCount.value.map(item => ({
value: item.count, value: item.count,
name: item.name name: item.name
})); }));
typeChartOption.value.series[0].data = data;
const option = {
tooltip: { // const option = {
trigger: "item", // tooltip: {
formatter: "{b}: {c} ({d}%)" // trigger: "item",
}, // formatter: "{b}: {c} ({d}%)"
color: [ // },
"#3B82F6", // 企业 - 蓝色 // color: [
"#feb64d", // 高校 - 橙色 // "#3B82F6", // 企业 - 蓝色
"#ff9f9f" // 科研院所 - 粉红 // "#feb64d", // 高校 - 橙色
], // "#ff9f9f" // 科研院所 - 粉红
series: [ // ],
{ // series: [
name: "制裁实体类型分布", // {
type: "pie", // name: "制裁实体类型分布",
radius: [73.5, 89.5], // type: "pie",
center: ["50%", "50%"], // radius: [73.5, 89.5],
startAngle: -90, // center: ["50%", "50%"],
data: data, // startAngle: -90,
label: { // data: data,
show: true, // label: {
alignTo: "edge", // show: true,
minMargin: 5, // alignTo: "edge",
edgeDistance: 10, // minMargin: 5,
formatter: params => { // edgeDistance: 10,
return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}"; // formatter: params => {
}, // return "{name|" + params.name + "}\n{value|" + params.value + "家 " + params.percent + "%}";
rich: { // },
name: { // rich: {
fontSize: 18, // name: {
fontWeight: 700, // fontSize: 18,
color: "rgb(59, 65, 75)", // fontWeight: 700,
padding: [0, 0, 5, 0], // color: "rgb(59, 65, 75)",
fontFamily: "Microsoft YaHei", // padding: [0, 0, 5, 0],
lineHeight: 26 // fontFamily: "Microsoft YaHei",
}, // lineHeight: 26
value: { // },
fontSize: 16, // value: {
fontWeight: 400, // fontSize: 16,
color: "rgb(95, 101, 108)", // fontWeight: 400,
fontFamily: "Microsoft YaHei", // color: "rgb(95, 101, 108)",
lineHeight: 24, // fontFamily: "Microsoft YaHei",
padding: [5, 0, 0, 0] // lineHeight: 24,
} // padding: [5, 0, 0, 0]
} // }
}, // }
labelLine: { // },
show: true, // labelLine: {
length: 15, // show: true,
length2: 0, // length: 15,
maxSurfaceAngle: 80, // length2: 0,
lineStyle: { // maxSurfaceAngle: 80,
width: 1 // lineStyle: {
} // width: 1
}, // }
labelLayout: function (params) { // },
const isLeft = params.labelRect.x < chart.getWidth() / 2; // labelLayout: function (params) {
const points = params.labelLinePoints; // const isLeft = params.labelRect.x < chart.getWidth() / 2;
// Update the end point. // const points = params.labelLinePoints;
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width; // // Update the end point.
return { // points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
labelLinePoints: points // return {
}; // labelLinePoints: points
}, // };
itemStyle: { // },
borderWidth: 0 // itemStyle: {
} // borderWidth: 0
} // }
] // }
}; // ]
// };
chart.setOption(option);
// chart.setOption(option);
window.addEventListener("resize", () => {
chart.resize(); // window.addEventListener("resize", () => {
}); // chart.resize();
// });
}; };
const sanTypeId = ref(""); const sanTypeId = ref("");
onMounted(() => { onMounted(() => {
...@@ -1086,4 +1260,49 @@ onMounted(() => { ...@@ -1086,4 +1260,49 @@ onMounted(() => {
} }
} }
} }
.data-origin-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 22px 0;
.data-origin-icon {
width: 16px;
height: 16px;
font-size: 0px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
}
}
.data-origin-text {
font-family: Source Han Sans CN;
font-size: 14px;
color: var(--text-primary-50-color);
}
}
.ai-pane {
position: absolute;
right: 0px;
bottom: 15px;
z-index: 2;
:deep(.ai-pane-wrapper) {
display: none;
}
:deep(.ai-button-wrapper) {
display: flex;
}
&:hover {
width: 100%;
bottom: 0px;
:deep(.ai-pane-wrapper) {
display: block;
}
:deep(.ai-button-wrapper) {
display: none;
}
}
}
</style> </style>
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<div ref="containerRef" class="graph-container"></div> <div ref="containerRef" class="graph-container"></div>
<!-- <GraphChart v-if="currentLayoutType === 1" :nodes="graphNodes" :links="graphLinks" /> -->
<div v-if="selectedNode" class="node-popup"> <div v-if="selectedNode" class="node-popup">
<div class="popup-header"> <div class="popup-header">
<img :src="selectedNode.image || defaultIcon" alt="" class="popup-icon" /> <img :src="selectedNode.image || defaultIcon" alt="" class="popup-icon" />
...@@ -55,6 +55,8 @@ import echartsIcon02 from "../assets/echartsIcon02.png"; ...@@ -55,6 +55,8 @@ import echartsIcon02 from "../assets/echartsIcon02.png";
import echartsIcon03 from "../assets/echartsIcon03.png"; import echartsIcon03 from "../assets/echartsIcon03.png";
import defaultIcon from "../assets/echartsIcon03.png"; import defaultIcon from "../assets/echartsIcon03.png";
// import GraphChart from "@/components/base/GraphChart/index.vue";
import GraphChart from "@/views/companyPages2/component/SupplyChain/GraphChart.vue";
import { getSingleSanctionEntityInfo } from "@/api/exportControlV2.0"; import { getSingleSanctionEntityInfo } from "@/api/exportControlV2.0";
const props = defineProps({ const props = defineProps({
...@@ -107,7 +109,7 @@ const initGraph = (layoutType = 1) => { ...@@ -107,7 +109,7 @@ const initGraph = (layoutType = 1) => {
const initNormalGraph = (layoutType, width, height) => { const initNormalGraph = (layoutType, width, height) => {
const data = processGraphData(props.graphData); const data = processGraphData(props.graphData);
console.log("初始数据", props.graphData);
if (!data.nodes || data.nodes.length === 0) return; if (!data.nodes || data.nodes.length === 0) return;
const layout = { const layout = {
...@@ -653,6 +655,56 @@ const bindGraphEvents = () => { ...@@ -653,6 +655,56 @@ const bindGraphEvents = () => {
}); });
}; };
// 将传入的 graphData 转换为 GraphChart.vue 需要的格式
// GraphChart.vue 需要节点有 x, y 坐标,所以我们需要一个简单的布局算法
const graphNodes = ref([]);
const graphLinks = ref([]);
const applyStarLayout = (rawNodes, rawLinks) => {
if (!rawNodes?.length) return { nodes: [], links: [] };
const centerX = 0;
const centerY = 0;
const radius = 200; // 环绕半径
const nodes = rawNodes.map((node, index) => {
let x, y;
if (index === 0) {
// 中心节点
x = centerX;
y = centerY;
} else {
// 其他节点均匀分布在圆周上
const angle = (2 * Math.PI * (index - 1)) / (rawNodes.length - 1);
x = centerX + radius * Math.cos(angle);
y = centerY + radius * Math.sin(angle);
}
return {
id: String(node.id ?? index),
name: node.name || "",
type: node.isSanctioned ? "sanctioned" : "normal", // 用于区分样式(如果需要)
symbolSize: node.symbolSize || (index === 0 ? 60 : 40),
symbol: `image://${node.image || defaultIcon}`,
isSanctioned: node.isSanctioned,
image: node.image || defaultIcon,
x: x,
y: y,
...node // 透传其他字段
};
});
const links = (rawLinks || []).map(link => ({
source: String(link.source),
target: String(link.target),
type: link.name || "relation", // GraphChart.vue 使用 type 字段
label: {
show: true,
formatter: link.name || ""
}
}));
return { nodes, links };
};
const handleClickControlBtn = btn => { const handleClickControlBtn = btn => {
currentLayoutType.value = btn; currentLayoutType.value = btn;
emit("layoutChange", btn); emit("layoutChange", btn);
...@@ -675,6 +727,16 @@ const handleResize = () => { ...@@ -675,6 +727,16 @@ const handleResize = () => {
} }
}; };
watch(
() => props.graphData,
newVal => {
const { nodes, links } = applyStarLayout(newVal.nodes, newVal.links);
graphNodes.value = JSON.parse(JSON.stringify(nodes));
graphLinks.value = JSON.parse(JSON.stringify(links));
},
{ deep: true, immediate: true }
);
watch( watch(
() => props.graphData, () => props.graphData,
() => { () => {
......
...@@ -72,8 +72,13 @@ ...@@ -72,8 +72,13 @@
<div class="company-list-container"> <div class="company-list-container">
<div class="list-header">企业名称</div> <div class="list-header">企业名称</div>
<div class="company-list"> <div class="company-list">
<div class="company-item" :class="{ active: selectedCompanyId === item.id }" v-for="item in entityList" <div
:key="item.id" @click="selectedCompanyId = item.id"> class="company-item"
:class="{ active: selectedCompanyId === item.id }"
v-for="item in entityList"
:key="item.id"
@click="selectedCompanyId = item.id"
>
<div class="icon-wrapper"> <div class="icon-wrapper">
<img :src="defaultTitle" alt="" /> <img :src="defaultTitle" alt="" />
</div> </div>
...@@ -114,18 +119,34 @@ ...@@ -114,18 +119,34 @@
<AnalysisBox title="企业规模"> <AnalysisBox title="企业规模">
<template #header-btn> <template #header-btn>
<div class="toggle-btns"> <div class="toggle-btns">
<div class="t-btn" :class="{ active: activeScale === item }" v-for="item in scaleOptions" :key="item" <div
@click="handleScaleClick(item)"> class="t-btn"
:class="{ active: activeScale === item }"
v-for="item in scaleOptions"
:key="item"
@click="handleScaleClick(item)"
>
{{ item }} {{ item }}
</div> </div>
</div> </div>
</template> </template>
<div class="right-main"> <div class="right-main">
<div class="echarts" ref="chartRef"></div> <!-- <div class="echarts" ref="chartRef"></div> -->
<div class="bottom"> <!-- <div class="bottom">
<img :src="ai" class="ai-icon" alt="" /> <img :src="ai" class="ai-icon" alt="" />
<span class="text">列入实体清单后企业营收初期下降,后基本趋于稳定。</span> <span class="text">列入实体清单后企业营收初期下降,后基本趋于稳定。</span>
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> -->
<EChart :option="revenueChartOption" autoresize :style="{ height: '300px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">企业规模情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="revenueChart.interpretation" />
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
...@@ -152,11 +173,22 @@ ...@@ -152,11 +173,22 @@
</div> --> </div> -->
<AnalysisBox title="企业市值变化"> <AnalysisBox title="企业市值变化">
<div class="right-main"> <div class="right-main">
<div class="echarts" ref="marketChartRef"></div> <!-- <div class="echarts" ref="marketChartRef"></div>
<div class="bottom"> <div class="bottom">
<img :src="ai" class="ai-icon" alt="" /> <img :src="ai" class="ai-icon" alt="" />
<span class="text">列入实体清单后企业营收初期下降,后基本趋于稳定。</span> <span class="text">列入实体清单后企业营收初期下降,后基本趋于稳定。</span>
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> -->
<EChart :option="marketChartOption" autoresize :style="{ height: '300px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">企业市值变化情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="marketChart.interpretation" />
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
...@@ -190,18 +222,34 @@ ...@@ -190,18 +222,34 @@
<AnalysisBox title="企业研发投入"> <AnalysisBox title="企业研发投入">
<template #header-btn> <template #header-btn>
<div class="toggle-btns"> <div class="toggle-btns">
<div class="t-btn" :class="{ active: activeRD === item }" v-for="item in rdOptions" :key="item" <div
@click="activeRD = item"> class="t-btn"
:class="{ active: activeRD === item }"
v-for="item in rdOptions"
:key="item"
@click="activeRD = item"
>
{{ item }} {{ item }}
</div> </div>
</div> </div>
</template> </template>
<div class="right-main"> <div class="right-main">
<div class="echarts" ref="rdChartRef"></div> <!-- <div class="echarts" ref="rdChartRef"></div>
<div class="bottom"> <div class="bottom">
<img :src="ai" class="ai-icon" alt="" /> <img :src="ai" class="ai-icon" alt="" />
<span class="text">列入实体清单后企业研发资金投入逐渐提高。</span> <span class="text">列入实体清单后企业研发资金投入逐渐提高。</span>
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> -->
<EChart :option="rdChartOption" autoresize :style="{ height: '300px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">企业研发投入情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="rdChart.interpretation" />
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
...@@ -232,23 +280,39 @@ ...@@ -232,23 +280,39 @@
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> </div>
</div> --> </div> -->
<AnalysisBox title="企业市场占比"> <AnalysisBox title="企业市场占比">
<template #header-btn> <template #header-btn>
<div class="toggle-btns"> <div class="toggle-btns">
<div class="t-btn" :class="{ active: activeMarketShare === item }" v-for="item in marketShareOptions" <div
:key="item" @click="activeMarketShare = item"> class="t-btn"
:class="{ active: activeMarketShare === item }"
v-for="item in marketShareOptions"
:key="item"
@click="activeMarketShare = item"
>
{{ item }} {{ item }}
</div> </div>
</div> </div>
</template> </template>
<div class="right-main"> <div class="right-main">
<div class="echarts" ref="shareChartRef"></div> <!-- <div class="echarts" ref="shareChartRef"></div>
<div class="bottom"> <div class="bottom">
<img :src="ai" class="ai-icon" alt="" /> <img :src="ai" class="ai-icon" alt="" />
<span class="text">列入实体清单后企业营收初期下降,后基本趋于稳定。</span> <span class="text">列入实体清单后企业营收初期下降,后基本趋于稳定。</span>
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> -->
<EChart :option="shareChartOption" autoresize :style="{ height: '300px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">企业市场占比情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="shareChart.interpretation" />
</div>
</div> </div>
</div>
</AnalysisBox> </AnalysisBox>
</div> </div>
</div> </div>
...@@ -264,8 +328,26 @@ import { Search } from "@element-plus/icons-vue"; ...@@ -264,8 +328,26 @@ import { Search } from "@element-plus/icons-vue";
import defaultTitle from "../../../../assets/default-icon2.png"; import defaultTitle from "../../../../assets/default-icon2.png";
import ai from "../../assets/ai.png"; import ai from "../../assets/ai.png";
import right from "../../assets/right.png"; import right from "../../assets/right.png";
import { getSingleSanctionEntityList, getSingleSanctionEntityRevenue, getSingleSanctionEntityNetProfit, getSingleSanctionEntityPersonnel, getSingleSanctionEntityMarketValue, getSingleSanctionEntityRDInvestment, getSingleSanctionEntityMarketShare } from "@/api/exportControlV2.0"; import tipsIcon from "../../../../../assets/icons/info-icon.png";
import {
getSingleSanctionEntityList,
getSingleSanctionEntityRevenue,
getSingleSanctionEntityNetProfit,
getSingleSanctionEntityPersonnel,
getSingleSanctionEntityMarketValue,
getSingleSanctionEntityRDInvestment,
getSingleSanctionEntityMarketShare
} from "@/api/exportControlV2.0";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import EChart from "@/components/Chart/index.vue";
import { useChartInterpretation } from "@/views/exportControl/utils/common";
const revenueChart = useChartInterpretation();
const marketChart = useChartInterpretation();
const rdChart = useChartInterpretation();
const shareChart = useChartInterpretation();
// 单次制裁-影响分析-企业市场占比 // 单次制裁-影响分析-企业市场占比
const marketShareData = ref([]); const marketShareData = ref([]);
...@@ -273,8 +355,8 @@ const getMarketShare = async () => { ...@@ -273,8 +355,8 @@ const getMarketShare = async () => {
if (!selectedCompanyId.value) return; if (!selectedCompanyId.value) return;
try { try {
const res = await getSingleSanctionEntityMarketShare({ const res = await getSingleSanctionEntityMarketShare({
id: selectedCompanyId.value, id: selectedCompanyId.value
}) });
if (res.code === 200) { if (res.code === 200) {
marketShareData.value = res.data || []; marketShareData.value = res.data || [];
...@@ -291,15 +373,13 @@ const getMarketShare = async () => { ...@@ -291,15 +373,13 @@ const getMarketShare = async () => {
nextTick(() => { nextTick(() => {
initShareChart(); initShareChart();
}); });
shareChart.interpret({ type: "柱状图", name: "企业市场占比", data: sortedData });
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} };
// 单次制裁-影响分析-企业研发投入 // 单次制裁-影响分析-企业研发投入
const rdInvestmentData = ref([]); const rdInvestmentData = ref([]);
...@@ -307,8 +387,8 @@ const getRDInvestment = async () => { ...@@ -307,8 +387,8 @@ const getRDInvestment = async () => {
if (!selectedCompanyId.value) return; if (!selectedCompanyId.value) return;
try { try {
const res = await getSingleSanctionEntityRDInvestment({ const res = await getSingleSanctionEntityRDInvestment({
id: selectedCompanyId.value, id: selectedCompanyId.value
}) });
if (res.code === 200) { if (res.code === 200) {
rdInvestmentData.value = res.data || []; rdInvestmentData.value = res.data || [];
...@@ -327,15 +407,13 @@ const getRDInvestment = async () => { ...@@ -327,15 +407,13 @@ const getRDInvestment = async () => {
nextTick(() => { nextTick(() => {
initRDChart(); initRDChart();
}); });
rdChart.interpret({ type: "折线图", name: "企业研发投入", data: sortedData });
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} };
// 单次制裁-影响分析-企业市值变化 // 单次制裁-影响分析-企业市值变化
const marketValueData = ref([]); const marketValueData = ref([]);
...@@ -343,8 +421,8 @@ const getMarketValue = async () => { ...@@ -343,8 +421,8 @@ const getMarketValue = async () => {
if (!selectedCompanyId.value) return; if (!selectedCompanyId.value) return;
try { try {
const res = await getSingleSanctionEntityMarketValue({ const res = await getSingleSanctionEntityMarketValue({
id: selectedCompanyId.value, id: selectedCompanyId.value
}) });
if (res.code === 200) { if (res.code === 200) {
marketValueData.value = res.data || []; marketValueData.value = res.data || [];
...@@ -363,14 +441,13 @@ const getMarketValue = async () => { ...@@ -363,14 +441,13 @@ const getMarketValue = async () => {
nextTick(() => { nextTick(() => {
initMarketChart(); initMarketChart();
}); });
marketChart.interpret({ type: "折线图", name: "企业市值变化", data: sortedData });
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} };
// 单次制裁-影响分析-企业规模-人员 // 单次制裁-影响分析-企业规模-人员
const personnelData = ref([]); const personnelData = ref([]);
...@@ -379,8 +456,8 @@ const getPersonnel = async () => { ...@@ -379,8 +456,8 @@ const getPersonnel = async () => {
if (!selectedCompanyId.value) return; if (!selectedCompanyId.value) return;
try { try {
const res = await getSingleSanctionEntityPersonnel({ const res = await getSingleSanctionEntityPersonnel({
id: selectedCompanyId.value, id: selectedCompanyId.value
}) });
if (res.code === 200) { if (res.code === 200) {
personnelData.value = res.data || []; personnelData.value = res.data || [];
...@@ -397,13 +474,12 @@ const getPersonnel = async () => { ...@@ -397,13 +474,12 @@ const getPersonnel = async () => {
nextTick(() => { nextTick(() => {
initRevenueChart(); initRevenueChart();
}); });
revenueChart.interpret({ type: "折线图", name: "企业规模", data: sortedData });
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} };
// 单次制裁-影响分析-企业规模-净利润 // 单次制裁-影响分析-企业规模-净利润
const netProfitData = ref([]); const netProfitData = ref([]);
...@@ -412,8 +488,8 @@ const getNetProfitData = async () => { ...@@ -412,8 +488,8 @@ const getNetProfitData = async () => {
if (!selectedCompanyId.value) return; if (!selectedCompanyId.value) return;
try { try {
const res = await getSingleSanctionEntityNetProfit({ const res = await getSingleSanctionEntityNetProfit({
id: selectedCompanyId.value, id: selectedCompanyId.value
}) });
if (res.code === 200) { if (res.code === 200) {
netProfitData.value = res.data || []; netProfitData.value = res.data || [];
...@@ -430,14 +506,12 @@ const getNetProfitData = async () => { ...@@ -430,14 +506,12 @@ const getNetProfitData = async () => {
nextTick(() => { nextTick(() => {
initRevenueChart(); initRevenueChart();
}); });
revenueChart.interpret({ type: "折线图", name: "企业规模", data: sortedData });
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} };
// 单次制裁-影响分析-制裁企业列表 // 单次制裁-影响分析-制裁企业列表
const entityList = ref([]); const entityList = ref([]);
...@@ -453,8 +527,8 @@ const getRevenueData = async () => { ...@@ -453,8 +527,8 @@ const getRevenueData = async () => {
if (!selectedCompanyId.value) return; if (!selectedCompanyId.value) return;
try { try {
const res = await getSingleSanctionEntityRevenue({ const res = await getSingleSanctionEntityRevenue({
id: selectedCompanyId.value, id: selectedCompanyId.value
}) });
if (res.code === 200) { if (res.code === 200) {
revenueData.value = res.data || []; revenueData.value = res.data || [];
// 将数据格式化为图表所需格式 // 将数据格式化为图表所需格式
...@@ -469,31 +543,32 @@ const getRevenueData = async () => { ...@@ -469,31 +543,32 @@ const getRevenueData = async () => {
nextTick(() => { nextTick(() => {
initRevenueChart(); initRevenueChart();
}); });
revenueChart.interpret({ type: "折线图", name: "企业规模", data: sortedData });
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} };
const handleScaleClick = (item) => { const handleScaleClick = item => {
activeScale.value = item; activeScale.value = item;
if (item === '营收') { if (item === "营收") {
getRevenueData(); getRevenueData();
} else if (item === '净利润') { } else if (item === "净利润") {
getNetProfitData(); getNetProfitData();
} else if (item === '人员') { } else if (item === "人员") {
getPersonnel(); getPersonnel();
} }
} };
watch(selectedCompanyId, (val) => { watch(selectedCompanyId, val => {
if (val) { if (val) {
// 切换企业时,根据当前激活的按钮更新企业规模数据 // 切换企业时,根据当前激活的按钮更新企业规模数据
if (activeScale.value === '营收') { if (activeScale.value === "营收") {
getRevenueData(); getRevenueData();
} else if (activeScale.value === '净利润') { } else if (activeScale.value === "净利润") {
getNetProfitData(); getNetProfitData();
} else if (activeScale.value === '人员') { } else if (activeScale.value === "人员") {
getPersonnel(); getPersonnel();
} }
// 更新其他图表数据 // 更新其他图表数据
...@@ -510,15 +585,17 @@ const getEntityList = async () => { ...@@ -510,15 +585,17 @@ const getEntityList = async () => {
sanRecordId: sanRecordId.value, sanRecordId: sanRecordId.value,
isOnlyCn: false, isOnlyCn: false,
domainId: searchDomain.value || undefined, domainId: searchDomain.value || undefined,
searchText: searchKeyword.value || undefined, searchText: searchKeyword.value || undefined
}) });
if (res.code === 200) { if (res.code === 200) {
entityList.value = (res.data || []).reduce((acc, group) => { entityList.value = (res.data || []).reduce((acc, group) => {
if (group.orgType === "企业" && group.orgInfoList) { if (group.orgType === "企业" && group.orgInfoList) {
acc.push(...group.orgInfoList.map(org => ({ acc.push(
id: org.id, ...group.orgInfoList.map(org => ({
name: org.orgNameZh id: org.id,
}))); name: org.orgNameZh
}))
);
} }
return acc; return acc;
}, []); }, []);
...@@ -529,9 +606,9 @@ const getEntityList = async () => { ...@@ -529,9 +606,9 @@ const getEntityList = async () => {
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} };
const debouncedGetEntityList = debounce(() => { const debouncedGetEntityList = debounce(() => {
getEntityList(); getEntityList();
...@@ -547,8 +624,8 @@ watch(searchKeyword, () => { ...@@ -547,8 +624,8 @@ watch(searchKeyword, () => {
const getUrlParams = () => { const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || "" sanRecordId.value = urlParams.get("id") || "";
} };
const activeScale = ref("营收"); const activeScale = ref("营收");
const scaleOptions = ["营收", "净利润", "人员"]; const scaleOptions = ["营收", "净利润", "人员"];
...@@ -584,7 +661,7 @@ const shareChartData = ref({ ...@@ -584,7 +661,7 @@ const shareChartData = ref({
dates: [], dates: [],
values: [], values: [],
sanctionDate: "", sanctionDate: "",
unit: "%", unit: "%"
}); });
const chartRef = ref(null); const chartRef = ref(null);
...@@ -593,7 +670,7 @@ const rdChartRef = ref(null); ...@@ -593,7 +670,7 @@ const rdChartRef = ref(null);
const shareChartRef = ref(null); const shareChartRef = ref(null);
// 提取通用配置生成器 // 提取通用配置生成器
const getBaseOption = (data) => { const getBaseOption = data => {
return { return {
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
...@@ -609,7 +686,7 @@ const getBaseOption = (data) => { ...@@ -609,7 +686,7 @@ const getBaseOption = (data) => {
xAxis: { xAxis: {
type: "category", type: "category",
boundaryGap: true, boundaryGap: true,
data: data.dates, data: data.dates || [],
axisLine: { lineStyle: { color: "#E6EBF5" } }, axisLine: { lineStyle: { color: "#E6EBF5" } },
axisLabel: { axisLabel: {
color: "#606266", color: "#606266",
...@@ -627,20 +704,20 @@ const getBaseOption = (data) => { ...@@ -627,20 +704,20 @@ const getBaseOption = (data) => {
axisLabel: { axisLabel: {
color: "#606266", color: "#606266",
fontSize: 12, fontSize: 12,
formatter: (value) => value.toLocaleString() formatter: value => value.toLocaleString()
}, },
splitLine: { lineStyle: { type: "dashed", color: "rgba(231, 243, 255, 1)" } } splitLine: { lineStyle: { type: "dashed", color: "rgba(231, 243, 255, 1)" } }
} }
}; };
}; };
const revenueChartOption = ref(getBaseOption({}));
// 1. 初始化企业营收图表 (应用原型图置顶样式) // 1. 初始化企业营收图表 (应用原型图置顶样式)
const initRevenueChart = () => { const initRevenueChart = () => {
const dom = chartRef.value; // const dom = chartRef.value;
const data = chartData.value; const data = chartData.value;
if (!dom || !data.values || data.values.length === 0) return; // if (!dom || !data.values || data.values.length === 0) return;
let myChart = echarts.getInstanceByDom(dom) || echarts.init(dom); // let myChart = echarts.getInstanceByDom(dom) || echarts.init(dom);
const lastDate = data.dates[data.dates.length - 1]; const lastDate = data.dates[data.dates.length - 1];
const lastValue = data.values[data.values.length - 1]; const lastValue = data.values[data.values.length - 1];
const unit = data.unit || " 亿元"; const unit = data.unit || " 亿元";
...@@ -651,64 +728,70 @@ const initRevenueChart = () => { ...@@ -651,64 +728,70 @@ const initRevenueChart = () => {
const option = getBaseOption(data); const option = getBaseOption(data);
option.grid.top = "20%"; // 增加顶部空间 option.grid.top = "20%"; // 增加顶部空间
option.yAxis.max = maxY; option.yAxis.max = maxY;
option.series = [{ option.series = [
data: data.values, {
type: "line", data: data.values,
symbol: "none", type: "line",
lineStyle: { color: "#055FC2", width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(5, 95, 194, 0.2)" },
{ offset: 1, color: "rgba(5, 95, 194, 0)" }
])
},
markLine: {
symbol: "none", symbol: "none",
data: [ lineStyle: { color: "#055FC2", width: 2 },
data.sanctionDate ? { areaStyle: {
xAxis: data.sanctionDate, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
label: { { offset: 0, color: "rgba(5, 95, 194, 0.2)" },
formatter: "列入实体清单", { offset: 1, color: "rgba(5, 95, 194, 0)" }
position: "end", ])
color: "#F56C6C",
backgroundColor: "rgba(255, 238, 238, 1)",
borderRadius: 4,
padding: [4, 8]
},
lineStyle: { color: "#F56C6C", type: "dotted", width: 1 }
} : null,
[
{ coord: [lastDate, lastValue], symbol: "none" },
{ coord: [lastDate, maxY], symbol: "none", lineStyle: { color: "#055FC2", type: "dotted" } }
]
].filter(Boolean)
},
markPoint: {
symbol: "circle",
symbolSize: 6,
itemStyle: { color: "#055FC2" },
label: {
show: true,
position: "left",
formatter: "{c}" + unit.trim(),
color: "#055FC2",
fontSize: 16,
fontWeight: "bold",
offset: [-10, 0]
}, },
data: [{ coord: [lastDate, maxY], value: data.endValue || lastValue }] markLine: {
symbol: "none",
data: [
data.sanctionDate
? {
xAxis: data.sanctionDate,
label: {
formatter: "列入实体清单",
position: "end",
color: "#F56C6C",
backgroundColor: "rgba(255, 238, 238, 1)",
borderRadius: 4,
padding: [4, 8]
},
lineStyle: { color: "#F56C6C", type: "dotted", width: 1 }
}
: null,
[
{ coord: [lastDate, lastValue], symbol: "none" },
{ coord: [lastDate, maxY], symbol: "none", lineStyle: { color: "#055FC2", type: "dotted" } }
]
].filter(Boolean)
},
markPoint: {
symbol: "circle",
symbolSize: 6,
itemStyle: { color: "#055FC2" },
label: {
show: true,
position: "left",
formatter: "{c}" + unit.trim(),
color: "#055FC2",
fontSize: 16,
fontWeight: "bold",
offset: [-10, 0]
},
data: [{ coord: [lastDate, maxY], value: data.endValue || lastValue }]
}
} }
}]; ];
myChart.setOption(option, true); // myChart.setOption(option, true);
revenueChartOption.value = option;
}; };
const marketChartOption = ref(getBaseOption({}));
// 2. 初始化企业市值图表 // 2. 初始化企业市值图表
const initMarketChart = () => { const initMarketChart = () => {
const dom = marketChartRef.value; // const dom = marketChartRef.value;
const data = marketChartData.value; const data = marketChartData.value;
if (!dom || !data.values || data.values.length === 0) return; // if (!dom || !data.values || data.values.length === 0) return;
let myChart = echarts.getInstanceByDom(dom) || echarts.init(dom); // let myChart = echarts.getInstanceByDom(dom) || echarts.init(dom);
const lastDate = data.dates[data.dates.length - 1]; const lastDate = data.dates[data.dates.length - 1];
const lastValue = data.values[data.values.length - 1]; const lastValue = data.values[data.values.length - 1];
const unit = data.unit || " 万"; const unit = data.unit || " 万";
...@@ -719,57 +802,63 @@ const initMarketChart = () => { ...@@ -719,57 +802,63 @@ const initMarketChart = () => {
const option = getBaseOption(data); const option = getBaseOption(data);
option.grid.top = "20%"; // 增加顶部空间 option.grid.top = "20%"; // 增加顶部空间
option.yAxis.max = maxY; option.yAxis.max = maxY;
option.series = [{ option.series = [
data: data.values, {
type: "line", data: data.values,
symbol: "none", type: "line",
lineStyle: { color: "#055FC2", width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(5, 95, 194, 0.2)" },
{ offset: 1, color: "rgba(5, 95, 194, 0)" }
])
},
markLine: {
symbol: "none", symbol: "none",
data: [ lineStyle: { color: "#055FC2", width: 2 },
data.sanctionDate ? { areaStyle: {
xAxis: data.sanctionDate, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
label: { formatter: "列入实体清单", position: "end", color: "#F56C6C" }, { offset: 0, color: "rgba(5, 95, 194, 0.2)" },
lineStyle: { color: "#F56C6C", type: "dotted" } { offset: 1, color: "rgba(5, 95, 194, 0)" }
} : null, ])
[ },
{ coord: [lastDate, lastValue], symbol: "none" }, markLine: {
{ coord: [lastDate, maxY], symbol: "none", lineStyle: { color: "#055FC2", type: "dotted" } } symbol: "none",
] data: [
].filter(Boolean) data.sanctionDate
}, ? {
markPoint: { xAxis: data.sanctionDate,
symbol: "circle", label: { formatter: "列入实体清单", position: "end", color: "#F56C6C" },
symbolSize: 6, lineStyle: { color: "#F56C6C", type: "dotted" }
itemStyle: { color: "#055FC2" }, }
label: { : null,
show: true, [
position: "left", { coord: [lastDate, lastValue], symbol: "none" },
formatter: "{c}" + unit.trim(), { coord: [lastDate, maxY], symbol: "none", lineStyle: { color: "#055FC2", type: "dotted" } }
color: "#055FC2", ]
fontSize: 16, ].filter(Boolean)
fontWeight: "bold",
offset: [-10, 0]
}, },
data: [{ coord: [lastDate, maxY], value: data.endValue || lastValue }] markPoint: {
symbol: "circle",
symbolSize: 6,
itemStyle: { color: "#055FC2" },
label: {
show: true,
position: "left",
formatter: "{c}" + unit.trim(),
color: "#055FC2",
fontSize: 16,
fontWeight: "bold",
offset: [-10, 0]
},
data: [{ coord: [lastDate, maxY], value: data.endValue || lastValue }]
}
} }
}]; ];
myChart.setOption(option, true); // myChart.setOption(option, true);
marketChartOption.value = option;
}; };
const rdChartOption = ref(getBaseOption({}));
// 3. 初始化研发投入图表 // 3. 初始化研发投入图表
const initRDChart = () => { const initRDChart = () => {
const dom = rdChartRef.value; // const dom = rdChartRef.value;
const data = rdChartData.value; const data = rdChartData.value;
if (!dom || !data.values || data.values.length === 0) return; // if (!dom || !data.values || data.values.length === 0) return;
let myChart = echarts.getInstanceByDom(dom) || echarts.init(dom); // let myChart = echarts.getInstanceByDom(dom) || echarts.init(dom);
const lastDate = data.dates[data.dates.length - 1]; const lastDate = data.dates[data.dates.length - 1];
const lastValue = data.values[data.values.length - 1]; const lastValue = data.values[data.values.length - 1];
const unit = data.unit || " 万"; const unit = data.unit || " 万";
...@@ -781,62 +870,67 @@ const initRDChart = () => { ...@@ -781,62 +870,67 @@ const initRDChart = () => {
const option = getBaseOption(data); const option = getBaseOption(data);
option.grid.top = "20%"; // 增加顶部空间 option.grid.top = "20%"; // 增加顶部空间
option.yAxis.max = maxY; option.yAxis.max = maxY;
option.series = [{ option.series = [
data: data.values, {
type: "line", data: data.values,
symbol: "none", type: "line",
lineStyle: { color: "#055FC2", width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(5, 95, 194, 0.2)" },
{ offset: 1, color: "rgba(5, 95, 194, 0)" }
])
},
markLine: {
symbol: "none", symbol: "none",
data: [ lineStyle: { color: "#055FC2", width: 2 },
data.sanctionDate ? { areaStyle: {
xAxis: data.sanctionDate, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
label: { formatter: "列入实体清单", position: "end", color: "#F56C6C" }, { offset: 0, color: "rgba(5, 95, 194, 0.2)" },
lineStyle: { color: "#F56C6C", type: "dotted" } { offset: 1, color: "rgba(5, 95, 194, 0)" }
} : null, ])
[
{ coord: [lastDate, lastValue], symbol: "none" },
{ coord: [lastDate, maxY], symbol: "none", lineStyle: { color: "#055FC2", type: "dotted" } }
]
].filter(Boolean)
},
markPoint: {
symbol: "circle",
symbolSize: 6,
itemStyle: { color: "#055FC2" },
label: {
show: true,
position: "left",
formatter: "{c}" + unit.trim(),
color: "#055FC2",
fontSize: 16,
fontWeight: "bold",
offset: [-10, 0]
}, },
data: [{ coord: [lastDate, maxY], value: data.endValue || lastValue }] markLine: {
symbol: "none",
data: [
data.sanctionDate
? {
xAxis: data.sanctionDate,
label: { formatter: "列入实体清单", position: "end", color: "#F56C6C" },
lineStyle: { color: "#F56C6C", type: "dotted" }
}
: null,
[
{ coord: [lastDate, lastValue], symbol: "none" },
{ coord: [lastDate, maxY], symbol: "none", lineStyle: { color: "#055FC2", type: "dotted" } }
]
].filter(Boolean)
},
markPoint: {
symbol: "circle",
symbolSize: 6,
itemStyle: { color: "#055FC2" },
label: {
show: true,
position: "left",
formatter: "{c}" + unit.trim(),
color: "#055FC2",
fontSize: 16,
fontWeight: "bold",
offset: [-10, 0]
},
data: [{ coord: [lastDate, maxY], value: data.endValue || lastValue }]
}
} }
}]; ];
myChart.setOption(option, true); // myChart.setOption(option, true);
rdChartOption.value = option;
}; };
const shareChartOption = ref(getBaseOption({}));
// 4. 初始化市场占比图表 (柱状图) // 4. 初始化市场占比图表 (柱状图)
const initShareChart = () => { const initShareChart = () => {
const dom = shareChartRef.value; // const dom = shareChartRef.value;
const data = shareChartData.value; const data = shareChartData.value;
if (!dom || !data.values || data.values.length === 0) return; // if (!dom || !data.values || data.values.length === 0) return;
let myChart = echarts.getInstanceByDom(dom) || echarts.init(dom); // let myChart = echarts.getInstanceByDom(dom) || echarts.init(dom);
const option = getBaseOption(data); const option = getBaseOption(data);
// 针对柱状图优化 X 轴 // 针对柱状图优化 X 轴
option.xAxis.boundaryGap = true; option.xAxis.boundaryGap = true;
option.xAxis.axisLabel.interval = 'auto'; // 自动计算间隔,避免标签重叠导致柱子变细 option.xAxis.axisLabel.interval = "auto"; // 自动计算间隔,避免标签重叠导致柱子变细
option.yAxis.name = "百分比"; option.yAxis.name = "百分比";
// 动态计算 Y 轴最大值,避免数据太小时展示不明显 // 动态计算 Y 轴最大值,避免数据太小时展示不明显
...@@ -844,38 +938,43 @@ const initShareChart = () => { ...@@ -844,38 +938,43 @@ const initShareChart = () => {
const maxY = maxVal === 0 ? 100 : maxVal * 1.5; const maxY = maxVal === 0 ? 100 : maxVal * 1.5;
option.yAxis.max = maxY; option.yAxis.max = maxY;
option.series = [{ option.series = [
data: data.values, {
type: "bar", data: data.values,
// 移除固定 barWidth,让其根据数据量自适应,或设置一个合理的最小值 type: "bar",
barMaxWidth: 20, // 移除固定 barWidth,让其根据数据量自适应,或设置一个合理的最小值
barMinHeight: 2, // 确保即使数值极小也能看到一点 barMaxWidth: 20,
itemStyle: { barMinHeight: 2, // 确保即使数值极小也能看到一点
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ itemStyle: {
{ offset: 0, color: "#055FC2" }, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 1, color: "rgba(5, 95, 194, 0.1)" } { offset: 0, color: "#055FC2" },
]), { offset: 1, color: "rgba(5, 95, 194, 0.1)" }
borderRadius: [8, 8, 0, 0] ]),
}, borderRadius: [8, 8, 0, 0]
markLine: { },
symbol: "none", markLine: {
data: [ symbol: "none",
data.sanctionDate ? { data: [
xAxis: data.sanctionDate, data.sanctionDate
label: { ? {
formatter: "列入实体清单", xAxis: data.sanctionDate,
position: "end", label: {
color: "#F56C6C", formatter: "列入实体清单",
backgroundColor: "rgba(245, 108, 108, 0.1)", position: "end",
padding: [2, 4], color: "#F56C6C",
borderRadius: 10 backgroundColor: "rgba(245, 108, 108, 0.1)",
}, padding: [2, 4],
lineStyle: { color: "#F56C6C", type: "dotted" } borderRadius: 10
} : null },
].filter(Boolean) lineStyle: { color: "#F56C6C", type: "dotted" }
}
: null
].filter(Boolean)
}
} }
}]; ];
myChart.setOption(option, true); // myChart.setOption(option, true);
shareChartOption.value = option;
}; };
onMounted(async () => { onMounted(async () => {
...@@ -901,7 +1000,6 @@ onMounted(async () => { ...@@ -901,7 +1000,6 @@ onMounted(async () => {
padding-top: 16px; padding-top: 16px;
padding-bottom: 50px; padding-bottom: 50px;
.toggle-btns { .toggle-btns {
display: flex; display: flex;
gap: 8px; gap: 8px;
...@@ -1214,4 +1312,50 @@ onMounted(async () => { ...@@ -1214,4 +1312,50 @@ onMounted(async () => {
} }
} }
} }
.data-origin-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 30px 0;
.data-origin-icon {
width: 16px;
height: 16px;
font-size: 0px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
}
}
.data-origin-text {
font-family: Source Han Sans CN;
font-size: 14px;
color: var(--text-primary-50-color);
}
}
.ai-pane {
position: absolute;
right: 0px;
bottom: 15px;
z-index: 2;
:deep(.ai-pane-wrapper) {
display: none;
}
:deep(.ai-button-wrapper) {
display: flex;
}
&:hover {
width: 100%;
bottom: 0px;
:deep(.ai-pane-wrapper) {
display: block;
}
:deep(.ai-button-wrapper) {
display: none;
}
}
}
</style> </style>
...@@ -12,10 +12,19 @@ ...@@ -12,10 +12,19 @@
<div class="company-list-container"> <div class="company-list-container">
<div class="list-header">科研机构名称</div> <div class="list-header">科研机构名称</div>
<div class="company-list"> <div class="company-list">
<el-empty v-if="entityList.length === 0" description="暂无数据" style="padding-top: 200px" <el-empty
:image-size="100" /> v-if="entityList.length === 0"
<div class="company-item" :class="{ active: selectedCompanyId === item.id }" v-for="item in entityList" description="暂无数据"
:key="item.id" @click="selectedCompanyId = item.id"> style="padding-top: 200px"
:image-size="100"
/>
<div
class="company-item"
:class="{ active: selectedCompanyId === item.id }"
v-for="item in entityList"
:key="item.id"
@click="selectedCompanyId = item.id"
>
<div class="icon-wrapper"> <div class="icon-wrapper">
<img :src="defaultTitle" alt="" /> <img :src="defaultTitle" alt="" />
</div> </div>
...@@ -30,13 +39,28 @@ ...@@ -30,13 +39,28 @@
<div class="right-item"> <div class="right-item">
<AnalysisBox title="科研仪器对美依赖情况"> <AnalysisBox title="科研仪器对美依赖情况">
<div class="right-main"> <div class="right-main">
<el-empty v-if="rdInstrumentDependency.length === 0" description="暂无数据" style="padding-top: 100px" <el-empty
:image-size="100" /> v-if="rdInstrumentDependency.length === 0"
<div v-if="rdInstrumentDependency.length !== 0" class="echarts" ref="chartRef"></div> description="暂无数据"
style="padding-top: 100px"
:image-size="100"
/>
<!-- <div v-if="rdInstrumentDependency.length !== 0" class="echarts" ref="chartRef"></div>
<div v-if="rdInstrumentDependency.length !== 0" class="bottom"> <div v-if="rdInstrumentDependency.length !== 0" class="bottom">
<img :src="ai" class="ai-icon" alt="" /> <img :src="ai" class="ai-icon" alt="" />
<span class="text">受制裁实体中,电子测量仪器对美依赖程度最高,此外,电子策略仪器、物理性能测试仪器等也有较高的对美依赖度。</span> <span class="text">受制裁实体中,电子测量仪器对美依赖程度最高,此外,电子策略仪器、物理性能测试仪器等也有较高的对美依赖度。</span>
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> -->
<EChart :option="rdInstrumentDependencyOption" autoresize :style="{ height: '300px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">科研仪器对美依赖情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="rdInstrumentDependencyChart.interpretation" />
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
...@@ -44,13 +68,35 @@ ...@@ -44,13 +68,35 @@
<div class="right-item"> <div class="right-item">
<AnalysisBox title="科研仪器进口国分布"> <AnalysisBox title="科研仪器进口国分布">
<div class="right-main"> <div class="right-main">
<el-empty v-if="rdInstrumentImportCountry.length === 0" description="暂无数据" style="padding-top: 100px" <el-empty
:image-size="100" /> v-if="rdInstrumentImportCountry.length === 0"
<div v-if="rdInstrumentImportCountry.length !== 0" class="echarts" ref="marketChartRef"></div> description="暂无数据"
style="padding-top: 100px"
:image-size="100"
/>
<!-- <div v-if="rdInstrumentImportCountry.length !== 0" class="echarts" ref="marketChartRef"></div>
<div v-if="rdInstrumentImportCountry.length !== 0" class="bottom"> <div v-if="rdInstrumentImportCountry.length !== 0" class="bottom">
<img :src="ai" class="ai-icon" alt="" /> <img :src="ai" class="ai-icon" alt="" />
<span class="text">电子测量仪器来源国主要为美国,但是德国、日本等国也占有较大比重,后续可考虑将上述国家作为仪器进口来源替代国。</span> <span class="text"
>电子测量仪器来源国主要为美国,但是德国、日本等国也占有较大比重,后续可考虑将上述国家作为仪器进口来源替代国。</span
>
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> -->
<EChart
v-if="rdInstrumentImportCountry.length !== 0"
:option="rdInstrumentImportCountryOption"
autoresize
:style="{ height: '300px' }"
/>
<div v-if="rdInstrumentImportCountry.length !== 0" class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">科研仪器进口国分布,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="rdInstrumentImportCountryChart.interpretation" />
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
...@@ -84,18 +130,42 @@ ...@@ -84,18 +130,42 @@
<AnalysisBox title="新增国际合作项目"> <AnalysisBox title="新增国际合作项目">
<template #header-btn> <template #header-btn>
<el-select v-model="activeProjectDomain" class="project-domain-select" placeholder="全部领域"> <el-select v-model="activeProjectDomain" class="project-domain-select" placeholder="全部领域">
<el-option v-for="item in projectDomainOptions" :key="item.value" :label="item.label" <el-option
:value="item.value" /> v-for="item in projectDomainOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
</template> </template>
<div class="right-main"> <div class="right-main">
<el-empty v-if="internationalCooperation.length === 0" description="暂无数据" style="padding-top: 100px" <el-empty
:image-size="100" /> v-if="internationalCooperation.length === 0"
<div v-if="internationalCooperation.length !== 0" class="echarts" ref="rdChartRef"></div> description="暂无数据"
style="padding-top: 100px"
:image-size="100"
/>
<!-- <div v-if="internationalCooperation.length !== 0" class="echarts" ref="rdChartRef"></div>
<div v-if="internationalCooperation.length !== 0" class="bottom"> <div v-if="internationalCooperation.length !== 0" class="bottom">
<img :src="ai" class="ai-icon" alt="" /> <img :src="ai" class="ai-icon" alt="" />
<span class="text">列入实体清单后新增国际项目数量明显减少。</span> <span class="text">列入实体清单后新增国际项目数量明显减少。</span>
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> -->
<EChart
v-if="internationalCooperation.length !== 0"
:option="internationalCooperationOption"
autoresize
:style="{ height: '300px' }"
/>
<div v-if="internationalCooperation.length !== 0" class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">新增国际合作项目,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="internationalCooperationChart.interpretation" />
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
...@@ -129,18 +199,42 @@ ...@@ -129,18 +199,42 @@
<AnalysisBox title="新增国际合著论文"> <AnalysisBox title="新增国际合著论文">
<template #header-btn> <template #header-btn>
<el-select v-model="activePaperDomain" class="paper-domain-select" placeholder="全部领域"> <el-select v-model="activePaperDomain" class="paper-domain-select" placeholder="全部领域">
<el-option v-for="item in paperDomainOptions" :key="item.value" :label="item.label" <el-option
:value="item.value" /> v-for="item in paperDomainOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
</template> </template>
<div class="right-main"> <div class="right-main">
<el-empty v-if="internationalPaper.length === 0" description="暂无数据" style="padding-top: 100px" <el-empty
:image-size="100" /> v-if="internationalPaper.length === 0"
<div v-if="internationalPaper.length !== 0" class="echarts" ref="shareChartRef"></div> description="暂无数据"
style="padding-top: 100px"
:image-size="100"
/>
<!-- <div v-if="internationalPaper.length !== 0" class="echarts" ref="shareChartRef"></div>
<div v-if="internationalPaper.length !== 0" class="bottom"> <div v-if="internationalPaper.length !== 0" class="bottom">
<img :src="ai" class="ai-icon" alt="" /> <img :src="ai" class="ai-icon" alt="" />
<span class="text">总量下降,特别是在人工智能、高端通信等敏感技术领域。</span> <span class="text">总量下降,特别是在人工智能、高端通信等敏感技术领域。</span>
<img :src="right" class="right-icon" alt="" /> <img :src="right" class="right-icon" alt="" />
</div> -->
<EChart
v-if="internationalPaper.length !== 0"
:option="internationalPaperOption"
autoresize
:style="{ height: '300px' }"
/>
<div v-if="internationalPaper.length !== 0" class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">新增国际合著论文,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="internationalPaperChart.interpretation" />
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
...@@ -163,6 +257,15 @@ import { ...@@ -163,6 +257,15 @@ import {
getSingleSanctionEntityInternationalCooperation, getSingleSanctionEntityInternationalCooperation,
getSingleSanctionEntityInternationalPaper getSingleSanctionEntityInternationalPaper
} from "@/api/exportControlV2.0"; } from "@/api/exportControlV2.0";
import tipsIcon from "../../../../../assets/icons/info-icon.png";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import EChart from "@/components/Chart/index.vue";
import { useChartInterpretation } from "@/views/exportControl/utils/common";
const rdInstrumentDependencyChart = useChartInterpretation();
const rdInstrumentImportCountryChart = useChartInterpretation();
const internationalCooperationChart = useChartInterpretation();
const internationalPaperChart = useChartInterpretation();
const domainOptions = [ const domainOptions = [
{ label: "全部领域", value: "" }, { label: "全部领域", value: "" },
...@@ -216,6 +319,7 @@ const getInternationalPaper = async () => { ...@@ -216,6 +319,7 @@ const getInternationalPaper = async () => {
nextTick(() => { nextTick(() => {
initShareChart(); initShareChart();
}); });
internationalPaperChart.interpret({ type: "折线图", name: "新增国际合著论文", data: res.data });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -238,6 +342,7 @@ const getInternationalCooperation = async () => { ...@@ -238,6 +342,7 @@ const getInternationalCooperation = async () => {
nextTick(() => { nextTick(() => {
initRdChart(); initRdChart();
}); });
internationalCooperationChart.interpret({ type: "折线图", name: "新增国际合作项目", data: res.data });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -255,6 +360,7 @@ const getRDInstrumentImportCountry = async () => { ...@@ -255,6 +360,7 @@ const getRDInstrumentImportCountry = async () => {
nextTick(() => { nextTick(() => {
initMarketChart(); initMarketChart();
}); });
rdInstrumentImportCountryChart.interpret({ type: "饼图", name: "科研仪器进口国分布", data: res.data });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -272,6 +378,7 @@ const getRDInstrumentDependency = async () => { ...@@ -272,6 +378,7 @@ const getRDInstrumentDependency = async () => {
nextTick(() => { nextTick(() => {
initChart(); initChart();
}); });
rdInstrumentDependencyChart.interpret({ type: "柱状图", name: "科研仪器对美依赖情况", data: res.data });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
...@@ -340,9 +447,72 @@ const marketChartRef = ref(null); ...@@ -340,9 +447,72 @@ const marketChartRef = ref(null);
const rdChartRef = ref(null); const rdChartRef = ref(null);
const shareChartRef = ref(null); const shareChartRef = ref(null);
const rdInstrumentDependencyOption = ref({
grid: {
top: "5%",
left: "0%",
right: "5%",
bottom: "0%",
containLabel: true
},
dataZoom: [
{
type: "inside",
orient: "vertical",
start: 0,
end: 100,
zoomLock: true
}
],
xAxis: {
show: false,
type: "value"
},
yAxis: [
{
type: "category",
data: [],
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: "rgb(59, 65, 75)",
fontSize: 16,
fontWeight: 400,
fontFamily: "Microsoft YaHei",
margin: 20,
interval: 0
},
inverse: true
},
{
type: "category",
data: [],
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
margin: 20,
interval: 0,
fontSize: 16,
fontFamily: "Microsoft YaHei",
color: (value, index) => {
return index < 3 ? "rgba(209, 90, 90, 1)" : "rgba(59, 139, 245, 1)";
}
},
inverse: true
}
],
series: [
{
type: "bar",
data: [],
barWidth: 10,
showBackground: false
}
]
});
const initChart = () => { const initChart = () => {
if (!chartRef.value || rdInstrumentDependency.value.length === 0) return; // if (!chartRef.value || rdInstrumentDependency.value.length === 0) return;
const myChart = echarts.getInstanceByDom(chartRef.value) || echarts.init(chartRef.value); // const myChart = echarts.getInstanceByDom(chartRef.value) || echarts.init(chartRef.value);
// 处理接口数据,映射为图表所需的 name 和 value // 处理接口数据,映射为图表所需的 name 和 value
const chartData = rdInstrumentDependency.value.map(item => ({ const chartData = rdInstrumentDependency.value.map(item => ({
...@@ -350,95 +520,181 @@ const initChart = () => { ...@@ -350,95 +520,181 @@ const initChart = () => {
value: item.count value: item.count
})); }));
const option = { // const option = {
grid: { // grid: {
top: "5%", // top: "5%",
left: "0%", // left: "0%",
right: "5%", // right: "5%",
bottom: "0%", // bottom: "0%",
containLabel: true // containLabel: true
}, // },
dataZoom: [ // dataZoom: [
{ // {
type: "inside", // type: "inside",
orient: "vertical", // orient: "vertical",
start: 0, // start: 0,
end: chartData.length > 7 ? (7 / chartData.length) * 100 : 100, // end: chartData.length > 7 ? (7 / chartData.length) * 100 : 100,
zoomLock: true // zoomLock: true
// }
// ],
// xAxis: {
// show: false,
// type: "value"
// },
// yAxis: [
// {
// type: "category",
// data: chartData.map(item => item.name),
// axisLine: { show: false },
// axisTick: { show: false },
// axisLabel: {
// color: "rgb(59, 65, 75)",
// fontSize: 16,
// fontWeight: 400,
// fontFamily: "Microsoft YaHei",
// margin: 20,
// interval: 0
// },
// inverse: true
// },
// {
// type: "category",
// data: chartData.map(item => item.value),
// axisLine: { show: false },
// axisTick: { show: false },
// axisLabel: {
// margin: 20,
// interval: 0,
// fontSize: 16,
// fontFamily: "Microsoft YaHei",
// color: (value, index) => {
// return index < 3 ? "rgba(209, 90, 90, 1)" : "rgba(59, 139, 245, 1)";
// }
// },
// inverse: true
// }
// ],
// series: [
// {
// type: "bar",
// data: chartData.map((item, index) => {
// return {
// value: item.value,
// itemStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
// {
// offset: 0,
// color: index < 3 ? "rgba(209, 90, 90, 0.05)" : "rgba(59, 139, 245, 0.05)"
// },
// {
// offset: 1,
// color: index < 3 ? "rgba(209, 90, 90, 1)" : "rgba(59, 139, 245, 1)"
// }
// ]),
// borderRadius: [0, 10, 10, 0]
// },
// label: {
// show: false
// }
// };
// }),
// barWidth: 10,
// showBackground: false
// }
// ]
// };
// myChart.setOption(option);
// rdInstrumentDependencyOption.value = option;
rdInstrumentDependencyOption.value.dataZoom[0].end = chartData.length > 7 ? (7 / chartData.length) * 100 : 100;
rdInstrumentDependencyOption.value.yAxis[0].data = chartData.map(item => item.name);
rdInstrumentDependencyOption.value.yAxis[1].data = chartData.map(item => item.value);
rdInstrumentDependencyOption.value.series[0].data = chartData.map((item, index) => {
return {
value: item.value,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: index < 3 ? "rgba(209, 90, 90, 0.05)" : "rgba(59, 139, 245, 0.05)"
},
{
offset: 1,
color: index < 3 ? "rgba(209, 90, 90, 1)" : "rgba(59, 139, 245, 1)"
}
]),
borderRadius: [0, 10, 10, 0]
},
label: {
show: false
} }
], };
xAxis: { });
show: false, };
type: "value"
}, const colors = ["#66b1ff", "#ffba63", "#7ce3d3", "#8a9dff", "#ff7c7c", "#b386f2", "#4080ff"];
yAxis: [ const rdInstrumentImportCountryOption = ref({
{ color: colors,
type: "category", tooltip: {
data: chartData.map(item => item.name), trigger: "item",
axisLine: { show: false }, formatter: "{b}: {c}%"
axisTick: { show: false }, },
axisLabel: { legend: {
color: "rgb(59, 65, 75)", type: "scroll",
orient: "vertical",
right: "5%",
top: "middle",
bottom: "20",
itemGap: 20,
icon: "circle",
// formatter: function (name) {
// const item = chartData.find(i => i.name === name);
// const total = chartData.reduce((sum, i) => sum + i.value, 0);
// const percent = total > 0 ? ((item.value / total) * 100).toFixed(0) : 0;
// return `{name|${name}} {value|${percent}%}`;
// },
textStyle: {
rich: {
name: {
fontSize: 16, fontSize: 16,
fontWeight: 400, color: "#333",
fontFamily: "Microsoft YaHei", width: 60,
margin: 20, padding: [0, 0, 0, 5],
interval: 0 fontFamily: "Microsoft YaHei"
}, },
inverse: true value: {
},
{
type: "category",
data: chartData.map(item => item.value),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
margin: 20,
interval: 0,
fontSize: 16, fontSize: 16,
fontFamily: "Microsoft YaHei", color: "#333",
color: (value, index) => { fontFamily: "Microsoft YaHei"
return index < 3 ? "rgba(209, 90, 90, 1)" : "rgba(59, 139, 245, 1)"; }
}
},
inverse: true
}
],
series: [
{
type: "bar",
data: chartData.map((item, index) => {
return {
value: item.value,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: index < 3 ? "rgba(209, 90, 90, 0.05)" : "rgba(59, 139, 245, 0.05)"
},
{
offset: 1,
color: index < 3 ? "rgba(209, 90, 90, 1)" : "rgba(59, 139, 245, 1)"
}
]),
borderRadius: [0, 10, 10, 0]
},
label: {
show: false
}
};
}),
barWidth: 10,
showBackground: false
} }
] }
}; },
myChart.setOption(option); series: [
}; {
name: "科研仪器进口国分布",
type: "pie",
radius: ["80px", "98px"],
center: ["30%", "50%"],
avoidLabelOverlap: false,
label: {
show: false,
position: "center"
},
emphasis: {
label: {
show: false
}
},
labelLine: {
show: false
},
data: []
}
]
});
const initMarketChart = () => { const initMarketChart = () => {
if (!marketChartRef.value || rdInstrumentImportCountry.value.length === 0) return; // if (!marketChartRef.value || rdInstrumentImportCountry.value.length === 0) return;
const myChart = echarts.getInstanceByDom(marketChartRef.value) || echarts.init(marketChartRef.value); // const myChart = echarts.getInstanceByDom(marketChartRef.value) || echarts.init(marketChartRef.value);
const chartData = rdInstrumentImportCountry.value.map(item => ({ const chartData = rdInstrumentImportCountry.value.map(item => ({
name: item.name, name: item.name,
...@@ -507,12 +763,84 @@ const initMarketChart = () => { ...@@ -507,12 +763,84 @@ const initMarketChart = () => {
} }
] ]
}; };
myChart.setOption(option); // myChart.setOption(option);
rdInstrumentImportCountryOption.value = option;
}; };
const internationalCooperationOption = ref({
tooltip: {
trigger: "axis"
},
grid: {
top: "15%",
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true
},
xAxis: {
type: "category",
boundaryGap: false,
data: [],
axisLine: {
lineStyle: {
color: "#E6EBF5"
}
},
axisLabel: {
color: "#606266",
fontSize: 12,
lineHeight: 18,
fontFamily: "Microsoft YaHei"
},
axisTick: {
show: false
}
},
yAxis: {
type: "value",
min: 0,
max: 5,
interval: 10 / 5,
axisLabel: {
color: "#606266",
fontSize: 12,
fontFamily: "Microsoft YaHei"
},
splitLine: {
lineStyle: {
type: "dashed",
color: "rgba(231, 243, 255, 1)"
}
}
},
series: [
{
data: [],
type: "line",
symbol: "none",
smooth: false,
lineStyle: {
color: "#055FC2",
width: 2
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(5, 95, 194, 0.2)"
},
{
offset: 1,
color: "rgba(5, 95, 194, 0)"
}
])
}
}
]
});
const initRdChart = () => { const initRdChart = () => {
if (!rdChartRef.value) return; // if (!rdChartRef.value) return;
const myChart = echarts.getInstanceByDom(rdChartRef.value) || echarts.init(rdChartRef.value); // const myChart = echarts.getInstanceByDom(rdChartRef.value) || echarts.init(rdChartRef.value);
// 处理接口返回的数据,按年份升序排序 // 处理接口返回的数据,按年份升序排序
const chartData = [...internationalCooperation.value].sort((a, b) => a.year - b.year); const chartData = [...internationalCooperation.value].sort((a, b) => a.year - b.year);
...@@ -523,84 +851,159 @@ const initRdChart = () => { ...@@ -523,84 +851,159 @@ const initRdChart = () => {
const maxVal = Math.max(...data, 10); const maxVal = Math.max(...data, 10);
const yMax = Math.ceil(maxVal / 5) * 5; const yMax = Math.ceil(maxVal / 5) * 5;
const option = { // const option = {
tooltip: { // tooltip: {
trigger: "axis" // trigger: "axis"
// },
// grid: {
// top: "15%",
// left: "3%",
// right: "4%",
// bottom: "3%",
// containLabel: true
// },
// xAxis: {
// type: "category",
// boundaryGap: false,
// data: years,
// axisLine: {
// lineStyle: {
// color: "#E6EBF5"
// }
// },
// axisLabel: {
// color: "#606266",
// fontSize: 12,
// lineHeight: 18,
// fontFamily: "Microsoft YaHei"
// },
// axisTick: {
// show: false
// }
// },
// yAxis: {
// type: "value",
// min: 0,
// max: yMax,
// interval: yMax / 5,
// axisLabel: {
// color: "#606266",
// fontSize: 12,
// fontFamily: "Microsoft YaHei"
// },
// splitLine: {
// lineStyle: {
// type: "dashed",
// color: "rgba(231, 243, 255, 1)"
// }
// }
// },
// series: [
// {
// data: data,
// type: "line",
// symbol: "none",
// smooth: false,
// lineStyle: {
// color: "#055FC2",
// width: 2
// },
// areaStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// {
// offset: 0,
// color: "rgba(5, 95, 194, 0.2)"
// },
// {
// offset: 1,
// color: "rgba(5, 95, 194, 0)"
// }
// ])
// }
// }
// ]
// };
// myChart.setOption(option);
internationalCooperationOption.value.xAxis.data = years;
internationalCooperationOption.value.yAxis.max = yMax;
internationalCooperationOption.value.yAxis.interval = yMax / 5;
internationalCooperationOption.value.series[0].data = data;
};
const internationalPaperOption = ref({
tooltip: {
trigger: "axis"
},
grid: {
top: "15%",
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true
},
xAxis: {
type: "category",
boundaryGap: false,
data: [],
axisLine: {
lineStyle: {
color: "#E6EBF5"
}
}, },
grid: { axisLabel: {
top: "15%", color: "#606266",
left: "3%", fontSize: 12,
right: "4%", lineHeight: 18,
bottom: "3%", fontFamily: "Microsoft YaHei"
containLabel: true
}, },
xAxis: { axisTick: {
type: "category", show: false
boundaryGap: false, }
data: years, },
axisLine: { yAxis: {
lineStyle: { type: "value",
color: "#E6EBF5" min: 0,
} max: 5,
}, interval: 10 / 5,
axisLabel: { axisLabel: {
color: "#606266", color: "#606266",
fontSize: 12, fontSize: 12,
lineHeight: 18, fontFamily: "Microsoft YaHei"
fontFamily: "Microsoft YaHei"
},
axisTick: {
show: false
}
}, },
yAxis: { splitLine: {
type: "value", lineStyle: {
min: 0, type: "dashed",
max: yMax, color: "rgba(231, 243, 255, 1)"
interval: yMax / 5,
axisLabel: {
color: "#606266",
fontSize: 12,
fontFamily: "Microsoft YaHei"
},
splitLine: {
lineStyle: {
type: "dashed",
color: "rgba(231, 243, 255, 1)"
}
} }
}, }
series: [ },
{ series: [
data: data, {
type: "line", data: [],
symbol: "none", type: "line",
smooth: false, symbol: "none",
lineStyle: { smooth: false,
color: "#055FC2", lineStyle: {
width: 2 color: "#055FC2",
}, width: 2
areaStyle: { },
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ areaStyle: {
{ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
offset: 0, {
color: "rgba(5, 95, 194, 0.2)" offset: 0,
}, color: "rgba(5, 95, 194, 0.2)"
{ },
offset: 1, {
color: "rgba(5, 95, 194, 0)" offset: 1,
} color: "rgba(5, 95, 194, 0)"
]) }
} ])
} }
] }
}; ]
myChart.setOption(option); });
};
const initShareChart = () => { const initShareChart = () => {
if (!shareChartRef.value) return; // if (!shareChartRef.value) return;
const myChart = echarts.getInstanceByDom(shareChartRef.value) || echarts.init(shareChartRef.value); // const myChart = echarts.getInstanceByDom(shareChartRef.value) || echarts.init(shareChartRef.value);
// 处理接口返回的数据,按年份升序排序 // 处理接口返回的数据,按年份升序排序
const chartData = [...internationalPaper.value].sort((a, b) => a.year - b.year); const chartData = [...internationalPaper.value].sort((a, b) => a.year - b.year);
...@@ -611,79 +1014,83 @@ const initShareChart = () => { ...@@ -611,79 +1014,83 @@ const initShareChart = () => {
const maxVal = Math.max(...data, 10); const maxVal = Math.max(...data, 10);
const yMax = Math.ceil(maxVal / 5) * 5; const yMax = Math.ceil(maxVal / 5) * 5;
const option = { // const option = {
tooltip: { // tooltip: {
trigger: "axis" // trigger: "axis"
}, // },
grid: { // grid: {
top: "15%", // top: "15%",
left: "3%", // left: "3%",
right: "4%", // right: "4%",
bottom: "3%", // bottom: "3%",
containLabel: true // containLabel: true
}, // },
xAxis: { // xAxis: {
type: "category", // type: "category",
boundaryGap: false, // boundaryGap: false,
data: years, // data: years,
axisLine: { // axisLine: {
lineStyle: { // lineStyle: {
color: "#E6EBF5" // color: "#E6EBF5"
} // }
}, // },
axisLabel: { // axisLabel: {
color: "#606266", // color: "#606266",
fontSize: 12, // fontSize: 12,
lineHeight: 18, // lineHeight: 18,
fontFamily: "Microsoft YaHei" // fontFamily: "Microsoft YaHei"
}, // },
axisTick: { // axisTick: {
show: false // show: false
} // }
}, // },
yAxis: { // yAxis: {
type: "value", // type: "value",
min: 0, // min: 0,
max: yMax, // max: yMax,
interval: yMax / 5, // interval: yMax / 5,
axisLabel: { // axisLabel: {
color: "#606266", // color: "#606266",
fontSize: 12, // fontSize: 12,
fontFamily: "Microsoft YaHei" // fontFamily: "Microsoft YaHei"
}, // },
splitLine: { // splitLine: {
lineStyle: { // lineStyle: {
type: "dashed", // type: "dashed",
color: "rgba(231, 243, 255, 1)" // color: "rgba(231, 243, 255, 1)"
} // }
} // }
}, // },
series: [ // series: [
{ // {
data: data, // data: data,
type: "line", // type: "line",
symbol: "none", // symbol: "none",
smooth: false, // smooth: false,
lineStyle: { // lineStyle: {
color: "#055FC2", // color: "#055FC2",
width: 2 // width: 2
}, // },
areaStyle: { // areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ // {
offset: 0, // offset: 0,
color: "rgba(5, 95, 194, 0.2)" // color: "rgba(5, 95, 194, 0.2)"
}, // },
{ // {
offset: 1, // offset: 1,
color: "rgba(5, 95, 194, 0)" // color: "rgba(5, 95, 194, 0)"
} // }
]) // ])
} // }
} // }
] // ]
}; // };
myChart.setOption(option); // myChart.setOption(option);
internationalPaperOption.value.xAxis.data = years;
internationalPaperOption.value.yAxis.max = yMax;
internationalPaperOption.value.yAxis.interval = yMax / 5;
internationalPaperOption.value.series[0].data = data;
}; };
const handleResize = () => { const handleResize = () => {
...@@ -1131,4 +1538,50 @@ onBeforeUnmount(() => { ...@@ -1131,4 +1538,50 @@ onBeforeUnmount(() => {
font-size: 14px; font-size: 14px;
} }
} }
.data-origin-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 35px 0;
.data-origin-icon {
width: 16px;
height: 16px;
font-size: 0px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
}
}
.data-origin-text {
font-family: Source Han Sans CN;
font-size: 14px;
color: var(--text-primary-50-color);
}
}
.ai-pane {
position: absolute;
right: 0px;
bottom: 15px;
z-index: 2;
:deep(.ai-pane-wrapper) {
display: none;
}
:deep(.ai-button-wrapper) {
display: flex;
}
&:hover {
width: 100%;
bottom: 0px;
:deep(.ai-pane-wrapper) {
display: block;
}
:deep(.ai-button-wrapper) {
display: none;
}
}
}
</style> </style>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
{{ headerTitle.department }} {{ headerTitle.department }}
</div> </div>
</div> </div>
<div class="btn"><img :src="icon01" alt="" />切换</div> <div class="btn" @click="openSanctionModal"><img :src="icon01" alt="" />切换</div>
</div> </div>
<div class="header-nav"> <div class="header-nav">
<div <div
...@@ -44,12 +44,46 @@ ...@@ -44,12 +44,46 @@
<deep-mining v-if="activeIndex === 2"></deep-mining> <deep-mining v-if="activeIndex === 2"></deep-mining>
<impact-analysis v-if="activeIndex === 3"></impact-analysis> <impact-analysis v-if="activeIndex === 3"></impact-analysis>
</div> </div>
<el-dialog
v-model="sanctionModalVisible"
title="制裁事件列表"
width="600px"
:close-on-click-modal="false"
@close="resetModal"
>
<div class="sanction-list">
<div
v-for="item in sanctionList"
:key="item.id"
class="sanction-item"
:class="{ active: selectedSanctionId === item.id }"
@click="selectSanction(item)"
>
{{ item.name }}
</div>
</div>
<template #footer>
<div class="pagination-footer">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalElements"
:page-sizes="[10]"
layout="total, prev, pager, next"
background
@current-change="handlePageChange"
/>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { ElDialog, ElPagination } from "element-plus";
import sanctionsOverview from "./components/sanctionsOverview/index.vue"; import sanctionsOverview from "./components/sanctionsOverview/index.vue";
import dataStatistics from "./components/dataStatistics/index.vue"; import dataStatistics from "./components/dataStatistics/index.vue";
import deepMining from "./components/deepMining/index.vue"; import deepMining from "./components/deepMining/index.vue";
...@@ -67,6 +101,7 @@ import icon3 from "../assets/icons/icon3.png"; ...@@ -67,6 +101,7 @@ import icon3 from "../assets/icons/icon3.png";
import icon3Active from "../assets/icons/icon3_active.png"; import icon3Active from "../assets/icons/icon3_active.png";
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js"; import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js";
import { getSanctionProcess } from "@/api/exportControl";
import router from "@/router"; import router from "@/router";
// 处理点击实体清单原文按钮 // 处理点击实体清单原文按钮
...@@ -157,6 +192,77 @@ const handleAnalysisClick = () => { ...@@ -157,6 +192,77 @@ const handleAnalysisClick = () => {
}); });
}; };
// ========== 新增响应式状态 ==========
const sanctionModalVisible = ref(false);
const sanctionList = ref([]);
const selectedSanctionId = ref(null);
const currentPage = ref(1);
const pageSize = ref(10);
const totalElements = ref(0);
// ========== 打开弹窗并加载第一页 ==========
const openSanctionModal = async () => {
sanctionModalVisible.value = true;
console.log("制裁事件列表11:", sanctionList.value);
await fetchSanctionData();
};
// ========== 获取制裁数据 ==========
const fetchSanctionData = async () => {
try {
const res = await getSanctionProcess([1], currentPage.value, 10);
if (res && !!res.content) {
sanctionList.value = res.content || [];
totalElements.value = res.totalElements || 0;
// 如果当前选中的不在新数据中,清空选中
if (selectedSanctionId.value && !sanctionList.value.some(item => item.id === selectedSanctionId.value)) {
selectedSanctionId.value = null;
}
console.log("制裁事件列表:", sanctionList.value);
} else {
sanctionList.value = [];
totalElements.value = 0;
}
} catch (error) {
console.error("获取制裁事件失败:", error);
sanctionList.value = [];
totalElements.value = 0;
}
};
// ========== 翻页处理 ==========
const handlePageChange = async newPage => {
currentPage.value = newPage;
await fetchSanctionData();
};
// ========== 选择某项 ==========
const selectSanction = item => {
selectedSanctionId.value = item.id;
// 可在此处触发其他逻辑,如查看详情
// 替换当前路由参数,并重新加载数据
router.replace({
path: window.location.pathname,
query: {
id: item.id,
sanTypeId: item.sanTypeId
}
});
// 3. 立即重新加载数据(关键!)
getSingleSanctionOverviewData();
// 4. 可选:关闭弹窗
sanctionModalVisible.value = false;
};
// ========== 关闭弹窗时重置 ==========
const resetModal = () => {
sanctionList.value = [];
selectedSanctionId.value = null;
currentPage.value = 1;
totalElements.value = 0;
};
onMounted(() => { onMounted(() => {
getUrlParams(); getUrlParams();
getSingleSanctionOverviewData(); getSingleSanctionOverviewData();
...@@ -350,4 +456,40 @@ onMounted(() => { ...@@ -350,4 +456,40 @@ onMounted(() => {
background-color: #f7f8f9; background-color: #f7f8f9;
} }
} }
.sanction-list {
max-height: 400px;
overflow-y: auto;
padding: 0 10px;
}
.sanction-item {
padding: 7px;
margin: 6px 0;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #333;
transition: background-color 0.2s;
}
.sanction-item:hover {
background-color: #f5f7fa;
}
.sanction-item.active {
background-color: #e6f0ff;
color: #1890ff;
font-weight: bold;
}
.pagination-footer {
display: flex;
justify-content: center;
margin-top: 12px;
}
:deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
background-color: #0e78f1;
}
</style> </style>
{
"content": [
{
"id": 138,
"sanTypeId": 1,
"postDate": "2025-10-08",
"name": "实体清单的增补与修订",
"summary": "在该规则中,工业和安全局(BIS)修订了《出口管理条例》(EAR),在中华人民共和国(中国)(19)、土耳其(9)和阿拉伯联合酋长国(阿联酋)(1)的目的地下的实体清单中增加了29个条目(26个实体和3个地址)。美国政府认定这些实体的行为违背了美国的国家安全或外交政策利益。",
"cnEntityCount": 15,
"techDomainList": ["新能源", "先进制造", "航空航天", "人工智能", "新一代通信网络"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 121,
"sanTypeId": 1,
"postDate": "2025-09-16",
"name": "美国工业与安全局新增32个实体至实体清单,多数为中国企业",
"summary": "美国工业与安全局(BIS)通过在《出口管理条例》(EAR)中新增32个实体至实体清单。这些实体分别位于中国(23个)、印度(1个)、伊朗(1个)、新加坡(1个)、台湾(1个)、土耳其(3个)和阿拉伯联合酋长国(UAE)(2个)。这些实体已被美国政府认定为违反美国国家安全或外交政策利益。其中,中国的23个实体包括中国科学院国家授时中心、北京复旦微电子科技有限公司、上海复旦微电子有限公司等。这些实体因收购或试图收购美国原产物品以支持中国军事及国防相关的太空领域活动、量子技术能力发展以及参与中国先进计算和集成电路制造与分销领域而被加入。此外,还包括一些向中国军队、政府和安全部门供应产品的公司。被列入实体清单的实体将面临更严格的许可证要求,并实行拒绝推定的许可证审查政策。此次更新还修正了部分现有条目中的拼写错误,涉及多个国家和地区,其中包括中国的11个实体。",
"cnEntityCount": 23,
"techDomainList": ["航空航天", "人工智能", "集成电路", "先进制造", "生物科技", "太空", "量子科技"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 85,
"sanTypeId": 1,
"postDate": "2025-03-28",
"name": "美国将12个中国和台湾实体列入出口管制清单",
"summary": "根据美国商务部工业与安全局(BIS)发布的最终规则,12个实体被列入实体清单,其中11个位于中国,1个位于台湾地区。这些实体包括北京人工智能研究院、北京创新智慧科技有限公司、河南鼎鑫信息产业有限公司、浪潮(北京)电子信息产业有限公司及其相关子公司等。这些实体被认定为参与或支持中国的军事现代化项目,特别是开发用于国防目的的大规模人工智能模型和先进计算芯片,以及参与超级计算机的开发。对于这些实体,BIS对所有受EAR管辖的物品实施许可证要求,并采取推定拒绝的许可审查政策。此外,这些实体还被赋予脚注4标识,意味着“受EAR管辖的物品”包括根据EAR第734.9(e)(2)条受EAR管辖的外国生产物品。此决定自2025年3月25日起生效。",
"cnEntityCount": 12,
"techDomainList": ["先进制造", "集成电路", "人工智能"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 110,
"sanTypeId": 1,
"postDate": "2025-03-28",
"name": "美国商务部工业和安全局更新实体清单,新增70个涉及多国的实体",
"summary": "美国商务部工业和安全局(BIS)根据《出口管理条例》(EAR)对实体清单进行了修订,将70个实体列入实体清单,其中包括多个中国实体。这些实体被认定为违反了美国国家安全或外交政策利益。涉及中国的实体包括航空计算技术研究院、航天星科技应用有限公司、空军工程大学、安徽科华科技贸易有限公司、重庆关联光电有限公司等。这些实体因参与支持中国军事现代化、量子技术发展以及向其他实体清单上的中国企业供货等活动而被列入。对于这些实体,所有受EAR管辖的物品均需申请许可证,并且许可证申请将按否决推定审查。此外,规则还对实体清单中的四个现有条目进行了修改,涉及法国、伊朗、塞内加尔和英国的实体。该规则自2025年3月28日起生效。",
"cnEntityCount": 40,
"techDomainList": ["新一代通信网络", "先进制造", "航空航天", "新材料", "量子科技", "集成电路", "太空", "核"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 64,
"sanTypeId": 1,
"postDate": "2025-01-16",
"name": "美国将16个实体列入出口管制清单,其中14家为中国企业",
"summary": "美国工业与安全局(BIS)通过修订《出口管理条例》(EAR),将16个实体列入实体清单,其中包括14家位于中国的公司和2家位于新加坡的公司。这些实体被认定为参与了支持或直接促进先进计算集成电路(ICs)的发展,从而推动中国发展先进武器系统、大规模杀伤性武器以及高科技监控应用,并且存在向被列入实体的华为公司转移的风险。涉及的中国企业包括成都算峰科技有限公司、福建索菲昂科技有限公司、福建算信科技有限公司等。对于这些新增实体,BIS实施对所有受EAR管辖物品的许可证要求,并采取“推定拒绝”的许可证审查政策。此外,该规则自2025年1月16日起生效。",
"cnEntityCount": 14,
"techDomainList": ["人工智能", "集成电路"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 118,
"sanTypeId": 1,
"postDate": "2025-01-16",
"name": "美国将11家中国实体列入《实体清单》并修订印度相关条目",
"summary": "美国工业与安全局(BIS)通过最终规则,将11个中国实体列入《实体清单》,这些实体包括北京智谱华章科技有限公司、北京灵芯智能科技有限公司等。这些实体因参与先进人工智能研究的开发与集成,推动了中国的军事现代化,违反了美国国家安全和外交政策利益。所有受EAR管辖的商品均需许可,许可审查政策为“推定拒绝”。此外,BIS还修改了《实体清单》中关于印度的一个现有条目,移除了三个实体(印度稀土公司、英迪拉·甘地原子能研究中心和巴哈夫原子能研究中心),以支持美印两国在能源合作及关键矿产和清洁能源供应链方面的合作。",
"cnEntityCount": 11,
"techDomainList": ["人工智能", "先进制造", "集成电路"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 107,
"sanTypeId": 1,
"postDate": "2025-01-06",
"name": "实体清单修订",
"summary": "在本规则中,工业与安全局(BIS)通过将13个实体下的13个条目添加到实体清单中,对《出口管理条例》(EAR)进行了修订。这些条目在实体清单中分别位于缅甸(1个)、中华人民共和国(中国)(11个)和巴基斯坦(1个)目的地项下。美国政府已认定这些实体的行为违背美国国家安全和/或外交政策利益。本规则还通过进行某些编辑性更正和澄清来修订EAR。BIS进行这些更正和澄清是为了尽量减少混淆,不妨碍商业的自由流动。",
"cnEntityCount": 11,
"techDomainList": ["新材料", "集成电路", "先进制造", "航空航天", "新一代通信网络", "量子科技", "人工智能"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 14,
"sanTypeId": 1,
"postDate": "2024-12-11",
"name": "实体清单增补项",
"summary": "根据本规定,工业与安全局(BIS)通过将8个实体添加到实体清单中,对《出口管理条例》(EAR)进行了修订,这些实体分别位于缅甸(2个)、中华人民共和国(中国)(2个)和俄罗斯(4个)。美国政府已判定这些实体的行为违背了美国的国家安全或外交政策利益。",
"cnEntityCount": 2,
"techDomainList": ["人工智能", "先进制造", "航空航天", "新一代通信网络"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 4,
"sanTypeId": 1,
"postDate": "2024-12-05",
"name": "实体清单的增补与修订;经验证最终用户(VEU)计划的移除",
"summary": "在最终规则中,工业与安全局(BIS)通过将140个实体加入实体清单,对《出口管理条例》(EAR)进行了修订。这些实体被列入中国(中华人民共和国)、日本、韩国和新加坡目的地的实体清单中,并被美国政府认定为行为违背美国国家安全和外交政策利益。本最终规则还修改了实体清单上的14个现有条目,包括对中国项下14个条目的修订。此最终规则与BIS的临时最终规则《外国生产直接产品规则增补及对先进计算和半导体制造物项管控的完善》(0694-AJ74)同时发布,后者对先进计算物项和半导体制造物项的EAR管控进行了额外修改。本最终规则是确保对这些物项实施适当EAR管控的更广泛努力的一部分,包括涉及被加入实体清单的实体的交易,以及正在修改的实体清单现有条目。所有这些实体(新增及修改的)均涉及“先进节点集成电路”(“先进节点IC”)和/或半导体制造物项的开发与生产,和/或支持了中国政府的军民融合发展战略。此外,本最终规则将新增的9个实体及修改的7个条目指定为适用涉及外国生产物项的实体特定限制的实体。本最终规则还通过从已验证最终用户(VEU)计划中移除三个实体,对EAR进行了修订。",
"cnEntityCount": 125,
"techDomainList": ["先进制造", "集成电路", "新材料"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
},
{
"id": 49,
"sanTypeId": 1,
"postDate": "2024-11-01",
"name": "实体清单中实体的增补与修订",
"summary": "在本规则中,工业与安全局(Bureau of Industry and Security,简称BIS)通过向实体清单添加42个条目下的40个实体和4个条目下的4个地址,修订了《出口管理条例》(Export Administration Regulations,简称EAR)。这些条目被列入实体清单,目的地包括中华人民共和国(中国)(11个)、印度(5个)、马来西亚(2个)、俄罗斯(13个)、新加坡(1个)和土耳其(14个)。两个实体被添加到两个目的地下的实体清单中,这解释了总数上的差异。本最终规则还修改了实体清单上52个现有条目,这些条目位于中国、爱沙尼亚、芬兰、印度、土耳其、阿拉伯联合酋长国(UAE)和英国。美国政府已确定这些实体的行为违背了美国的国家安全或外交政策利益。本最终规则与BIS的另一项最终规则《根据〈出口管理条例〉实施针对俄罗斯和白俄罗斯的额外出口管制;及澄清》(RIN 0694-AJ93)同时发布,该规则包含了与俄罗斯和白俄罗斯相关的出口管制的额外变更。",
"cnEntityCount": 5,
"techDomainList": ["先进制造", "航空航天", "海洋", "生物科技", "集成电路", "新材料"],
"orgId": "241",
"orgName": "商务部工业与安全局",
"orgLogoUrl": "http://8.140.26.4:10010/kjb-files/images/org/round/BIS美国商务部工业与安全局.png"
}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"pageNumber": 0,
"pageSize": 10,
"offset": 0,
"unpaged": false,
"paged": true
},
"last": false,
"totalPages": 15,
"totalElements": 141,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 10,
"first": true,
"size": 10,
"number": 0,
"empty": false
}
...@@ -14,13 +14,17 @@ ...@@ -14,13 +14,17 @@
<div class="tag">{{ value.industryName }}</div> <div class="tag">{{ value.industryName }}</div>
</div> --> </div> -->
<AreaTag v-for="value, index in thinkInfo.tags" :key="index" :tagName="value.industryName"></AreaTag> <AreaTag
v-for="(value, index) in thinkInfo.tags"
:key="index"
:tagName="value.industryName"
></AreaTag>
</div> </div>
</div> </div>
</div> </div>
<div class="header-top-right"> <div class="header-top-right">
<div class="image-name-box"> <div class="image-name-box">
<div class="image"> <img :src=thinkInfo.thinkTankLogoUrl alt="" /></div> <div class="image"><img :src="thinkInfo.thinkTankLogoUrl" alt="" /></div>
<div class="name">{{ thinkInfo.thinkTankName }}</div> <div class="name">{{ thinkInfo.thinkTankName }}</div>
</div> </div>
<div class="time">{{ thinkInfo.times }}</div> <div class="time">{{ thinkInfo.times }}</div>
...@@ -95,7 +99,7 @@ const router = useRouter(); ...@@ -95,7 +99,7 @@ const router = useRouter();
const route = useRoute(); const route = useRoute();
const reportUrl = ref(""); const reportUrl = ref("");
const thinkInfo = ref({}); const thinkInfo = ref({});
const reportList = ref({}) const reportList = ref({});
// 获取报告全局信息 // 获取报告全局信息
const handleGetThinkTankReportSummary = async () => { const handleGetThinkTankReportSummary = async () => {
...@@ -105,7 +109,7 @@ const handleGetThinkTankReportSummary = async () => { ...@@ -105,7 +109,7 @@ const handleGetThinkTankReportSummary = async () => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
reportUrl.value = res.data.reportUrl; reportUrl.value = res.data.reportUrl;
thinkInfo.value = res.data; thinkInfo.value = res.data;
console.log(reportUrl.value, 'reportUrl.value') console.log(reportUrl.value, "reportUrl.value");
} }
} catch (error) { } catch (error) {
console.error("获取报告全局信息error", error); console.error("获取报告全局信息error", error);
...@@ -117,9 +121,7 @@ const handleGetThinkTankReport = async () => { ...@@ -117,9 +121,7 @@ const handleGetThinkTankReport = async () => {
const res = await getThinkTankReportRelated(router.currentRoute._value.params.id); const res = await getThinkTankReportRelated(router.currentRoute._value.params.id);
console.log("报告全局信息", res); console.log("报告全局信息", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
reportList.value = res.data; reportList.value = res.data;
} }
} catch (error) { } catch (error) {
console.error("获取相关报告error", error); console.error("获取相关报告error", error);
...@@ -133,6 +135,8 @@ const toReport = () => { ...@@ -133,6 +135,8 @@ const toReport = () => {
id: router.currentRoute._value.params.id id: router.currentRoute._value.params.id
} }
}); });
console.log("route.href", route.href);
window.open(route.href, "_blank"); window.open(route.href, "_blank");
}; };
const tabActiveName = ref("报告分析"); const tabActiveName = ref("报告分析");
...@@ -167,7 +171,7 @@ const goToOfficialWebsite = () => { ...@@ -167,7 +171,7 @@ const goToOfficialWebsite = () => {
const downloadOnePdf = async (url, filename) => { const downloadOnePdf = async (url, filename) => {
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
headers: { "Content-Type": "application/pdf" }, headers: { "Content-Type": "application/pdf" }
}); });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const blob = await response.blob(); const blob = await response.blob();
...@@ -209,10 +213,9 @@ const handleDownloadDocument = async () => { ...@@ -209,10 +213,9 @@ const handleDownloadDocument = async () => {
try { try {
const { ElMessage } = await import("element-plus"); const { ElMessage } = await import("element-plus");
ElMessage.error("PDF 下载失败,请稍后重试"); ElMessage.error("PDF 下载失败,请稍后重试");
} catch (_) { } } catch (_) {}
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -305,7 +308,6 @@ const handleDownloadDocument = async () => { ...@@ -305,7 +308,6 @@ const handleDownloadDocument = async () => {
line-height: 24px; line-height: 24px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: right; text-align: right;
} }
.image { .image {
...@@ -314,12 +316,9 @@ const handleDownloadDocument = async () => { ...@@ -314,12 +316,9 @@ const handleDownloadDocument = async () => {
margin-top: 5px; margin-top: 5px;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
} }
...@@ -447,4 +446,4 @@ const handleDownloadDocument = async () => { ...@@ -447,4 +446,4 @@ const handleDownloadDocument = async () => {
width: 100%; width: 100%;
} }
} }
</style> </style>
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论