提交 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) { ...@@ -77,6 +77,58 @@ function extractInterpretationFromLooseText(text) {
return String(m[1]).replace(/\\n/g, "\n").trim(); 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 流式) * 图表解读(SSE 流式)
* @param {object} data - 请求体 * @param {object} data - 请求体
...@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) { ...@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let buffer = ""; let buffer = "";
let latestInterpretation = ""; let latestInterpretation = "";
/** 已推给前端的「解读」正文长度,用于只增量回调 onChunk */
let lastStreamedInterpretationLen = 0;
let settled = false; let settled = false;
const abortController = new AbortController(); const abortController = new AbortController();
...@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) { ...@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) {
return; return;
} }
// 每收到一条消息即回调,用于流式渲染 // 流式渲染:不把 ```json、[、{ 等 markdown/JSON 外壳拼到界面
if (chunk && onDelta) { 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,则提前解析并中断连接 // 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
......
...@@ -3,9 +3,9 @@ import request from "@/api/request.js"; ...@@ -3,9 +3,9 @@ import request from "@/api/request.js";
// 最新科技政令 // 最新科技政令
export function getDepartmentList(params) { export function getDepartmentList(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeDict/department`, url: `/api/administrativeDict/department`,
params data: params
}) })
} }
...@@ -99,15 +99,6 @@ export function getKeyOrganization(params) { ...@@ -99,15 +99,6 @@ export function getKeyOrganization(params) {
}) })
} }
// 所有机构
export function getAllOrganization(params) {
return request({
method: 'POST',
url: `/api/administrativeOrderOverview/orderCount`,
data: params
})
}
// AI智能总结 // AI智能总结
export function getChartInterpretation(params) { export function getChartInterpretation(params) {
return request({ return request({
......
...@@ -33,7 +33,7 @@ export function getDecreeEntities(params) { ...@@ -33,7 +33,7 @@ export function getDecreeEntities(params) {
export function getDecreeRelatedChain(params) { export function getDecreeRelatedChain(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeOrderInfo/relatedChain/${params.id}`, url: `/api/chain/relatedChain/${params.id}`,
}) })
} }
...@@ -41,7 +41,7 @@ export function getDecreeRelatedChain(params) { ...@@ -41,7 +41,7 @@ export function getDecreeRelatedChain(params) {
export function getDecreeChainNodes(params) { export function getDecreeChainNodes(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeOrderInfo/relatedChainNodes/${params.id}`, url: `/api/chain/relatedChainNodes/${params.id}`,
}) })
} }
...@@ -49,7 +49,8 @@ export function getDecreeChainNodes(params) { ...@@ -49,7 +49,8 @@ export function getDecreeChainNodes(params) {
export function getDecreeRelatedEntitie(params) { export function getDecreeRelatedEntitie(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeOrderInfo/listRelatedEntitie/${params.id}`, url: `/api/organization/shareholding`,
params
}) })
} }
......
...@@ -46,7 +46,10 @@ export function getThinkTankPolicyIndustryChange(params) { ...@@ -46,7 +46,10 @@ export function getThinkTankPolicyIndustryChange(params) {
params: { params: {
startDate: params.startDate, startDate: params.startDate,
endDate: params.endDate 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) { ...@@ -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) { export function getThinkPolicy(params) {
return request({ return request({
......
...@@ -35,7 +35,7 @@ const tipText = computed(() => props.text || `数据来源:${props.dataSource} ...@@ -35,7 +35,7 @@ const tipText = computed(() => props.text || `数据来源:${props.dataSource}
width: 100%; width: 100%;
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: center; justify-content: flex-start;
align-items: center; align-items: center;
height: 22px; height: 22px;
......
...@@ -14,13 +14,13 @@ ...@@ -14,13 +14,13 @@
</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>
...@@ -31,21 +31,21 @@ ...@@ -31,21 +31,21 @@
</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,
...@@ -56,39 +56,36 @@ const props = defineProps({ ...@@ -56,39 +56,36 @@ const props = defineProps({
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>
...@@ -97,7 +94,7 @@ const emit = defineEmits(['save', 'download', 'collect']) ...@@ -97,7 +94,7 @@ const emit = defineEmits(['save', 'download', 'collect'])
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;
...@@ -123,7 +120,7 @@ const emit = defineEmits(['save', 'download', 'collect']) ...@@ -123,7 +120,7 @@ const emit = defineEmits(['save', 'download', 'collect'])
// 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;
......
...@@ -30,12 +30,12 @@ ...@@ -30,12 +30,12 @@
<div class="organization-list" ref="refOrganization" v-loading="organizationInfo.loading"> <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="organization-item" v-for="(item, index) in organizationInfo.list" :key="index" @click="handleToInstitution(item)">
<div class="item-left"> <div class="item-left">
<img :src="item.imgUrl || DefaultIcon2" alt="" /> <img :src="item.orgImage || DefaultIcon2" alt="" />
</div> </div>
<div class="item-right one-line-ellipsis">{{ item.orgName }}</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> <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> </div>
<div class="pagination-box"> <div class="pagination-box">
...@@ -56,7 +56,7 @@ import { Search } from '@element-plus/icons-vue' ...@@ -56,7 +56,7 @@ import { Search } from '@element-plus/icons-vue'
import router from "@/router"; import router from "@/router";
import TimeTabPane from '@/components/base/TimeTabPane/index.vue'; 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 tipsTcon from "./assets/icons/tips-icon.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png"; import DefaultIcon2 from "@/assets/icons/default-icon2.png";
...@@ -77,7 +77,7 @@ const onAllOrganization = async (num) => { ...@@ -77,7 +77,7 @@ const onAllOrganization = async (num) => {
organizationInfo.loading = true organizationInfo.loading = true
try { try {
let {keyWord, pageNum, pageSize, day} = organizationInfo 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); console.log("机构列表", res);
if (res.code === 200) { if (res.code === 200) {
organizationInfo.list = res.data.orgList || []; organizationInfo.list = res.data.orgList || [];
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
<div class="item-dot" v-if="item.recentOrderNum">+{{item.recentOrderNum}}</div> <div class="item-dot" v-if="item.recentOrderNum">+{{item.recentOrderNum}}</div>
</div> </div>
<div class="organization-item" @click="onNavigateTo()"> <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> <el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
</div> </div>
</div> </div>
...@@ -233,7 +233,7 @@ ...@@ -233,7 +233,7 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsTcon" alt=""> <img :src="tipsTcon" alt="">
</div> </div>
<div class="data-origin-text">科技政领域分布情况,数据来源:美国各行政机构官网</div> <div class="data-origin-text">科技政领域分布情况,数据来源:美国各行政机构官网</div>
</div> </div>
<div class="ai-pane"> <div class="ai-pane">
<AiButton /> <AiButton />
...@@ -287,7 +287,7 @@ ...@@ -287,7 +287,7 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsTcon" alt=""> <img :src="tipsTcon" alt="">
</div> </div>
<div class="data-origin-text">关键科技政列表,数据来源:美国各行政机构官网</div> <div class="data-origin-text">关键科技政列表,数据来源:美国各行政机构官网</div>
</div> </div>
</div> </div>
<div class="box8"> <div class="box8">
...@@ -321,7 +321,7 @@ ...@@ -321,7 +321,7 @@
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsTcon" alt=""> <img :src="tipsTcon" alt="">
</div> </div>
<div class="data-origin-text">科技政重点条款词云,数据来源:美国各行政机构官网</div> <div class="data-origin-text">科技政重点条款词云,数据来源:美国各行政机构官网</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -502,10 +502,10 @@ const govInsList = ref([]); ...@@ -502,10 +502,10 @@ const govInsList = ref([]);
const checkedGovIns = ref([]); const checkedGovIns = ref([]);
const handleGetDepartmentList = async () => { const handleGetDepartmentList = async () => {
try { try {
const res = await getDepartmentList({day:7}); const res = await getDepartmentList({});
console.log("机构列表", res); console.log("机构列表", res);
if (res.code === 200 && res.data) { if (res.code === 200) {
govInsList.value = res.data; govInsList.value = res.data.orgList;
} }
} catch (error) { } catch (error) {
console.error("获取机构列表error", error); console.error("获取机构列表error", error);
......
...@@ -100,7 +100,7 @@ import { ...@@ -100,7 +100,7 @@ import {
getDecreeRelatedEntitie getDecreeRelatedEntitie
} from "@/api/decree/influence"; } from "@/api/decree/influence";
import ChartChain from "./com/ChartChain.vue"; 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 GraphChart from "@/components/base/GraphChart/index.vue";
import defaultIcon2 from "@/assets/icons/default-icon2.png"; import defaultIcon2 from "@/assets/icons/default-icon2.png";
import noticeIcon from "./assets/images/notice-icon.png"; import noticeIcon from "./assets/images/notice-icon.png";
...@@ -186,7 +186,7 @@ const headerChartData = (row) => { ...@@ -186,7 +186,7 @@ const headerChartData = (row) => {
onDecreeRelatedChain(row.id) onDecreeRelatedChain(row.id)
break; break;
case 2: case 2:
onDecreeRelatedEntitie(row.id) onDecreeRelatedEntitie(row.orgId)
break; break;
} }
} }
...@@ -247,14 +247,17 @@ const graphInfo = reactive({ ...@@ -247,14 +247,17 @@ const graphInfo = reactive({
nodes: [], nodes: [],
links: [], links: [],
}); });
const onDecreeRelatedEntitie = async (id) => { const onDecreeRelatedEntitie = async (orgId) => {
try { try {
const res = await getDecreeRelatedEntitie({ id }); const res = await getDecreeRelatedEntitie({ orgId, rule:false, withSanInfo:false });
console.log("实体关系:", res); console.log("实体关系:", res);
if (res.code === 200) { if (res.code === 200) {
graphInfo.links = res.data.map(onFormatLink) let arr1 = res.data.parentOrgList.map(item => ({ ...item, level: 1 }))
graphInfo.nodes = res.data.map(onFormatNode) let arr3 = res.data.childrenOrgList.map(item => ({ ...item, level: 3 }))
graphInfo.nodes.unshift(onFormatNode(entityInfo.node))
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) { } catch (error) {
console.log("获取实体关系失败", error); console.log("获取实体关系失败", error);
...@@ -262,17 +265,18 @@ const onDecreeRelatedEntitie = async (id) => { ...@@ -262,17 +265,18 @@ const onDecreeRelatedEntitie = async (id) => {
} }
const onFormatLink = (item, index) => { const onFormatLink = (item, index) => {
return { return {
id: `link-${index+1}`, id: `link-${index}-${item.id}`,
source: item.id+'', target: entityInfo.id+'', target: item.level==3 ? `${index}-${item.id}` : `-1-${entityInfo.node.orgId}`,
label: { show: true, color: "#055fc2", backgroundColor: "#eef7ff", borderWidth: 0, offset: [0, 15], formatter: item.relation }, 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 } lineStyle: { color: '#B9DCFF', type: "solid", opacity: 1 }
} }
} }
const onFormatNode = (item) => { const onFormatNode = (item, index) => {
let leader = item.id == entityInfo.id; let leader = item.id == entityInfo.node.orgId;
return { return {
id: item.id+'', id: `${index}-${item.id}`,
name: onWordWrap(item.companyName, 7), name: onWordWrap(item.name, 7),
label: { label: {
show: true, show: true,
color: "#3b414b", color: "#3b414b",
......
...@@ -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,
() => { () => {
......
...@@ -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>
...@@ -2033,13 +2033,13 @@ onMounted(async () => { ...@@ -2033,13 +2033,13 @@ onMounted(async () => {
.up { .up {
width: 32px; width: 32px;
height: 32px; height: 32px;
background-image: url("src/views/innovationSubject/assets/images/icon-up.png"); background-image: url("./assets/images/icon-up.png");
} }
.down { .down {
width: 32px; width: 32px;
height: 32px; height: 32px;
background-image: url("src/views/innovationSubject/assets/images/icon-down.png"); background-image: url("./assets/images/icon-down.png");
} }
} }
......
...@@ -160,7 +160,7 @@ ...@@ -160,7 +160,7 @@
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="box2"> <div class="box2">
<AnalysisBox title="共识观点列表" :showAllBtn="true" v-if="isBox2 || isAnalysisLoading"> <AnalysisBox title="核心观点分析" :showAllBtn="true" v-if="isBox2 || isAnalysisLoading">
<div class="box2-main"> <div class="box2-main">
<div class="empty-image" v-if="isBox2 && !isAnalysisLoading"> <div class="empty-image" v-if="isBox2 && !isAnalysisLoading">
<img src="../assets/images/empty-image.png" alt="" /> <img src="../assets/images/empty-image.png" alt="" />
...@@ -228,7 +228,11 @@ ...@@ -228,7 +228,11 @@
<div v-for="(sv, svIdx) in item.sourceViewDetails" :key="`${sv.report_id}-${sv.view_id}-${svIdx}`" <div v-for="(sv, svIdx) in item.sourceViewDetails" :key="`${sv.report_id}-${sv.view_id}-${svIdx}`"
class="source-view-detail"> class="source-view-detail">
<div class="source-view-detail-title"> <div class="source-view-detail-title">
<span class="source-view-detail-title-text">{{ getSourceViewDisplayTitle(sv, svIdx) }}</span> <span
class="source-view-detail-title-text"
:class="{ 'is-clickable-report': hasReportLinkForSourceView(sv) }"
@click.stop="handleOpenReportOriginalFromSource(sv)"
>{{ getSourceViewDisplayTitle(sv) }}</span>
<span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl"> <span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl">
<img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" /> <img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" />
<span class="source-view-detail-org-text">{{ sv.thinktankName }}</span> <span class="source-view-detail-org-text">{{ sv.thinktankName }}</span>
...@@ -274,8 +278,11 @@ ...@@ -274,8 +278,11 @@
<div v-for="(sv, svIdx) in item.sourceViewDetails" <div v-for="(sv, svIdx) in item.sourceViewDetails"
:key="`${sv.report_id}-${sv.view_id}-${svIdx}`" class="source-view-detail"> :key="`${sv.report_id}-${sv.view_id}-${svIdx}`" class="source-view-detail">
<div class="source-view-detail-title"> <div class="source-view-detail-title">
<span class="source-view-detail-title-text">{{ getSourceViewDisplayTitle(sv, svIdx) <span
}}</span> class="source-view-detail-title-text"
:class="{ 'is-clickable-report': hasReportLinkForSourceView(sv) }"
@click.stop="handleOpenReportOriginalFromSource(sv)"
>{{ getSourceViewDisplayTitle(sv) }}</span>
<span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl"> <span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl">
<img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" /> <img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" />
<span class="source-view-detail-org-text">{{ sv.thinktankName }}</span> <span class="source-view-detail-org-text">{{ sv.thinktankName }}</span>
...@@ -323,7 +330,7 @@ const sort = ref(""); ...@@ -323,7 +330,7 @@ const sort = ref("");
const searchPolicy = ref(""); const searchPolicy = ref("");
const isBox2 = ref(true) const isBox2 = ref(true)
const isAnalysisLoading = ref(false) const isAnalysisLoading = ref(false)
const isBeingAnalysisExpanded = ref(false) const isBeingAnalysisExpanded = ref(true)
const beingAnalysisContent = ref("") const beingAnalysisContent = ref("")
const beingAnalysisContentRef = ref(null) const beingAnalysisContentRef = ref(null)
const activeOpinionTab = ref('consensus') const activeOpinionTab = ref('consensus')
...@@ -435,11 +442,23 @@ const getViewpointDetailForSource = (reportId, viewId) => { ...@@ -435,11 +442,23 @@ const getViewpointDetailForSource = (reportId, viewId) => {
thinktankLogoUrl: "" thinktankLogoUrl: ""
} }
} }
/** 展开区标题:优先中文标题,否则英文;无标题则返回空串(由上游过滤) */ /** 根据 report_id 找到报告名(用于展开区标题展示) */
const getSourceViewDisplayTitle = (sv, idx) => { const getReportNameById = (reportId) => {
const zh = String(sv.titleZh ?? "").trim() const id = String(reportId ?? "")
if (!id) return ""
const list = selectedReportList.value || []
const hit = Array.isArray(list) ? list.find((r) => String(r?.id ?? "") === id) : null
return String(hit?.name ?? "").trim()
}
/** 展开区标题:显示报告名《xxx》(优先);否则回退中文标题/英文标题;无标题则返回空串(由上游过滤) */
const getSourceViewDisplayTitle = (sv) => {
const reportName = String(sv?.reportName ?? "").trim()
if (reportName) return `《${reportName}》`
const fromId = getReportNameById(sv?.report_id)
if (fromId) return `《${fromId}》`
const zh = String(sv?.titleZh ?? "").trim()
if (zh) return zh if (zh) return zh
const en = String(sv.title ?? "").trim() const en = String(sv?.title ?? "").trim()
if (en) return en if (en) return en
return "" return ""
} }
...@@ -449,6 +468,20 @@ const getSourceViewDisplayContent = (sv) => { ...@@ -449,6 +468,20 @@ const getSourceViewDisplayContent = (sv) => {
if (zh) return zh if (zh) return zh
return String(sv.content ?? "").trim() return String(sv.content ?? "").trim()
} }
/** 是否存在可跳转的报告 id(source_views 的 report_id) */
const hasReportLinkForSourceView = (sv) => Boolean(String(sv?.report_id ?? "").trim())
/** 点击报告标题:新标签打开该报告原文页 */
const handleOpenReportOriginalFromSource = (sv) => {
const id = String(sv?.report_id ?? "").trim()
if (!id) return
const route = router.resolve({
name: "ReportOriginal",
params: { id }
})
window.open(route.href, "_blank")
}
const tryParseAnswerFromStreamText = (text) => { const tryParseAnswerFromStreamText = (text) => {
const lines = String(text || "") const lines = String(text || "")
.split(/\r?\n/) .split(/\r?\n/)
...@@ -492,9 +525,11 @@ const consensusList = computed(() => { ...@@ -492,9 +525,11 @@ const consensusList = computed(() => {
const sourceViewDetails = sourceViews const sourceViewDetails = sourceViews
.map((v) => { .map((v) => {
const detail = getViewpointDetailForSource(v.report_id, v.view_id) const detail = getViewpointDetailForSource(v.report_id, v.view_id)
const reportName = getReportNameById(v.report_id)
return { return {
report_id: v.report_id, report_id: v.report_id,
view_id: v.view_id, view_id: v.view_id,
reportName,
titleZh: detail.titleZh, titleZh: detail.titleZh,
contentZh: detail.contentZh, contentZh: detail.contentZh,
title: detail.title, title: detail.title,
...@@ -504,14 +539,15 @@ const consensusList = computed(() => { ...@@ -504,14 +539,15 @@ const consensusList = computed(() => {
} }
}) })
.filter((sv) => { .filter((sv) => {
const title = getSourceViewDisplayTitle(sv, 0) const title = getSourceViewDisplayTitle(sv)
return Boolean(title) return Boolean(title)
}) })
const uniqueReportCount = new Set(sourceViewDetails.map((sv) => String(sv.report_id ?? ""))).size
const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";") const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";")
return { return {
id: `consensus-${index + 1}`, id: `consensus-${index + 1}`,
consensusContent: item?.consensus_content || "", consensusContent: item?.consensus_content || "",
reportCount: sourceViewDetails.length, reportCount: uniqueReportCount,
sourceViewText, sourceViewText,
sourceViewDetails sourceViewDetails
} }
...@@ -525,9 +561,11 @@ const differenceList = computed(() => { ...@@ -525,9 +561,11 @@ const differenceList = computed(() => {
const sourceViewDetails = sourceViews const sourceViewDetails = sourceViews
.map((v) => { .map((v) => {
const detail = getViewpointDetailForSource(v.report_id, v.view_id) const detail = getViewpointDetailForSource(v.report_id, v.view_id)
const reportName = getReportNameById(v.report_id)
return { return {
report_id: v.report_id, report_id: v.report_id,
view_id: v.view_id, view_id: v.view_id,
reportName,
titleZh: detail.titleZh, titleZh: detail.titleZh,
contentZh: detail.contentZh, contentZh: detail.contentZh,
title: detail.title, title: detail.title,
...@@ -537,19 +575,43 @@ const differenceList = computed(() => { ...@@ -537,19 +575,43 @@ const differenceList = computed(() => {
} }
}) })
.filter((sv) => { .filter((sv) => {
const title = getSourceViewDisplayTitle(sv, 0) const title = getSourceViewDisplayTitle(sv)
return Boolean(title) return Boolean(title)
}) })
const uniqueReportCount = new Set(sourceViewDetails.map((sv) => String(sv.report_id ?? ""))).size
const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";") const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";")
return { return {
id: `difference-${index + 1}`, id: `difference-${index + 1}`,
disagreementContent: item?.disagreement_content || "", disagreementContent: item?.disagreement_content || "",
reportCount: sourceViewDetails.length, reportCount: uniqueReportCount,
sourceViewText, sourceViewText,
sourceViewDetails sourceViewDetails
} }
}) })
}) })
// 默认展开:每次分析结果就绪后,共识/分歧列表第一条展开,其余关闭
watch(
consensusList,
(list) => {
if (!Array.isArray(list) || list.length === 0) return
if (openConsensusIds.value.size > 0) return
if (!list[0]?.id) return
openConsensusIds.value = new Set([list[0].id])
},
{ immediate: true }
)
watch(
differenceList,
(list) => {
if (!Array.isArray(list) || list.length === 0) return
if (openDifferencesIds.value.size > 0) return
if (!list[0]?.id) return
openDifferencesIds.value = new Set([list[0].id])
},
{ immediate: true }
)
// 近N年发布(用于 startDate) // 近N年发布(用于 startDate)
const selectedYears = ref(5); const selectedYears = ref(5);
const yearsOptions = [ const yearsOptions = [
...@@ -585,9 +647,12 @@ const handleAnalysis = async () => { ...@@ -585,9 +647,12 @@ const handleAnalysis = async () => {
if (!canProceed.value) return if (!canProceed.value) return
isBox2.value = false isBox2.value = false
isAnalysisLoading.value = true isAnalysisLoading.value = true
isBeingAnalysisExpanded.value = false isBeingAnalysisExpanded.value = true
beingAnalysisContent.value = "" beingAnalysisContent.value = ""
domainViewAnalysisRes.value = null domainViewAnalysisRes.value = null
// 默认:共识/分歧第一条展开,其余关闭(每次开始分析先清空旧展开状态)
openConsensusIds.value = new Set()
openDifferencesIds.value = new Set()
// 每次进入“开始分析”默认回到共识观点,避免上次状态残留导致内容错位 // 每次进入“开始分析”默认回到共识观点,避免上次状态残留导致内容错位
activeOpinionTab.value = 'consensus' activeOpinionTab.value = 'consensus'
await handlePostReportDomainViewAnalysis() await handlePostReportDomainViewAnalysis()
...@@ -601,6 +666,8 @@ const handleBack = () => { ...@@ -601,6 +666,8 @@ const handleBack = () => {
beingAnalysisContent.value = "" beingAnalysisContent.value = ""
// 返回选择时也重置,确保下次进入分析展示一致 // 返回选择时也重置,确保下次进入分析展示一致
activeOpinionTab.value = 'consensus' activeOpinionTab.value = 'consensus'
openConsensusIds.value = new Set()
openDifferencesIds.value = new Set()
} }
const pageSize = 10; const pageSize = 10;
const total = ref(0); const total = ref(0);
...@@ -1029,7 +1096,7 @@ onMounted(async () => { ...@@ -1029,7 +1096,7 @@ onMounted(async () => {
.being-analysis-detail-box { .being-analysis-detail-box {
width: 1063px; width: 1063px;
height: 160px; height: 260px;
background-color: rgb(246, 250, 255); background-color: rgb(246, 250, 255);
border-radius: 10px; border-radius: 10px;
...@@ -1087,7 +1154,7 @@ onMounted(async () => { ...@@ -1087,7 +1154,7 @@ onMounted(async () => {
.being-analysis-box-content { .being-analysis-box-content {
width: 983px; width: 983px;
height: 104px; height: 204px;
margin-left: 40px; margin-left: 40px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
......
...@@ -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>
...@@ -50,24 +54,24 @@ ...@@ -50,24 +54,24 @@
</div> </div>
<div class="text">{{ "查看官网" }}</div> <div class="text">{{ "查看官网" }}</div>
</div> --> </div> -->
<div class="btn"> <!-- <div class="btn">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon2.png" alt="" /> <img src="./images/btn-icon2.png" alt="" />
</div> </div>
<div class="text" @click="goToOfficialWebsite()">{{ "查看官网" }}</div> <div class="text" @click="goToOfficialWebsite()">{{ "查看官网" }}</div>
</div> </div> -->
<div class="btn"> <div class="btn">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon2.png" alt="" /> <img src="./images/btn-icon2.png" alt="" />
</div> </div>
<div class="text" @click="toReport()">{{ "报告原文" }}</div> <div class="text" @click="toReport()">{{ "报告原文" }}</div>
</div> </div>
<div class="btn" @click="handleDownloadDocument"> <!-- <div class="btn" @click="handleDownloadDocument">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon3.png" alt="" /> <img src="./images/btn-icon3.png" alt="" />
</div> </div>
<div class="text">{{ "文档下载" }}</div> <div class="text">{{ "文档下载" }}</div>
</div> </div> -->
<div class="btn btn1" @click="handleAnalysisClick"> <div class="btn btn1" @click="handleAnalysisClick">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon4.png" alt="" /> <img src="./images/btn-icon4.png" alt="" />
...@@ -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%;
} }
} }
} }
......
...@@ -103,17 +103,19 @@ ...@@ -103,17 +103,19 @@
</div> </div>
<div class="right-footer-box"> <div class="right-footer-box">
<div class="relatedBills" v-for="(item, index) in box1DataItem.relatedBills" :key="index" v-show="item"> <div class="relatedBills" v-for="(item, index) in box1DataItem.relatedBills" :key="index" v-show="item">
<div class="tag">{{ "政令" }}</div> <div class="tag">{{ "法案" }}</div>
<div class="tag">{{ "科技领域相似" }}</div> <div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedBills-content">{{ item.name }}</div> <div class="relatedBills-content">{{ item.name }}</div>
<div class="footer-image"> <img src="../images/image-right.png" alt="" /></div> <div class="footer-image" @click.stop="handleBillMoreClick(item)"> <img src="../images/image-right.png"
alt="" /></div>
</div> </div>
<div class="relatedAdministrativeOrders" v-for="(item, index) in box1DataItem.relatedAdministrativeOrders" <div class="relatedAdministrativeOrders" v-for="(item, index) in box1DataItem.relatedAdministrativeOrders"
v-show="item" :key="index"> v-show="item" :key="index">
<div class="tag">{{ "政令" }}</div> <div class="tag">{{ "政令" }}</div>
<div class="tag">{{ "科技领域相似" }}</div> <div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedAdministrativeOrders-content">{{ item.content }}</div> <div class="relatedAdministrativeOrders-content">{{ item.content }}</div>
<div class="footer-image"> <img src="../images/image-right.png" alt="" /></div> <div class="footer-image" @click.stop="handleAdministrativeMoreClick(item)"> <img
src="../images/image-right.png" alt="" /></div>
</div> </div>
</div> </div>
</div> </div>
...@@ -217,6 +219,29 @@ const handleTagClick = tag => { ...@@ -217,6 +219,29 @@ const handleTagClick = tag => {
activeItemIndex.value = 0; activeItemIndex.value = 0;
updatePageData(); updatePageData();
}; };
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
/** 政策建议关联法案:新标签页打开政令介绍页*/
const handleAdministrativeMoreClick = (ad) => {
const id = ad?.bcId;
if (!id) {
return;
}
const route = router.resolve({
path: "/decreeLayout/overview/introduction",
query: { id: String(id) }
});
window.open(route.href, "_blank");
};
const handleSearchOpinions = () => { const handleSearchOpinions = () => {
// 搜索时默认切回“全部”标签 // 搜索时默认切回“全部”标签
activeTag.value = ""; activeTag.value = "";
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
<!-- 多个作者:显示第一个 + 等 --> <!-- 多个作者:显示第一个 + 等 -->
<span v-else> <span v-else>
{{ reportAuthors[0].name }} {{ reportAuthors[0].name }}{{ reportAuthors.length }}
</span> </span>
</template> </template>
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
@error="() => { if (author.avatar) author.avatar = null; }" /></div> @error="() => { if (author.avatar) author.avatar = null; }" /></div>
<div class="author-text"> <div class="author-text">
<div class="author-name">{{ author.name }}</div> <div class="author-name">{{ author.name }}</div>
<div class="author-position">{{ author.job }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -67,6 +67,10 @@ ...@@ -67,6 +67,10 @@
<div class="box5"> <div class="box5">
<AnalysisBox title="报告关键词云" :showAllBtn="true"> <AnalysisBox title="报告关键词云" :showAllBtn="true">
<div class="box5-main"> <div class="box5-main">
<template v-if="!hasBox5ChartData">
<el-empty class="box5-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div class="box5Chart"> <div class="box5Chart">
<!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 --> <!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 -->
<WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%" <WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%"
...@@ -74,14 +78,15 @@ ...@@ -74,14 +78,15 @@
</div> </div>
<div class="box5-footer"> <div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div>
</div> </div>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)"> <div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" /> <AiPane :aiContent="aiContentBox5" />
</div> </div>
</template>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -254,7 +259,8 @@ const props = defineProps({ ...@@ -254,7 +259,8 @@ const props = defineProps({
}); });
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网"; "智库报告关键词云,数据来源:美国兰德公司官网";
const isShowAiContentBox5 = ref(false); // 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
...@@ -467,6 +473,7 @@ const box2Data = ref([ ...@@ -467,6 +473,7 @@ const box2Data = ref([
]); ]);
// 报告关键词云 // 报告关键词云
const box5Data = ref([]); const box5Data = ref([]);
const hasBox5ChartData = computed(() => Array.isArray(box5Data.value) && box5Data.value.length > 0);
/** 词云子组件不 watch 数据,每次接口成功有数据时递增 key,强制重新挂载以触发 onMounted */ /** 词云子组件不 watch 数据,每次接口成功有数据时递增 key,强制重新挂载以触发 onMounted */
const box5WordCloudKey = ref(0); const box5WordCloudKey = ref(0);
...@@ -489,6 +496,10 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -489,6 +496,10 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) { if (data.length) {
box5WordCloudKey.value += 1; box5WordCloudKey.value += 1;
} }
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -581,11 +592,16 @@ const handleGetThinkTankReportViewpoint = async () => { ...@@ -581,11 +592,16 @@ const handleGetThinkTankReportViewpoint = async () => {
const res = await getThinkTankReportViewpoint(params); const res = await getThinkTankReportViewpoint(params);
console.log("核心论点", res.data); console.log("核心论点", res.data);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
majorOpinions.value = res.data.content || []; const nextOpinions = res.data.content || [];
majorOpinions.value = nextOpinions;
total.value = res.data.totalElements || 0; total.value = res.data.totalElements || 0;
// 重置展开状态 // 默认:第一条展开,其余关闭
expandedOpinionKeys.value = new Set(); const nextExpandedKeys = new Set();
if (Array.isArray(nextOpinions) && nextOpinions.length > 0) {
nextExpandedKeys.add(getOpinionExpandKey(nextOpinions[0], 0));
}
expandedOpinionKeys.value = nextExpandedKeys;
} }
} catch (error) { } catch (error) {
console.error("获取主要观点error", error); console.error("获取主要观点error", error);
...@@ -927,9 +943,18 @@ onMounted(() => { ...@@ -927,9 +943,18 @@ onMounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
position: absolute;
bottom: 20px;
left: 32px;
}
.ai-wrap {
position: absolute; position: absolute;
bottom: 18px; bottom: 18px;
right: 0; right: 0;
cursor: pointer;
} }
...@@ -942,12 +967,7 @@ onMounted(() => { ...@@ -942,12 +967,7 @@ onMounted(() => {
} }
.ai-wrap {
position: relative;
cursor: pointer;
}
} }
} }
......
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
<div class="file" v-for="(file, idxx) in item.relatedBills" :key="`${file.id}-${idxx}`"> <div class="file" v-for="(file, idxx) in item.relatedBills" :key="`${file.id}-${idxx}`">
<div class="type">法案</div> <div class="type">法案</div>
<div class="title">{{ file.name }}</div> <div class="title">{{ file.name }}</div>
<div class="more"> <div class="more" @click.stop="handleBillMoreClick(file)">
<img src="./images/arrow-right.png" alt="" /> <img src="./images/arrow-right.png" alt="" />
</div> </div>
</div> </div>
...@@ -260,7 +260,7 @@ ...@@ -260,7 +260,7 @@
:key="file.bcId != null ? String(file.bcId) + '-' + idxx : idxx"> :key="file.bcId != null ? String(file.bcId) + '-' + idxx : idxx">
<div class="type">政令</div> <div class="type">政令</div>
<div class="title">{{ file.content }}</div> <div class="title">{{ file.content }}</div>
<div class="more"> <div class="more" @click.stop="handleAdministrativeMoreClick(file)">
<img src="./images/arrow-right.png" alt="" /> <img src="./images/arrow-right.png" alt="" />
</div> </div>
</div> </div>
...@@ -302,11 +302,46 @@ import AiButton from "@/components/base/Ai/AiButton/index.vue"; ...@@ -302,11 +302,46 @@ import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue"; import AiPane from "@/components/base/Ai/AiPane/index.vue";
import TipTab from "@/views/thinkTank/TipTab/index.vue"; import TipTab from "@/views/thinkTank/TipTab/index.vue";
import defaultNewsIcon from "@/assets/icons/default-icon-news.png"; import defaultNewsIcon from "@/assets/icons/default-icon-news.png";
import AreaTag from "@/components/base/AreaTag/index.vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
/** 与 AreaTag 一致的领域色(取 tag 的文字色) */
const AREA_TAG_COLOR_BY_NAME = {
"人工智能": "rgba(245, 34, 45, 1)", // tag1
"生物科技": "rgba(19, 168, 168, 1)", // tag2
"新一代通信网络": "rgba(5, 95, 194, 1)", // tag3
// 兼容常见写法
"通信网络": "rgba(5, 95, 194, 1)",
"量子科技": "rgba(114, 46, 209, 1)", // tag4
"新能源": "rgba(82, 196, 26, 1)", // tag5
"集成电路": "rgba(22, 119, 255, 1)", // tag6
"海洋": "rgba(15, 120, 199, 1)", // tag7
"先进制造": "rgba(250, 173, 20, 1)", // tag8
"新材料": "rgba(250, 140, 22, 1)", // tag9
"航空航天": "rgba(47, 84, 235, 1)", // tag10
"太空": "rgba(47, 84, 235, 1)", // tag11
"深海": "rgba(73, 104, 161, 1)", // tag12
"极地": "rgba(133, 165, 255, 1)", // tag13
"核": "rgba(250, 84, 28, 1)", // tag14
"其他": "rgba(82, 196, 26, 1)" // tag15
};
const AREA_TAG_FALLBACK_COLORS = [
"rgba(5, 95, 194, 1)",
"rgba(245, 34, 45, 1)",
"rgba(19, 168, 168, 1)",
"rgba(250, 140, 22, 1)",
"rgba(114, 46, 209, 1)",
"rgba(82, 196, 26, 1)",
"rgba(22, 119, 255, 1)",
"rgba(250, 84, 28, 1)",
"rgba(47, 84, 235, 1)"
];
const getAreaTagColor = (name, idx = 0) =>
AREA_TAG_COLOR_BY_NAME[name] || AREA_TAG_FALLBACK_COLORS[idx % AREA_TAG_FALLBACK_COLORS.length];
/** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */ /** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */
const POLICY_TRACKING_TIP_BOX1 = const POLICY_TRACKING_TIP_BOX1 =
"智库报告中政策建议的领域分布情况,数据来源:美国兰德公司官网"; "智库报告中政策建议的领域分布情况,数据来源:美国兰德公司官网";
...@@ -320,7 +355,8 @@ const POLICY_FILTER_ALL_AREA = "全部领域"; ...@@ -320,7 +355,8 @@ const POLICY_FILTER_ALL_AREA = "全部领域";
const POLICY_FILTER_ALL_TIME = "全部时间"; const POLICY_FILTER_ALL_TIME = "全部时间";
const POLICY_FILTER_ALL_DEPT = "全部部门"; const POLICY_FILTER_ALL_DEPT = "全部部门";
const isShowAiContentPolicyPt1 = ref(false); // 刷新后默认展示 3 个图表 AI 总结
const isShowAiContentPolicyPt1 = ref(true);
const aiContentPolicyPt1 = ref(""); const aiContentPolicyPt1 = ref("");
const isPolicyPt1InterpretLoading = ref(false); const isPolicyPt1InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt1 = (val) => { const handleSwitchAiContentShowPolicyPt1 = (val) => {
...@@ -330,7 +366,7 @@ const handleSwitchAiContentShowPolicyPt1 = (val) => { ...@@ -330,7 +366,7 @@ const handleSwitchAiContentShowPolicyPt1 = (val) => {
} }
}; };
const isShowAiContentPolicyPt2 = ref(false); const isShowAiContentPolicyPt2 = ref(true);
const aiContentPolicyPt2 = ref(""); const aiContentPolicyPt2 = ref("");
const isPolicyPt2InterpretLoading = ref(false); const isPolicyPt2InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt2 = (val) => { const handleSwitchAiContentShowPolicyPt2 = (val) => {
...@@ -340,7 +376,7 @@ const handleSwitchAiContentShowPolicyPt2 = (val) => { ...@@ -340,7 +376,7 @@ const handleSwitchAiContentShowPolicyPt2 = (val) => {
} }
}; };
const isShowAiContentPolicyPt3 = ref(false); const isShowAiContentPolicyPt3 = ref(true);
const aiContentPolicyPt3 = ref(""); const aiContentPolicyPt3 = ref("");
const isPolicyPt3InterpretLoading = ref(false); const isPolicyPt3InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt3 = (val) => { const handleSwitchAiContentShowPolicyPt3 = (val) => {
...@@ -439,16 +475,20 @@ const handleGetThinkPolicyIndustry = async () => { ...@@ -439,16 +475,20 @@ const handleGetThinkPolicyIndustry = async () => {
box1Data.value = []; box1Data.value = [];
return; return;
} }
const data = list.map(item => ({ const data = list.map((item, idx) => ({
name: item.industry, name: item.industry,
value: item.amount, value: item.amount,
percent: item.percent percent: item.percent,
color: getAreaTagColor(item.industry, idx)
})); }));
box1Data.value = data; box1Data.value = data;
/* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */ /* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */
await nextTick(); await nextTick();
const box1Chart = getPieChart(box1Data.value); const box1Chart = getPieChart(box1Data.value);
setChart(box1Chart, "box1Chart"); setChart(box1Chart, "box1Chart");
if (isShowAiContentPolicyPt1.value) {
fetchPolicyPtBox1ChartInterpretation();
}
} else { } else {
box1Data.value = []; box1Data.value = [];
} }
...@@ -458,6 +498,30 @@ const handleGetThinkPolicyIndustry = async () => { ...@@ -458,6 +498,30 @@ const handleGetThinkPolicyIndustry = async () => {
} }
}; };
/** 政策建议关联法案:新标签页打开法案介绍页,billId 随接口 id 变化 */
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
/** 政策建议关联法案:新标签页打开政令介绍页*/
const handleAdministrativeMoreClick = (ad) => {
const id = ad?.bcId;
if (!id) {
return;
}
const route = router.resolve({
path: "/decreeLayout/overview/introduction",
query: { id: String(id) }
});
window.open(route.href, "_blank");
};
// 政策建议涉及部门分布(饼图) // 政策建议涉及部门分布(饼图)
const box2Data = ref([]); const box2Data = ref([]);
...@@ -494,6 +558,9 @@ const handleGetPolicyAdviceDeptDistribution = async () => { ...@@ -494,6 +558,9 @@ const handleGetPolicyAdviceDeptDistribution = async () => {
await nextTick(); await nextTick();
const box2Chart = getPieChart(box2Data.value); const box2Chart = getPieChart(box2Data.value);
setChart(box2Chart, "box2Chart"); setChart(box2Chart, "box2Chart");
if (isShowAiContentPolicyPt2.value) {
fetchPolicyPtBox2ChartInterpretation();
}
} else { } else {
box2Data.value = []; box2Data.value = [];
} }
...@@ -646,7 +713,8 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -646,7 +713,8 @@ const handleGetThinkPolicyIndustryChange = async () => {
const industryAmount = const industryAmount =
quarterData?.industryList?.find(i => i.industry === industry)?.amount || 0; quarterData?.industryList?.find(i => i.industry === industry)?.amount || 0;
return industryAmount; return industryAmount;
}) }),
color: getAreaTagColor(industry, frontendData.data.length)
}; };
frontendData.data.push(industryData); frontendData.data.push(industryData);
}); });
...@@ -656,6 +724,9 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -656,6 +724,9 @@ const handleGetThinkPolicyIndustryChange = async () => {
} }
box3Data.value = frontendData; box3Data.value = frontendData;
await renderBox3Chart(); await renderBox3Chart();
if (isShowAiContentPolicyPt3.value) {
fetchPolicyPtBox3ChartInterpretation();
}
} else { } else {
box3Data.value = { title: [], data: [] }; box3Data.value = { title: [], data: [] };
} }
...@@ -1231,14 +1302,14 @@ watch( ...@@ -1231,14 +1302,14 @@ watch(
} }
); );
onMounted(() => { onMounted(async () => {
handleGetThinkPolicyIndustry(); await handleGetThinkPolicyIndustry();
handleGetThinkPolicyIndustryTotal(); handleGetThinkPolicyIndustryTotal();
handleGetThinkPolicyIndustryChange(); await handleGetThinkPolicyIndustryChange();
handleGetHylyList(); handleGetHylyList();
handleGetGovAgencyList(); handleGetGovAgencyList();
handleGetThinkPolicy(); handleGetThinkPolicy();
handleGetPolicyAdviceDeptDistribution(); await handleGetPolicyAdviceDeptDistribution();
}); });
</script> </script>
...@@ -1423,9 +1494,8 @@ onMounted(() => { ...@@ -1423,9 +1494,8 @@ onMounted(() => {
.source { .source {
position: absolute; position: absolute;
bottom: 21px; bottom: 21px;
left: 50%; left: 24px;
transform: translateX(-50%); width: 420px;
width: 350px;
height: 22px; height: 22px;
display: flex; display: flex;
} }
...@@ -1453,7 +1523,7 @@ onMounted(() => { ...@@ -1453,7 +1523,7 @@ onMounted(() => {
width: 520px; width: 520px;
height: 372px; height: 372px;
box-sizing: border-box; box-sizing: border-box;
padding: 24px 24px 64px 24px; padding: 0px 24px 64px 24px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
...@@ -1481,15 +1551,13 @@ onMounted(() => { ...@@ -1481,15 +1551,13 @@ onMounted(() => {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
width: 472px; width: 472px;
height: 284px; height: 308px;
} }
.source { .source {
position: absolute; position: absolute;
bottom: 21px; bottom: 24px;
left: 50%; width: 420px;
transform: translateX(-50%);
width: 350px;
height: 22px; height: 22px;
display: flex; display: flex;
......
import * as echarts from 'echarts' import * as echarts from 'echarts'
const colorList = [ // 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色)
const AREA_TAG_COLOR_BY_NAME = {
'人工智能': 'rgba(245, 34, 45, 1)', // tag1
'生物科技': 'rgba(19, 168, 168, 1)', // tag2
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
// 兼容常见写法
'通信网络': 'rgba(5, 95, 194, 1)',
'量子科技': 'rgba(114, 46, 209, 1)', // tag4
'新能源': 'rgba(82, 196, 26, 1)', // tag5
'集成电路': 'rgba(22, 119, 255, 1)', // tag6
'海洋': 'rgba(15, 120, 199, 1)', // tag7
'先进制造': 'rgba(250, 173, 20, 1)', // tag8
'新材料': 'rgba(250, 140, 22, 1)', // tag9
'航空航天': 'rgba(47, 84, 235, 1)', // tag10
'太空': 'rgba(47, 84, 235, 1)', // tag11
'深海': 'rgba(73, 104, 161, 1)', // tag12
'极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
const fallbackColorList = [
'rgba(5, 95, 194, 1)', 'rgba(5, 95, 194, 1)',
'rgba(245, 34, 45, 1)',
'rgba(19, 168, 168, 1)', 'rgba(19, 168, 168, 1)',
'rgba(250, 140, 22, 1)', 'rgba(250, 140, 22, 1)',
'rgba(114, 46, 209, 1)', 'rgba(114, 46, 209, 1)',
'rgba(115, 209, 61, 1)', 'rgba(82, 196, 26, 1)',
'rgba(206, 79, 81, 1)', 'rgba(22, 119, 255, 1)',
'rgba(145, 202, 255, 1)',
'rgba(95, 101, 108, 1)',
'rgba(250, 84, 28, 1)', 'rgba(250, 84, 28, 1)',
'rgba(47, 84, 235, 1)', 'rgba(47, 84, 235, 1)',
'rgba(64, 150, 255, 1)', 'rgba(133, 165, 255, 1)'
'rgba(34, 41, 52, 1)',
'rgba(173, 198, 255, 1)',
'rgba(255, 169, 64, 1)'
] ]
const parseRgba = (colorStr) => { const parseRgba = (colorStr) => {
...@@ -50,7 +67,8 @@ const getMultiLineChart = (chartInput) => { ...@@ -50,7 +67,8 @@ const getMultiLineChart = (chartInput) => {
const echartsSeries = series.map((item, index) => { const echartsSeries = series.map((item, index) => {
const baseColor = const baseColor =
item.color || item.color ||
colorList[index % colorList.length] || AREA_TAG_COLOR_BY_NAME[item.name] ||
fallbackColorList[index % fallbackColorList.length] ||
`rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)` `rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)`
const { r, g, b } = parseRgba(baseColor) const { r, g, b } = parseRgba(baseColor)
...@@ -58,9 +76,11 @@ const getMultiLineChart = (chartInput) => { ...@@ -58,9 +76,11 @@ const getMultiLineChart = (chartInput) => {
name: item.name, name: item.name,
type: 'line', type: 'line',
smooth: true, smooth: true,
lineStyle: { color: baseColor },
itemStyle: { color: baseColor },
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `rgba(${r}, ${g}, ${b}, 0.3)` }, { offset: 0, color: `rgba(${r}, ${g}, ${b}, 0.1)` },
{ offset: 1, color: `rgba(${r}, ${g}, ${b}, 0)` } { offset: 1, color: `rgba(${r}, ${g}, ${b}, 0)` }
]) ])
}, },
...@@ -147,7 +167,7 @@ const getMultiLineChart = (chartInput) => { ...@@ -147,7 +167,7 @@ const getMultiLineChart = (chartInput) => {
itemHeight: 12 itemHeight: 12
} }
], ],
color: colorList, // 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
xAxis: [ xAxis: [
{ {
type: 'category', type: 'category',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论