提交 84861ffe authored 作者: coderBryanFu's avatar coderBryanFu

feat:新增goToPage.js文件

流水线 #586 已通过 于阶段
in 1 分 38 秒
...@@ -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
} }
}, },
......
...@@ -13,8 +13,39 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -13,8 +13,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) {
...@@ -121,6 +152,7 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -121,6 +152,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) {
if (error?.name !== "AbortError") {
console.error("合作限制政策对比图表解读请求失败", error); console.error("合作限制政策对比图表解读请求失败", error);
aiContentLeft.value = "解读加载失败"; 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) {
if (error?.name !== "AbortError") {
console.error("合作限制领域分布图表解读请求失败", error); console.error("合作限制领域分布图表解读请求失败", error);
aiContentRight.value = "解读加载失败"; 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;
if (e?.name !== "AbortError") {
console.error("图表解读请求失败", e); console.error("图表解读请求失败", e);
targetRef.value = "解读加载失败"; 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) {
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error); console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败"; 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) {
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error); console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败"; 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) {
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error); console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败"; aiContentBox5.value = "解读加载失败";
}
} finally { } finally {
isBox5InterpretLoading.value = false; isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
} }
}; };
......
<template> <template>
<div class="wrap"> <div class="wrap" ref="pageScrollRef">
<div class="top"> <div class="top">
<div class="box1 box"> <div class="box1 box">
<div class="box-header"> <div class="box-header">
...@@ -228,6 +228,7 @@ ...@@ -228,6 +228,7 @@
</div> </div>
<div class="right" v-loading="loading"> <div class="right" v-loading="loading">
<div class="right-main"> <div class="right-main">
<template v-if="policyList && policyList.length">
<div class="right-main-item" v-for="item in policyList" :key="item.id"> <div class="right-main-item" v-for="item in policyList" :key="item.id">
<div class="item-left"> <div class="item-left">
<img :src="item.imageUrl" alt="" /> <img :src="item.imageUrl" alt="" />
...@@ -240,7 +241,6 @@ ...@@ -240,7 +241,6 @@
<span v-html="highlightPolicyText(`${item.reportDateDisplay}·${item.reportName}`)"></span> <span v-html="highlightPolicyText(`${item.reportDateDisplay}·${item.reportName}`)"></span>
</div> </div>
<div class="more" @click="toDetail(item)"> <div class="more" @click="toDetail(item)">
<img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" /> <img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" />
</div> </div>
</div> </div>
...@@ -269,11 +269,17 @@ ...@@ -269,11 +269,17 @@
<div></div> <div></div>
</div> </div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="policy-tracking-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="10" background layout="prev, pager, next" :total="total" <el-pagination :page-size="10" :page-count="pageCount" background layout="prev, pager, next" :total="total"
@current-change="handleCurrentChange" :current-page="currentPage" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -309,6 +315,7 @@ import { useRouter } from "vue-router"; ...@@ -309,6 +315,7 @@ import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
const pageScrollRef = ref(null);
/** 与 AreaTag 一致的领域色(取 tag 的文字色) */ /** 与 AreaTag 一致的领域色(取 tag 的文字色) */
const AREA_TAG_COLOR_BY_NAME = { const AREA_TAG_COLOR_BY_NAME = {
...@@ -357,34 +364,55 @@ const POLICY_FILTER_ALL_AREA = "全部领域"; ...@@ -357,34 +364,55 @@ const POLICY_FILTER_ALL_AREA = "全部领域";
const POLICY_FILTER_ALL_TIME = "全部时间"; const POLICY_FILTER_ALL_TIME = "全部时间";
const POLICY_FILTER_ALL_DEPT = "全部部门"; const POLICY_FILTER_ALL_DEPT = "全部部门";
// 刷新后默认展示 3 个图表 AI 总结 // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentPolicyPt1 = ref(true); const isShowAiContentPolicyPt1 = ref(false);
const aiContentPolicyPt1 = ref(""); const aiContentPolicyPt1 = ref("");
const isPolicyPt1InterpretLoading = ref(false); const isPolicyPt1InterpretLoading = ref(false);
const pt1AiAbortController = ref(null);
const handleSwitchAiContentShowPolicyPt1 = (val) => { const handleSwitchAiContentShowPolicyPt1 = (val) => {
isShowAiContentPolicyPt1.value = val; isShowAiContentPolicyPt1.value = val;
if (val) { if (val) {
fetchPolicyPtBox1ChartInterpretation(); fetchPolicyPtBox1ChartInterpretation();
} else {
if (pt1AiAbortController.value) {
pt1AiAbortController.value.abort();
pt1AiAbortController.value = null;
}
isPolicyPt1InterpretLoading.value = false;
} }
}; };
const isShowAiContentPolicyPt2 = ref(true); const isShowAiContentPolicyPt2 = ref(false);
const aiContentPolicyPt2 = ref(""); const aiContentPolicyPt2 = ref("");
const isPolicyPt2InterpretLoading = ref(false); const isPolicyPt2InterpretLoading = ref(false);
const pt2AiAbortController = ref(null);
const handleSwitchAiContentShowPolicyPt2 = (val) => { const handleSwitchAiContentShowPolicyPt2 = (val) => {
isShowAiContentPolicyPt2.value = val; isShowAiContentPolicyPt2.value = val;
if (val) { if (val) {
fetchPolicyPtBox2ChartInterpretation(); fetchPolicyPtBox2ChartInterpretation();
} else {
if (pt2AiAbortController.value) {
pt2AiAbortController.value.abort();
pt2AiAbortController.value = null;
}
isPolicyPt2InterpretLoading.value = false;
} }
}; };
const isShowAiContentPolicyPt3 = ref(true); const isShowAiContentPolicyPt3 = ref(false);
const aiContentPolicyPt3 = ref(""); const aiContentPolicyPt3 = ref("");
const isPolicyPt3InterpretLoading = ref(false); const isPolicyPt3InterpretLoading = ref(false);
const pt3AiAbortController = ref(null);
const handleSwitchAiContentShowPolicyPt3 = (val) => { const handleSwitchAiContentShowPolicyPt3 = (val) => {
isShowAiContentPolicyPt3.value = val; isShowAiContentPolicyPt3.value = val;
if (val) { if (val) {
fetchPolicyPtBox3ChartInterpretation(); fetchPolicyPtBox3ChartInterpretation();
} else {
if (pt3AiAbortController.value) {
pt3AiAbortController.value.abort();
pt3AiAbortController.value = null;
}
isPolicyPt3InterpretLoading.value = false;
} }
}; };
// import Img1 from "./images/img1.png"; // import Img1 from "./images/img1.png";
...@@ -460,7 +488,7 @@ const box1YearList = ref([ ...@@ -460,7 +488,7 @@ const box1YearList = ref([
value: "2022" value: "2022"
}, },
]); ]);
const selectableYears = ref(["2025年", "2024年", "2023年", "2022年", "2021年", "更早"]); const selectableYears = ref(["2026年", "2025年", "2024年", "2023年", "2022年", "2021年", "更早"]);
const selectableDepartment = ref([]); const selectableDepartment = ref([]);
const handleGetThinkPolicyIndustry = async () => { const handleGetThinkPolicyIndustry = async () => {
aiContentPolicyPt1.value = ""; aiContentPolicyPt1.value = "";
...@@ -472,25 +500,48 @@ const handleGetThinkPolicyIndustry = async () => { ...@@ -472,25 +500,48 @@ const handleGetThinkPolicyIndustry = async () => {
const res = await getThinkPolicyIndustry(parmas); const res = await getThinkPolicyIndustry(parmas);
console.log("提出建议领域分布", res); console.log("提出建议领域分布", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const list = Array.isArray(res.data) ? res.data.slice(0, 7) : []; const raw = Array.isArray(res.data) ? res.data : [];
if (!list.length) { if (!raw.length) {
box1Data.value = []; box1Data.value = [];
return; return;
} }
const data = list.map((item, idx) => ({ // 前端统一计算:总项数 + 百分比(保留两位小数),展示前 7,若超过 7 则追加“其他”
const listSorted = raw
.map((item) => ({
industry: String(item?.industry || "").trim(),
amount: Number(item?.amount ?? 0) || 0
}))
.filter((item) => item.industry)
.sort((a, b) => b.amount - a.amount);
const total = listSorted.reduce((s, it) => s + (Number(it.amount) || 0), 0);
const top = listSorted.slice(0, 7);
const rest = listSorted.slice(7);
const topSum = top.reduce((s, it) => s + (Number(it.amount) || 0), 0);
const data = top.map((item, idx) => ({
name: item.industry, name: item.industry,
value: item.amount, value: item.amount,
percent: item.percent, percent: total > 0 ? ((item.amount / total) * 100).toFixed(2) : "0.00",
color: getAreaTagColor(item.industry, idx) color: getAreaTagColor(item.industry, idx)
})); }));
// 超过 7 个领域时:追加“其他”
if (rest.length > 0 && top.length === 7) {
const otherValue = Math.max(0, total - topSum);
if (otherValue > 0) {
data.push({
name: "其他",
value: otherValue,
percent: total > 0 ? ((otherValue / total) * 100).toFixed(2) : "0.00",
color: getAreaTagColor("其他", data.length)
});
}
}
box1Data.value = data; box1Data.value = data;
/* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */ /* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */
await nextTick(); await nextTick();
const box1Chart = getPieChart(box1Data.value); const box1Chart = getPieChart(box1Data.value);
setChart(box1Chart, "box1Chart"); setChart(box1Chart, "box1Chart");
if (isShowAiContentPolicyPt1.value) { // 仅在用户打开 AI 面板时才请求解读
fetchPolicyPtBox1ChartInterpretation();
}
} else { } else {
box1Data.value = []; box1Data.value = [];
} }
...@@ -560,9 +611,7 @@ const handleGetPolicyAdviceDeptDistribution = async () => { ...@@ -560,9 +611,7 @@ const handleGetPolicyAdviceDeptDistribution = async () => {
await nextTick(); await nextTick();
const box2Chart = getPieChart(box2Data.value); const box2Chart = getPieChart(box2Data.value);
setChart(box2Chart, "box2Chart"); setChart(box2Chart, "box2Chart");
if (isShowAiContentPolicyPt2.value) { // 仅在用户打开 AI 面板时才请求解读
fetchPolicyPtBox2ChartInterpretation();
}
} else { } else {
box2Data.value = []; box2Data.value = [];
} }
...@@ -726,9 +775,7 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -726,9 +775,7 @@ const handleGetThinkPolicyIndustryChange = async () => {
} }
box3Data.value = frontendData; box3Data.value = frontendData;
await renderBox3Chart(); await renderBox3Chart();
if (isShowAiContentPolicyPt3.value) { // 仅在用户打开 AI 面板时才请求解读
fetchPolicyPtBox3ChartInterpretation();
}
} else { } else {
box3Data.value = { title: [], data: [] }; box3Data.value = { title: [], data: [] };
} }
...@@ -776,6 +823,10 @@ const fetchPolicyPtBox1ChartInterpretation = async () => { ...@@ -776,6 +823,10 @@ const fetchPolicyPtBox1ChartInterpretation = async () => {
if (hasValidContent || isPolicyPt1InterpretLoading.value) { if (hasValidContent || isPolicyPt1InterpretLoading.value) {
return; return;
} }
if (pt1AiAbortController.value) {
pt1AiAbortController.value.abort();
}
pt1AiAbortController.value = new AbortController();
isPolicyPt1InterpretLoading.value = true; isPolicyPt1InterpretLoading.value = true;
aiContentPolicyPt1.value = "解读生成中…"; aiContentPolicyPt1.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -791,6 +842,7 @@ const fetchPolicyPtBox1ChartInterpretation = async () => { ...@@ -791,6 +842,7 @@ const fetchPolicyPtBox1ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: pt1AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentPolicyPt1, chunk); appendAiInterpretationChunk(aiContentPolicyPt1, chunk);
} }
...@@ -799,10 +851,13 @@ const fetchPolicyPtBox1ChartInterpretation = async () => { ...@@ -799,10 +851,13 @@ const fetchPolicyPtBox1ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentPolicyPt1.value = text || aiContentPolicyPt1.value || "未返回有效解读内容"; aiContentPolicyPt1.value = text || aiContentPolicyPt1.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("政策追踪领域分布图表解读请求失败", error); console.error("政策追踪领域分布图表解读请求失败", error);
aiContentPolicyPt1.value = "解读加载失败"; aiContentPolicyPt1.value = "解读加载失败";
}
} finally { } finally {
isPolicyPt1InterpretLoading.value = false; isPolicyPt1InterpretLoading.value = false;
pt1AiAbortController.value = null;
} }
}; };
...@@ -821,6 +876,10 @@ const fetchPolicyPtBox2ChartInterpretation = async () => { ...@@ -821,6 +876,10 @@ const fetchPolicyPtBox2ChartInterpretation = async () => {
if (hasValidContent || isPolicyPt2InterpretLoading.value) { if (hasValidContent || isPolicyPt2InterpretLoading.value) {
return; return;
} }
if (pt2AiAbortController.value) {
pt2AiAbortController.value.abort();
}
pt2AiAbortController.value = new AbortController();
isPolicyPt2InterpretLoading.value = true; isPolicyPt2InterpretLoading.value = true;
aiContentPolicyPt2.value = "解读生成中…"; aiContentPolicyPt2.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -836,6 +895,7 @@ const fetchPolicyPtBox2ChartInterpretation = async () => { ...@@ -836,6 +895,7 @@ const fetchPolicyPtBox2ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: pt2AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentPolicyPt2, chunk); appendAiInterpretationChunk(aiContentPolicyPt2, chunk);
} }
...@@ -844,10 +904,13 @@ const fetchPolicyPtBox2ChartInterpretation = async () => { ...@@ -844,10 +904,13 @@ const fetchPolicyPtBox2ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentPolicyPt2.value = text || aiContentPolicyPt2.value || "未返回有效解读内容"; aiContentPolicyPt2.value = text || aiContentPolicyPt2.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("政策追踪部门分布图表解读请求失败", error); console.error("政策追踪部门分布图表解读请求失败", error);
aiContentPolicyPt2.value = "解读加载失败"; aiContentPolicyPt2.value = "解读加载失败";
}
} finally { } finally {
isPolicyPt2InterpretLoading.value = false; isPolicyPt2InterpretLoading.value = false;
pt2AiAbortController.value = null;
} }
}; };
...@@ -872,6 +935,10 @@ const fetchPolicyPtBox3ChartInterpretation = async () => { ...@@ -872,6 +935,10 @@ const fetchPolicyPtBox3ChartInterpretation = async () => {
if (hasValidContent || isPolicyPt3InterpretLoading.value) { if (hasValidContent || isPolicyPt3InterpretLoading.value) {
return; return;
} }
if (pt3AiAbortController.value) {
pt3AiAbortController.value.abort();
}
pt3AiAbortController.value = new AbortController();
isPolicyPt3InterpretLoading.value = true; isPolicyPt3InterpretLoading.value = true;
aiContentPolicyPt3.value = "解读生成中…"; aiContentPolicyPt3.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -889,6 +956,7 @@ const fetchPolicyPtBox3ChartInterpretation = async () => { ...@@ -889,6 +956,7 @@ const fetchPolicyPtBox3ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: pt3AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentPolicyPt3, chunk); appendAiInterpretationChunk(aiContentPolicyPt3, chunk);
} }
...@@ -897,10 +965,13 @@ const fetchPolicyPtBox3ChartInterpretation = async () => { ...@@ -897,10 +965,13 @@ const fetchPolicyPtBox3ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentPolicyPt3.value = text || aiContentPolicyPt3.value || "未返回有效解读内容"; aiContentPolicyPt3.value = text || aiContentPolicyPt3.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("政策追踪研究领域趋势图表解读请求失败", error); console.error("政策追踪研究领域趋势图表解读请求失败", error);
aiContentPolicyPt3.value = "解读加载失败"; aiContentPolicyPt3.value = "解读加载失败";
}
} finally { } finally {
isPolicyPt3InterpretLoading.value = false; isPolicyPt3InterpretLoading.value = false;
pt3AiAbortController.value = null;
} }
}; };
...@@ -1124,16 +1195,23 @@ const POLICY_YEAR_EARLIER_START = 2000; ...@@ -1124,16 +1195,23 @@ const POLICY_YEAR_EARLIER_START = 2000;
const POLICY_YEAR_EARLIER_END = 2020; const POLICY_YEAR_EARLIER_END = 2020;
const POLICY_YEAR_LABEL_RE = /^(\d{4})年$/; const POLICY_YEAR_LABEL_RE = /^(\d{4})年$/;
/** 勾选「全部时间」时的固定起止(结束日按产品要求写死) */ /** 勾选「全部时间」时的固定起止(结束日为当前日期的前一天) */
const POLICY_ALL_TIME_START_DATE = "2000-01-01"; const POLICY_ALL_TIME_START_DATE = "2000-01-01";
const POLICY_ALL_TIME_END_DATE = "2025-12-31"; const getPolicyAllTimeEndYmd = () => {
const d = new 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}`;
};
/** /**
* 根据发布时间多选(如「2025年」「更早」)推导列表查询 startDate / endDate * 根据发布时间多选(如「2025年」「更早」)推导列表查询 startDate / endDate
* - 仅选「2025年」→ 2025-01-01 ~ 2025-12-31 * - 仅选当前年(如「2026年」)→ 2026-01-01 ~ 昨天
* - 多选多个自然年 → 取最小年 01-01 与最大年 12-31 的包络 * - 多选多个自然年 → 取最小年 01-01 与最大年 12-31 的包络
* - 「更早」→ 2000-01-01 ~ 2020-12-31,可与具体年份合并为并集区间 * - 「更早」→ 2000-01-01 ~ 2020-12-31,可与具体年份合并为并集区间
* - 仅「全部时间」→ 2000-01-01 ~ 2025-12-31(写死) * - 仅「全部时间」→ 2000-01-01 ~ 昨天
* - 未选任何项(无「全部时间」)→ 与顶部「近一年/两年/三年」一致 * - 未选任何项(无「全部时间」)→ 与顶部「近一年/两年/三年」一致
*/ */
function getPolicyListDateRangeFromYearList(labels, relativeYearYears) { function getPolicyListDateRangeFromYearList(labels, relativeYearYears) {
...@@ -1143,7 +1221,7 @@ function getPolicyListDateRangeFromYearList(labels, relativeYearYears) { ...@@ -1143,7 +1221,7 @@ function getPolicyListDateRangeFromYearList(labels, relativeYearYears) {
set.delete(POLICY_FILTER_ALL_TIME); set.delete(POLICY_FILTER_ALL_TIME);
if (set.size === 0) { if (set.size === 0) {
if (hasAllTime) { if (hasAllTime) {
return { startDate: POLICY_ALL_TIME_START_DATE, endDate: POLICY_ALL_TIME_END_DATE }; return { startDate: POLICY_ALL_TIME_START_DATE, endDate: getPolicyAllTimeEndYmd() };
} }
const years = Number(relativeYearYears) > 0 ? Number(relativeYearYears) : 1; const years = Number(relativeYearYears) > 0 ? Number(relativeYearYears) : 1;
return { startDate: getDateYearsAgo(years), endDate: getTodayYmd() }; return { startDate: getDateYearsAgo(years), endDate: getTodayYmd() };
...@@ -1169,7 +1247,7 @@ function getPolicyListDateRangeFromYearList(labels, relativeYearYears) { ...@@ -1169,7 +1247,7 @@ function getPolicyListDateRangeFromYearList(labels, relativeYearYears) {
} }
return { return {
startDate: `${minY}-01-01`, startDate: `${minY}-01-01`,
endDate: `${maxY}-12-31` endDate: maxY === new Date().getFullYear() ? getPolicyAllTimeEndYmd() : `${maxY}-12-31`
}; };
} }
...@@ -1200,10 +1278,11 @@ function normalizePolicyDomains(domains) { ...@@ -1200,10 +1278,11 @@ function normalizePolicyDomains(domains) {
.filter(Boolean); .filter(Boolean);
} }
/** 列表行映射:左侧图为 organizations[0].logoUrl,文案与法案/政令用接口字段名 */ /** 列表行映射:左侧图优先 coverImageUrl(兼容 coverImage),回退 organizations[0].logoUrl */
function mapPolicyRowToView(row) { function mapPolicyRowToView(row) {
const orgs = Array.isArray(row.organizations) ? row.organizations : []; const orgs = Array.isArray(row.organizations) ? row.organizations : [];
const logoUrl = orgs[0]?.logoUrl || defaultNewsIcon; const cover = row?.coverImageUrl || row?.coverImage || "";
const logoUrl = cover || orgs[0]?.logoUrl || defaultNewsIcon;
const reportDateDisplay = formatPolicyReportDateToCn(row.reportDate); const reportDateDisplay = formatPolicyReportDateToCn(row.reportDate);
return { return {
...row, ...row,
...@@ -1241,9 +1320,35 @@ const handleSwithSort = () => { ...@@ -1241,9 +1320,35 @@ const handleSwithSort = () => {
}; };
const currentPage = ref(1); const currentPage = ref(1);
const pageCount = computed(() => {
const size = 10;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const getScrollableParent = (el) => {
let cur = el;
while (cur && cur !== document.body && cur !== document.documentElement) {
const style = window.getComputedStyle(cur);
const overflowY = style?.overflowY;
const isScrollable = overflowY === "auto" || overflowY === "scroll";
if (isScrollable && cur.scrollHeight > cur.clientHeight + 1) {
return cur;
}
cur = cur.parentElement;
}
return null;
};
const scrollToTop = async () => {
await nextTick();
const anchor = pageScrollRef.value;
if (!anchor) return;
const scrollEl = getScrollableParent(anchor) || anchor;
scrollEl.scrollTop = 0;
};
// 处理页码改变事件 // 处理页码改变事件
const handleCurrentChange = page => { const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
scrollToTop();
handleGetThinkPolicy(); handleGetThinkPolicy();
}; };
...@@ -1323,6 +1428,7 @@ onMounted(async () => { ...@@ -1323,6 +1428,7 @@ onMounted(async () => {
.wrap { .wrap {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto;
.policy-tracking-el-empty { .policy-tracking-el-empty {
padding: 0; padding: 0;
...@@ -2051,6 +2157,8 @@ onMounted(async () => { ...@@ -2051,6 +2157,8 @@ onMounted(async () => {
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
display: flex;
flex-direction: column;
.right-main { .right-main {
margin-top: 17px; margin-top: 17px;
...@@ -2060,6 +2168,15 @@ onMounted(async () => { ...@@ -2060,6 +2168,15 @@ onMounted(async () => {
padding-left: 37px; padding-left: 37px;
padding-right: 0; padding-right: 0;
max-height: 1540px; max-height: 1540px;
flex: 1;
.right-empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.right-main-item { .right-main-item {
// height: 154px; // height: 154px;
...@@ -2075,7 +2192,7 @@ onMounted(async () => { ...@@ -2075,7 +2192,7 @@ onMounted(async () => {
display: flex; display: flex;
.item-left { .item-left {
width: 57px; width: 156px;
height: 77px; height: 77px;
img { img {
......
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
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
// 兼容常见写法
'通信网络': 'rgba(5, 95, 194, 1)',
'量子科技': 'rgba(114, 46, 209, 1)', // tag4
'新能源': 'rgba(82, 196, 26, 1)', // tag5
'集成电路': 'rgba(22, 119, 255, 1)', // tag6
'海洋': 'rgba(15, 120, 199, 1)', // tag7
'先进制造': 'rgba(250, 173, 20, 1)', // tag8
'新材料': 'rgba(250, 140, 22, 1)', // tag9
'航空航天': 'rgba(47, 84, 235, 1)', // tag10
'太空': 'rgba(47, 84, 235, 1)', // tag11
'深海': 'rgba(73, 104, 161, 1)', // tag12
'极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
const fallbackColorList = [
'rgba(5, 95, 194, 1)',
'rgba(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 { return {
r: parseInt(match[1], 10), r: parseInt(h.slice(0, 2), 16),
g: parseInt(match[2], 10), g: parseInt(h.slice(2, 4), 16),
b: parseInt(match[3], 10), b: parseInt(h.slice(4, 6), 16),
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,39 +58,31 @@ const getMultiLineChart = (chartInput) => { ...@@ -102,39 +58,31 @@ 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: 'plain', type: 'scroll',
data: legendLine1, orient: 'horizontal',
top: 4, left: 8,
left: 'center', top: 6,
width: '95%',
height: 24,
padding: [0, 24, 0, 24],
icon: 'circle', icon: 'circle',
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
},
itemWidth: 12, itemWidth: 12,
itemHeight: 12 itemHeight: 12,
}, data: allNames,
{ // 隐藏内置分页按钮与页码(使用 graphic 自定义左右箭头)
show: legendLine2.length > 0, pageButtonPosition: 'end',
type: 'plain', pageIconSize: 0,
data: legendLine2, pageButtonGap: 0,
top: 30, pageFormatter: () => '',
left: 'center', pageTextStyle: { fontSize: 0, color: 'transparent' },
icon: 'circle',
textStyle: { textStyle: {
fontFamily: 'Source Han Sans CN', fontFamily: 'Source Han Sans CN',
fontWeight: 400, fontWeight: 400,
...@@ -143,28 +91,32 @@ const getMultiLineChart = (chartInput) => { ...@@ -143,28 +91,32 @@ const getMultiLineChart = (chartInput) => {
letterSpacing: 0, letterSpacing: 0,
align: 'left', align: 'left',
color: 'rgb(95, 101, 108)' color: 'rgb(95, 101, 108)'
}
}, },
itemWidth: 12, graphic: [
itemHeight: 12
},
{ {
show: legendLine3.length > 0, type: 'polygon',
type: 'plain', name: '__legend_prev__',
data: legendLine3, left: 10,
top: 56, 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 type: 'polygon',
name: '__legend_next__',
right: 10,
top: 14,
shape: { points: [[0, 0], [8, 6], [0, 12]] },
style: { fill: 'rgb(95, 101, 108)' },
cursor: 'pointer',
tooltip: { show: false },
silent: false,
z: 100
} }
], ],
// 不使用全局 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,8 +72,9 @@ ...@@ -72,8 +72,9 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="card-content"> <div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id"> <div v-for="(item, index) in hearingData" :key="item.id">
<div class="card-item"> <div class="card-item">
...@@ -87,7 +88,8 @@ ...@@ -87,7 +88,8 @@
<span v-html="highlightText(item.time + ' · ' + item.content)"></span> <span v-html="highlightText(item.time + ' · ' + item.content)"></span>
<img src="../images/image open.png" alt="open icon" class="card-open-image" /> <img src="../images/image open.png" alt="open icon" class="card-open-image" />
</div> </div>
<div class="card-item-category"> <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" /> <AreaTag v-for="(val, idx) in item.category" :key="idx" :tagName="val" />
</div> </div>
</div> </div>
...@@ -97,13 +99,19 @@ ...@@ -97,13 +99,19 @@
</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,8 +52,9 @@ ...@@ -52,8 +52,9 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)"> @click="handleToReportDetail(item)">
<div class="footer-card-top"> <div class="footer-card-top">
...@@ -67,13 +68,19 @@ ...@@ -67,13 +68,19 @@
<div class="from">{{ item.thinktankName }}</div> <div class="from">{{ item.thinktankName }}</div>
</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="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,8 +52,9 @@ ...@@ -52,8 +52,9 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)"> @click="handleToReportDetail(item)">
<div class="footer-card-top"> <div class="footer-card-top">
...@@ -67,13 +68,19 @@ ...@@ -67,13 +68,19 @@
<div class="from">{{ item.thinkTankName }}</div> <div class="from">{{ item.thinkTankName }}</div>
</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="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,8 +36,9 @@ ...@@ -36,8 +36,9 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)"> @click="emit('report-click', item)">
<div class="footer-card-top"> <div class="footer-card-top">
...@@ -51,11 +52,17 @@ ...@@ -51,11 +52,17 @@
<div class="from">{{ item.thinkTankName }}</div> <div class="from">{{ item.thinkTankName }}</div>
</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="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,8 +36,9 @@ ...@@ -36,8 +36,9 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)"> @click="emit('report-click', item)">
<div class="footer-card-top"> <div class="footer-card-top">
...@@ -51,11 +52,17 @@ ...@@ -51,11 +52,17 @@
<div class="from">{{ item.thinktankName }}</div> <div class="from">{{ item.thinktankName }}</div>
</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="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,10 +55,10 @@ ...@@ -55,10 +55,10 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="card-content"> <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" />
...@@ -72,7 +72,8 @@ ...@@ -72,7 +72,8 @@
class="card-open-image" /> class="card-open-image" />
</div> </div>
<div class="card-item-category" v-if="item.domains"> <div class="card-item-category"
v-if="Array.isArray(item.domains) && item.domains.some(v => String(v || '').trim())">
<div v-for="(value, index) in item.domains" :key="`domain-${index}`"> <div v-for="(value, index) in item.domains" :key="`domain-${index}`">
<AreaTag :key="`cat-${item.id}`" :tagName="value" /> <AreaTag :key="`cat-${item.id}`" :tagName="value" />
</div> </div>
...@@ -81,16 +82,20 @@ ...@@ -81,16 +82,20 @@
</div> </div>
<div class="divider" v-if="index !== hearingData.length - 1"></div> <div class="divider" v-if="index !== hearingData.length - 1"></div>
</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">
{{ 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,8 +36,9 @@ ...@@ -36,8 +36,9 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="card-content"> <div class="card-content">
<div v-for="(item, index) in list" :key="item.id ?? index"> <div v-for="(item, index) in list" :key="item.id ?? index">
<div class="card-item" @click="emit('item-click', item)"> <div class="card-item" @click="emit('item-click', item)">
...@@ -79,11 +80,17 @@ ...@@ -79,11 +80,17 @@
<div class="divider" v-if="index !== list.length - 1"></div> <div class="divider" v-if="index !== list.length - 1"></div>
</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;
......
...@@ -209,14 +209,10 @@ ...@@ -209,14 +209,10 @@
</el-carousel> </el-carousel>
</OverviewMainBox> </OverviewMainBox>
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" postDate="time" name="title" <RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" postDate="time" name="title"
@item-click="handleRiskSignalItemToManage" /> riskLevel="status" @item-click="handleRiskSignalItemToManage" />
<RiskSignalOverviewDetailDialog <RiskSignalOverviewDetailDialog v-model="isRiskDetailVisible" :row="riskOverviewDetailRow" name-field="title"
v-model="isRiskDetailVisible" post-date-field="time" risk-level-field="status" />
:row="riskOverviewDetailRow"
name-field="title"
post-date-field="time"
/>
</div> </div>
<DivideHeader id="position2" class="divide-header" :titleText="'资讯要闻'"></DivideHeader> <DivideHeader id="position2" class="divide-header" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center"> <div class="center-center">
...@@ -353,7 +349,11 @@ ...@@ -353,7 +349,11 @@
</div> </div>
</div> </div>
<div class="box8-main"> <div class="box8-main" :class="{ 'box8-main--empty': !hasBox8Data }">
<template v-if="!hasBox8Data">
<el-empty class="box8-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div class="box8-main-item"> <div class="box8-main-item">
<div class="box8-item" v-for="(item, index) in box8Data" :key="index" <div class="box8-item" v-for="(item, index) in box8Data" :key="index"
@click="handleBox8ToDataLibrary(item)"> @click="handleBox8ToDataLibrary(item)">
...@@ -361,21 +361,14 @@ ...@@ -361,21 +361,14 @@
:class="{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }"> :class="{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }">
{{ index + 1 }} {{ index + 1 }}
</div> </div>
<!-- <el-popover effect="dark" :content="item.clause" placement="top-start">
<template #reference> -->
<div class="item-center" <div class="item-center"
:class="{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }"> :class="{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }">
{{ item.clause }} {{ item.clause }}
</div> </div>
<!-- </template>
</el-popover> -->
<!-- <div class="item-right">{{ `${item.count}份报告 >` }}</div> -->
<div class="item-count">{{ item.count + "份报告 >" }}</div> <div class="item-count">{{ item.count + "份报告 >" }}</div>
</div> </div>
</div> </div>
</template>
</div> </div>
</div> </div>
</div> </div>
...@@ -416,12 +409,14 @@ ...@@ -416,12 +409,14 @@
v-model:selectedAreaList="selectedAreaList" :pub-time-list="pubTimeList" v-model:selectedAreaList="selectedAreaList" :pub-time-list="pubTimeList"
v-model:selectedPubTimeList="selectedPubTimeList" @filter-change="handleThinkTankReportFilterChange" v-model:selectedPubTimeList="selectedPubTimeList" @filter-change="handleThinkTankReportFilterChange"
:cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :cur-footer-list="curFooterList" :total="total" :current-page="currentPage"
:loading="isResourceReportLoading"
@report-click="handleToReportDetail" @page-change="handleCurrentChange" /> @report-click="handleToReportDetail" @page-change="handleCurrentChange" />
<HomeMainFooterSurvey v-else-if="activeCate === '调查项目'" :area-list="areaList" <HomeMainFooterSurvey v-else-if="activeCate === '调查项目'" :area-list="areaList"
v-model:selectedAreaList="surveySelectedAreaList" :pub-time-list="pubTimeList" v-model:selectedAreaList="surveySelectedAreaList" :pub-time-list="pubTimeList"
v-model:selectedPubTimeList="surveySelectedPubTimeList" @filter-change="handleSurveyFilterChange" v-model:selectedPubTimeList="surveySelectedPubTimeList" @filter-change="handleSurveyFilterChange"
:cur-footer-list="surveyFooterList" :total="surveyTotal" :current-page="surveyCurrentPage" :cur-footer-list="surveyFooterList" :total="surveyTotal" :current-page="surveyCurrentPage"
:loading="isResourceSurveyLoading"
@report-click="handleToSurveyProjectView" @page-change="handleSurveyCurrentChange" /> @report-click="handleToSurveyProjectView" @page-change="handleSurveyCurrentChange" />
<ThinkTankCongressHearingOverview v-else-if="activeCate === '国会听证会'" :key="`congress-${resourceTabResetKey}`" <ThinkTankCongressHearingOverview v-else-if="activeCate === '国会听证会'" :key="`congress-${resourceTabResetKey}`"
...@@ -429,11 +424,13 @@ ...@@ -429,11 +424,13 @@
v-model:selectedAreaList="congressSelectedAreaList" v-model:selectedAreaList="congressSelectedAreaList"
v-model:selectedPubTimeList="congressSelectedPubTimeList" :total="congressTotal" v-model:selectedPubTimeList="congressSelectedPubTimeList" :total="congressTotal"
:current-page="congressCurrentPage" @filter-change="handleCongressFilterChange" :current-page="congressCurrentPage" @filter-change="handleCongressFilterChange"
:loading="isResourceHearingLoading"
@page-change="handleCongressCurrentChange" @report-click="handleToHearingDetail" /> @page-change="handleCongressCurrentChange" @report-click="handleToHearingDetail" />
<ThinkTankPolicyAdviceOverview v-else :key="`policy-${resourceTabResetKey}`" :research-type-list="areaList" <ThinkTankPolicyAdviceOverview v-else :key="`policy-${resourceTabResetKey}`" :research-type-list="areaList"
:research-time-list="pubTimeList" :list="policyFooterList" :total="policyTotal" :research-time-list="pubTimeList" :list="policyFooterList" :total="policyTotal"
:current-page="policyCurrentPage" :page-size="7" @filter-change="handlePolicyFilterChange" :current-page="policyCurrentPage" :page-size="7" @filter-change="handlePolicyFilterChange"
:loading="isResourcePolicyLoading"
@page-change="handlePolicyCurrentChange" /> @page-change="handlePolicyCurrentChange" />
</div> </div>
...@@ -462,7 +459,6 @@ import { ...@@ -462,7 +459,6 @@ import {
getThinkTankList, getThinkTankList,
getThinkTankRiskSignal, getThinkTankRiskSignal,
getThinkTankReportDomainStats, getThinkTankReportDomainStats,
getThinkTankPolicyIndustry,
getThinkTankDonation, getThinkTankDonation,
getAllThinkTankList, getAllThinkTankList,
getThinkTankHot, getThinkTankHot,
...@@ -478,6 +474,7 @@ import { ...@@ -478,6 +474,7 @@ import {
import { getPersonSummaryInfo } from "@/api/common/index"; import { getPersonSummaryInfo } from "@/api/common/index";
import getMultiLineChart from "./utils/multiLineChart"; import getMultiLineChart from "./utils/multiLineChart";
import getPieChart from "./utils/piechart"; import getPieChart from "./utils/piechart";
import { MUTICHARTCOLORS } from "@/common/constant.js";
import getSankeyChart from "./utils/sankey"; import getSankeyChart from "./utils/sankey";
import { getChartAnalysis } from "@/api/aiAnalysis/index"; import { getChartAnalysis } from "@/api/aiAnalysis/index";
import defaultNewsIcon from "@/assets/icons/default-icon-news.png"; import defaultNewsIcon from "@/assets/icons/default-icon-news.png";
...@@ -524,33 +521,59 @@ const isRiskDetailVisible = ref(false); ...@@ -524,33 +521,59 @@ const isRiskDetailVisible = ref(false);
const statCountInfo = ref([]); const statCountInfo = ref([]);
const pageSize = ref(15) const pageSize = ref(15)
const totalAllItem = ref(0) const totalAllItem = ref(0)
const isShowAiContentBox5 = ref(true); // 资源库四个 tab 的列表 loading(右侧居中显示)
const isResourceReportLoading = ref(false);
const isResourceSurveyLoading = ref(false);
const isResourceHearingLoading = ref(false);
const isResourcePolicyLoading = ref(false);
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;
} }
}; };
// 刷新后默认展示「领域分布情况」AI 总结 // 刷新后默认展示「领域分布情况」AI 总结
const isShowAiContentBox6 = ref(true); const isShowAiContentBox6 = ref(false);
const aiContentBox6 = ref(""); const aiContentBox6 = ref("");
const isBox6InterpretLoading = ref(false); const isBox6InterpretLoading = ref(false);
const box6AiAbortController = ref(null);
const handleSwitchAiContentShowBox6 = (val) => { const handleSwitchAiContentShowBox6 = (val) => {
isShowAiContentBox6.value = val; isShowAiContentBox6.value = val;
if (val) { if (val) {
fetchBox6ChartInterpretation(); fetchBox6ChartInterpretation();
} else {
if (box6AiAbortController.value) {
box6AiAbortController.value.abort();
box6AiAbortController.value = null;
}
isBox6InterpretLoading.value = false;
} }
}; };
// 刷新后默认展示「智库资金流向」AI 总结 // 刷新后默认展示「智库资金流向」AI 总结
const isShowAiContentBox7 = ref(true); const isShowAiContentBox7 = ref(false);
const aiContentBox7 = ref(""); const aiContentBox7 = ref("");
const isBox7InterpretLoading = ref(false); const isBox7InterpretLoading = ref(false);
const box7AiAbortController = ref(null);
const handleSwitchAiContentShowBox7 = (val) => { const handleSwitchAiContentShowBox7 = (val) => {
isShowAiContentBox7.value = val; isShowAiContentBox7.value = val;
if (val) { if (val) {
fetchBox7ChartInterpretation(); fetchBox7ChartInterpretation();
} else {
if (box7AiAbortController.value) {
box7AiAbortController.value.abort();
box7AiAbortController.value = null;
}
isBox7InterpretLoading.value = false;
} }
}; };
const handleGetAllThinkTankList = async () => { const handleGetAllThinkTankList = async () => {
...@@ -985,9 +1008,17 @@ function getDateMonthsAgo(months) { ...@@ -985,9 +1008,17 @@ function getDateMonthsAgo(months) {
/** 自然年日期范围(传给 policyIndustryChange) */ /** 自然年日期范围(传给 policyIndustryChange) */
const getBox5YearDateRange = year => { const getBox5YearDateRange = year => {
const y = Number(year); const y = Number(year);
const getYesterdayYmd = () => {
const d = new Date();
d.setDate(d.getDate() - 1);
const yy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yy}-${mm}-${dd}`;
};
return { return {
startDate: `${y}-01-01`, startDate: `${y}-01-01`,
endDate: `${y}-12-31` endDate: y === new Date().getFullYear() ? getYesterdayYmd() : `${y}-12-31`
}; };
}; };
...@@ -1161,6 +1192,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -1161,6 +1192,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 = {
...@@ -1178,6 +1213,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -1178,6 +1213,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);
} }
...@@ -1186,10 +1222,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -1186,10 +1222,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) {
if (error?.name !== "AbortError") {
console.error("图表解读请求失败", error); console.error("图表解读请求失败", error);
aiContentBox5.value = "解读加载失败"; aiContentBox5.value = "解读加载失败";
}
} finally { } finally {
isBox5InterpretLoading.value = false; isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
} }
}; };
...@@ -1301,6 +1340,10 @@ const fetchBox6ChartInterpretation = async () => { ...@@ -1301,6 +1340,10 @@ const fetchBox6ChartInterpretation = async () => {
if (hasValidContent || isBox6InterpretLoading.value) { if (hasValidContent || isBox6InterpretLoading.value) {
return; return;
} }
if (box6AiAbortController.value) {
box6AiAbortController.value.abort();
}
box6AiAbortController.value = new AbortController();
isBox6InterpretLoading.value = true; isBox6InterpretLoading.value = true;
aiContentBox6.value = "解读生成中…"; aiContentBox6.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -1316,6 +1359,7 @@ const fetchBox6ChartInterpretation = async () => { ...@@ -1316,6 +1359,7 @@ const fetchBox6ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: box6AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox6, chunk); appendAiInterpretationChunk(aiContentBox6, chunk);
} }
...@@ -1324,10 +1368,13 @@ const fetchBox6ChartInterpretation = async () => { ...@@ -1324,10 +1368,13 @@ const fetchBox6ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox6.value = text || aiContentBox6.value || "未返回有效解读内容"; aiContentBox6.value = text || aiContentBox6.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("领域分布图表解读请求失败", error); console.error("领域分布图表解读请求失败", error);
aiContentBox6.value = "解读加载失败"; aiContentBox6.value = "解读加载失败";
}
} finally { } finally {
isBox6InterpretLoading.value = false; isBox6InterpretLoading.value = false;
box6AiAbortController.value = null;
} }
}; };
...@@ -1342,64 +1389,82 @@ const box6TankList = ref([ ...@@ -1342,64 +1389,82 @@ const box6TankList = ref([
} }
]); ]);
function transformToChartFormat(data) { function transformToChartFormat(data) {
// 按 AreaTag 的颜色规则映射到饼图配色(取 tag 的文字色) // 兼容 /thinkTankReport/domainStats 返回:[{ year:'2025-Q1', areaList:[{industry,amount}...] }]
const areaTagColorByName = { const rawList = Array.isArray(data) ? data : [];
"人工智能": "rgba(245, 34, 45, 1)", // tag1 const amountByIndustry = new Map();
"生物科技": "rgba(19, 168, 168, 1)", // tag2 rawList.forEach((row) => {
"新一代通信网络": "rgba(5, 95, 194, 1)", // tag3 const list = Array.isArray(row?.areaList)
// 兼容常见写法 ? row.areaList
"通信网络": "rgba(5, 95, 194, 1)", : Array.isArray(row?.industryList)
"量子科技": "rgba(114, 46, 209, 1)", // tag4 ? row.industryList
"新能源": "rgba(82, 196, 26, 1)", // tag5 : [];
"集成电路": "rgba(22, 119, 255, 1)", // tag6 list.forEach((it) => {
"海洋": "rgba(15, 120, 199, 1)", // tag7 const name = String(it?.industry || "").trim();
"先进制造": "rgba(250, 173, 20, 1)", // tag8 if (!name) return;
"新材料": "rgba(250, 140, 22, 1)", // tag9 const amt = Number(it?.amount ?? 0) || 0;
"航空航天": "rgba(47, 84, 235, 1)", // tag10 amountByIndustry.set(name, (amountByIndustry.get(name) || 0) + amt);
"太空": "rgba(47, 84, 235, 1)", // tag11 });
"深海": "rgba(73, 104, 161, 1)", // tag12 });
"极地": "rgba(133, 165, 255, 1)", // tag13 const all = [...amountByIndustry.entries()]
"核": "rgba(250, 84, 28, 1)", // tag14 .map(([name, value]) => ({ name, value }))
"其他": "rgba(82, 196, 26, 1)" // tag15 .sort((a, b) => (Number(b.value) || 0) - (Number(a.value) || 0));
}; const total = all.reduce((sum, it) => sum + (Number(it.value) || 0), 0);
const top = all.slice(0, 7);
// 未命中 AreaTag 映射时的兜底色板 const rest = all.slice(7);
const fallbackColorPalette = [ const topSum = top.reduce((sum, it) => sum + (Number(it.value) || 0), 0);
"rgba(5, 95, 194, 1)",
"rgba(245, 34, 45, 1)", const out = top.map((item, index) => ({
"rgba(19, 168, 168, 1)", name: item.name,
"rgba(250, 140, 22, 1)", value: item.value,
"rgba(114, 46, 209, 1)", percent: total > 0 ? ((Number(item.value) || 0) / total * 100).toFixed(2) : "0.00",
"rgba(82, 196, 26, 1)", color: MUTICHARTCOLORS[index % MUTICHARTCOLORS.length]
"rgba(22, 119, 255, 1)",
"rgba(250, 84, 28, 1)",
"rgba(47, 84, 235, 1)"
];
const list = Array.isArray(data) ? data.slice(0, 7) : [];
return list.map((item, index) => ({
name: item.industry,
value: item.amount,
color: areaTagColorByName[item.industry] || fallbackColorPalette[index % fallbackColorPalette.length]
})); }));
// 超过 7 个领域时,追加「其他」汇总;<=7 则不展示
if (rest.length > 0 && top.length === 7) {
const otherValue = Math.max(0, total - topSum);
if (otherValue > 0) {
out.push({
name: "其他",
value: otherValue,
percent: total > 0 ? (otherValue / total * 100).toFixed(2) : "0.00",
color: MUTICHARTCOLORS[out.length % MUTICHARTCOLORS.length]
});
}
}
return out;
} }
// 政策建议领域分布
const handleGetThinkTankPolicyIndustry = async () => { const getYesterdayYmd = () => {
const params = { const d = new Date();
year: box6selectetedYear.value 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}`;
};
const buildBox6DateRange = () => {
const y = Number(box6selectetedYear.value);
const currentY = new Date().getFullYear();
return {
startDate: `${y}-01-01`,
endDate: y === currentY ? getYesterdayYmd() : `${y}-12-31`
}; };
try { };
const res = await getThinkTankPolicyIndustry(params);
console.log("政策建议领域分布", res); // 领域分布情况(/thinkTankReport/domainStats):前端汇总项数与占比,展示前 7 + 其他
const handleGetThinkTankDomainStatsForBox6 = async () => {
try {
const range = buildBox6DateRange();
const res = await getThinkTankReportDomainStats(range);
console.log("领域分布情况(domainStats)", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
box6Data.value = transformToChartFormat(res.data); box6Data.value = transformToChartFormat(res.data);
console.log(transformToChartFormat(res.data), "datadatadata");
} else { } else {
box6Data.value = []; box6Data.value = [];
} }
} catch (error) { } catch (error) {
console.error("获取政策建议领域分布error", error); console.error("获取领域分布情况(domainStats)error", error);
box6Data.value = []; box6Data.value = [];
} }
}; };
...@@ -1415,7 +1480,19 @@ const renderBox6Chart = () => { ...@@ -1415,7 +1480,19 @@ const renderBox6Chart = () => {
const selectParam = { const selectParam = {
moduleType: '科技智库报告', moduleType: '科技智库报告',
key: 2, key: 2,
selectedDate: JSON.stringify([box6selectetedYear.value + '-01-01', box6selectetedYear.value + '-12-31']), selectedDate: JSON.stringify([
box6selectetedYear.value + "-01-01",
Number(box6selectetedYear.value) === new Date().getFullYear()
? (() => {
const d = new Date();
d.setDate(d.getDate() - 1);
const yy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yy}-${mm}-${dd}`;
})()
: box6selectetedYear.value + "-12-31"
]),
orgnizationName: box6selectetedTank.value, orgnizationName: box6selectetedTank.value,
} }
const box6Chart = getPieChart(pieData); const box6Chart = getPieChart(pieData);
...@@ -1430,7 +1507,7 @@ const handleBox6AreaChange = () => { ...@@ -1430,7 +1507,7 @@ const handleBox6AreaChange = () => {
const handleBox6 = async () => { const handleBox6 = async () => {
box6selectetedArea.value = "全部领域"; box6selectetedArea.value = "全部领域";
aiContentBox6.value = ""; aiContentBox6.value = "";
await handleGetThinkTankPolicyIndustry(); await handleGetThinkTankDomainStatsForBox6();
renderBox6Chart(); renderBox6Chart();
// 若 AI 面板已打开,让解读在首次加载时自动生成 // 若 AI 面板已打开,让解读在首次加载时自动生成
if (isShowAiContentBox6.value) { if (isShowAiContentBox6.value) {
...@@ -1459,6 +1536,21 @@ const hasBox7ChartData = computed(() => { ...@@ -1459,6 +1536,21 @@ const hasBox7ChartData = computed(() => {
function transformDataToSankey(inputData) { function transformDataToSankey(inputData) {
const nodes = []; const nodes = [];
const links = []; const links = [];
const colorByName = new Map();
let colorCursor = 0;
const getNodeColor = (name) => {
const key = String(name || "").trim();
if (!key) {
return MUTICHARTCOLORS[0];
}
if (colorByName.has(key)) {
return colorByName.get(key);
}
const c = MUTICHARTCOLORS[colorCursor % MUTICHARTCOLORS.length];
colorCursor += 1;
colorByName.set(key, c);
return c;
};
// 遍历输入数据 // 遍历输入数据
inputData.forEach(item => { inputData.forEach(item => {
...@@ -1466,7 +1558,7 @@ function transformDataToSankey(inputData) { ...@@ -1466,7 +1558,7 @@ function transformDataToSankey(inputData) {
// 添加智库节点(如果尚未添加) // 添加智库节点(如果尚未添加)
if (!nodes.some(node => node.name === thinkTankName)) { if (!nodes.some(node => node.name === thinkTankName)) {
nodes.push({ name: thinkTankName }); nodes.push({ name: thinkTankName, itemStyle: { color: getNodeColor(thinkTankName) } });
} }
// 遍历捐赠来源 // 遍历捐赠来源
...@@ -1477,12 +1569,12 @@ function transformDataToSankey(inputData) { ...@@ -1477,12 +1569,12 @@ function transformDataToSankey(inputData) {
// 添加捐赠机构节点(如果尚未添加) // 添加捐赠机构节点(如果尚未添加)
if (!nodes.some(node => node.name === institution)) { if (!nodes.some(node => node.name === institution)) {
nodes.push({ name: institution }); nodes.push({ name: institution, itemStyle: { color: getNodeColor(institution) } });
} }
// 如果存在二级机构,也添加二级机构节点(如果尚未添加) // 如果存在二级机构,也添加二级机构节点(如果尚未添加)
if (secondInstitution && !nodes.some(node => node.name === secondInstitution)) { if (secondInstitution && !nodes.some(node => node.name === secondInstitution)) {
nodes.push({ name: secondInstitution }); nodes.push({ name: secondInstitution, itemStyle: { color: getNodeColor(secondInstitution) } });
} }
// 添加链接 // 添加链接
...@@ -1570,6 +1662,10 @@ const fetchBox7ChartInterpretation = async () => { ...@@ -1570,6 +1662,10 @@ const fetchBox7ChartInterpretation = async () => {
if (hasValidContent || isBox7InterpretLoading.value) { if (hasValidContent || isBox7InterpretLoading.value) {
return; return;
} }
if (box7AiAbortController.value) {
box7AiAbortController.value.abort();
}
box7AiAbortController.value = new AbortController();
isBox7InterpretLoading.value = true; isBox7InterpretLoading.value = true;
aiContentBox7.value = "解读生成中…"; aiContentBox7.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -1586,6 +1682,7 @@ const fetchBox7ChartInterpretation = async () => { ...@@ -1586,6 +1682,7 @@ const fetchBox7ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: box7AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox7, chunk); appendAiInterpretationChunk(aiContentBox7, chunk);
} }
...@@ -1594,10 +1691,13 @@ const fetchBox7ChartInterpretation = async () => { ...@@ -1594,10 +1691,13 @@ const fetchBox7ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox7.value = text || aiContentBox7.value || "未返回有效解读内容"; aiContentBox7.value = text || aiContentBox7.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("智库资金流向图表解读请求失败", error); console.error("智库资金流向图表解读请求失败", error);
aiContentBox7.value = "解读加载失败"; aiContentBox7.value = "解读加载失败";
}
} finally { } finally {
isBox7InterpretLoading.value = false; isBox7InterpretLoading.value = false;
box7AiAbortController.value = null;
} }
}; };
...@@ -1645,6 +1745,11 @@ const box8Data = ref([ ...@@ -1645,6 +1745,11 @@ const box8Data = ref([
// } // }
]); ]);
const hasBox8Data = computed(() => {
const list = box8Data.value;
return Array.isArray(list) && list.length > 0;
});
const box8selectetedYear = ref(1); const box8selectetedYear = ref(1);
const box8YearList = ref([ const box8YearList = ref([
{ {
...@@ -1795,6 +1900,10 @@ const handleThinkTankReportFilterChange = () => { ...@@ -1795,6 +1900,10 @@ const handleThinkTankReportFilterChange = () => {
}; };
const pubTimeList = ref([ const pubTimeList = ref([
{
id: 2026,
name: "2026"
},
{ {
id: 2025, id: 2025,
name: "2025" name: "2025"
...@@ -1866,7 +1975,7 @@ const handleResourceLibrarySortChange = () => { ...@@ -1866,7 +1975,7 @@ const handleResourceLibrarySortChange = () => {
} else if (activeCate.value === "政策建议") { } else if (activeCate.value === "政策建议") {
handleGetThinkTankPolicyAdvice(); handleGetThinkTankPolicyAdvice();
} else if (activeCate.value === "国会听证会") { } else if (activeCate.value === "国会听证会") {
return; handleGetThinkTankHearings();
} else { } else {
handleGetetThinkTankReport(); handleGetetThinkTankReport();
} }
...@@ -1881,6 +1990,7 @@ const toggleResourceLibrarySortPrefix = () => { ...@@ -1881,6 +1990,7 @@ const toggleResourceLibrarySortPrefix = () => {
handleGetThinkTankPolicyAdvice(); handleGetThinkTankPolicyAdvice();
} else if (activeCate.value === "国会听证会") { } else if (activeCate.value === "国会听证会") {
congressResourceSort.value = congressResourceSort.value === true ? false : true; congressResourceSort.value = congressResourceSort.value === true ? false : true;
handleGetThinkTankHearings();
} else { } else {
sort.value = sort.value === true ? false : true; sort.value = sort.value === true ? false : true;
handleGetetThinkTankReport(); handleGetetThinkTankReport();
...@@ -1908,6 +2018,26 @@ const congressSelectedPubTimeList = ref([RESOURCE_FILTER_ALL_TIME]); ...@@ -1908,6 +2018,26 @@ const congressSelectedPubTimeList = ref([RESOURCE_FILTER_ALL_TIME]);
const congressCurrentPage = ref(1); const congressCurrentPage = ref(1);
const congressTotal = ref(0); const congressTotal = ref(0);
const scrollToResourceLibraryHeader = () => {
const header = document.getElementById("position4");
const wrapper = containerRef.value;
if (!header || !wrapper) return;
const wrapperRect = wrapper.getBoundingClientRect();
const headerRect = header.getBoundingClientRect();
// 滚动到「资源库」标题上方一点点(在 home-wrapper 这个滚动容器内)
const delta = headerRect.top - wrapperRect.top;
const targetTop = (wrapper.scrollTop || 0) + delta - 20;
wrapper.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" });
};
const scrollToResourceLibraryHeaderAfterRender = async () => {
// 等待分页数据渲染完成(对齐风险信号管理:nextTick 后再滚动)
await nextTick();
await new Promise((resolve) => requestAnimationFrame(resolve));
scrollToResourceLibraryHeader();
};
const handleCongressFilterChange = () => { const handleCongressFilterChange = () => {
congressCurrentPage.value = 1; congressCurrentPage.value = 1;
handleGetThinkTankHearings(); handleGetThinkTankHearings();
...@@ -1916,6 +2046,8 @@ const handleCongressFilterChange = () => { ...@@ -1916,6 +2046,8 @@ const handleCongressFilterChange = () => {
const handleCongressCurrentChange = (page) => { const handleCongressCurrentChange = (page) => {
congressCurrentPage.value = page; congressCurrentPage.value = page;
handleGetThinkTankHearings(); handleGetThinkTankHearings();
// 分页后让滚动条回到资源库标题上方一点点
scrollToResourceLibraryHeaderAfterRender();
}; };
const handleSurveyFilterChange = () => { const handleSurveyFilterChange = () => {
...@@ -1926,9 +2058,11 @@ const handleSurveyFilterChange = () => { ...@@ -1926,9 +2058,11 @@ const handleSurveyFilterChange = () => {
const handleSurveyCurrentChange = page => { const handleSurveyCurrentChange = page => {
surveyCurrentPage.value = page; surveyCurrentPage.value = page;
handleGetThinkTankSurvey(); handleGetThinkTankSurvey();
scrollToResourceLibraryHeaderAfterRender();
}; };
const handleGetThinkTankSurvey = async () => { const handleGetThinkTankSurvey = async () => {
isResourceSurveyLoading.value = true;
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection( const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
stripAllTimeForRequest(surveySelectedPubTimeList.value), stripAllTimeForRequest(surveySelectedPubTimeList.value),
(pubTimeList.value || []).map((x) => x.id) (pubTimeList.value || []).map((x) => x.id)
...@@ -1961,9 +2095,12 @@ const handleGetThinkTankSurvey = async () => { ...@@ -1961,9 +2095,12 @@ const handleGetThinkTankSurvey = async () => {
} }
} catch (error) { } catch (error) {
console.error("获取调查项目 error", error); console.error("获取调查项目 error", error);
} finally {
isResourceSurveyLoading.value = false;
} }
}; };
const handleGetThinkTankHearings = async () => { const handleGetThinkTankHearings = async () => {
isResourceHearingLoading.value = true;
try { try {
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection( const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
stripAllTimeForRequest(congressSelectedPubTimeList.value), stripAllTimeForRequest(congressSelectedPubTimeList.value),
...@@ -1973,6 +2110,9 @@ const handleGetThinkTankHearings = async () => { ...@@ -1973,6 +2110,9 @@ const handleGetThinkTankHearings = async () => {
pageNum: congressCurrentPage.value, pageNum: congressCurrentPage.value,
pageSize: 10, pageSize: 10,
sortFun: congressResourceSort.value === true, sortFun: congressResourceSort.value === true,
// 国会听证会:排序语义与其它资源库相反(正序→desc,倒序→asc)
sortField: "createTime",
sortOrder: congressResourceSort.value === true ? "desc" : "asc",
domainIds: (() => { domainIds: (() => {
const areas = stripAllAreaForRequest(congressSelectedAreaList.value); const areas = stripAllAreaForRequest(congressSelectedAreaList.value);
const allAreaIds = (areaList.value || []).map((a) => a.id); const allAreaIds = (areaList.value || []).map((a) => a.id);
...@@ -1994,6 +2134,8 @@ const handleGetThinkTankHearings = async () => { ...@@ -1994,6 +2134,8 @@ const handleGetThinkTankHearings = async () => {
} }
} catch (error) { } catch (error) {
console.error("获取调查项目 error", error); console.error("获取调查项目 error", error);
} finally {
isResourceHearingLoading.value = false;
} }
}; };
// ===== 政策建议:独立状态(不影响智库报告/调查项目)===== // ===== 政策建议:独立状态(不影响智库报告/调查项目)=====
...@@ -2016,9 +2158,11 @@ const handlePolicyFilterChange = payload => { ...@@ -2016,9 +2158,11 @@ const handlePolicyFilterChange = payload => {
const handlePolicyCurrentChange = page => { const handlePolicyCurrentChange = page => {
policyCurrentPage.value = page; policyCurrentPage.value = page;
handleGetThinkTankPolicyAdvice(); handleGetThinkTankPolicyAdvice();
scrollToResourceLibraryHeaderAfterRender();
}; };
const handleGetThinkTankPolicyAdvice = async () => { const handleGetThinkTankPolicyAdvice = async () => {
isResourcePolicyLoading.value = true;
const strippedPolicyYears = stripAllTimeForRequest(policySelectedYearIds.value); const strippedPolicyYears = stripAllTimeForRequest(policySelectedYearIds.value);
const allPubTimeIds = (pubTimeList.value || []).map((x) => x.id); const allPubTimeIds = (pubTimeList.value || []).map((x) => x.id);
/** 与智库报告一致:仅「全部时间」或选满所有具体年份 → 不按 years 狭义过滤 */ /** 与智库报告一致:仅「全部时间」或选满所有具体年份 → 不按 years 狭义过滤 */
...@@ -2056,7 +2200,8 @@ const handleGetThinkTankPolicyAdvice = async () => { ...@@ -2056,7 +2200,8 @@ const handleGetThinkTankPolicyAdvice = async () => {
name: item.title, name: item.title,
reportName: item.reportName, reportName: item.reportName,
times: item.date, times: item.date,
imageUrl: item.coverImage, // 列表图片:优先 coverImageUrl,兼容旧字段 coverImage
imageUrl: item.coverImageUrl || item.coverImage,
tagList: item.domains || [], tagList: item.domains || [],
thinkTankName: item.thinkTankName ?? null, thinkTankName: item.thinkTankName ?? null,
reportId: item.reportId ?? item.id, reportId: item.reportId ?? item.id,
...@@ -2079,6 +2224,8 @@ const handleGetThinkTankPolicyAdvice = async () => { ...@@ -2079,6 +2224,8 @@ const handleGetThinkTankPolicyAdvice = async () => {
policyTotal.value = 0; policyTotal.value = 0;
console.error("获取政策建议 error", error); console.error("获取政策建议 error", error);
ElMessage.warning("获取政策建议失败"); ElMessage.warning("获取政策建议失败");
} finally {
isResourcePolicyLoading.value = false;
} }
}; };
// 处理页码改变事件 // 处理页码改变事件
...@@ -2086,6 +2233,7 @@ const handleCurrentChange = page => { ...@@ -2086,6 +2233,7 @@ const handleCurrentChange = page => {
console.log(page, "pagepagepage"); console.log(page, "pagepagepage");
currentPage.value = page; currentPage.value = page;
handleGetetThinkTankReport(); handleGetetThinkTankReport();
scrollToResourceLibraryHeaderAfterRender();
}; };
function arrayToString(arr) { function arrayToString(arr) {
return arr.reduce((acc, item) => { return arr.reduce((acc, item) => {
...@@ -2098,6 +2246,7 @@ function arrayToString(arr) { ...@@ -2098,6 +2246,7 @@ function arrayToString(arr) {
//获取智库报告 //获取智库报告
const handleGetetThinkTankReport = async () => { const handleGetetThinkTankReport = async () => {
isResourceReportLoading.value = true;
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection( const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
stripAllTimeForRequest(selectedPubTimeList.value), stripAllTimeForRequest(selectedPubTimeList.value),
(pubTimeList.value || []).map((x) => x.id) (pubTimeList.value || []).map((x) => x.id)
...@@ -2128,6 +2277,8 @@ const handleGetetThinkTankReport = async () => { ...@@ -2128,6 +2277,8 @@ const handleGetetThinkTankReport = async () => {
} }
} catch (error) { } catch (error) {
console.error("获取智库报告error", error); console.error("获取智库报告error", error);
} finally {
isResourceReportLoading.value = false;
} }
}; };
...@@ -2297,9 +2448,8 @@ onMounted(async () => { ...@@ -2297,9 +2448,8 @@ onMounted(async () => {
handleGetNewReport(); handleGetNewReport();
handleGetThinkTankRiskSignal(); handleGetThinkTankRiskSignal();
// 先拉到图表数据,再打开 AI 面板并触发解读,避免初始为空导致“无内容” // 先把图表数据准备好;AI 解读仅在用户悬停打开面板时触发
await handleBox5(box5selectetedYear.value); await handleBox5(box5selectetedYear.value);
handleSwitchAiContentShowBox5(true);
// 先把图表数据准备好,避免用户悬浮太快触发解读但数据未就绪 // 先把图表数据准备好,避免用户悬浮太快触发解读但数据未就绪
await handleBox6(); await handleBox6();
await handleBox7(); await handleBox7();
...@@ -2639,7 +2789,7 @@ onBeforeUnmount(() => { ...@@ -2639,7 +2789,7 @@ onBeforeUnmount(() => {
display: inline-flex; display: inline-flex;
position: absolute; position: absolute;
left: 233px; 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;
...@@ -4165,6 +4315,22 @@ onBeforeUnmount(() => { ...@@ -4165,6 +4315,22 @@ onBeforeUnmount(() => {
height: 412px; height: 412px;
&.box8-main--empty {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
:deep(.el-empty__image) {
margin-bottom: 0;
}
}
.box8-el-empty {
padding: 0;
margin: 0;
}
.box8-main-item { .box8-main-item {
margin: 0 auto; margin: 0 auto;
margin-top: 5px; margin-top: 5px;
...@@ -4612,6 +4778,7 @@ onBeforeUnmount(() => { ...@@ -4612,6 +4778,7 @@ onBeforeUnmount(() => {
<style lang="scss"> <style lang="scss">
/* 弹窗打开时禁用轮播箭头穿透(样式仅作用于智库页结构) */ /* 弹窗打开时禁用轮播箭头穿透(样式仅作用于智库页结构) */
.home-main.is-risk-detail-open { .home-main.is-risk-detail-open {
.box1-left, .box1-left,
.box1-right { .box1-right {
pointer-events: none; pointer-events: none;
......
...@@ -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
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
// 兼容后端/页面常见写法
'通信网络': 'rgba(5, 95, 194, 1)',
'量子科技': 'rgba(114, 46, 209, 1)', // tag4
'新能源': 'rgba(82, 196, 26, 1)', // tag5
'集成电路': 'rgba(22, 119, 255, 1)', // tag6
'海洋': 'rgba(15, 120, 199, 1)', // tag7
'先进制造': 'rgba(250, 173, 20, 1)', // tag8
'新材料': 'rgba(250, 140, 22, 1)', // tag9
'航空航天': 'rgba(47, 84, 235, 1)', // tag10
'太空': 'rgba(47, 84, 235, 1)', // tag11
'深海': 'rgba(73, 104, 161, 1)', // tag12
'极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
// 兜底颜色池(未命中 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 { return {
r: parseInt(match[1]), r: parseInt(h.slice(0, 2), 16),
g: parseInt(match[2]), g: parseInt(h.slice(2, 4), 16),
b: parseInt(match[3]), b: parseInt(h.slice(4, 6), 16),
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,20 +70,24 @@ const getMultiLineChart = (data) => { ...@@ -118,20 +70,24 @@ 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',
data: legendFirstLine, orient: 'horizontal',
top: 8,
left: 'center', left: 'center',
top: 8,
width: '90%',
height: 24,
icon: 'circle', icon: 'circle',
itemWidth: 12,
itemHeight: 12,
data: allNames,
textStyle: { textStyle: {
fontFamily: 'Source Han Sans CN', fontFamily: 'Source Han Sans CN',
fontWeight: 400, fontWeight: 400,
...@@ -141,23 +97,6 @@ const getMultiLineChart = (data) => { ...@@ -141,23 +97,6 @@ const getMultiLineChart = (data) => {
align: 'left' align: 'left'
} }
}, },
{
show: legendSecondLine.length > 0,
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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论