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

合并分支 'zz-dev' 到 'pre'

feat:智库概览页风险信号的风险等级显示功能修改;智库bug修复 查看合并请求 !379
流水线 #583 已通过 于阶段
in 4 分 1 秒
...@@ -75,7 +75,7 @@ export function getThinkTankReportDomainStats(params) { ...@@ -75,7 +75,7 @@ export function getThinkTankReportDomainStats(params) {
export function getThinkTankPolicyIndustry(params) { export function getThinkTankPolicyIndustry(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankOverview/policyIndustry/${params.year}`, url: `/api/thinkTankOverview/policyIndustry`,
params params
}) })
} }
...@@ -180,7 +180,8 @@ export function getThinkTankTestimoniesByThinkTankId(params) { ...@@ -180,7 +180,8 @@ export function getThinkTankTestimoniesByThinkTankId(params) {
params: { params: {
pageNum: params.pageNum, pageNum: params.pageNum,
pageSize: params.pageSize, pageSize: params.pageSize,
sortField: params.sortField,
sortOrder: params.sortOrder,
domainIds: params.domainIds, domainIds: params.domainIds,
startDate: params.startDate, startDate: params.startDate,
endDate: params.endDate, endDate: params.endDate,
......
...@@ -15,7 +15,7 @@ const thinktankRoutes = [ ...@@ -15,7 +15,7 @@ const thinktankRoutes = [
name: "thinkTank", name: "thinkTank",
component: thinkTank, component: thinkTank,
meta: { meta: {
title: "科技智库概览", title: "美国科技智库概览",
isShowHeader: true isShowHeader: true
} }
}, },
......
...@@ -4,6 +4,69 @@ import getQuarterRange from './getQuarterRange'; ...@@ -4,6 +4,69 @@ import getQuarterRange from './getQuarterRange';
import * as echarts from 'echarts' import * as echarts from 'echarts'
import 'echarts-wordcloud'; import 'echarts-wordcloud';
import router from '@/router/index' import router from '@/router/index'
const LEGEND_ARROW_NAMES = new Set(['__legend_prev__', '__legend_next__'])
const shouldShowLegendPagingArrows = (chart, option) => {
const w = typeof chart?.getWidth === 'function' ? chart.getWidth() : 0
const legendOpt = option?.legend
const legend = Array.isArray(legendOpt) ? legendOpt[0] : legendOpt
if (!legend || legend.type !== 'scroll') return false
const names = Array.isArray(legend.data) ? legend.data : []
if (!names.length) return false
const resolveSize = (val, base) => {
if (val == null) return 0
if (typeof val === 'number' && Number.isFinite(val)) return val
const s = String(val).trim()
if (!s) return 0
if (s.endsWith('%')) {
const n = parseFloat(s.slice(0, -1))
return Number.isFinite(n) ? (base * n) / 100 : 0
}
const n = Number(s)
return Number.isFinite(n) ? n : 0
}
const available = resolveSize(legend.width, w) || Math.floor(w * 0.82)
if (!available || available <= 0) return true
const fontSize = Number(legend?.textStyle?.fontSize || 14)
const fontFamily = legend?.textStyle?.fontFamily || 'Source Han Sans CN'
const fontWeight = legend?.textStyle?.fontWeight || 400
const iconW = Number(legend?.itemWidth || 12)
const gap = 10
const paddingPerItem = iconW + gap + 10
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) return true
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`
const total = names.reduce((sum, n) => {
const text = String(n ?? '')
const textW = ctx.measureText(text).width || 0
return sum + textW + paddingPerItem
}, 0)
return total > available
}
const applyLegendPagingArrowVisibility = (chart, option) => {
const shouldShow = shouldShowLegendPagingArrows(chart, option)
const graphic = option?.graphic
if (!Array.isArray(graphic) || graphic.length === 0) return
const nextGraphic = graphic.map((g) => {
if (!g || !LEGEND_ARROW_NAMES.has(g.name)) return g
return {
...g,
invisible: !shouldShow,
silent: !shouldShow
}
})
chart.setOption({ graphic: nextGraphic }, false, true)
}
const setChart = (option, chartId, allowClick, selectParam) => { const setChart = (option, chartId, allowClick, selectParam) => {
let chartDom = document.getElementById(chartId); let chartDom = document.getElementById(chartId);
if (!chartDom) { if (!chartDom) {
...@@ -12,8 +75,39 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -12,8 +75,39 @@ const setChart = (option, chartId, allowClick, selectParam) => {
chartDom.removeAttribute("_echarts_instance_"); chartDom.removeAttribute("_echarts_instance_");
let chart = echarts.init(chartDom); let chart = echarts.init(chartDom);
chart.setOption(option); chart.setOption(option);
// 处理自定义图例分页箭头(左右分布,隐藏页码)
// 约定:graphic 元素 name 为 __legend_prev__ / __legend_next__
chart.on('click', function (params) {
if (params?.componentType !== 'graphic') return;
if (!LEGEND_ARROW_NAMES.has(params?.name)) return;
const opt = chart.getOption?.() || {};
const legend = Array.isArray(opt.legend) ? opt.legend[0] : null;
if (!legend || legend.type !== 'scroll') return;
const dataLen = Array.isArray(legend.data) ? legend.data.length : 0;
if (dataLen <= 0) return;
const cur = Number(legend.scrollDataIndex || 0);
const nextIndex = params.name === '__legend_prev__' ? Math.max(0, cur - 1) : Math.min(dataLen - 1, cur + 1);
if (nextIndex === cur) return;
chart.dispatchAction({
type: 'legendScroll',
scrollDataIndex: nextIndex
});
});
// 初次渲染后判断是否需要显示左右箭头(可一行展示则隐藏)
applyLegendPagingArrowVisibility(chart, option)
if (allowClick) { if (allowClick) {
chart.on('click', function (params) { chart.on('click', function (params) {
// 图例分页箭头只负责翻页,不走任何跳转
if (params?.componentType === 'graphic' && LEGEND_ARROW_NAMES.has(params?.name)) {
return
}
switch (selectParam.moduleType) { switch (selectParam.moduleType) {
case '国会法案': case '国会法案':
if (selectParam.key === 1) { if (selectParam.key === 1) {
...@@ -112,6 +206,7 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -112,6 +206,7 @@ const setChart = (option, chartId, allowClick, selectParam) => {
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致 // 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout(() => { setTimeout(() => {
chart.resize(); chart.resize();
applyLegendPagingArrowVisibility(chart, option)
}, 0); }, 0);
return chart; return chart;
}; };
......
...@@ -186,18 +186,26 @@ let leftChart; ...@@ -186,18 +186,26 @@ let leftChart;
const rightChartRef = ref(null); const rightChartRef = ref(null);
let rightChart; let rightChart;
const isShowAiLeft = ref(true); const isShowAiLeft = ref(false);
const aiContentLeft = ref(""); const aiContentLeft = ref("");
const isLeftInterpretLoading = ref(false); const isLeftInterpretLoading = ref(false);
const leftAiAbortController = ref(null);
const isShowAiRight = ref(true); const isShowAiRight = ref(false);
const aiContentRight = ref(""); const aiContentRight = ref("");
const isRightInterpretLoading = ref(false); const isRightInterpretLoading = ref(false);
const rightAiAbortController = ref(null);
const handleSwitchAiLeft = (val) => { const handleSwitchAiLeft = (val) => {
isShowAiLeft.value = val; isShowAiLeft.value = val;
if (val) { if (val) {
fetchLeftInterpretation(); fetchLeftInterpretation();
} else {
if (leftAiAbortController.value) {
leftAiAbortController.value.abort();
leftAiAbortController.value = null;
}
isLeftInterpretLoading.value = false;
} }
}; };
...@@ -205,6 +213,12 @@ const handleSwitchAiRight = (val) => { ...@@ -205,6 +213,12 @@ const handleSwitchAiRight = (val) => {
isShowAiRight.value = val; isShowAiRight.value = val;
if (val) { if (val) {
fetchRightInterpretation(); fetchRightInterpretation();
} else {
if (rightAiAbortController.value) {
rightAiAbortController.value.abort();
rightAiAbortController.value = null;
}
isRightInterpretLoading.value = false;
} }
}; };
...@@ -276,12 +290,17 @@ const fetchLeftInterpretation = async () => { ...@@ -276,12 +290,17 @@ const fetchLeftInterpretation = async () => {
if (hasValidContent || isLeftInterpretLoading.value) { if (hasValidContent || isLeftInterpretLoading.value) {
return; return;
} }
if (leftAiAbortController.value) {
leftAiAbortController.value.abort();
}
leftAiAbortController.value = new AbortController();
isLeftInterpretLoading.value = true; isLeftInterpretLoading.value = true;
aiContentLeft.value = "解读生成中…"; aiContentLeft.value = "解读生成中…";
try { try {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(payload) }, { text: JSON.stringify(payload) },
{ {
signal: leftAiAbortController.value.signal,
onChunk: (chunk) => { onChunk: (chunk) => {
// 与智库概览「数量变化趋势」一致:按 chunk 增量拼接展示 // 与智库概览「数量变化趋势」一致:按 chunk 增量拼接展示
appendAiInterpretationChunk(aiContentLeft, chunk); appendAiInterpretationChunk(aiContentLeft, chunk);
...@@ -292,10 +311,13 @@ const fetchLeftInterpretation = async () => { ...@@ -292,10 +311,13 @@ const fetchLeftInterpretation = async () => {
// 与智库概览一致:优先用最终「解读」收口;否则保留已拼接内容 // 与智库概览一致:优先用最终「解读」收口;否则保留已拼接内容
aiContentLeft.value = text || aiContentLeft.value || "未返回有效解读内容"; aiContentLeft.value = text || aiContentLeft.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("合作限制政策对比图表解读请求失败", error); if (error?.name !== "AbortError") {
aiContentLeft.value = "解读加载失败"; console.error("合作限制政策对比图表解读请求失败", error);
aiContentLeft.value = "解读加载失败";
}
} finally { } finally {
isLeftInterpretLoading.value = false; isLeftInterpretLoading.value = false;
leftAiAbortController.value = null;
} }
}; };
...@@ -348,12 +370,17 @@ const fetchRightInterpretation = async () => { ...@@ -348,12 +370,17 @@ const fetchRightInterpretation = async () => {
if (hasValidContent || isRightInterpretLoading.value) { if (hasValidContent || isRightInterpretLoading.value) {
return; return;
} }
if (rightAiAbortController.value) {
rightAiAbortController.value.abort();
}
rightAiAbortController.value = new AbortController();
isRightInterpretLoading.value = true; isRightInterpretLoading.value = true;
aiContentRight.value = "解读生成中…"; aiContentRight.value = "解读生成中…";
try { try {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(payload) }, { text: JSON.stringify(payload) },
{ {
signal: rightAiAbortController.value.signal,
onChunk: (chunk) => { onChunk: (chunk) => {
appendAiInterpretationChunk(aiContentRight, chunk); appendAiInterpretationChunk(aiContentRight, chunk);
} }
...@@ -362,10 +389,13 @@ const fetchRightInterpretation = async () => { ...@@ -362,10 +389,13 @@ const fetchRightInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentRight.value = text || aiContentRight.value || "未返回有效解读内容"; aiContentRight.value = text || aiContentRight.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("合作限制领域分布图表解读请求失败", error); if (error?.name !== "AbortError") {
aiContentRight.value = "解读加载失败"; console.error("合作限制领域分布图表解读请求失败", error);
aiContentRight.value = "解读加载失败";
}
} finally { } finally {
isRightInterpretLoading.value = false; isRightInterpretLoading.value = false;
rightAiAbortController.value = null;
} }
}; };
......
...@@ -61,17 +61,17 @@ ...@@ -61,17 +61,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="page">
<div class="count">{{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
</template> </template>
<template v-else> <template v-else>
<div class="right-main-empty"> <div class="right-main-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" /> <el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div> </div>
</template> </template>
<div class="page">
<div class="count">共 {{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :page-count="pageCount"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -293,6 +293,11 @@ const total = ref(0); ...@@ -293,6 +293,11 @@ const total = ref(0);
const pageSize = ref(10); const pageSize = ref(10);
const currentPage = ref(1); const currentPage = ref(1);
const reslibContainer = ref(null); const reslibContainer = ref(null);
const pageCount = computed(() => {
const size = Number(pageSize.value || 10) || 10;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const getTypeClass = (type) => { const getTypeClass = (type) => {
const map = { const map = {
...@@ -658,13 +663,16 @@ watch(currentPage, () => { ...@@ -658,13 +663,16 @@ watch(currentPage, () => {
.title { .title {
font-size: 20px; font-size: 20px;
width: 950px; max-width: 950px;
font-weight: 700; font-weight: 700;
font-family: "Microsoft YaHei"; font-family: "Microsoft YaHei";
line-height: 26px; line-height: 26px;
color: rgb(59, 65, 75); color: rgb(59, 65, 75);
margin-bottom: 9px; margin-bottom: 9px;
cursor: pointer; cursor: pointer;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
} }
.content { .content {
......
...@@ -26,27 +26,15 @@ ...@@ -26,27 +26,15 @@
<div class="btn-box"> <div class="btn-box">
<div class="translate"> <div class="translate">
<div class="search-input-wrap" v-if="showSearchInput"> <div class="search-input-wrap" v-if="showSearchInput">
<input <input v-model="searchKeywordText" class="search-input" placeholder="回车查询"
v-model="searchKeywordText" @keyup.enter="handleSearchInPdf" />
class="search-input"
placeholder="回车查询"
@keyup.enter="handleSearchInPdf"
/>
<div class="search-match-count">{{ matchInfo.current }}/{{ matchInfo.total }}</div> <div class="search-match-count">{{ matchInfo.current }}/{{ matchInfo.total }}</div>
<button <button class="search-nav-btn" type="button" @click="handlePrevMatch"
class="search-nav-btn" :disabled="matchInfo.total === 0 || matchInfo.current <= 1">
type="button"
@click="handlePrevMatch"
:disabled="matchInfo.total === 0 || matchInfo.current <= 1"
>
上一个 上一个
</button> </button>
<button <button class="search-nav-btn" type="button" @click="handleNextMatch"
class="search-nav-btn" :disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total">
type="button"
@click="handleNextMatch"
:disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total"
>
下一个 下一个
</button> </button>
</div> </div>
...@@ -54,19 +42,14 @@ ...@@ -54,19 +42,14 @@
<el-switch v-model="valueSwitch" /> <el-switch v-model="valueSwitch" />
</div> </div>
<div class="translate-image"> <div class="translate-image">
<img <img class="translate-icon" src="../assets/icon-translation.png" alt="" style="
class="translate-icon"
src="../assets/icon-translation.png"
alt=""
style="
width: 16px; width: 16px;
height: 16px; height: 16px;
max-width: 16px; max-width: 16px;
max-height: 16px; max-height: 16px;
display: block; display: block;
object-fit: contain; object-fit: contain;
" " />
/>
</div> </div>
<div class="translate-text">{{ "显示原文" }}</div> <div class="translate-text">{{ "显示原文" }}</div>
</div> </div>
...@@ -83,12 +66,8 @@ ...@@ -83,12 +66,8 @@
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" /> <pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div> </div>
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage"> <div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf <pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
:key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" class="pdf-pane-inner" />
ref="rightPdfRef"
:pdfUrl="reportUrlWithPage"
class="pdf-pane-inner"
/>
</div> </div>
</div> </div>
</div> </div>
...@@ -203,7 +182,7 @@ const handleSearchInPdf = async () => { ...@@ -203,7 +182,7 @@ const handleSearchInPdf = async () => {
try { try {
const { ElMessage } = await import("element-plus"); const { ElMessage } = await import("element-plus");
ElMessage.warning("未找到包含该关键词的页面"); ElMessage.warning("未找到包含该关键词的页面");
} catch (_) {} } catch (_) { }
} }
}; };
...@@ -246,7 +225,7 @@ const handleDownload = async () => { ...@@ -246,7 +225,7 @@ const handleDownload = async () => {
try { try {
const { ElMessage } = await import("element-plus"); const { ElMessage } = await import("element-plus");
ElMessage.warning("暂无下载链接"); ElMessage.warning("暂无下载链接");
} catch (_) {} } catch (_) { }
return; return;
} }
const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-"); const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-");
...@@ -659,6 +638,7 @@ onMounted(async () => { ...@@ -659,6 +638,7 @@ onMounted(async () => {
display: flex; display: flex;
gap: 8px; gap: 8px;
cursor: pointer; cursor: pointer;
.icon { .icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
......
...@@ -171,6 +171,10 @@ const options = [ ...@@ -171,6 +171,10 @@ const options = [
]; ];
/** value 须与 v-model 类型一致(数字),否则 el-select 无法匹配 label,会显示成「2025」而非「2025年」 */ /** value 须与 v-model 类型一致(数字),否则 el-select 无法匹配 label,会显示成「2025」而非「2025年」 */
const options1 = [ const options1 = [
{
value: 2026,
label: "2026年"
},
{ {
value: 2025, value: 2025,
label: "2025年" label: "2025年"
...@@ -719,11 +723,11 @@ let rightChart1; ...@@ -719,11 +723,11 @@ let rightChart1;
let leftSankey; let leftSankey;
let boxplotChart; let boxplotChart;
// ------- AI 解读(刷新后默认展开,行为对齐智库概览) ------- // ------- AI 解读(默认仅展示 AiButton,悬停后再请求 AI) -------
const isShowAiContentLeft1 = ref(true); const isShowAiContentLeft1 = ref(false);
const isShowAiContentLeft2 = ref(true); const isShowAiContentLeft2 = ref(false);
const isShowAiContentRight1 = ref(true); const isShowAiContentRight1 = ref(false);
const isShowAiContentRight2 = ref(true); const isShowAiContentRight2 = ref(false);
const aiContentLeft1 = ref(""); const aiContentLeft1 = ref("");
const aiContentLeft2 = ref(""); const aiContentLeft2 = ref("");
...@@ -735,6 +739,11 @@ const isAiLoadingLeft2 = ref(false); ...@@ -735,6 +739,11 @@ const isAiLoadingLeft2 = ref(false);
const isAiLoadingRight1 = ref(false); const isAiLoadingRight1 = ref(false);
const isAiLoadingRight2 = ref(false); const isAiLoadingRight2 = ref(false);
const left1AiAbortController = ref(null);
const left2AiAbortController = ref(null);
const right1AiAbortController = ref(null);
const right2AiAbortController = ref(null);
const AI_LOADING_TEXT = "解读生成中…"; const AI_LOADING_TEXT = "解读生成中…";
// 用于保证“切换筛选后只写入最新一次解读结果” // 用于保证“切换筛选后只写入最新一次解读结果”
const left1AiSeq = ref(0); const left1AiSeq = ref(0);
...@@ -767,7 +776,13 @@ const getInterpretationTextFromChartResponse = (res) => { ...@@ -767,7 +776,13 @@ const getInterpretationTextFromChartResponse = (res) => {
); );
}; };
const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSeqRef) => { const fetchChartInterpretationOnce = async (
payload,
targetRef,
loadingRef,
aiSeqRef,
abortControllerRef
) => {
if (loadingRef.value) return; if (loadingRef.value) return;
const hasValidContent = const hasValidContent =
...@@ -778,12 +793,19 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe ...@@ -778,12 +793,19 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe
if (hasValidContent) return; if (hasValidContent) return;
const localSeq = aiSeqRef.value; const localSeq = aiSeqRef.value;
if (abortControllerRef?.value) {
abortControllerRef.value.abort();
}
if (abortControllerRef) {
abortControllerRef.value = new AbortController();
}
loadingRef.value = true; loadingRef.value = true;
targetRef.value = AI_LOADING_TEXT; targetRef.value = AI_LOADING_TEXT;
try { try {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(payload) }, { text: JSON.stringify(payload) },
{ {
...(abortControllerRef?.value?.signal ? { signal: abortControllerRef.value.signal } : {}),
onChunk: (chunk) => { onChunk: (chunk) => {
if (aiSeqRef.value !== localSeq) return; if (aiSeqRef.value !== localSeq) return;
appendAiInterpretationChunk(targetRef, chunk, AI_LOADING_TEXT); appendAiInterpretationChunk(targetRef, chunk, AI_LOADING_TEXT);
...@@ -796,10 +818,13 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe ...@@ -796,10 +818,13 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe
targetRef.value = text || targetRef.value || "未返回有效解读内容"; targetRef.value = text || targetRef.value || "未返回有效解读内容";
} catch (e) { } catch (e) {
if (aiSeqRef.value !== localSeq) return; if (aiSeqRef.value !== localSeq) return;
console.error("图表解读请求失败", e); if (e?.name !== "AbortError") {
targetRef.value = "解读加载失败"; console.error("图表解读请求失败", e);
targetRef.value = "解读加载失败";
}
} finally { } finally {
if (aiSeqRef.value === localSeq) loadingRef.value = false; if (aiSeqRef.value === localSeq) loadingRef.value = false;
if (abortControllerRef) abortControllerRef.value = null;
} }
}; };
...@@ -842,43 +867,71 @@ const buildPayloadRight2 = () => { ...@@ -842,43 +867,71 @@ const buildPayloadRight2 = () => {
const handleSwitchAiLeft1 = async (val) => { const handleSwitchAiLeft1 = async (val) => {
isShowAiContentLeft1.value = val; isShowAiContentLeft1.value = val;
if (!val) return; if (!val) {
if (left1AiAbortController.value) {
left1AiAbortController.value.abort();
left1AiAbortController.value = null;
}
isAiLoadingLeft1.value = false;
return;
}
const payload = buildPayloadLeft1(); const payload = buildPayloadLeft1();
if (!payload) { if (!payload) {
aiContentLeft1.value = "暂无图表数据"; aiContentLeft1.value = "暂无图表数据";
return; return;
} }
await fetchChartInterpretationOnce(payload, aiContentLeft1, isAiLoadingLeft1, left1AiSeq); await fetchChartInterpretationOnce(payload, aiContentLeft1, isAiLoadingLeft1, left1AiSeq, left1AiAbortController);
}; };
const handleSwitchAiLeft2 = async (val) => { const handleSwitchAiLeft2 = async (val) => {
isShowAiContentLeft2.value = val; isShowAiContentLeft2.value = val;
if (!val) return; if (!val) {
if (left2AiAbortController.value) {
left2AiAbortController.value.abort();
left2AiAbortController.value = null;
}
isAiLoadingLeft2.value = false;
return;
}
const payload = buildPayloadLeft2(); const payload = buildPayloadLeft2();
if (!payload) { if (!payload) {
aiContentLeft2.value = "暂无图表数据"; aiContentLeft2.value = "暂无图表数据";
return; return;
} }
await fetchChartInterpretationOnce(payload, aiContentLeft2, isAiLoadingLeft2, left2AiSeq); await fetchChartInterpretationOnce(payload, aiContentLeft2, isAiLoadingLeft2, left2AiSeq, left2AiAbortController);
}; };
const handleSwitchAiRight1 = async (val) => { const handleSwitchAiRight1 = async (val) => {
isShowAiContentRight1.value = val; isShowAiContentRight1.value = val;
if (!val) return; if (!val) {
if (right1AiAbortController.value) {
right1AiAbortController.value.abort();
right1AiAbortController.value = null;
}
isAiLoadingRight1.value = false;
return;
}
const payload = buildPayloadRight1(); const payload = buildPayloadRight1();
if (!payload) { if (!payload) {
aiContentRight1.value = "暂无图表数据"; aiContentRight1.value = "暂无图表数据";
return; return;
} }
await fetchChartInterpretationOnce(payload, aiContentRight1, isAiLoadingRight1, right1AiSeq); await fetchChartInterpretationOnce(payload, aiContentRight1, isAiLoadingRight1, right1AiSeq, right1AiAbortController);
}; };
const handleSwitchAiRight2 = async (val) => { const handleSwitchAiRight2 = async (val) => {
isShowAiContentRight2.value = val; isShowAiContentRight2.value = val;
if (!val) return; if (!val) {
if (right2AiAbortController.value) {
right2AiAbortController.value.abort();
right2AiAbortController.value = null;
}
isAiLoadingRight2.value = false;
return;
}
const payload = buildPayloadRight2(); const payload = buildPayloadRight2();
if (!payload) { if (!payload) {
aiContentRight2.value = "暂无图表数据"; aiContentRight2.value = "暂无图表数据";
return; return;
} }
await fetchChartInterpretationOnce(payload, aiContentRight2, isAiLoadingRight2, right2AiSeq); await fetchChartInterpretationOnce(payload, aiContentRight2, isAiLoadingRight2, right2AiSeq, right2AiAbortController);
}; };
...@@ -1136,21 +1189,15 @@ const initLeftSankey = (data) => { ...@@ -1136,21 +1189,15 @@ const initLeftSankey = (data) => {
// }; // };
onMounted(() => { onMounted(() => {
// 刷新后 AiPane 默认展开:先给出“解读生成中…”占位,再在数据到位后触发解读请求 // 仅拉取图表数据;AI 解读仅在用户悬停打开面板时触发
aiContentLeft1.value = "解读生成中…"; void handleGetFundField();
aiContentLeft2.value = "解读生成中…";
aiContentRight1.value = "解读生成中…";
aiContentRight2.value = "解读生成中…";
// 先拉数据;每块数据到位后立即触发一次 AI 解读(不必等其它块完成)
void handleGetFundField().then(() => handleSwitchAiLeft1(true));
void handleFindCountryProjectAreaList(); void handleFindCountryProjectAreaList();
void handlegetCountryFundingChange().then(() => handleSwitchAiRight1(true)); void handlegetCountryFundingChange();
void handlegetCountryFundProjectChange(); void handlegetCountryFundProjectChange();
void handleGetOrgFundsArea().then(() => handleSwitchAiLeft2(true)); void handleGetOrgFundsArea();
void handlegetOrgFundStrength().then(() => handleSwitchAiRight2(true)); void handlegetOrgFundStrength();
}); });
// onBeforeUnmount(() => { // onBeforeUnmount(() => {
// window.removeEventListener("resize", handleResize); // window.removeEventListener("resize", handleResize);
......
...@@ -166,6 +166,10 @@ const handleTimeGroupChange = (val) => { ...@@ -166,6 +166,10 @@ const handleTimeGroupChange = (val) => {
}; };
const pubTimeList = ref([ const pubTimeList = ref([
{
id: 2026,
name: "2026年"
},
{ {
id: 2025, id: 2025,
name: "2025年" name: "2025年"
...@@ -192,13 +196,13 @@ const pubTimeList = ref([ ...@@ -192,13 +196,13 @@ const pubTimeList = ref([
} }
]); ]);
/** 选择「全部时间」时,yearlist 传 2000~2025 逐年 */ /** 选择「全部时间」时,yearlist 传 2000~2026 逐年 */
const YEAR_ALL_RANGE_START = 2000; const YEAR_ALL_RANGE_START = 2000;
const YEAR_ALL_RANGE_END = 2025; const YEAR_ALL_RANGE_END = 2026;
const buildYearlistForRequest = (selectedTimeModel) => { const buildYearlistForRequest = (selectedTimeModel) => {
const strippedTime = stripAllTimeForRequest(selectedTimeModel); const strippedTime = stripAllTimeForRequest(selectedTimeModel);
// 仅勾选「全部时间」、未选具体年份时,传 2000~2025 逐年 // 仅勾选「全部时间」、未选具体年份时,传 2000~2026 逐年
if (strippedTime.length === 0) { if (strippedTime.length === 0) {
const out = []; const out = [];
for (let y = YEAR_ALL_RANGE_START; y <= YEAR_ALL_RANGE_END; y += 1) { for (let y = YEAR_ALL_RANGE_START; y <= YEAR_ALL_RANGE_END; y += 1) {
......
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
<div class="box5-footer"> <div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div> </div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div> </div>
...@@ -305,14 +305,21 @@ const handleGetThinkTankHearingInfo = async () => { ...@@ -305,14 +305,21 @@ const handleGetThinkTankHearingInfo = async () => {
}; };
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"国会听证会关键词云,数据来源:美国兰德公司官网"; "国会听证会关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结 // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(true); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val; isShowAiContentBox5.value = val;
if (val) { if (val) {
fetchBox5ChartInterpretation(); fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
} }
}; };
...@@ -510,10 +517,7 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -510,10 +517,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) { if (data.length) {
box5WordCloudKey.value += 1; box5WordCloudKey.value += 1;
} }
// 刷新后默认展开 AI:数据就绪即触发解读 // 仅在用户打开 AI 面板时才请求解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -663,6 +667,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -663,6 +667,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) { if (hasValidContent || isBox5InterpretLoading.value) {
return; return;
} }
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true; isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…"; aiContentBox5.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -677,6 +685,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -677,6 +685,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: box5AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk); appendAiInterpretationChunk(aiContentBox5, chunk);
} }
...@@ -685,10 +694,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -685,10 +694,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容"; aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("报告关键词云图表解读请求失败", error); if (error?.name !== "AbortError") {
aiContentBox5.value = "解读加载失败"; console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally { } finally {
isBox5InterpretLoading.value = false; isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
} }
}; };
......
...@@ -248,7 +248,7 @@ const handleDownloadDocument = async () => { ...@@ -248,7 +248,7 @@ const handleDownloadDocument = async () => {
display: flex; display: flex;
img { img {
width: 72px; width: 178px;
height: 88px; height: 88px;
} }
......
...@@ -68,11 +68,11 @@ ...@@ -68,11 +68,11 @@
<div class="info-content"> <div class="info-content">
<div class="info-item"> <div class="info-item">
<div class="info-text">{{ "相关领域:" }}</div> <div class="info-text">{{ "相关领域:" }}</div>
<div class="info-right" v-if="box1DataItem && box1DataItem.domains"> <div class="info-right" v-if="hasDomainTags">
<div class="tag-box"> <div class="tag-box">
<div class="tag" v-for="(item, index) in box1DataItem.domains" :key="index" v-show="item">{{ <div class="tag" v-for="(name, index) in domainTags" :key="name + '-' + index">
item.industryName {{ name }}
}}</div> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -155,6 +155,12 @@ const activeItemIndex = ref(0); ...@@ -155,6 +155,12 @@ const activeItemIndex = ref(0);
const pageSize = ref(10); const pageSize = ref(10);
// 当前选中 item 的数据 // 当前选中 item 的数据
const box1DataItem = ref(null); const box1DataItem = ref(null);
const domainTags = computed(() => {
const domains = box1DataItem.value?.domains;
const list = Array.isArray(domains) ? domains : [];
return list.map((d) => d?.industryName).filter(Boolean);
});
const hasDomainTags = computed(() => domainTags.value.length > 0);
// 整个页面容器,用于分页后滚回 wrap 顶部 // 整个页面容器,用于分页后滚回 wrap 顶部
const wrapRef = ref(null); const wrapRef = ref(null);
const reportUrl = ref(""); const reportUrl = ref("");
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
<div class="box5-footer"> <div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div> </div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div> </div>
...@@ -259,14 +259,21 @@ const props = defineProps({ ...@@ -259,14 +259,21 @@ const props = defineProps({
}); });
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网"; "智库报告关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结 // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(true); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val; isShowAiContentBox5.value = val;
if (val) { if (val) {
fetchBox5ChartInterpretation(); fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
} }
}; };
...@@ -463,10 +470,7 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -463,10 +470,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) { if (data.length) {
box5WordCloudKey.value += 1; box5WordCloudKey.value += 1;
} }
// 刷新后默认展开 AI:数据就绪即触发解读 // 仅在用户打开 AI 面板时才请求解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -617,6 +621,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -617,6 +621,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) { if (hasValidContent || isBox5InterpretLoading.value) {
return; return;
} }
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true; isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…"; aiContentBox5.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -631,6 +639,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -631,6 +639,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: box5AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk); appendAiInterpretationChunk(aiContentBox5, chunk);
} }
...@@ -639,10 +648,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -639,10 +648,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容"; aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("报告关键词云图表解读请求失败", error); if (error?.name !== "AbortError") {
aiContentBox5.value = "解读加载失败"; console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally { } finally {
isBox5InterpretLoading.value = false; isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
} }
}; };
...@@ -963,7 +975,7 @@ onMounted(() => { ...@@ -963,7 +975,7 @@ onMounted(() => {
display: flex; display: flex;
.left { .left {
width: 56px; width: 150px;
height: 74px; height: 74px;
margin-top: 8px; margin-top: 8px;
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
<div class="box1-footer"> <div class="box1-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div> </div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div> </div>
...@@ -185,14 +185,21 @@ const applySurveyProjectDocumentTitle = (title) => { ...@@ -185,14 +185,21 @@ const applySurveyProjectDocumentTitle = (title) => {
}; };
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"调查项目关键词云,数据来源:美国兰德公司官网"; "调查项目关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结 // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(true); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val; isShowAiContentBox5.value = val;
if (val) { if (val) {
fetchBox5ChartInterpretation(); fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
} }
}; };
const searchOpinions = ref(''); const searchOpinions = ref('');
...@@ -403,10 +410,7 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -403,10 +410,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) { if (data.length) {
box5WordCloudKey.value += 1; box5WordCloudKey.value += 1;
} }
// 刷新后默认展开 AI:数据就绪即触发解读 // 仅在用户打开 AI 面板时才请求解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -548,6 +552,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -548,6 +552,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) { if (hasValidContent || isBox5InterpretLoading.value) {
return; return;
} }
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true; isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…"; aiContentBox5.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -562,6 +570,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -562,6 +570,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: box5AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk); appendAiInterpretationChunk(aiContentBox5, chunk);
} }
...@@ -570,10 +579,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -570,10 +579,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容"; aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("报告关键词云图表解读请求失败", error); if (error?.name !== "AbortError") {
aiContentBox5.value = "解读加载失败"; console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally { } finally {
isBox5InterpretLoading.value = false; isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
} }
}; };
......
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { MUTICHARTCOLORS } from "@/common/constant.js";
// 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色) const parseHexToRgb = (hex) => {
const AREA_TAG_COLOR_BY_NAME = { const h = String(hex || '').replace('#', '').trim()
'人工智能': 'rgba(245, 34, 45, 1)', // tag1 if (h.length !== 6) return { r: 0, g: 0, b: 0 }
'生物科技': 'rgba(19, 168, 168, 1)', // tag2 return {
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3 r: parseInt(h.slice(0, 2), 16),
// 兼容常见写法 g: parseInt(h.slice(2, 4), 16),
'通信网络': 'rgba(5, 95, 194, 1)', b: parseInt(h.slice(4, 6), 16),
'量子科技': '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(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)',
'rgba(133, 165, 255, 1)'
]
const parseRgba = (colorStr) => {
const match = colorStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/)
if (match) {
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
a: parseFloat(match[4])
}
} }
return { r: 0, g: 0, b: 0, a: 1 }
} }
/** /**
...@@ -55,22 +19,14 @@ const getMultiLineChart = (chartInput) => { ...@@ -55,22 +19,14 @@ const getMultiLineChart = (chartInput) => {
const series = chartInput.data || [] const series = chartInput.data || []
const allNames = series.map((item) => item.name) const allNames = series.map((item) => item.name)
const lineSize = Math.ceil(allNames.length / 3)
const legendLine1 = allNames.slice(0, lineSize)
const legendLine2 = allNames.slice(lineSize, lineSize * 2)
const legendLine3 = allNames.slice(lineSize * 2)
const xCount = Array.isArray(title) ? title.length : 0 const xCount = Array.isArray(title) ? title.length : 0
const labelFontSize = xCount > 8 ? 10 : xCount > 5 ? 11 : 12 const labelFontSize = xCount > 8 ? 10 : xCount > 5 ? 11 : 12
const labelRotate = xCount > 6 ? 28 : 0 const labelRotate = xCount > 6 ? 28 : 0
const echartsSeries = series.map((item, index) => { const echartsSeries = series.map((item, index) => {
const baseColor = const baseColor = MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] || '#055FC2'
item.color || const { r, g, b } = parseHexToRgb(baseColor)
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)`
const { r, g, b } = parseRgba(baseColor)
return { return {
name: item.name, name: item.name,
...@@ -102,69 +58,65 @@ const getMultiLineChart = (chartInput) => { ...@@ -102,69 +58,65 @@ const getMultiLineChart = (chartInput) => {
}, },
/* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */ /* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */
grid: { grid: {
top: 92, top: 60,
right: 10, right: 10,
bottom: 0, bottom: 0,
left: 20, left: 20,
containLabel: true containLabel: true
}, },
legend: [ legend: {
{ show: true,
show: true, type: 'scroll',
type: 'plain', orient: 'horizontal',
data: legendLine1, left: 8,
top: 4, top: 6,
left: 'center', width: '95%',
icon: 'circle', height: 24,
textStyle: { padding: [0, 24, 0, 24],
fontFamily: 'Source Han Sans CN', icon: 'circle',
fontWeight: 400, itemWidth: 12,
fontSize: 14, itemHeight: 12,
lineHeight: 24, data: allNames,
letterSpacing: 0, // 隐藏内置分页按钮与页码(使用 graphic 自定义左右箭头)
align: 'left', pageButtonPosition: 'end',
color: 'rgb(95, 101, 108)' pageIconSize: 0,
}, pageButtonGap: 0,
itemWidth: 12, pageFormatter: () => '',
itemHeight: 12 pageTextStyle: { fontSize: 0, color: 'transparent' },
}, textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
}
},
graphic: [
{ {
show: legendLine2.length > 0, type: 'polygon',
type: 'plain', name: '__legend_prev__',
data: legendLine2, left: 10,
top: 30, top: 14,
left: 'center', shape: { points: [[8, 0], [0, 6], [8, 12]] },
icon: 'circle', style: { fill: 'rgb(95, 101, 108)' },
textStyle: { cursor: 'pointer',
fontFamily: 'Source Han Sans CN', tooltip: { show: false },
fontWeight: 400, silent: false,
fontSize: 14, z: 100
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
},
itemWidth: 12,
itemHeight: 12
}, },
{ {
show: legendLine3.length > 0, type: 'polygon',
type: 'plain', name: '__legend_next__',
data: legendLine3, right: 10,
top: 56, top: 14,
left: 'center', shape: { points: [[0, 0], [8, 6], [0, 12]] },
icon: 'circle', style: { fill: 'rgb(95, 101, 108)' },
textStyle: { cursor: 'pointer',
fontFamily: 'Source Han Sans CN', tooltip: { show: false },
fontWeight: 400, silent: false,
fontSize: 14, z: 100
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
},
itemWidth: 12,
itemHeight: 12
} }
], ],
// 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致) // 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => { const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => { const seriesData = (Array.isArray(data) ? data : []).map((d, index) => {
const color = d?.color const color = MUTICHARTCOLORS[index % MUTICHARTCOLORS.length]
if (!color) return d
return { return {
...d, ...d,
itemStyle: { ...(d.itemStyle || {}), color }, itemStyle: { ...(d?.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色 // “飞线”(labelLine)跟随领域色
labelLine: { labelLine: {
...(d.labelLine || {}), ...(d?.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color } lineStyle: { ...(d?.labelLine?.lineStyle || {}), color }
} }
} }
}) })
...@@ -29,10 +30,14 @@ const getPieChart = (data) => { ...@@ -29,10 +30,14 @@ const getPieChart = (data) => {
alignTo: 'edge', alignTo: 'edge',
formatter: params => { formatter: params => {
const name = params.name || ""; const name = params.name || "";
const value = params.value ?? ""; const value = Number(params.value ?? 0) || 0;
const percent = params.percent != null ? Math.round(params.percent) : 0; const rawPercent =
params?.data?.percent != null
? params.data.percent
: params.percent;
const percent = rawPercent != null ? Number(rawPercent).toFixed(2) : "0.00";
return `{name|${name}}\n{time| ${percent}%}`; return `{name|${name}}\n{time|${value} ${percent}%}`;
}, },
minMargin: 10, minMargin: 10,
edgeDistance: 20, edgeDistance: 20,
......
...@@ -72,38 +72,46 @@ ...@@ -72,38 +72,46 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<div class="card-content"> <template v-if="hasData">
<div v-for="(item, index) in hearingData" :key="item.id"> <div class="card-content">
<div class="card-item"> <div v-for="(item, index) in hearingData" :key="item.id">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text"> <img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-title" @click="emit('report-click', item)"> <div class="card-item-text">
<span v-html="highlightText(item.title)"></span> <div class="card-item-title" @click="emit('report-click', item)">
<span v-html="highlightText(item.title)"></span>
</div>
<div class="card-item-time">
<span v-html="highlightText(item.time + ' · ' + item.content)"></span>
<img src="../images/image open.png" alt="open icon" class="card-open-image" />
</div>
<div class="card-item-category"
v-if="Array.isArray(item.category) && item.category.some(v => String(v || '').trim())">
<AreaTag v-for="(val, idx) in item.category" :key="idx" :tagName="val" />
</div>
</div> </div>
<div class="card-item-time">
<span v-html="highlightText(item.time + ' · ' + item.content)"></span>
<img src="../images/image open.png" alt="open icon" class="card-open-image" />
</div>
<div class="card-item-category">
<AreaTag v-for="(val, idx) in item.category" :key="idx" :tagName="val" />
</div>
</div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div> </div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info"> <div class="info">
共 {{ total }} 篇国会听证会 共 {{ total }} 篇国会听证会
</div> </div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="10" background layout="prev, pager, next" :total="total" <el-pagination :page-size="10" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -169,6 +177,10 @@ const props = defineProps({ ...@@ -169,6 +177,10 @@ const props = defineProps({
searchKeyword: { searchKeyword: {
type: String, type: String,
default: "" default: ""
},
loading: {
type: Boolean,
default: false
} }
}); });
...@@ -181,6 +193,12 @@ const emit = defineEmits([ ...@@ -181,6 +193,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变 // 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage, researchHearingList, hearingData, selectedYear } = toRefs(props); const { researchTypeList, researchTimeList, curFooterList, total, currentPage, researchHearingList, hearingData, selectedYear } = toRefs(props);
const hasData = computed(() => Array.isArray(hearingData.value) && hearingData.value.length > 0);
const pageCount = computed(() => {
const size = 10;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const pageSize = 10; const pageSize = 10;
function getDateYearsAgo(years) { function getDateYearsAgo(years) {
...@@ -488,8 +506,11 @@ const handleToReportDetail = item => { ...@@ -488,8 +506,11 @@ const handleToReportDetail = item => {
.right { .right {
width: 1224px; width: 1224px;
display: flex;
flex-direction: column;
.card-box { .card-box {
flex: 1;
...@@ -498,6 +519,15 @@ const handleToReportDetail = item => { ...@@ -498,6 +519,15 @@ const handleToReportDetail = item => {
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1); border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px; border-radius: 10px;
......
...@@ -52,28 +52,35 @@ ...@@ -52,28 +52,35 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <template v-if="hasData">
@click="handleToReportDetail(item)"> <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
<div class="footer-card-top"> @click="handleToReportDetail(item)">
<img :src=item.projectCoverImgUrl alt="" /> <div class="footer-card-top">
<img :src=item.projectCoverImgUrl alt="" />
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.projectNameZh)"></span>
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div> </div>
<div class="footer-card-title"> </template>
<span v-html="highlightText(item.projectNameZh)"></span> <template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div> </div>
<div class="footer-card-footer"> </template>
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info"> <div class="info">
共 {{ total }} 篇调查项目 共 {{ total }} 篇调查项目
</div> </div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total" <el-pagination :page-size="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -81,7 +88,7 @@ ...@@ -81,7 +88,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, toRefs, watch } from "vue"; import { ref, toRefs, watch, computed } from "vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME, RESOURCE_FILTER_ALL_TIME,
...@@ -126,6 +133,10 @@ const props = defineProps({ ...@@ -126,6 +133,10 @@ const props = defineProps({
searchKeyword: { searchKeyword: {
type: String, type: String,
default: "" default: ""
},
loading: {
type: Boolean,
default: false
} }
}); });
...@@ -138,6 +149,12 @@ const emit = defineEmits([ ...@@ -138,6 +149,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变 // 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props); const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props);
const hasData = computed(() => Array.isArray(curFooterList.value) && curFooterList.value.length > 0);
const pageCount = computed(() => {
const size = 12;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]); const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]); const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
...@@ -360,15 +377,27 @@ const handleToReportDetail = item => { ...@@ -360,15 +377,27 @@ const handleToReportDetail = item => {
.right { .right {
width: 1284px; width: 1284px;
display: flex;
flex-direction: column;
.card-box { .card-box {
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 13px; gap: 13px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card { .footer-card {
width: 398px; width: 398px;
height: 300px; height: 300px;
......
...@@ -52,28 +52,35 @@ ...@@ -52,28 +52,35 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <template v-if="hasData">
@click="handleToReportDetail(item)"> <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
<div class="footer-card-top"> @click="handleToReportDetail(item)">
<img :src="item.imageUrl" alt="" /> <div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.name)"></span>
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div> </div>
<div class="footer-card-title"> </template>
<span v-html="highlightText(item.name)"></span> <template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div> </div>
<div class="footer-card-footer"> </template>
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info"> <div class="info">
共 {{ total }} 篇智库报告 共 {{ total }} 篇智库报告
</div> </div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total" <el-pagination :page-size="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -81,7 +88,7 @@ ...@@ -81,7 +88,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, toRefs, watch } from "vue"; import { ref, toRefs, watch, computed } from "vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME, RESOURCE_FILTER_ALL_TIME,
...@@ -126,6 +133,10 @@ const props = defineProps({ ...@@ -126,6 +133,10 @@ const props = defineProps({
searchKeyword: { searchKeyword: {
type: String, type: String,
default: "" default: ""
},
loading: {
type: Boolean,
default: false
} }
}); });
...@@ -138,6 +149,12 @@ const emit = defineEmits([ ...@@ -138,6 +149,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变 // 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props); const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props);
const hasData = computed(() => Array.isArray(curFooterList.value) && curFooterList.value.length > 0);
const pageCount = computed(() => {
const size = 12;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]); const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]); const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
...@@ -360,15 +377,28 @@ const handleToReportDetail = item => { ...@@ -360,15 +377,28 @@ const handleToReportDetail = item => {
} }
.right { .right {
width: 1284px;
display: flex;
flex-direction: column;
.card-box { .card-box {
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 13px; gap: 13px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card { .footer-card {
width: 398px; width: 398px;
height: 300px; height: 300px;
......
...@@ -53,18 +53,20 @@ ...@@ -53,18 +53,20 @@
<ThinkTankReport v-if="isThinkTankReport" :research-type-list="researchTypeList" <ThinkTankReport v-if="isThinkTankReport" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters" :research-time-list="researchTimeList" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters"
:cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :search-keyword="searchReport" :cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :search-keyword="searchReport"
:loading="isThinkTankReportLoading"
@update:selected-filters="handleSelectedFiltersUpdate" @update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange" @filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToReportDetail" /> @report-click="handleToReportDetail" />
<CongressHearing v-else-if="isCongressHearing" :research-type-list="researchTypeList" <CongressHearing v-else-if="isCongressHearing" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`国会听证会-${tabResetKey}`" :research-hearing-list="researchHearingList" :research-time-list="researchTimeList" :key="`国会听证会-${tabResetKey}`" :research-hearing-list="researchHearingList"
:selected-filters="selectedFilters" :selected-year="selectedYear" :total="total" :current-page="currentPage" :selected-filters="selectedFilters" :selected-year="selectedYear" :total="total" :current-page="currentPage"
:search-keyword="searchReport" :hearing-data="hearingData" @update:selected-filters="handleSelectedFiltersUpdate" :search-keyword="searchReport" :hearing-data="hearingData" :loading="isCongressHearingLoading"
@update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange" @filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToHearingDetail" /> @report-click="handleToHearingDetail" />
<SurveyForm v-else-if="isSurveyForm" :research-type-list="researchTypeList" :research-time-list="researchTimeList" <SurveyForm v-else-if="isSurveyForm" :research-type-list="researchTypeList" :research-time-list="researchTimeList"
:key="`调查项目-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterProjectList" :key="`调查项目-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterProjectList"
:total="total" :current-page="currentPage" :search-keyword="searchReport" :total="total" :current-page="currentPage" :search-keyword="searchReport" :loading="isSurveyFormLoading"
@update:selected-filters="handleSelectedFiltersUpdate" @update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange" @filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToProjectDetail" /> @report-click="handleToProjectDetail" />
...@@ -108,6 +110,10 @@ const isThinkTankReport = ref(true); ...@@ -108,6 +110,10 @@ const isThinkTankReport = ref(true);
const isSurveyForm = ref(false); const isSurveyForm = ref(false);
const isCongressHearing = ref(false); const isCongressHearing = ref(false);
const searchReport = ref('') const searchReport = ref('')
// 智库详情-智库动态:右侧列表 loading(居中显示)
const isThinkTankReportLoading = ref(false);
const isSurveyFormLoading = ref(false);
const isCongressHearingLoading = ref(false);
const handleToReportDetail = (item) => { const handleToReportDetail = (item) => {
window.sessionStorage.setItem('curTabName', item.name) window.sessionStorage.setItem('curTabName', item.name)
...@@ -248,6 +254,10 @@ const handleChooseType = async (type) => { ...@@ -248,6 +254,10 @@ const handleChooseType = async (type) => {
await handleGetThinkDynamicsReport() await handleGetThinkDynamicsReport()
} }
const researchTimeList = ref([ const researchTimeList = ref([
{
id: '2026年',
name: '2026年',
},
{ {
id: '2025年', id: '2025年',
name: '2025年', name: '2025年',
...@@ -395,6 +405,11 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -395,6 +405,11 @@ const handleGetThinkDynamicsReport = async (payload) => {
return; return;
} }
try { try {
// 仅当前 tab 显示 loading,其它置 false,避免切换后残留
isThinkTankReportLoading.value = isThinkTankReport.value;
isSurveyFormLoading.value = isSurveyForm.value;
isCongressHearingLoading.value = isCongressHearing.value;
const strippedTime = stripAllTimeForRequest(nextFilters.researchTimeIds || []); const strippedTime = stripAllTimeForRequest(nextFilters.researchTimeIds || []);
const allTimeIds = (researchTimeList.value || []).map((x) => x.id); const allTimeIds = (researchTimeList.value || []).map((x) => x.id);
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection( const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
...@@ -415,6 +430,9 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -415,6 +430,9 @@ const handleGetThinkDynamicsReport = async (payload) => {
thinkTankId, thinkTankId,
pageNum: Math.max(0, (currentPage.value || 1)), pageNum: Math.max(0, (currentPage.value || 1)),
pageSize: 10, pageSize: 10,
// 国会听证会:排序语义与其它资源库相反(正序→desc,倒序→asc)
sortField: "createTime",
sortOrder: sort.value === true ? "desc" : "asc",
domainIds, domainIds,
startDate, startDate,
...@@ -481,6 +499,10 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -481,6 +499,10 @@ const handleGetThinkDynamicsReport = async (payload) => {
} }
} catch (error) { } catch (error) {
console.error("获取智库动态报告error", error); console.error("获取智库动态报告error", error);
} finally {
isThinkTankReportLoading.value = false;
isSurveyFormLoading.value = false;
isCongressHearingLoading.value = false;
} }
}; };
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => { const getPieChart = (data) => {
let option = { let option = {
series: [ series: [
...@@ -53,7 +55,10 @@ const getPieChart = (data) => { ...@@ -53,7 +55,10 @@ const getPieChart = (data) => {
labelLinePoints: points labelLinePoints: points
}; };
}, },
data: data data: (Array.isArray(data) ? data : []).map((d, index) => ({
...d,
itemStyle: { ...(d?.itemStyle || {}), color: d?.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] }
}))
}] }]
} }
return option return option
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getTreeMapChart = (treemapData) => { const getTreeMapChart = (treemapData) => {
const list = Array.isArray(treemapData) ? treemapData : []
const dataWithColors = list.map((node, idx) => ({
...node,
itemStyle: {
...(node?.itemStyle || {}),
color: node?.itemStyle?.color || MUTICHARTCOLORS[idx % MUTICHARTCOLORS.length]
}
}))
const option = { const option = {
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
...@@ -14,7 +25,7 @@ const getTreeMapChart = (treemapData) => { ...@@ -14,7 +25,7 @@ const getTreeMapChart = (treemapData) => {
series: [ series: [
{ {
type: 'treemap', type: 'treemap',
data: treemapData, data: dataWithColors,
roam: false, roam: false,
nodeClick: false, nodeClick: false,
breadcrumb: { breadcrumb: {
......
...@@ -474,7 +474,7 @@ onMounted(async () => { ...@@ -474,7 +474,7 @@ onMounted(async () => {
display: inline-flex; display: inline-flex;
position: absolute; position: absolute;
left: 277px; right: -8px;
bottom: 208px; bottom: 208px;
background-color: rgba(255, 77, 79, 1); background-color: rgba(255, 77, 79, 1);
align-items: center; align-items: center;
......
...@@ -36,26 +36,33 @@ ...@@ -36,26 +36,33 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <template v-if="hasData">
@click="emit('report-click', item)"> <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
<div class="footer-card-top"> @click="emit('report-click', item)">
<img :src="item.imageUrl" alt="" /> <div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
</div>
<div class="footer-card-title">
{{ item.name }}
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div> </div>
<div class="footer-card-title"> </template>
{{ item.name }} <template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div> </div>
<div class="footer-card-footer"> </template>
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info">共 {{ total }} 篇智库报告</div> <div class="info">共 {{ total }} 篇智库报告</div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total" <el-pagination :page-size="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="emit('page-change', $event)" :current-page="currentPage" /> @current-change="emit('page-change', $event)" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -64,20 +71,22 @@ ...@@ -64,20 +71,22 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME, RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters"; } from "../utils/resourceLibraryFilters";
defineProps({ const props = defineProps({
areaList: { type: Array, default: () => [] }, areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] }, selectedAreaList: { type: Array, default: () => [] },
pubTimeList: { type: Array, default: () => [] }, pubTimeList: { type: Array, default: () => [] },
selectedPubTimeList: { type: Array, default: () => [] }, selectedPubTimeList: { type: Array, default: () => [] },
curFooterList: { type: Array, default: () => [] }, curFooterList: { type: Array, default: () => [] },
total: { type: Number, default: 0 }, total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 } currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
}); });
const emit = defineEmits([ const emit = defineEmits([
...@@ -102,6 +111,13 @@ const formatDate = (str) => { ...@@ -102,6 +111,13 @@ const formatDate = (str) => {
const [y, m, d] = str.split('T')[0].split('-') const [y, m, d] = str.split('T')[0].split('-')
return `${y}${+m}${+d}日` return `${y}${+m}${+d}日`
}; };
const hasData = computed(() => Array.isArray(props.curFooterList) && props.curFooterList.length > 0);
const pageCount = computed(() => {
const size = 12;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -195,14 +211,25 @@ const formatDate = (str) => { ...@@ -195,14 +211,25 @@ const formatDate = (str) => {
.right { .right {
width: 1284px; width: 1284px;
max-height: 1377px; max-height: 1377px;
display: flex;
flex-direction: column;
.card-box { .card-box {
width: 1226px; width: 1226px;
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px 16px; gap: 16px 16px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card { .footer-card {
width: 398px; width: 398px;
height: 300px; height: 300px;
......
...@@ -36,26 +36,33 @@ ...@@ -36,26 +36,33 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <template v-if="hasData">
@click="emit('report-click', item)"> <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
<div class="footer-card-top"> @click="emit('report-click', item)">
<img :src="item.projectCoverImgUrl" alt="" /> <div class="footer-card-top">
<img :src="item.projectCoverImgUrl" alt="" />
</div>
<div class="footer-card-title">
{{ item.projectNameZh }}
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div> </div>
<div class="footer-card-title"> </template>
{{ item.projectNameZh }} <template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div> </div>
<div class="footer-card-footer"> </template>
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info">共 {{ total }} 篇调查项目</div> <div class="info">共 {{ total }} 篇调查项目</div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total" <el-pagination :page-size="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="emit('page-change', $event)" :current-page="currentPage" /> @current-change="emit('page-change', $event)" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -64,20 +71,22 @@ ...@@ -64,20 +71,22 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME, RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters"; } from "../utils/resourceLibraryFilters";
defineProps({ const props = defineProps({
areaList: { type: Array, default: () => [] }, areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] }, selectedAreaList: { type: Array, default: () => [] },
pubTimeList: { type: Array, default: () => [] }, pubTimeList: { type: Array, default: () => [] },
selectedPubTimeList: { type: Array, default: () => [] }, selectedPubTimeList: { type: Array, default: () => [] },
curFooterList: { type: Array, default: () => [] }, curFooterList: { type: Array, default: () => [] },
total: { type: Number, default: 0 }, total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 } currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
}); });
const formatDate = (str) => { const formatDate = (str) => {
if (!str) return '' if (!str) return ''
...@@ -101,6 +110,13 @@ const handleTimeGroupChange = (val) => { ...@@ -101,6 +110,13 @@ const handleTimeGroupChange = (val) => {
emit("update:selectedPubTimeList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME)); emit("update:selectedPubTimeList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME));
emit("filter-change"); emit("filter-change");
}; };
const hasData = computed(() => Array.isArray(props.curFooterList) && props.curFooterList.length > 0);
const pageCount = computed(() => {
const size = 12;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -170,14 +186,25 @@ const handleTimeGroupChange = (val) => { ...@@ -170,14 +186,25 @@ const handleTimeGroupChange = (val) => {
.right { .right {
width: 1284px; width: 1284px;
max-height: 1377px; max-height: 1377px;
display: flex;
flex-direction: column;
.card-box { .card-box {
width: 1226px; width: 1226px;
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px 16px; gap: 16px 16px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card { .footer-card {
width: 398px; width: 398px;
......
...@@ -55,42 +55,47 @@ ...@@ -55,42 +55,47 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<div class="card-content"> <template v-if="hasData">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id ?? index"> <div v-for="(item, index) in hearingData" :key="item.id ?? index">
<div class="card-item"> <div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" /> <img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text"> <div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)"> <div class="card-item-title" @click="emit('report-click', item)">
{{ item.titleZh }} {{ item.titleZh }}
</div> </div>
<div class="card-item-time"> <div class="card-item-time">
{{ item.testimonyDate + ' · ' + item.thinkTankName + ' · ' + item.committeeZh }} {{ item.testimonyDate + ' · ' + item.thinkTankName + ' · ' + item.committeeZh }}
<img src="../ThinkTankDetail/thinkDynamics/images/image open.png" alt="open icon" <img src="../ThinkTankDetail/thinkDynamics/images/image open.png" alt="open icon"
class="card-open-image" /> class="card-open-image" />
</div> </div>
<div class="card-item-category" v-if="item.domains"> <div class="card-item-category"
<div v-for="(value, index) in item.domains" :key="`domain-${index}`"> v-if="Array.isArray(item.domains) && item.domains.some(v => String(v || '').trim())">
<AreaTag :key="`cat-${item.id}`" :tagName="value" /> <div v-for="(value, index) in item.domains" :key="`domain-${index}`">
<AreaTag :key="`cat-${item.id}`" :tagName="value" />
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div> </div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div> </div>
</template>
</div> <template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info"> <div class="info">
{{ hearingData.length }} 篇国会听证会 共 {{ total }} 篇国会听证会
</div> </div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="pageSize" background layout="prev, pager, next" :total="total" <el-pagination :page-size="pageSize" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handlePageChange" :current-page="currentPage" /> @current-change="handlePageChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -121,7 +126,8 @@ const props = defineProps({ ...@@ -121,7 +126,8 @@ const props = defineProps({
selectedPubTimeList: { type: Array, default: () => [] }, selectedPubTimeList: { type: Array, default: () => [] },
hearingData: { type: Array, default: () => [] }, hearingData: { type: Array, default: () => [] },
total: { type: Number, default: 0 }, total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 } currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
}); });
const emit = defineEmits([ const emit = defineEmits([
...@@ -133,6 +139,12 @@ const emit = defineEmits([ ...@@ -133,6 +139,12 @@ const emit = defineEmits([
]); ]);
const pageSize = 10; const pageSize = 10;
const hasData = computed(() => Array.isArray(props.hearingData) && props.hearingData.length > 0);
const pageCount = computed(() => {
const size = Number(pageSize || 10) || 10;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
const selectedResearchIds = computed(() => ( const selectedResearchIds = computed(() => (
Array.isArray(props.selectedAreaList) && props.selectedAreaList.length Array.isArray(props.selectedAreaList) && props.selectedAreaList.length
...@@ -274,11 +286,13 @@ const handlePageChange = page => { ...@@ -274,11 +286,13 @@ const handlePageChange = page => {
.right { .right {
width: 1224px; width: 1224px;
display: flex;
flex-direction: column;
.card-box { .card-box {
width: 100%; width: 100%;
flex: 1;
display: flex; display: flex;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
box-sizing: border-box; box-sizing: border-box;
...@@ -287,6 +301,15 @@ const handlePageChange = page => { ...@@ -287,6 +301,15 @@ const handlePageChange = page => {
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1); box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
padding-right: 36px; padding-right: 36px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card-content { .card-content {
width: 1211px; width: 1211px;
......
...@@ -36,54 +36,61 @@ ...@@ -36,54 +36,61 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<div class="card-content"> <template v-if="hasData">
<div v-for="(item, index) in list" :key="item.id ?? index"> <div class="card-content">
<div class="card-item" @click="emit('item-click', item)"> <div v-for="(item, index) in list" :key="item.id ?? index">
<div class="card-item-img"> <div class="card-item" @click="emit('item-click', item)">
<img :src="item.imageUrl" alt="" /> <div class="card-item-img">
</div> <img :src="item.imageUrl" alt="" />
<div class="card-item-text">
<div class="card-item-title">{{ item.name }}</div>
<div class="card-item-time">
<span class="info-text">{{ formatDate(item.times) }} · {{ item.thinkTankName }} · {{ item.reportName
}}</span>
<div class="card-open-image" @click.stop="handleOpenReportOriginal(item)">
<img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" />
</div>
</div> </div>
<div class="card-item-category" v-if="item.tagList && item.tagList.length"> <div class="card-item-text">
<AreaTag v-for="d in item.tagList" :key="d" :tagName="d" /> <div class="card-item-title">{{ item.name }}</div>
</div> <div class="card-item-time">
<span class="info-text">{{ formatDate(item.times) }} · {{ item.thinkTankName }} · {{ item.reportName
<div class="file-box"> }}</span>
<div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name"> <div class="card-open-image" @click.stop="handleOpenReportOriginal(item)">
<div class="type">法案</div> <img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" />
<div class="title">{{ sv.name }}</div>
<div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div> </div>
</div> </div>
<div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index"> <div class="card-item-category" v-if="item.tagList && item.tagList.length">
<div class="type">政令</div> <AreaTag v-for="d in item.tagList" :key="d" :tagName="d" />
<div class="title">{{ sv.content }}</div>
<div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div> </div>
<div class="file-box">
<div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name">
<div class="type">法案</div>
<div class="title">{{ sv.name }}</div>
<div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
<div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div>
<div class="title">{{ sv.content }}</div>
<div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="divider" v-if="index !== list.length - 1"></div>
</div> </div>
<div class="divider" v-if="index !== list.length - 1"></div>
</div> </div>
</div> </template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info">共{{ total }}篇政策建议</div> <div class="info">共{{ total }}篇政策建议</div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="pageSize" background layout="prev, pager, next" :total="total" <el-pagination :page-size="pageSize" :page-count="pageCount" background layout="prev, pager, next"
@current-change="p => emit('page-change', p)" :current-page="currentPage" /> @current-change="p => emit('page-change', p)" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -92,7 +99,7 @@ ...@@ -92,7 +99,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { ref, computed } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import AreaTag from "@/components/base/AreaTag/index.vue"; import AreaTag from "@/components/base/AreaTag/index.vue";
import { import {
...@@ -103,13 +110,14 @@ import { ...@@ -103,13 +110,14 @@ import {
stripAllTimeForRequest stripAllTimeForRequest
} from "../utils/resourceLibraryFilters"; } from "../utils/resourceLibraryFilters";
defineProps({ const props = defineProps({
researchTypeList: { type: Array, default: () => [] }, researchTypeList: { type: Array, default: () => [] },
researchTimeList: { type: Array, default: () => [] }, researchTimeList: { type: Array, default: () => [] },
list: { type: Array, default: () => [] }, list: { type: Array, default: () => [] },
total: { type: Number, default: 0 }, total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 }, currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 7 }, pageSize: { type: Number, default: 7 },
loading: { type: Boolean, default: false },
}); });
const emit = defineEmits(["filter-change", "page-change", "item-click"]); const emit = defineEmits(["filter-change", "page-change", "item-click"]);
...@@ -158,6 +166,13 @@ const handleAdministrativeMoreClick = (ad) => { ...@@ -158,6 +166,13 @@ const handleAdministrativeMoreClick = (ad) => {
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]);
const hasData = computed(() => Array.isArray(props.list) && props.list.length > 0);
const pageCount = computed(() => {
const size = Number(props.pageSize || 7) || 7;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
const emitFilterToParent = () => { const emitFilterToParent = () => {
emit("filter-change", { emit("filter-change", {
researchTypeIds: stripAllAreaForRequest(selectedTypeIds.value), researchTypeIds: stripAllAreaForRequest(selectedTypeIds.value),
...@@ -298,6 +313,8 @@ const handleYearGroupChange = (val) => { ...@@ -298,6 +313,8 @@ const handleYearGroupChange = (val) => {
.right { .right {
width: 1224px; width: 1224px;
display: flex;
flex-direction: column;
.card-box { .card-box {
...@@ -311,6 +328,15 @@ const handleYearGroupChange = (val) => { ...@@ -311,6 +328,15 @@ const handleYearGroupChange = (val) => {
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1); box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
padding-right: 36px; padding-right: 36px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card-content { .card-content {
width: 1211px; width: 1211px;
height: 1067px; height: 1067px;
...@@ -332,7 +358,7 @@ const handleYearGroupChange = (val) => { ...@@ -332,7 +358,7 @@ const handleYearGroupChange = (val) => {
.card-item-img { .card-item-img {
width: 56px; width: 156px;
height: 77px; height: 77px;
margin-right: 22px; margin-right: 22px;
flex-shrink: 0; flex-shrink: 0;
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
<img class="translate-icon" src="../ReportDetail/images/image-translate.png" alt="" <img class="translate-icon" src="../ReportDetail/images/image-translate.png" alt=""
style="width: 16px; height: 16px; max-width: 16px; max-height: 16px; display: block; object-fit: contain;" /> style="width: 16px; height: 16px; max-width: 16px; max-height: 16px; display: block; object-fit: contain;" />
</div> </div>
<div class="translate-text">{{ "显示文" }}</div> <div class="translate-text">{{ "显示文" }}</div>
</div> </div>
<div class="btn" @click="handleDownload"> <div class="btn" @click="handleDownload">
<div class="icon"> <div class="icon">
...@@ -61,13 +61,15 @@ ...@@ -61,13 +61,15 @@
</div> </div>
</div> </div>
<div class="report-box"> <div class="report-box">
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage"> <!-- 英文原文:始终展示;关闭「显示译文」时占满宽度 -->
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" /> <div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlEnWithPage">
</div> <pdf :key="`left-pdf-${valueSwitch ? 'split' : 'full'}`" ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage"
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
class="pdf-pane-inner" /> class="pdf-pane-inner" />
</div> </div>
<!-- 中文译文:仅在开关打开时展示 -->
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlWithPage">
<pdf ref="rightPdfRef" :pdfUrl="reportUrlWithPage" class="pdf-pane-inner" />
</div>
</div> </div>
</div> </div>
</div> </div>
......
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { MUTICHARTCOLORS } from "@/common/constant.js";
/** /**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data * @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data
...@@ -9,70 +10,21 @@ const getMultiLineChart = (data) => { ...@@ -9,70 +10,21 @@ const getMultiLineChart = (data) => {
const series = data.data const series = data.data
const allNames = series.map((item) => item.name) const allNames = series.map((item) => item.name)
const legendSplitAt = Math.ceil(allNames.length / 2)
const legendFirstLine = allNames.slice(0, legendSplitAt)
const legendSecondLine = allNames.slice(legendSplitAt)
// 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色) const parseHexToRgb = (hex) => {
const AREA_TAG_COLOR_BY_NAME = { const h = String(hex || '').replace('#', '').trim()
'人工智能': 'rgba(245, 34, 45, 1)', // tag1 if (h.length !== 6) return { r: 0, g: 0, b: 0 }
'生物科技': 'rgba(19, 168, 168, 1)', // tag2 return {
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3 r: parseInt(h.slice(0, 2), 16),
// 兼容后端/页面常见写法 g: parseInt(h.slice(2, 4), 16),
'通信网络': 'rgba(5, 95, 194, 1)', b: parseInt(h.slice(4, 6), 16),
'量子科技': '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
}
// 兜底颜色池(未命中 AreaTag 映射时使用)
const fallbackColorList = [
'rgba(5, 95, 194, 1)',
'rgba(19, 168, 168, 1)',
'rgba(250, 140, 22, 1)',
'rgba(114, 46, 209, 1)',
'rgba(82, 196, 26, 1)',
'rgba(250, 84, 28, 1)',
'rgba(22, 119, 255, 1)',
'rgba(95, 101, 108, 1)',
'rgba(47, 84, 235, 1)',
'rgba(133, 165, 255, 1)',
]
// 解析 RGBA 颜色的辅助函数
const parseRgba = (colorStr) => {
// 匹配 rgba(r, g, b, a) 格式
const match = colorStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/);
if (match) {
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
a: parseFloat(match[4])
};
} }
// 默认返回黑色 }
return { r: 0, g: 0, b: 0, a: 1 };
};
// 动态生成 series 配置 // 动态生成 series 配置
const echartsSeries = series.map((item, index) => { const echartsSeries = series.map((item, index) => {
// 获取当前系列的颜色(优先使用item.color,否则用预设颜色,再否则随机) const baseColor = item.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] || '#055FC2'
const baseColor = const { r, g, b } = parseHexToRgb(baseColor)
item.color ||
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)`;
const { r, g, b } = parseRgba(baseColor);
return ({ return ({
name: item.name, name: item.name,
...@@ -89,11 +41,11 @@ const getMultiLineChart = (data) => { ...@@ -89,11 +41,11 @@ const getMultiLineChart = (data) => {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, // 顶部 offset: 0, // 顶部
color: `rgba(${r}, ${g}, ${b}, 0.1)` // 按需求:0.1 -> 0 color: `rgba(${r}, ${g}, ${b}, 0.1)`
}, },
{ {
offset: 1, // 底部 offset: 1, // 底部
color: `rgba(${r}, ${g}, ${b}, 0)` // 0 透明度 color: `rgba(${r}, ${g}, ${b}, 0)`
} }
]) ])
}, },
...@@ -118,46 +70,33 @@ const getMultiLineChart = (data) => { ...@@ -118,46 +70,33 @@ const getMultiLineChart = (data) => {
}, },
/* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */ /* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */
grid: { grid: {
top: '34%', top: 68,
right: '3%', right: '3%',
bottom: '5%', bottom: '5%',
left: '2%', left: '2%',
containLabel: true containLabel: true
}, },
legend: [ legend: {
{ show: true,
show: true, type: 'plain',
type: 'plain', orient: 'horizontal',
data: legendFirstLine, left: 'center',
top: 8, top: 8,
left: 'center', width: '90%',
icon: 'circle', height: 24,
textStyle: { icon: 'circle',
fontFamily: 'Source Han Sans CN', itemWidth: 12,
fontWeight: 400, itemHeight: 12,
fontSize: 14, data: allNames,
lineHeight: 24, textStyle: {
letterSpacing: 0, fontFamily: 'Source Han Sans CN',
align: 'left' fontWeight: 400,
} fontSize: 14,
}, lineHeight: 24,
{ letterSpacing: 0,
show: legendSecondLine.length > 0, align: 'left'
type: 'plain',
data: legendSecondLine,
top: 32,
left: 'center',
icon: 'circle',
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left'
}
} }
], },
// 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致) // 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
xAxis: [ xAxis: [
{ {
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => { const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => { const seriesData = (Array.isArray(data) ? data : []).map((d, index) => {
const color = d?.color const color = d?.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length]
if (!color) return d
return { return {
...d, ...d,
itemStyle: { ...(d.itemStyle || {}), color }, itemStyle: { ...(d?.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色 // “飞线”(labelLine)跟随领域色
labelLine: { labelLine: {
...(d.labelLine || {}), ...(d?.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color } lineStyle: { ...(d?.labelLine?.lineStyle || {}), color }
} }
} }
}) })
...@@ -37,10 +38,15 @@ const getPieChart = (data) => { ...@@ -37,10 +38,15 @@ const getPieChart = (data) => {
alignTo: 'edge', alignTo: 'edge',
formatter: params => { formatter: params => {
const name = params.name || ""; const name = params.name || "";
const value = params.value ?? ""; const value = Number(params.value ?? 0) || 0;
const percent = params.percent != null ? Math.round(params.percent) : 0; const rawPercent =
params?.data?.percent != null
? params.data.percent
: params.percent;
const percent = rawPercent != null ? Number(rawPercent).toFixed(2) : "0.00";
return `{name|${name}}\n{time| ${percent}%}`; // 第二行:数值 + 百分比(同一行、同一文字样式),保持与旧版一致的两行结构
return `{name|${name}}\n{time|${value}${percent}%}`;
}, },
minMargin: 5, minMargin: 5,
edgeDistance: 10, edgeDistance: 10,
......
...@@ -124,7 +124,20 @@ export function matchesEarlierChineseDate(timeStr) { ...@@ -124,7 +124,20 @@ export function matchesEarlierChineseDate(timeStr) {
/** 与政策追踪「仅全部时间」一致:固定起止(结束日按产品要求) */ /** 与政策追踪「仅全部时间」一致:固定起止(结束日按产品要求) */
export const RESOURCE_REPORT_ALL_TIME_START = "2000-01-01"; export const RESOURCE_REPORT_ALL_TIME_START = "2000-01-01";
export const RESOURCE_REPORT_ALL_TIME_END = "2025-12-31";
function getYesterdayYmd() {
const d = new Date();
// JS Date 自动处理跨月/跨年
d.setDate(d.getDate() - 1);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
export function getResourceReportAllTimeEndYmd() {
return getYesterdayYmd();
}
function getResourceReportDateYearsAgo(years) { function getResourceReportDateYearsAgo(years) {
const currentDate = new Date(); const currentDate = new Date();
...@@ -145,7 +158,7 @@ function getResourceReportTodayYmd() { ...@@ -145,7 +158,7 @@ function getResourceReportTodayYmd() {
/** /**
* 资源库 /thinkTankOverview/report:由发布时间多选(数字年 +「更早」)推导 startDate/endDate,语义对齐政策追踪 getPolicyListDateRangeFromYearList。 * 资源库 /thinkTankOverview/report:由发布时间多选(数字年 +「更早」)推导 startDate/endDate,语义对齐政策追踪 getPolicyListDateRangeFromYearList。
* - 仅「全部时间」或选满全部具体年份 → 2000-01-01 ~ 2025-12-31 * - 仅「全部时间」或选满全部具体年份 → 2000-01-01 ~ 昨天
* - 单选/多选自然年 +「更早」→ 取最小年 01-01 与最大年 12-31 包络(「更早」为 2000~2020) * - 单选/多选自然年 +「更早」→ 取最小年 01-01 与最大年 12-31 包络(「更早」为 2000~2020)
* - 无法解析时的兜底:近 relativeYearsAgo 年至今天 * - 无法解析时的兜底:近 relativeYearsAgo 年至今天
* *
...@@ -169,7 +182,7 @@ export function getResourceLibraryReportDateRangeFromTimeSelection( ...@@ -169,7 +182,7 @@ export function getResourceLibraryReportDateRangeFromTimeSelection(
if (isAllTime) { if (isAllTime) {
return { return {
startDate: RESOURCE_REPORT_ALL_TIME_START, startDate: RESOURCE_REPORT_ALL_TIME_START,
endDate: RESOURCE_REPORT_ALL_TIME_END, endDate: getResourceReportAllTimeEndYmd(),
}; };
} }
...@@ -206,6 +219,6 @@ export function getResourceLibraryReportDateRangeFromTimeSelection( ...@@ -206,6 +219,6 @@ export function getResourceLibraryReportDateRangeFromTimeSelection(
} }
return { return {
startDate: `${minY}-01-01`, startDate: `${minY}-01-01`,
endDate: `${maxY}-12-31`, endDate: maxY === new Date().getFullYear() ? getYesterdayYmd() : `${maxY}-12-31`,
}; };
} }
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getSankeyChart = (nodes, links) => { const getSankeyChart = (nodes, links) => {
const formatAmountWan = (v) => {
const n = Number(v)
if (!Number.isFinite(n)) return '0.00万'
return `${n.toFixed(2)}万`
}
const option = { const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: 'rgba(234, 236, 238, 1)',
borderWidth: 1,
textStyle: {
color: 'rgb(59, 65, 75)',
fontFamily: 'Microsoft YaHei',
fontSize: 14,
lineHeight: 22
},
formatter: function (params) {
// 仅对连线展示资金金额;节点保持默认名称提示
if (params?.dataType === 'edge') {
const amount = formatAmountWan(params?.data?.value)
return `资金金额:${amount}`
}
return params?.name ?? ''
}
},
series: { series: {
type: 'sankey', type: 'sankey',
layout: 'none', layout: 'none',
...@@ -9,6 +37,11 @@ const getSankeyChart = (nodes, links) => { ...@@ -9,6 +37,11 @@ const getSankeyChart = (nodes, links) => {
right: '15%', right: '15%',
top: '5%', top: '5%',
bottom: '5%', bottom: '5%',
color: MUTICHARTCOLORS,
lineStyle: {
color: 'rgb(230, 231, 232)',
opacity: 1
},
emphasis: { emphasis: {
focus: 'adjacency' focus: 'adjacency'
}, },
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论