提交 4cd32036 authored 作者: coderBryanFu's avatar coderBryanFu

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

...@@ -24,7 +24,7 @@ build_pre: ...@@ -24,7 +24,7 @@ build_pre:
- du -sh "$CI_PROJECT_DIR/.npm" "$CI_PROJECT_DIR/.npm/_cacache" 2>/dev/null || true - du -sh "$CI_PROJECT_DIR/.npm" "$CI_PROJECT_DIR/.npm/_cacache" 2>/dev/null || true
- echo "=== npm ci start ===" - echo "=== npm ci start ==="
- date -Iseconds - date -Iseconds
- npm ci --cache "$CI_PROJECT_DIR/.npm" --no-audit --no-fund --loglevel=verbose --timing --foreground-scripts - npm ci --cache "$CI_PROJECT_DIR/.npm" --no-audit --no-fund --include=optional --loglevel=verbose --timing --foreground-scripts
- echo "=== npm ci end ===" - echo "=== npm ci end ==="
- date -Iseconds - date -Iseconds
- npm run build - npm run build
...@@ -91,7 +91,7 @@ build_check: ...@@ -91,7 +91,7 @@ build_check:
- du -sh "$CI_PROJECT_DIR/.npm" "$CI_PROJECT_DIR/.npm/_cacache" 2>/dev/null || true - du -sh "$CI_PROJECT_DIR/.npm" "$CI_PROJECT_DIR/.npm/_cacache" 2>/dev/null || true
- echo "=== npm ci start ===" - echo "=== npm ci start ==="
- date -Iseconds - date -Iseconds
- npm ci --cache "$CI_PROJECT_DIR/.npm" --no-audit --no-fund --loglevel=verbose --timing --foreground-scripts - npm ci --cache "$CI_PROJECT_DIR/.npm" --no-audit --no-fund --include=optional --loglevel=verbose --timing --foreground-scripts
- echo "=== npm ci end ===" - echo "=== npm ci end ==="
- date -Iseconds - date -Iseconds
- npm run build - npm run build
\ No newline at end of file
差异被折叠。
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"json5": "^2.2.3", "json5": "^2.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pdfjs-dist": "^3.11.174", "pdfjs-dist": "^5.5.207",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
......
<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>
...@@ -196,10 +196,10 @@ const setActivePanelId = id => { ...@@ -196,10 +196,10 @@ const setActivePanelId = id => {
background-image: url("../../assets/images/activeDown.png"); background-image: url("../../assets/images/activeDown.png");
} }
.iconUp { .iconUp {
background-image: url("../../assets/images/UnActiveUp.png"); background-image: url("../../assets/images/unActiveUp.png");
} }
.iconDown { .iconDown {
background-image: url("../../assets/images/UnActiveDown.png"); background-image: url("../../assets/images/unActiveDown.png");
} }
} }
.activeItem { .activeItem {
......
...@@ -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
};
};
...@@ -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
......
...@@ -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,
() => { () => {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论