提交 846d853c authored 作者: yanpeng's avatar yanpeng

图表AI解读

上级 3f645e0f
<template>
<div class="analysis-box-wrapper" :style="{ width: width ? width : '100%', height: height ? height : '100%' }">
<div class="wrapper-header">
<div class="header-icon"></div>
<div class="header-title">
<div v-if="title">{{ title }}</div>
<slot v-else name="custom-title"></slot>
</div>
<div class="header-btn" v-if="!showAllBtn">
<slot name="header-btn"></slot>
</div>
<div class="header-btn1" v-else>
<slot name="header-btn"></slot>
</div>
<div class="header-right">
<div class="header-right-btn" @click="handleSave" v-if="showAllBtn">
<img src="@/assets/icons/box-header-icon1.png" alt="">
</div>
<div class="header-right-btn" @click="handleDownload">
<img src="@/assets/icons/box-header-icon2.png" alt="">
</div>
<div class="header-right-btn" @click="handleCollect">
<img src="@/assets/icons/box-header-icon3.png" alt="">
</div>
</div>
</div>
<div class="wrapper-main">
<slot></slot>
</div>
</div>
<div class="analysis-box-wrapper" :style="{ width: width ? width : '100%', height: height ? height : '100%' }">
<div class="wrapper-header">
<div class="header-icon"></div>
<div class="header-title">
<div v-if="title">{{ title }}</div>
<slot v-else name="custom-title"></slot>
</div>
<div class="header-btn" v-if="!showAllBtn">
<slot name="header-btn"></slot>
</div>
<div class="header-btn1" v-else>
<slot name="header-btn"></slot>
</div>
<div class="header-right">
<div class="header-right-btn" @click="handleSave" v-if="showAllBtn">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="header-right-btn" @click="handleDownload">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="header-right-btn" @click="handleCollect">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="wrapper-main">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { ref, computed } from 'vue'
import { ElMessage } from "element-plus";
import { ref, computed } from "vue";
const props = defineProps({
title: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
height: {
type: String,
default: ''
},
showAllBtn: {
type: Boolean,
default: true
},
// 当业务功能尚未实现时,点击右上角图标仅弹出统一提示
devTip: {
type: Boolean,
default: false
}
})
title: {
type: String,
default: ""
},
width: {
type: String,
default: ""
},
height: {
type: String,
default: ""
},
showAllBtn: {
type: Boolean,
default: true
},
// 当业务功能尚未实现时,点击右上角图标仅弹出统一提示
devTip: {
type: Boolean,
default: false
}
});
const handleSave = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
}
ElMessage.success('保存当前内容')
// emit('save')
}
if (props.devTip) {
ElMessage.warning("当前功能正在开发中,敬请期待!");
return;
}
ElMessage.success("保存当前内容");
// emit('save')
};
const handleDownload = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
}
ElMessage.success('下载当前内容')
// emit('download')
}
if (props.devTip) {
ElMessage.warning("当前功能正在开发中,敬请期待!");
return;
}
ElMessage.success("下载当前内容");
// emit('download')
};
const handleCollect = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
}
ElMessage.success('收藏当前内容')
// emit('collect')
}
const emit = defineEmits(['save', 'download', 'collect'])
if (props.devTip) {
ElMessage.warning("当前功能正在开发中,敬请期待!");
return;
}
ElMessage.success("收藏当前内容");
// emit('collect')
};
const emit = defineEmits(["save", "download", "collect"]);
</script>
<style lang="scss" scoped>
.analysis-box-wrapper {
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
.wrapper-header {
height: 45px;
display: flex;
padding-right: 14px;
align-items: center;
box-sizing: border-box;
.header-icon {
width: 8px;
height: 20px;
background: var(--color-main-active);
border-radius: 0 4px 4px 0;
margin-right: 14px;
}
.header-title {
flex: auto;
width: 20px;
// color: var(--color-main-active);
// font-family: Source Han Sans CN;
// font-size: 20px;
// font-weight: 700;
// line-height: 26px;
// letter-spacing: 0px;
height: 100%;
&>div {
height: 100%;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 20px;
line-height: 45px;
font-weight: 700;
}
}
.header-btn {
// display: flex;
// justify-content: flex-end;
// gap: 8px;
margin-right: 10px;
}
// .header-btn1 {
// position: absolute;
// top: 14px;
// right: 116px;
// }
.header-right {
height: 28px;
display: flex;
justify-content: flex-end;
gap: 4px;
.header-right-btn {
width: 28px;
height: 28px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
}
.wrapper-main {
height: calc(100% - 45px);
overflow: hidden;
// overflow-y: auto;
padding: 5px auto;
}
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: relative;
.wrapper-header {
height: 45px;
display: flex;
padding-right: 14px;
align-items: center;
box-sizing: border-box;
.header-icon {
width: 8px;
height: 20px;
background: var(--color-main-active);
border-radius: 0 4px 4px 0;
margin-right: 14px;
}
.header-title {
flex: auto;
width: 20px;
// color: var(--color-main-active);
// font-family: Source Han Sans CN;
// font-size: 20px;
// font-weight: 700;
// line-height: 26px;
// letter-spacing: 0px;
height: 100%;
& > div {
height: 100%;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 20px;
line-height: 45px;
font-weight: 700;
}
}
.header-btn {
// display: flex;
// justify-content: flex-end;
// gap: 8px;
margin-right: 10px;
}
// .header-btn1 {
// position: absolute;
// top: 14px;
// right: 116px;
// }
.header-right {
height: 28px;
display: flex;
justify-content: flex-end;
gap: 4px;
.header-right-btn {
width: 28px;
height: 28px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
}
.wrapper-main {
height: calc(100% - 45px);
overflow: hidden;
// overflow-y: auto;
padding: 5px auto;
}
}
</style>
......@@ -416,7 +416,7 @@
</el-col>
</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" />
<div class="resource-tabs">
<div
......@@ -3603,7 +3603,7 @@ const handleMediaClick = item => {
}
.scroll-main {
height: calc(100% - 144px) !important;
// height: calc(100% - 144px) !important;
}
.center-center {
......
......@@ -12,7 +12,7 @@
{{ headerTitle.department }}
</div>
</div>
<div class="btn"><img :src="icon01" alt="" />切换</div>
<!-- <div class="btn"><img :src="icon01" alt="" />切换</div> -->
</div>
<div class="header-nav">
<div
......
......@@ -23,7 +23,7 @@
</div>
</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 class="popup-header">
<img :src="selectedNode.image || defaultIcon" alt="" class="popup-icon" />
......@@ -55,6 +55,8 @@ import echartsIcon02 from "../assets/echartsIcon02.png";
import echartsIcon03 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";
const props = defineProps({
......@@ -107,7 +109,7 @@ const initGraph = (layoutType = 1) => {
const initNormalGraph = (layoutType, width, height) => {
const data = processGraphData(props.graphData);
console.log("初始数据", props.graphData);
if (!data.nodes || data.nodes.length === 0) return;
const layout = {
......@@ -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 => {
currentLayoutType.value = btn;
emit("layoutChange", btn);
......@@ -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(
() => props.graphData,
() => {
......
......@@ -12,7 +12,7 @@
{{ headerTitle.department }}
</div>
</div>
<div class="btn"><img :src="icon01" alt="" />切换</div>
<div class="btn" @click="openSanctionModal"><img :src="icon01" alt="" />切换</div>
</div>
<div class="header-nav">
<div
......@@ -44,12 +44,46 @@
<deep-mining v-if="activeIndex === 2"></deep-mining>
<impact-analysis v-if="activeIndex === 3"></impact-analysis>
</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>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElDialog, ElPagination } from "element-plus";
import sanctionsOverview from "./components/sanctionsOverview/index.vue";
import dataStatistics from "./components/dataStatistics/index.vue";
import deepMining from "./components/deepMining/index.vue";
......@@ -67,6 +101,7 @@ import icon3 from "../assets/icons/icon3.png";
import icon3Active from "../assets/icons/icon3_active.png";
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js";
import { getSanctionProcess } from "@/api/exportControl";
import router from "@/router";
// 处理点击实体清单原文按钮
......@@ -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(() => {
getUrlParams();
getSingleSanctionOverviewData();
......@@ -350,4 +456,40 @@ onMounted(() => {
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>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论