提交 c9da112a authored 作者: 张伊明's avatar 张伊明

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

# Conflicts: # package-lock.json # package.json # src/views/thinkTank/reportOriginal/pdf.vue
流水线 #117 已通过 于阶段
in 1 分 33 秒
......@@ -77,6 +77,58 @@ function extractInterpretationFromLooseText(text) {
return String(m[1]).replace(/\\n/g, "\n").trim();
}
/**
* 从流式累积 buffer 中提取「解读」字符串的已生成部分(滤掉 ```json、[、{ 等外壳,避免界面出现 json\n[\n)
* 支持未闭合的字符串(流式进行中)
* @param {string} buffer
* @returns {string}
*/
function extractStreamingInterpretationFromBuffer(buffer) {
const s = String(buffer || "");
let rest = s.replace(/^\uFEFF/, "");
const fence = rest.match(/^```(?:json)?\s*/i);
if (fence) {
rest = rest.slice(fence[0].length);
}
const keyRe =
/["'](?:解读|interpretation|analysis|content)["']\s*:\s*"/i;
const m = rest.match(keyRe);
if (!m) {
return "";
}
let pos = m.index + m[0].length;
let out = "";
while (pos < rest.length) {
const ch = rest[pos];
if (ch === '"') {
break;
}
if (ch === "\\") {
pos += 1;
if (pos >= rest.length) {
break;
}
const esc = rest[pos];
if (esc === "n") {
out += "\n";
} else if (esc === "r") {
out += "\r";
} else if (esc === "t") {
out += "\t";
} else if (esc === '"' || esc === "\\") {
out += esc;
} else {
out += esc;
}
pos += 1;
continue;
}
out += ch;
pos += 1;
}
return out;
}
/**
* 图表解读(SSE 流式)
* @param {object} data - 请求体
......@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) {
return new Promise((resolve, reject) => {
let buffer = "";
let latestInterpretation = "";
/** 已推给前端的「解读」正文长度,用于只增量回调 onChunk */
let lastStreamedInterpretationLen = 0;
let settled = false;
const abortController = new AbortController();
......@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) {
return;
}
// 每收到一条消息即回调,用于流式渲染
// 流式渲染:不把 ```json、[、{ 等 markdown/JSON 外壳拼到界面
if (chunk && onDelta) {
onDelta(chunk);
let parsedMsg = null;
try {
parsedMsg = JSON.parse(raw);
} catch (_) {
parsedMsg = null;
}
const isReasoningChunk =
parsedMsg &&
typeof parsedMsg === "object" &&
parsedMsg.type === "reasoning" &&
typeof parsedMsg.chunk === "string";
if (isReasoningChunk) {
onDelta(parsedMsg.chunk);
} else {
const visible = extractStreamingInterpretationFromBuffer(buffer);
if (visible.length > lastStreamedInterpretationLen) {
onDelta(
visible.slice(lastStreamedInterpretationLen)
);
lastStreamedInterpretationLen = visible.length;
}
}
}
// 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
......
......@@ -3,9 +3,9 @@ import request from "@/api/request.js";
// 最新科技政令
export function getDepartmentList(params) {
return request({
method: 'GET',
method: 'POST',
url: `/api/administrativeDict/department`,
params
data: params
})
}
......@@ -99,15 +99,6 @@ export function getKeyOrganization(params) {
})
}
// 所有机构
export function getAllOrganization(params) {
return request({
method: 'POST',
url: `/api/administrativeOrderOverview/orderCount`,
data: params
})
}
// AI智能总结
export function getChartInterpretation(params) {
return request({
......
......@@ -33,7 +33,7 @@ export function getDecreeEntities(params) {
export function getDecreeRelatedChain(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/relatedChain/${params.id}`,
url: `/api/chain/relatedChain/${params.id}`,
})
}
......@@ -41,7 +41,7 @@ export function getDecreeRelatedChain(params) {
export function getDecreeChainNodes(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/relatedChainNodes/${params.id}`,
url: `/api/chain/relatedChainNodes/${params.id}`,
})
}
......@@ -49,7 +49,8 @@ export function getDecreeChainNodes(params) {
export function getDecreeRelatedEntitie(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/listRelatedEntitie/${params.id}`,
url: `/api/organization/shareholding`,
params
})
}
......
......@@ -46,7 +46,10 @@ export function getThinkTankPolicyIndustryChange(params) {
params: {
startDate: params.startDate,
endDate: params.endDate
}
},
// 无数据年份(如 2026)后端可能返回 HTTP 400/500,避免走全局错误提示
validateStatus: (status) =>
(status >= 200 && status < 300) || status === 400 || status === 500
});
}
......@@ -286,8 +289,7 @@ export function getThinkPolicyIndustryChange(params) {
/**
* 获取智库政策(政策追踪列表)
* GET /api/thinkTankInfo/policy
* Query: thinkTankId, startDate, endDate, orgIds, domainIds(科技领域/智库领域,逗号分隔 id), pageNum, pageSize, sortField, sortOrder, sortFun, reportId 等
*/
export function getThinkPolicy(params) {
return request({
......
......@@ -35,7 +35,7 @@ const tipText = computed(() => props.text || `数据来源:${props.dataSource}
width: 100%;
display: flex;
gap: 8px;
justify-content: center;
justify-content: flex-start;
align-items: center;
height: 22px;
......
<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>
......@@ -30,12 +30,12 @@
<div class="organization-list" ref="refOrganization" v-loading="organizationInfo.loading">
<div class="organization-item" v-for="(item, index) in organizationInfo.list" :key="index" @click="handleToInstitution(item)">
<div class="item-left">
<img :src="item.imgUrl || DefaultIcon2" alt="" />
<img :src="item.orgImage || DefaultIcon2" alt="" />
</div>
<div class="item-right one-line-ellipsis">{{ item.orgName }}</div>
<div class="item-total">{{ item.totalOrderNum }}项</div>
<div class="item-total">{{ item.total }}项</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
<div class="item-dot" v-if="item.recentOrderNum">+{{item.recentOrderNum}}</div>
<div class="item-dot" v-if="item.totalRecent">+{{item.totalRecent}}</div>
</div>
</div>
<div class="pagination-box">
......@@ -56,7 +56,7 @@ import { Search } from '@element-plus/icons-vue'
import router from "@/router";
import TimeTabPane from '@/components/base/TimeTabPane/index.vue';
import { getAllOrganization } from "@/api/decree/home";
import { getDepartmentList } from "@/api/decree/home";
import tipsTcon from "./assets/icons/tips-icon.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png";
......@@ -77,7 +77,7 @@ const onAllOrganization = async (num) => {
organizationInfo.loading = true
try {
let {keyWord, pageNum, pageSize, day} = organizationInfo
const res = await getAllOrganization({day, pageNum:pageNum-1, pageSize, keyWord: keyWord||undefined});
const res = await getDepartmentList({day, pageNum:pageNum-1, pageSize, keyWord: keyWord||undefined});
console.log("机构列表", res);
if (res.code === 200) {
organizationInfo.list = res.data.orgList || [];
......
......@@ -47,7 +47,7 @@
<div class="item-dot" v-if="item.recentOrderNum">+{{item.recentOrderNum}}</div>
</div>
<div class="organization-item" @click="onNavigateTo()">
<div class="item-more">查看全部机构 ({{govInsList.length+1}}家)</div>
<div class="item-more">查看全部机构 ({{govInsList.length}}家)</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
</div>
</div>
......@@ -233,7 +233,7 @@
<div class="data-origin-icon">
<img :src="tipsTcon" alt="">
</div>
<div class="data-origin-text">科技政领域分布情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">科技政领域分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -287,7 +287,7 @@
<div class="data-origin-icon">
<img :src="tipsTcon" alt="">
</div>
<div class="data-origin-text">关键科技政列表,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">关键科技政列表,数据来源:美国各行政机构官网</div>
</div>
</div>
<div class="box8">
......@@ -321,7 +321,7 @@
<div class="data-origin-icon">
<img :src="tipsTcon" alt="">
</div>
<div class="data-origin-text">科技政重点条款词云,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">科技政重点条款词云,数据来源:美国各行政机构官网</div>
</div>
</div>
</div>
......@@ -502,10 +502,10 @@ const govInsList = ref([]);
const checkedGovIns = ref([]);
const handleGetDepartmentList = async () => {
try {
const res = await getDepartmentList({day:7});
const res = await getDepartmentList({});
console.log("机构列表", res);
if (res.code === 200 && res.data) {
govInsList.value = res.data;
if (res.code === 200) {
govInsList.value = res.data.orgList;
}
} catch (error) {
console.error("获取机构列表error", error);
......
......@@ -100,7 +100,7 @@ import {
getDecreeRelatedEntitie
} from "@/api/decree/influence";
import ChartChain from "./com/ChartChain.vue";
import AiTips from "./com/AiTips.vue";
// import AiTips from "./com/AiTips.vue";
import GraphChart from "@/components/base/GraphChart/index.vue";
import defaultIcon2 from "@/assets/icons/default-icon2.png";
import noticeIcon from "./assets/images/notice-icon.png";
......@@ -186,7 +186,7 @@ const headerChartData = (row) => {
onDecreeRelatedChain(row.id)
break;
case 2:
onDecreeRelatedEntitie(row.id)
onDecreeRelatedEntitie(row.orgId)
break;
}
}
......@@ -247,14 +247,17 @@ const graphInfo = reactive({
nodes: [],
links: [],
});
const onDecreeRelatedEntitie = async (id) => {
const onDecreeRelatedEntitie = async (orgId) => {
try {
const res = await getDecreeRelatedEntitie({ id });
const res = await getDecreeRelatedEntitie({ orgId, rule:false, withSanInfo:false });
console.log("实体关系:", res);
if (res.code === 200) {
graphInfo.links = res.data.map(onFormatLink)
graphInfo.nodes = res.data.map(onFormatNode)
graphInfo.nodes.unshift(onFormatNode(entityInfo.node))
let arr1 = res.data.parentOrgList.map(item => ({ ...item, level: 1 }))
let arr3 = res.data.childrenOrgList.map(item => ({ ...item, level: 3 }))
graphInfo.links = [...arr1,...arr3].map(onFormatLink)
graphInfo.nodes = [...arr1,...arr3].map(onFormatNode)
graphInfo.nodes.unshift(onFormatNode({name:res.data.orgName, id:res.data.orgId}, -1))
}
} catch (error) {
console.log("获取实体关系失败", error);
......@@ -262,17 +265,18 @@ const onDecreeRelatedEntitie = async (id) => {
}
const onFormatLink = (item, index) => {
return {
id: `link-${index+1}`,
source: item.id+'', target: entityInfo.id+'',
label: { show: true, color: "#055fc2", backgroundColor: "#eef7ff", borderWidth: 0, offset: [0, 15], formatter: item.relation },
id: `link-${index}-${item.id}`,
target: item.level==3 ? `${index}-${item.id}` : `-1-${entityInfo.node.orgId}`,
source: item.level==3 ? `-1-${entityInfo.node.orgId}` : `${index}-${item.id}`,
label: { show: true, color: "#055fc2", backgroundColor: "#eef7ff", borderWidth: 0, offset: [0, 15], formatter: item.description },
lineStyle: { color: '#B9DCFF', type: "solid", opacity: 1 }
}
}
const onFormatNode = (item) => {
let leader = item.id == entityInfo.id;
const onFormatNode = (item, index) => {
let leader = item.id == entityInfo.node.orgId;
return {
id: item.id+'',
name: onWordWrap(item.companyName, 7),
id: `${index}-${item.id}`,
name: onWordWrap(item.name, 7),
label: {
show: true,
color: "#3b414b",
......
......@@ -196,10 +196,10 @@ const setActivePanelId = id => {
background-image: url("../../assets/images/activeDown.png");
}
.iconUp {
background-image: url("../../assets/images/UnActiveUp.png");
background-image: url("../../assets/images/unActiveUp.png");
}
.iconDown {
background-image: url("../../assets/images/UnActiveDown.png");
background-image: url("../../assets/images/unActiveDown.png");
}
}
.activeItem {
......
......@@ -368,19 +368,29 @@
</el-col>
</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">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="480px">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="540px">
<template #header-right>
<el-checkbox v-model="domainChecked" label="50%规则" size="large" />
</template>
<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>
</custom-container>
</el-col>
<el-col :span="16">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="480px">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="540px">
<template #header-right>
<div style="display: flex; align-items: center; gap: 16px">
<el-checkbox v-model="trendChecked" label="50%规则" size="large" />
......@@ -390,13 +400,23 @@
</div>
</template>
<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>
</custom-container>
</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
......@@ -714,11 +734,18 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { DArrowRight, Warning, Search } from "@element-plus/icons-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 { useGotoCompanyPages } from "@/router/modules/company";
import { useGotoNewsDetail } from "@/router/modules/news";
const gotoCompanyPages = useGotoCompanyPages();
const gotoNewsDetail = useGotoNewsDetail();
const trendChart = useChartInterpretation();
const radarChart = useChartInterpretation();
import { useRouter } from "vue-router";
......@@ -1026,6 +1053,7 @@ const fetchTrendData = async () => {
});
if (res && res[0] && res[0].yearDomainCount) {
trendOption.value = processYearDomainCountData(res[0].yearDomainCount);
trendChart.interpret({ type: "柱状图", name: "实体清单数量增长趋势", data: res[0].yearDomainCount });
}
} catch (error) {
console.error("获取趋势图数据失败:", error);
......@@ -1297,6 +1325,7 @@ const fetchRadarData = async checked => {
}
};
});
radarChart.interpret({ type: "雷达图", name: "实体领域分布", data: data });
}
} catch (error) {
console.error("获取雷达图数据失败:", error);
......@@ -3573,7 +3602,7 @@ const handleMediaClick = item => {
}
.scroll-main {
height: calc(100% - 144px) !important;
// height: calc(100% - 144px) !important;
}
.center-center {
......@@ -3596,4 +3625,50 @@ const handleMediaClick = item => {
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>
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 @@
{{ 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,
() => {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论