提交 eb3bba82 authored 作者: yanpeng's avatar yanpeng

Merge branch 'pre' into yp-dev

流水线 #106 已失败 于阶段
in 37 分 40 秒
--- ---
description:
alwaysApply: true alwaysApply: true
--- ---
# Overview # Overview
Insert overview text here. The agent will only see this should they choose to apply the rule. Insert overview text here. The agent will only see this should they choose to apply the rule.
......
...@@ -2,10 +2,13 @@ stages: ...@@ -2,10 +2,13 @@ stages:
- build - build
- deploy - deploy
# cache: cache:
# key: "$CI_COMMIT_REF_SLUG" # cache:key 这里使用字符串,兼容性更好(部分 linter 不支持 key: { files: [...] })
# paths: # 预分支 pre 需要快速构建并实时同步,因此让 .npm 下载缓存跨分支复用
# - .npm/ key: "npm-cache-global"
paths:
- .npm/
policy: pull-push
build_pre: build_pre:
stage: build stage: build
...@@ -17,8 +20,13 @@ build_pre: ...@@ -17,8 +20,13 @@ build_pre:
script: script:
- node -v - node -v
- npm -v - npm -v
- npm config set cache .npm --global - echo "cache dir sizes:"
- npm ci --prefer-offline --no-audit --no-fund - du -sh "$CI_PROJECT_DIR/.npm" "$CI_PROJECT_DIR/.npm/_cacache" 2>/dev/null || true
- echo "=== npm ci start ==="
- date -Iseconds
- npm ci --cache "$CI_PROJECT_DIR/.npm" --no-audit --no-fund --loglevel=verbose --timing --foreground-scripts
- echo "=== npm ci end ==="
- date -Iseconds
- npm run build - npm run build
artifacts: artifacts:
paths: paths:
...@@ -35,5 +43,55 @@ deploy_pre: ...@@ -35,5 +43,55 @@ deploy_pre:
dependencies: dependencies:
- build_pre - build_pre
script: script:
- apk add --no-cache rsync - apk add --no-cache rsync curl jq
- rsync -av --delete dist/ /nas/kjb_service/zm/pre-project/html/ # 只允许“最新一次 pre pipeline”部署到 nginx(加二次确认,避免短时间多次推送导致重复 rsync)
\ No newline at end of file - >
LATEST_PIPELINE_ID="$(
curl --silent --show-error --fail
--header "JOB-TOKEN: $CI_JOB_TOKEN"
"$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/pipelines?ref=pre&order_by=id&sort=desc&per_page=1"
| jq -r '.[0].id'
)"
- >
if [ -z "$LATEST_PIPELINE_ID" ] || [ "$LATEST_PIPELINE_ID" != "$CI_PIPELINE_ID" ]; then
echo "skip deploy: not latest pipeline (latest=$LATEST_PIPELINE_ID current=$CI_PIPELINE_ID)";
exit 0;
fi
- sleep 20
- >
LATEST_PIPELINE_ID="$(
curl --silent --show-error --fail
--header "JOB-TOKEN: $CI_JOB_TOKEN"
"$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/pipelines?ref=pre&order_by=id&sort=desc&per_page=1"
| jq -r '.[0].id'
)"
- >
if [ -z "$LATEST_PIPELINE_ID" ] || [ "$LATEST_PIPELINE_ID" != "$CI_PIPELINE_ID" ]; then
echo "skip deploy: not latest pipeline after debounce (latest=$LATEST_PIPELINE_ID current=$CI_PIPELINE_ID)";
exit 0;
fi
- rsync -avz --delete dist/ /nas/kjb_service/zm/pre-project/html/
# 非 protected 分支:push 时先做 build 校验(避免合并 pre 时出现 build 报错)
build_check:
stage: build
image: node:20-bullseye
tags:
- risk-monitor-frontend
# 只在 push 时做构建校验,且排除 protected 分支与目标分支 pre
only:
- pushes
except:
- pre
- protected_branches
script:
- node -v
- npm -v
- echo "cache dir sizes:"
- du -sh "$CI_PROJECT_DIR/.npm" "$CI_PROJECT_DIR/.npm/_cacache" 2>/dev/null || true
- echo "=== npm ci start ==="
- date -Iseconds
- npm ci --cache "$CI_PROJECT_DIR/.npm" --no-audit --no-fund --loglevel=verbose --timing --foreground-scripts
- echo "=== npm ci end ==="
- date -Iseconds
- npm run build
\ No newline at end of file
差异被折叠。
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"json5": "^2.2.3", "json5": "^2.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pdfjs-dist": "^5.4.449", "pdfjs-dist": "^3.11.174",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
......
...@@ -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({
......
...@@ -2,66 +2,67 @@ import request from "@/api/request.js"; ...@@ -2,66 +2,67 @@ import request from "@/api/request.js";
// 中美博弈概览V2:最新风险动态统计 // 中美博弈概览V2:最新风险动态统计
export function getLatestRiskUpdates(params) { export function getLatestRiskUpdates(params) {
return request({ return request({
method: "GET", method: 'GET',
url: `/api/rivalryIndexV2/LatestRiskUpdates`, url: `/api/rivalryIndexV2/LatestRiskUpdates`,
params: params params: params
}); })
} }
// 中美博弈概览V2:最新风险信号 // 中美博弈概览V2:最新风险信号
export function getLatestRisks() { export function getLatestRisks() {
return request({ return request({
method: "GET", method: 'GET',
url: `/api/rivalryIndexV2/LatestRisks` url: `/api/rivalryIndexV2/LatestRisks`,
});
})
} }
// 中美博弈概览V2:美对华制裁措施数量趋势 // 中美博弈概览V2:美对华制裁措施数量趋势
export function geDomainContainmentTrend(params) { export function geDomainContainmentTrend(params) {
return request({ return request({
method: "GET", method: 'GET',
url: `/api/rivalryIndexV2/DomainContainmentTrend`, url: `/api/rivalryIndexV2/DomainContainmentTrend`,
params: params params: params
}); })
} }
// 中美博弈概况:获取榜单字典 // 中美博弈概况:获取榜单字典
export function getChartDict() { export function getChartDict() {
return request({ return request({
method: "GET", method: 'GET',
url: `/api/union/summary/chartDict` url: `/api/union/summary/chartDict`,
}); })
} }
// 根据字典信息,获取年份信息
export function getYearDict(id) {
return request({
method: "GET",
url: `/api/union/summary/chartYear/${id}`
});
}
// 中美博弈概况:中美科技实力对比 // 中美博弈概况:中美科技实力对比
export function getCompare(id, year) { export function getCompare(id) {
return request({ return request({
method: "GET", method: 'GET',
url: `/api/union/summary/compare/${id}/${year}` url: `/api/union/summary/compare/${id}`,
}); })
} }
// 中美博弈分析 // 中美博弈分析
export function getTechnologyGameAnalysis() { export function getTechnologyGameAnalysis() {
return request({ return request({
method: "GET", method: 'GET',
url: `/api/rivalryIndexV2/TechnologyGameAnalysis` url: `/api/rivalryIndexV2/TechnologyGameAnalysis`,
});
})
} }
//中美博弈概览V7:美国政府部门对华制裁最新动态 //中美博弈概览V7:美国政府部门对华制裁最新动态
export function getGovernmentSanctionsDynamics() { export function getGovernmentSanctionsDynamics() {
return request({ return request({
method: "GET", method: 'GET',
url: `/api/rivalryIndex/governmentSanctionsDynamics` url: `/api/rivalryIndex/governmentSanctionsDynamics`,
});
} })
}
\ No newline at end of file
...@@ -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;
......
...@@ -18,15 +18,6 @@ ...@@ -18,15 +18,6 @@
> >
<el-option :value="value.id" :label="value.name" v-for="(value, index) in originList" :key="index" /> <el-option :value="value.id" :label="value.name" v-for="(value, index) in originList" :key="index" />
</el-select> </el-select>
<el-select
class="select-item"
size="default"
style="margin-left: 15px; width: 200px; height: 32px"
v-model="year"
@change="handleGetCompare()"
>
<el-option :value="value" :label="value" v-for="(value, index) in yearList" :key="index" />
</el-select>
</div> </div>
</div> </div>
<div style="display: flex; height: 650px; width: 100%; padding-top: 12px"> <div style="display: flex; height: 650px; width: 100%; padding-top: 12px">
...@@ -246,7 +237,7 @@ import Echarts from "@/components/Chart/index.vue"; ...@@ -246,7 +237,7 @@ import Echarts from "@/components/Chart/index.vue";
import mockData from "./mock.json"; import mockData from "./mock.json";
import radarChart from "./radarChart3.js"; import radarChart from "./radarChart3.js";
import { getCompare, getChartDict, getYearDict, getTechnologyGameAnalysis } from "@/api/zmOverview/risk/index.js"; import { getCompare, getChartDict, getTechnologyGameAnalysis } from "@/api/zmOverview/risk/index.js";
import icon1 from "./icon/btn-icon-0.png"; import icon1 from "./icon/btn-icon-0.png";
import icon2 from "./icon/btn-icon-1.png"; import icon2 from "./icon/btn-icon-1.png";
import icon3 from "./icon/btn-icon-2.png"; import icon3 from "./icon/btn-icon-2.png";
...@@ -360,27 +351,10 @@ const handleGetChartDict = async () => { ...@@ -360,27 +351,10 @@ const handleGetChartDict = async () => {
console.error("获取数据来源error", error); console.error("获取数据来源error", error);
} }
}; };
const yearList = ref([]);
const year = ref("");
//年份
const handleGetYearDict = async () => {
try {
const res = await getYearDict(origin.value);
console.log("年份", res);
if (res.code === 200 && res.data) {
yearList.value = res.data;
year.value = res.data[0];
}
} catch (error) {
console.error("获取年份error", error);
}
};
//中美科技实力对比 //中美科技实力对比
const handleGetCompare = async () => { const handleGetCompare = async () => {
try { try {
const res = await getCompare(origin.value, year.value); const res = await getCompare(origin.value);
console.log("中美科技实力对比", res); console.log("中美科技实力对比", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
tableData.value = res.data[0].children; tableData.value = res.data[0].children;
...@@ -418,7 +392,6 @@ const handlegetTechnologyGameAnalysis = async () => { ...@@ -418,7 +392,6 @@ const handlegetTechnologyGameAnalysis = async () => {
}; };
onMounted(async () => { onMounted(async () => {
await handleGetChartDict(); await handleGetChartDict();
await handleGetYearDict();
await handleGetCompare(); await handleGetCompare();
await handlegetTechnologyGameAnalysis(); await handlegetTechnologyGameAnalysis();
// const dom = document.getElementById("char"); // const dom = document.getElementById("char");
...@@ -593,7 +566,6 @@ const lineOption = ref({ ...@@ -593,7 +566,6 @@ const lineOption = ref({
}, },
yAxis: { yAxis: {
type: "value", type: "value",
min: 77,
// name: "指数", // name: "指数",
nameLocation: "top", nameLocation: "top",
nameGap: 35, nameGap: 35,
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
<fourSuppress></fourSuppress> <fourSuppress></fourSuppress>
<!-- 中美博弈概况 --> <!-- 中美博弈概况 -->
<commonTitle id="zm-overview" title="中美博弈概况" style="margin-top: 64px; margin-bottom: 36px"></commonTitle> <commonTitle id="zm-overview" title="中美博弈概况" style="margin-top: 64px; margin-bottom: 36px"></commonTitle>
<gameProfile /> <gameProfile></gameProfile>
<div class="bottom-info"> <div class="bottom-info">
<div class="info-item"> <div class="info-item">
<div class="info-item-left"> <div class="info-item-left">
......
...@@ -417,11 +417,12 @@ onMounted(() => { ...@@ -417,11 +417,12 @@ onMounted(() => {
} }
.risk-signal-pane-top { .risk-signal-pane-top {
width: 1600px; // width: 1600px;
margin-top: 16px; margin-top: 16px;
margin-right: 18px; margin-right: 18px;
height: 116px; padding-bottom: 16px;
min-height: 116px; min-height: 116px;
height: auto;
flex-shrink: 0; flex-shrink: 0;
} }
......
...@@ -65,8 +65,8 @@ ...@@ -65,8 +65,8 @@
@clickChartItem="handleSwitchActiveChart"> @clickChartItem="handleSwitchActiveChart">
<template #chart-box> <template #chart-box>
<LineChart v-if="activeChart === '折线图'" :lineChartData="curChartData" /> <LineChart v-if="activeChart === '折线图'" :lineChartData="curChartData" />
<PieChart v-if="activeChart === '饼状图'" :pieChartData="curChartData" />
<BarChart v-if="activeChart === '柱状图'" :barChartData="curChartData" /> <BarChart v-if="activeChart === '柱状图'" :barChartData="curChartData" />
<PieChart v-if="activeChart === '饼状图'" :pieChartData="curChartData" />
<RaderChart v-if="activeChart === '雷达图'" :radarChartData="curChartData" /> <RaderChart v-if="activeChart === '雷达图'" :radarChartData="curChartData" />
</template> </template>
</ChartContainer> </ChartContainer>
...@@ -200,6 +200,12 @@ const staticsDemensionList = ref([ ...@@ -200,6 +200,12 @@ const staticsDemensionList = ref([
data: { data: {
dataX: [], dataX: [],
dataY: [] dataY: []
},
quatarData: {
},
yearData: {
} }
}, },
{ {
...@@ -246,7 +252,7 @@ const curChartTypeList = computed(() => { ...@@ -246,7 +252,7 @@ const curChartTypeList = computed(() => {
}) })
const curChartTitle = computed(() => { const curChartTitle = computed(() => {
let arr = staticsDemensionList.value.filter(item => item.active) let arr = staticsDemensionList.value.filter(item => item.active)
return arr[0].chartTitle return arr[0].chartTitle
}) })
......
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
<el-select v-model="isSort" placeholder="发布时间" style="width: 166px" @change="handlePxChange"> <el-select v-model="isSort" placeholder="发布时间" style="width: 166px" @change="handlePxChange">
<template #prefix> <template #prefix>
<div style="display: flex; align-items: center; height: 100%"> <div style="display: flex; align-items: center; height: 100%">
<img src="../../assets/icons/desc-icon.svg" style="width: 14px; height: 14px" /> <img src="../assets/icons/desc-icon.svg" style="width: 14px; height: 14px" />
</div> </div>
</template> </template>
<el-option v-for="item in releaseTimeList" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in releaseTimeList" :key="item.value" :label="item.label" :value="item.value" />
...@@ -204,6 +204,12 @@ const staticsDemensionList = ref([ ...@@ -204,6 +204,12 @@ const staticsDemensionList = ref([
data: { data: {
dataX: [], dataX: [],
dataY: [] dataY: []
},
quatarData:{
},
yearData: {
} }
}, },
{ {
......
...@@ -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 || [];
......
...@@ -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",
......
...@@ -368,29 +368,19 @@ ...@@ -368,29 +368,19 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 540px; margin-top: 16px"> <el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 505px; margin-top: 16px">
<el-col :span="8"> <el-col :span="8">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="520px"> <custom-container title="实体领域分布" :titleIcon="radarIcon" height="480px">
<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: '400px' }" /> <EChart :option="radarOption" autoresize :style="{ height: '460px' }" />
<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.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="520px"> <custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="480px">
<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" />
...@@ -401,16 +391,6 @@ ...@@ -401,16 +391,6 @@
</template> </template>
<template #default> <template #default>
<EChart :option="trendOption" autoresize :style="{ height: '400px' }" /> <EChart :option="trendOption" autoresize :style="{ height: '400px' }" />
<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.interpretation" />
</div>
</template> </template>
</custom-container> </custom-container>
</el-col> </el-col>
...@@ -732,10 +712,9 @@ import * as echarts from "echarts"; ...@@ -732,10 +712,9 @@ import * as echarts from "echarts";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { DArrowRight, Warning, Search, TrendCharts } 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 { TAGTYPE } from "@/public/constant"; import { TAGTYPE } from "@/public/constant";
import { useChartInterpretation } from "@/views/exportControl/utils/common";
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();
...@@ -747,8 +726,6 @@ const router = useRouter(); ...@@ -747,8 +726,6 @@ const router = useRouter();
import CustomContainer from "@/components/Container/index.vue"; import CustomContainer from "@/components/Container/index.vue";
import ClickableCard from "./components/link.vue"; import ClickableCard from "./components/link.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import InfoCard from "./components/info.vue"; import InfoCard from "./components/info.vue";
import CustomTitle from "./components/title.vue"; import CustomTitle from "./components/title.vue";
import CommonPrompt from "./commonPrompt/index.vue"; import CommonPrompt from "./commonPrompt/index.vue";
...@@ -769,7 +746,6 @@ import entityIcon from "./assets/images/icon-entity.png"; ...@@ -769,7 +746,6 @@ import entityIcon from "./assets/images/icon-entity.png";
import comTitle from "./assets/images/panel1_1.png"; import comTitle from "./assets/images/panel1_1.png";
import getMultiLineChart from "./utils/multiLineChart"; import getMultiLineChart from "./utils/multiLineChart";
import icon01 from "./assets/images/jianzhu.png"; import icon01 from "./assets/images/jianzhu.png";
import tipsIcon from "./assets/icons/info-icon.png";
import { import {
getEntitiesDataCount, getEntitiesDataCount,
getEntitiesDataInfo, getEntitiesDataInfo,
...@@ -1039,7 +1015,6 @@ const handleToSocialDetail = item => { ...@@ -1039,7 +1015,6 @@ const handleToSocialDetail = item => {
}); });
window.open(route.href, "_blank"); window.open(route.href, "_blank");
}; };
const trendChart = useChartInterpretation();
// 获取趋势图数据 // 获取趋势图数据
const fetchTrendData = async () => { const fetchTrendData = async () => {
try { try {
...@@ -1051,7 +1026,6 @@ const fetchTrendData = async () => { ...@@ -1051,7 +1026,6 @@ 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);
...@@ -1231,7 +1205,7 @@ const radarOption = ref({ ...@@ -1231,7 +1205,7 @@ const radarOption = ref({
}, },
radar: { radar: {
radius: "60%", radius: "60%",
center: ["52%", "45%"], center: ["50%", "50%"],
// shape: 'circle', // shape: 'circle',
indicator: [], indicator: [],
axisName: { axisName: {
...@@ -1251,14 +1225,11 @@ const radarOption = ref({ ...@@ -1251,14 +1225,11 @@ const radarOption = ref({
] ]
}); });
const radarChart = useChartInterpretation();
// 获取雷达图数据 // 获取雷达图数据
const fetchRadarData = async checked => { const fetchRadarData = async checked => {
try { try {
const data = await getSanDomainCount(checked, "export"); const data = await getSanDomainCount(checked, "export");
console.log("雷达数据data", data);
if (data && Array.isArray(data) && data.length > 0) { if (data && Array.isArray(data) && data.length > 0) {
radarChart.interpret({ type: "雷达图", name: "实体领域分布", data: data });
// 收集所有可能的领域名称 // 收集所有可能的领域名称
const allDomains = new Set(); const allDomains = new Set();
data.forEach(item => { data.forEach(item => {
...@@ -1660,11 +1631,10 @@ const fetchNewsInfo = async () => { ...@@ -1660,11 +1631,10 @@ const fetchNewsInfo = async () => {
const handlePerClick = item => { const handlePerClick = item => {
// console.log("点击了社交媒体消息:", item); // console.log("点击了社交媒体消息:", item);
window.sessionStorage.setItem("curTabName", item.name); window.sessionStorage.setItem("curTabName", item.name);
console.log("人物点击 =>", item);
const route = router.resolve({ const route = router.resolve({
path: "/characterPage", path: "/characterPage",
query: { query: {
type: item.personType || [1, 2, 3][Math.floor(Math.random() * 3)], type: item.type || [1, 2, 3][Math.floor(Math.random() * 3)],
personId: item.personId personId: item.personId
} }
}); });
...@@ -3626,50 +3596,4 @@ const handleMediaClick = item => { ...@@ -3626,50 +3596,4 @@ 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 * as echarts from "echarts"; import * as echarts from "echarts";
import chinaJson from "./China.json"; import chinaJson from "./China.json";
import _ from "lodash"; import _ from "lodash";
import { name } from "dayjs/locale/zh-cn";
//饼图 //饼图
export function getPieOption(data, title) { export function getPieOption(data, title) {
let option = { let option = {
...@@ -1023,12 +1022,6 @@ export const getMultipleBarChart_m = object => { ...@@ -1023,12 +1022,6 @@ export const getMultipleBarChart_m = object => {
}, },
yAxis: { yAxis: {
type: "value", type: "value",
name: "数量",
nameLocation: "end",
nameGap: 5,
nameTextStyle: {
padding: [0, 40, 10, 0] // [上, 右, 下, 左] —— 减少右侧 padding 相当于左移
},
splitNumber: 5, splitNumber: 5,
alignTicks: false, alignTicks: false,
axisLabel: { axisLabel: {
......
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
};
};
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
<div class="info-row"> <div class="info-row">
<div class="label">发布人:</div> <div class="label">发布人:</div>
<div class="value link"> <div class="value link">
<img :src="formattedData.avartarUrl" alt="" class="icon avatar" /> <img :src="defaultTitle" alt="" class="icon avatar" />
<span @click="handleClick">{{ formattedData.postPersonName }} ></span> <span @click="handleClick">{{ formattedData.postPersonName }} ></span>
</div> </div>
</div> </div>
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
<div class="content-title">制裁实体分布:</div> <div class="content-title">制裁实体分布:</div>
<div class="distribution-list"> <div class="distribution-list">
<div class="list-item" v-for="(item, index) in entityDistribution" :key="index"> <div class="list-item" v-for="(item, index) in entityDistribution" :key="index">
<img :src="item.imageUrl || flag" alt="" class="flag" /> <img :src="flag" alt="" class="flag" />
<div class="country-name">{{ item.name }}</div> <div class="country-name">{{ item.name }}</div>
<div class="progress-bar-container"> <div class="progress-bar-container">
<div <div
...@@ -152,7 +152,6 @@ ...@@ -152,7 +152,6 @@
</div> </div>
<div class="right-content"> <div class="right-content">
<div class="sanction-group-list"> <div class="sanction-group-list">
<el-empty v-if="sanctionList.length === 0" description="暂无制裁清单数据" />
<div class="sanction-group" v-for="(group, index) in sanctionList" :key="index"> <div class="sanction-group" v-for="(group, index) in sanctionList" :key="index">
<el-table :data="group.entities" style="width: 100%"> <el-table :data="group.entities" style="width: 100%">
<el-table-column label="实体名称" min-width="280"> <el-table-column label="实体名称" min-width="280">
...@@ -303,7 +302,7 @@ const getSanctionOverviewList = async () => { ...@@ -303,7 +302,7 @@ const getSanctionOverviewList = async () => {
removeCount.value = data.removeCount || 0; removeCount.value = data.removeCount || 0;
removeRuleCount.value = data.removeRuleCount || 0; removeRuleCount.value = data.removeRuleCount || 0;
const list = searchType.value == "add" ? data.sanList : data.removeList || []; const list = data.sanList || [];
sanctionList.value = list.map(item => ({ sanctionList.value = list.map(item => ({
reason: item.sanReason, reason: item.sanReason,
entities: (item.orgList || []).map(org => ({ entities: (item.orgList || []).map(org => ({
...@@ -449,8 +448,7 @@ const formattedData = computed(() => { ...@@ -449,8 +448,7 @@ const formattedData = computed(() => {
fileCode: info.fileCode ? `${info.fileCode} ` : "", fileCode: info.fileCode ? `${info.fileCode} ` : "",
administrativeOrderId: info.administrativeOrderId ? `No. ${info.administrativeOrderId}` : "", administrativeOrderId: info.administrativeOrderId ? `No. ${info.administrativeOrderId}` : "",
postPersonName: info.postPersonName, postPersonName: info.postPersonName,
domains: info.domainNames, domains: info.domainNames
avartarUrl: info.postPersonAvatarUrl
}; };
}); });
......
...@@ -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;
......
...@@ -50,24 +50,24 @@ ...@@ -50,24 +50,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="" />
......
...@@ -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,21 +67,26 @@ ...@@ -67,21 +67,26 @@
<div class="box5"> <div class="box5">
<AnalysisBox title="报告关键词云" :showAllBtn="true"> <AnalysisBox title="报告关键词云" :showAllBtn="true">
<div class="box5-main"> <div class="box5-main">
<div class="box5Chart"> <template v-if="!hasBox5ChartData">
<!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 --> <el-empty class="box5-el-empty" description="暂无数据" :image-size="100" />
<WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%" </template>
height="100%" /> <template v-else>
</div> <div class="box5Chart">
<div class="box5-footer"> <!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 -->
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%"
height="100%" />
</div>
<div class="box5-footer">
<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 class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" />
</div> </div>
</div> </template>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" />
</div>
</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',
......
const getPieChart = (data) => { const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => {
const color = d?.color
if (!color) return d
return {
...d,
itemStyle: { ...(d.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色
labelLine: {
...(d.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color }
}
}
})
let option = { let option = {
series: [ series: [
{ {
...@@ -57,7 +71,7 @@ const getPieChart = (data) => { ...@@ -57,7 +71,7 @@ const getPieChart = (data) => {
labelLinePoints: points labelLinePoints: points
}; };
}, },
data: data data: seriesData
}] }]
} }
return option return option
......
...@@ -17,12 +17,12 @@ ...@@ -17,12 +17,12 @@
<AreaTag v-for="(tag, index) in thinkTank.tags" :key="index" :tagName="tag.industryName"></AreaTag> <AreaTag v-for="(tag, index) in thinkTank.tags" :key="index" :tagName="tag.industryName"></AreaTag>
</div> </div>
</div> </div>
<div class="header-top-right"> <!-- <div class="header-top-right">
<button class="blue-btn" @click="handleOpenThinkTankSite"> <button class="blue-btn" @click="handleOpenThinkTankSite">
<img class="btn-img" src="./images/image1.png" alt="" /> <img class="btn-img" src="./images/image1.png" alt="" />
<span class="text">{{ '查看智库官网' }}</span> <span class="text">{{ '查看智库官网' }}</span>
</button> </button>
</div> </div> -->
</div> </div>
<div class="header-footer"> <div class="header-footer">
<div class="tab" :class="{ tabActive: tabActiveName === '智库动态' }" @click="switchTab('智库动态')"> <div class="tab" :class="{ tabActive: tabActiveName === '智库动态' }" @click="switchTab('智库动态')">
......
...@@ -402,80 +402,8 @@ const handleSelectedFiltersUpdate = val => { ...@@ -402,80 +402,8 @@ const handleSelectedFiltersUpdate = val => {
const author = ref('') // 作者 const author = ref('') // 作者
const curFooterList = ref([ /** 智库报告 / 调查项目共用列表:与资源库一致,初始为空,接口非成功时清空 */
// { const curFooterList = ref([]);
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img1
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img2
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img3
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img4
// },
// {
// title: "中美经济竞争:复杂经济和地缘政治关系中的...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img5
// },
// {
// title: "中国、智慧城市和中东:留给地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img6
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img7
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "",
// img: Img8
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img9
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img10
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img11
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img12
// }
]);
...@@ -579,11 +507,10 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -579,11 +507,10 @@ const handleGetThinkDynamicsReport = async (payload) => {
params.keyword = keyword; params.keyword = keyword;
} }
const res = await getThinkTankReport(params); const res = await getThinkTankReport(params);
console.log("智库动态报告", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
curFooterList.value = res.data.content; curFooterList.value = res.data.content;
total.value = res.data.totalElements; total.value = res.data.totalElements;
} else if (res.code === 500 && res.message === "未找到对应筛选的报告") { } else {
curFooterList.value = []; curFooterList.value = [];
total.value = 0; total.value = 0;
} }
......
...@@ -453,9 +453,9 @@ const handleGetThinkTankFundsSource = async () => { ...@@ -453,9 +453,9 @@ const handleGetThinkTankFundsSource = async () => {
alignTo: 'edge', alignTo: 'edge',
offset: [0, -LABEL_OFFSET_UP], offset: [0, -LABEL_OFFSET_UP],
formatter(params) { formatter(params) {
const valueYi = (params.data.value || 0) / 100000000 const valueYi = (params.data.value || 0) / 10000
const percent = params.percent || 0 const percent = params.percent || 0
const valueStr = `${valueYi.toFixed(3)}亿 ${percent}%` const valueStr = `${valueYi.toFixed(2)} ${percent}%`
let cumulative = 0 let cumulative = 0
for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 0 for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 0
const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360 const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360
...@@ -1096,10 +1096,10 @@ onMounted(() => { ...@@ -1096,10 +1096,10 @@ onMounted(() => {
} }
.source { .source {
margin: 0 auto;
margin-top: 10px; margin-top: 10px;
margin-left: 240px;
/* 上下0,左右自动居中 */
width: 370px; width: 370px;
height: 22px; height: 22px;
display: flex; display: flex;
...@@ -1285,7 +1285,7 @@ onMounted(() => { ...@@ -1285,7 +1285,7 @@ onMounted(() => {
} }
.source { .source {
margin: 0 auto; margin-left: 35px;
margin-top: 10px; margin-top: 10px;
/* 上下0,左右自动居中 */ /* 上下0,左右自动居中 */
......
...@@ -7,18 +7,11 @@ ...@@ -7,18 +7,11 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<el-checkbox-group <el-checkbox-group class="checkbox-group" :model-value="selectedTypeIds" @change="handleAreaGroupChange">
class="checkbox-group"
:model-value="selectedTypeIds"
@change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA"> <el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }} {{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox <el-checkbox class="filter-checkbox" v-for="t in (researchTypeList || [])" :key="t.id" :label="t.id">
class="filter-checkbox"
v-for="t in (researchTypeList || [])"
:key="t.id"
:label="t.id">
{{ t.name }} {{ t.name }}
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
...@@ -31,18 +24,11 @@ ...@@ -31,18 +24,11 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<el-checkbox-group <el-checkbox-group class="checkbox-group" :model-value="selectedYearIds" @change="handleYearGroupChange">
class="checkbox-group"
:model-value="selectedYearIds"
@change="handleYearGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME"> <el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }} {{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox <el-checkbox class="filter-checkbox" v-for="y in (researchTimeList || [])" :key="y.id" :label="y.id">
class="filter-checkbox"
v-for="y in (researchTimeList || [])"
:key="y.id"
:label="y.id">
{{ y.name }} {{ y.name }}
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
...@@ -71,17 +57,17 @@ ...@@ -71,17 +57,17 @@
</div> </div>
<div class="file-box"> <div class="file-box">
<div class="file"> <div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name">
<div class="type">法案</div> <div class="type">法案</div>
<div class="title">{{ item.billInfoList[0].name }}</div> <div class="title">{{ sv.name }}</div>
<div class="more"> <div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" /> <img src="../assets/images/image-right.png" alt="" />
</div> </div>
</div> </div>
<div class="file"> <div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div> <div class="type">政令</div>
<div class="title">{{ item.billInfoList[0].name }}</div> <div class="title">{{ sv.content }}</div>
<div class="more"> <div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" /> <img src="../assets/images/image-right.png" alt="" />
</div> </div>
</div> </div>
...@@ -106,6 +92,7 @@ ...@@ -106,6 +92,7 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router";
import AreaTag from "@/components/base/AreaTag/index.vue"; import AreaTag from "@/components/base/AreaTag/index.vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
...@@ -126,6 +113,32 @@ defineProps({ ...@@ -126,6 +113,32 @@ defineProps({
const emit = defineEmits(["filter-change", "page-change", "item-click"]); const emit = defineEmits(["filter-change", "page-change", "item-click"]);
const router = useRouter();
/** 政策建议关联法案:新标签页打开法案介绍页,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 selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]); const selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]); const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]);
...@@ -428,6 +441,7 @@ const handleYearGroupChange = (val) => { ...@@ -428,6 +441,7 @@ const handleYearGroupChange = (val) => {
height: 20px; height: 20px;
display: flex; display: flex;
margin-top: 2px; margin-top: 2px;
cursor: pointer;
.img { .img {
width: 100%; width: 100%;
......
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论