提交 c675e799 authored 作者: 朱政's avatar 朱政

feat:智库全选功能选择框逻辑统一,高度自适应,智库部分图表格式修改,概览页近期美国智库机构发布涉华报告数量汇总样式修改

上级 09b8b813
...@@ -36,6 +36,47 @@ function parseChartInterpretationArray(buffer) { ...@@ -36,6 +36,47 @@ function parseChartInterpretationArray(buffer) {
throw new Error("无法解析图表解读 JSON 数组"); throw new Error("无法解析图表解读 JSON 数组");
} }
/**
* 从数组结果中提取可展示的解读文本
* @param {unknown[]} arr
* @returns {string}
*/
function pickInterpretationText(arr) {
if (!Array.isArray(arr) || arr.length === 0) {
return "";
}
const first = arr[0] || {};
return (
first["解读"] ||
first["interpretation"] ||
first["analysis"] ||
first["content"] ||
""
);
}
/**
* 从非标准 JSON 文本中兜底提取“解读”字段(兼容单引号/双引号)
* 示例:
* [{'图表标题': '数量变化趋势', '解读': 'xxx'}]
* [{"图表标题":"数量变化趋势","解读":"xxx"}]
* @param {string} text
* @returns {string}
*/
function extractInterpretationFromLooseText(text) {
const raw = String(text || "");
if (!raw) {
return "";
}
const reg =
/["']解读["']\s*:\s*["']([\s\S]*?)["']\s*(?:[,}\]])/;
const m = raw.match(reg);
if (!m || !m[1]) {
return "";
}
return String(m[1]).replace(/\\n/g, "\n").trim();
}
/** /**
* 图表解读(SSE 流式) * 图表解读(SSE 流式)
* @param {object} data - 请求体 * @param {object} data - 请求体
...@@ -44,9 +85,15 @@ function parseChartInterpretationArray(buffer) { ...@@ -44,9 +85,15 @@ function parseChartInterpretationArray(buffer) {
* @returns {Promise<{data: unknown[]}>} * @returns {Promise<{data: unknown[]}>}
*/ */
export function getChartAnalysis(data, options = {}) { export function getChartAnalysis(data, options = {}) {
const { onChunk } = options; const onDelta =
typeof options?.onChunk === "function"
? options.onChunk
: typeof options?.onInterpretationDelta === "function"
? options.onInterpretationDelta
: null;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let buffer = ""; let buffer = "";
let latestInterpretation = "";
let settled = false; let settled = false;
const abortController = new AbortController(); const abortController = new AbortController();
...@@ -99,6 +146,18 @@ export function getChartAnalysis(data, options = {}) { ...@@ -99,6 +146,18 @@ export function getChartAnalysis(data, options = {}) {
if (msg && typeof msg === "object" && "text" in msg) { if (msg && typeof msg === "object" && "text" in msg) {
chunk = String(msg.text ?? ""); chunk = String(msg.text ?? "");
buffer += chunk; buffer += chunk;
} else if (msg && Array.isArray(msg.data)) {
const arr = msg.data;
const interpretation = pickInterpretationText(arr);
if (interpretation) {
latestInterpretation = interpretation;
if (onDelta) {
onDelta(interpretation);
}
}
safeResolve({ data: arr });
abortController.abort();
return;
} else { } else {
chunk = raw; chunk = raw;
buffer += raw; buffer += raw;
...@@ -108,9 +167,18 @@ export function getChartAnalysis(data, options = {}) { ...@@ -108,9 +167,18 @@ export function getChartAnalysis(data, options = {}) {
buffer += raw; buffer += raw;
} }
// 兜底:非标准 JSON(如单引号 Python 风格)时,尝试直接从文本提取“解读”
const looseInterpretation = extractInterpretationFromLooseText(raw);
if (looseInterpretation) {
latestInterpretation = looseInterpretation;
safeResolve({ data: [{ 解读: looseInterpretation }] });
abortController.abort();
return;
}
// 每收到一条消息即回调,用于流式渲染 // 每收到一条消息即回调,用于流式渲染
if (chunk && typeof onChunk === "function") { if (chunk && onDelta) {
onChunk(chunk); onDelta(chunk);
} }
// 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接 // 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
...@@ -118,6 +186,10 @@ export function getChartAnalysis(data, options = {}) { ...@@ -118,6 +186,10 @@ export function getChartAnalysis(data, options = {}) {
if (trimmed.endsWith("```")) { if (trimmed.endsWith("```")) {
try { try {
const arr = parseChartInterpretationArray(trimmed); const arr = parseChartInterpretationArray(trimmed);
const interpretation = pickInterpretationText(arr);
if (interpretation) {
latestInterpretation = interpretation;
}
safeResolve({ data: arr }); safeResolve({ data: arr });
abortController.abort(); abortController.abort();
} catch (_) { } } catch (_) { }
...@@ -126,8 +198,22 @@ export function getChartAnalysis(data, options = {}) { ...@@ -126,8 +198,22 @@ export function getChartAnalysis(data, options = {}) {
onclose: () => { onclose: () => {
try { try {
const arr = parseChartInterpretationArray(buffer); const arr = parseChartInterpretationArray(buffer);
const interpretation = pickInterpretationText(arr);
if (interpretation) {
latestInterpretation = interpretation;
}
safeResolve({ data: arr }); safeResolve({ data: arr });
} catch (e) { } catch (e) {
// 兜底:整体 buffer 不是标准 JSON(如单引号)时直接提取“解读”
const looseInterpretation = extractInterpretationFromLooseText(buffer);
if (looseInterpretation) {
safeResolve({ data: [{ 解读: looseInterpretation }] });
return;
}
if (latestInterpretation) {
safeResolve({ data: [{ 解读: latestInterpretation }] });
return;
}
safeReject(e); safeReject(e);
} }
}, },
......
...@@ -1156,6 +1156,15 @@ ...@@ -1156,6 +1156,15 @@
| -------- | -------- | ----- | -------- | -------- | ------ | | -------- | -------- | ----- | -------- | -------- | ------ |
|areas|区域名称列表|query|false|array|string| |areas|区域名称列表|query|false|array|string|
|researchTypeIds|研究类型ID列表|query|false|array|string| |researchTypeIds|研究类型ID列表|query|false|array|string|
|domainIds|科技领域 ID 列表(逗号分隔)|query|false|string||
|startDate|发布时间起 YYYY-MM-DD(与政策追踪发布时间逻辑一致)|query|false|string||
|endDate|发布时间止 YYYY-MM-DD|query|false|string||
|category|分类(如调查项目)|query|false|string||
|pageNum|页码|query|false|integer||
|pageSize|每页条数|query|false|integer||
|sortFun|排序|query|false|boolean||
|thinkTankId|智库 ID(详情页动态列表限定当前智库)|query|false|string||
|keyword|关键词搜索(智库动态)|query|false|string||
|token|Token Request Header|header|false|string|| |token|Token Request Header|header|false|string||
......
...@@ -87,7 +87,11 @@ export function getHylyList() { ...@@ -87,7 +87,11 @@ export function getHylyList() {
} }
//获取智库报告 /**
* 智库概览/智库动态-智库报告、调查项目
* GET /api/thinkTankOverview/report
* 常用 query:pageNum, pageSize, sortFun, domainIds, startDate, endDate, category(调查项目), thinkTankId(详情页), keyword(动态搜索)
*/
export function getThinkTankReport(params) { export function getThinkTankReport(params) {
return request({ return request({
method: 'GET', method: 'GET',
...@@ -240,18 +244,12 @@ export function getThinkTankInfoBranch(params) { ...@@ -240,18 +244,12 @@ export function getThinkTankInfoBranch(params) {
}) })
} }
//获取经费来源统计
export function getThinkTankFundsTotal(params) {
return request({
method: 'GET',
url: `/api/thinkTankInfo/fundsTotal/${params}`,
})
}
//获取经费来源 //获取经费来源
export function getThinkTankFundsSource(params) { export function getThinkTankFundsSource(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankInfo/fundsSource/${params}`, url: `/api/thinkTankInfo/fundsStatistics/${params}`,
}) })
} }
...@@ -265,9 +263,17 @@ export function getThinkTankResearchAreae(params) { ...@@ -265,9 +263,17 @@ export function getThinkTankResearchAreae(params) {
//获取核心研究人员 //获取核心研究人员
export function getThinkTankPerson(params) { export function getThinkTankPerson(params) {
const { thinkTankId, currentPage, pageSize } = params
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankInfo/person/${params}`, url: `/api/thinkTankInfo/person/page`,
params: {
currentPage,
pageNum: currentPage,
page: currentPage,
pageSize,
thinkTankId
}
}) })
} }
......
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
</div> </div>
</div> </div>
<div class="box1-middle"> <div class="box1-middle">
<div class="box1-item" v-for="(item, index) in curFooterList" :key="item.id"> <div class="box1-item" v-for="(item, index) in curFooterList" :key="item.id" @click="handleToReportDetail(item)">
<div class="left"> <div class="left">
<img :src=item.imageUrl alt=""> <img :src=item.imageUrl alt="">
</div> </div>
...@@ -93,9 +93,9 @@ ...@@ -93,9 +93,9 @@
<div class="right-header"> {{ item.name }}</div> <div class="right-header"> {{ item.name }}</div>
<div class="right-footer"> <div class="right-footer">
<div class="time">{{ item.times }}</div> <div class="time">{{ item.times }}</div>
<div class="text-image"> <div class="text-image think-tank-link" @click.stop="handleToReportDetail(item)">
<div class="image"> <div class="image">
<img src="../assets/images/rand-image.png" alt="" /> <img :src=item.logoUrl alt="" />
</div> </div>
<div class="text"> <div class="text">
{{ item.thinkTankName }} {{ item.thinkTankName }}
...@@ -105,8 +105,8 @@ ...@@ -105,8 +105,8 @@
</div> </div>
<div class="image-change"> <div class="image-change">
<img src="../assets/images/plus.png" alt="" v-if="!selectedIds.has(item.id)" <img src="../assets/images/plus.png" alt="" v-if="!selectedIds.has(item.id)"
@click="toggleSelected(item.id, item)" /> @click.stop="toggleSelected(item.id, item)" />
<img src="../assets/images/Minus.png" alt="" v-else @click="toggleSelected(item.id, item)" /> <img src="../assets/images/Minus.png" alt="" v-else @click.stop="toggleSelected(item.id, item)" />
</div> </div>
</div> </div>
</div> </div>
...@@ -125,7 +125,8 @@ ...@@ -125,7 +125,8 @@
<div class="right-text-analysis">{{ "共选择" }}{{ selectedReportList.length }}{{ "篇智库报告" }}</div> <div class="right-text-analysis">{{ "共选择" }}{{ selectedReportList.length }}{{ "篇智库报告" }}</div>
</div> </div>
<div class="box1-middle-analysis"> <div class="box1-middle-analysis">
<div class="box1-item" v-for="(item, index) in selectedReportList" :key="item.id || index"> <div class="box1-item" v-for="(item, index) in selectedReportList" :key="item.id || index"
@click="handleToReportDetail(item)">
<div class="left"> <div class="left">
<img :src=item.imageUrl alt=""> <img :src=item.imageUrl alt="">
</div> </div>
...@@ -133,9 +134,9 @@ ...@@ -133,9 +134,9 @@
<div class="right-header"> {{ item.name }}</div> <div class="right-header"> {{ item.name }}</div>
<div class="right-footer"> <div class="right-footer">
<div class="time">{{ item.times }}</div> <div class="time">{{ item.times }}</div>
<div class="text-image"> <div class="text-image think-tank-link" @click.stop="handleToReportDetail(item)">
<div class="image"> <div class="image">
<img src="../assets/images/rand-image.png" alt="" /> <img :src=item.logoUrl alt="" />
</div> </div>
<div class="text"> <div class="text">
{{ item.thinkTankName }} {{ item.thinkTankName }}
...@@ -145,8 +146,8 @@ ...@@ -145,8 +146,8 @@
</div> </div>
<div class="image-change"> <div class="image-change">
<img src="../assets/images/plus.png" alt="" v-if="!selectedIds.has(item.id)" <img src="../assets/images/plus.png" alt="" v-if="!selectedIds.has(item.id)"
@click="toggleSelected(item.id, item)" /> @click.stop="toggleSelected(item.id, item)" />
<img src="../assets/images/Minus.png" alt="" v-else @click="toggleSelected(item.id, item)" /> <img src="../assets/images/Minus.png" alt="" v-else @click.stop="toggleSelected(item.id, item)" />
</div> </div>
</div> </div>
</div> </div>
...@@ -285,7 +286,7 @@ import router from '@/router'; ...@@ -285,7 +286,7 @@ import router from '@/router';
import { onMounted, ref, computed, reactive, nextTick } from "vue"; import { onMounted, ref, computed, reactive, nextTick } from "vue";
import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue" import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue"
import AnalysisResultBox from "./boxBackground/analysisBox.vue" import AnalysisResultBox from "./boxBackground/analysisBox.vue"
import { getThinkTankReport, getHylyList, getThinkDynamicsReport, postReportDomainViewAnalysis } from "@/api/thinkTank/overview"; import { getThinkTankReport, getHylyList, postReportDomainViewAnalysis } from "@/api/thinkTank/overview";
const sort = ref(""); const sort = ref("");
const searchPolicy = ref(""); const searchPolicy = ref("");
const isBox2 = ref(true) const isBox2 = ref(true)
...@@ -521,6 +522,18 @@ const handleCurrentChange = page => { ...@@ -521,6 +522,18 @@ const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
handleGetetThinkTankReport(page); handleGetetThinkTankReport(page);
}; };
const handleToReportDetail = (item) => {
if (!item?.id) return;
window.sessionStorage.setItem("curTabName", item.name);
const route = router.resolve({
name: "ReportDetail",
params: {
id: item.id
}
});
window.open(route.href, "_blank");
};
//获取行业领域字典 //获取行业领域字典
// getHylyList // getHylyList
const handleGetHylyList = async () => { const handleGetHylyList = async () => {
...@@ -536,8 +549,9 @@ const handleGetHylyList = async () => { ...@@ -536,8 +549,9 @@ const handleGetHylyList = async () => {
}; };
//获取智库报告 //获取智库报告
const handleGetetThinkTankReport = async (page = currentPage.value) => { const handleGetetThinkTankReport = async (page = currentPage.value) => {
const id = router.currentRoute?.value?.params?.id || ""; const thinkTankId = router.currentRoute?.value?.params?.id || "";
const getDateYearsAgo = years => {
const getDateYearsAgo = (years) => {
const d = new Date(); const d = new Date();
d.setFullYear(d.getFullYear() - Number(years || 1)); d.setFullYear(d.getFullYear() - Number(years || 1));
const y = d.getFullYear(); const y = d.getFullYear();
...@@ -545,20 +559,32 @@ const handleGetetThinkTankReport = async (page = currentPage.value) => { ...@@ -545,20 +559,32 @@ const handleGetetThinkTankReport = async (page = currentPage.value) => {
const day = String(d.getDate()).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`; return `${y}-${m}-${day}`;
}; };
const getTodayYmd = () => {
const d = new Date();
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 domainIds = arrayToString(selectedAreaList.value);
const keyword = (searchPolicy.value || "").trim();
const params = { const params = {
id, thinkTankId,
startDate: getDateYearsAgo(selectedYears.value), pageNum: Number(page),
// 不传 authorName(避免后端按空字符串筛选)
currentPage: Number(page) - 1, // 1-based
pageSize: pageSize, pageSize: pageSize,
researchTypeIds: arrayToString(selectedAreaList.value), sortFun: false,
searchText: (searchPolicy.value || "").trim(), domainIds,
startDate: getDateYearsAgo(selectedYears.value),
endDate: getTodayYmd(),
keyword: keyword || undefined,
}; };
try { try {
// 先清空,避免视觉上看起来没变化 // 先清空,避免视觉上看起来没变化
curFooterList.value = []; curFooterList.value = [];
const res = await getThinkDynamicsReport(params); const res = await getThinkTankReport(params);
console.log("智库报告", res); console.log("智库报告", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
curFooterList.value = res.data.content; curFooterList.value = res.data.content;
...@@ -621,6 +647,10 @@ onMounted(async () => { ...@@ -621,6 +647,10 @@ onMounted(async () => {
} }
} }
.think-tank-link {
cursor: pointer;
}
.box { .box {
display: flex; display: flex;
gap: 16px; gap: 16px;
......
...@@ -330,11 +330,11 @@ onMounted(async () => { ...@@ -330,11 +330,11 @@ onMounted(async () => {
.box1 { .box1 {
margin-top: 19px; margin-top: 19px;
width: 1600px; width: 1600px;
height: 1173px;
.box1-main { .box1-main {
margin-top: 8px; margin-top: 8px;
height: 1097px; min-height: 778px;
padding-left: 21px; padding-left: 21px;
padding-right: 50px; padding-right: 50px;
padding-bottom: 21px; padding-bottom: 21px;
...@@ -421,7 +421,7 @@ onMounted(async () => { ...@@ -421,7 +421,7 @@ onMounted(async () => {
.item-box { .item-box {
width: 506px; width: 506px;
height: 100%; min-height: 639.2px;
border-top: 1px solid rgb(234, 236, 238); border-top: 1px solid rgb(234, 236, 238);
.item { .item {
......
<template> <template>
<div class="wrap"> <div class="wrap">
<div class="top"> <div class="top">
<WarningPane :warnningLevel="riskSignal?.level" :warnningContent="riskSignal?.content"> <WarningPane :warnningLevel="riskSignal?.level" :warnningContent="riskSignal?.content" v-if="riskSignal?.level">
</WarningPane> </WarningPane>
</div> </div>
<div class="bottom-row"> <div class="bottom-row">
...@@ -52,8 +52,9 @@ ...@@ -52,8 +52,9 @@
</div> </div>
<div class="author-box"> <div class="author-box">
<div class="author-item" v-for="(author, idx) in reportAuthors" :key="idx" <div class="author-item" v-for="(author, idx) in reportAuthors" :key="idx"
v-if="Array.isArray(reportAuthors) && reportAuthors.length"> v-if="Array.isArray(reportAuthors) && reportAuthors.length" @click="handleClickReportAuthor(author)">
<div class="image"><img :src="author.avatar" alt="" /></div> <div class="image"><img :src="author.avatar ? author.avatar : DefaultIcon1" alt=""
@error="() => { if (author.avatar) author.avatar = null; }" /></div>
<div class="author-text"> <div class="author-text">
<div class="author-name">{{ author.name }}</div> <div class="author-name">{{ author.name }}</div>
<div class="author-position">{{ author.job }}</div> <div class="author-position">{{ author.job }}</div>
...@@ -66,7 +67,12 @@ ...@@ -66,7 +67,12 @@
<div class="box5"> <div class="box5">
<AnalysisBox title="报告关键词云" :showAllBtn="true"> <AnalysisBox title="报告关键词云" :showAllBtn="true">
<div class="box5-main"> <div class="box5-main">
<div id="box5Chart"></div>
<div class="box5Chart">
<!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 -->
<WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%"
height="100%" />
</div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -141,28 +147,28 @@ ...@@ -141,28 +147,28 @@
</div> </div>
<div class="box4-main"> <div class="box4-main">
<div class="box4-main-main"> <div class="box4-main-main">
<div class="box4-item" v-for="(item, index) in filteredOpinions" :key="index"> <div class="box4-item" v-for="(item, index) in filteredOpinions"
:key="item.id != null ? item.id : index">
<div class="top-row"> <div class="top-row">
<div class="left"> <div class="left">
{{ index + 1 }} {{ index + 1 }}
</div> </div>
<div class="center"> <div class="center">
<div class="title">{{ item.titleZh }}</div> <div class="title" v-html="highlightOpinionText(item.titleZh)"></div>
<div> <div>
<img src="../images/image-open.png" alt="" class="center-image" <img src="../images/image-open.png" alt="" class="center-image"
@click="handleOpenReportOriginal(item)" /> @click="handleOpenReportOriginal(item)" />
</div> </div>
<div> <div>
<img v-if="expandedIndex !== index" src="../images/image-down.png" alt="" class="center-image" <img v-if="!isOpinionExpanded(item, index)" src="../images/image-down.png" alt=""
@click="toggleOpinion(index)" /> class="center-image" @click="toggleOpinion(item, index)" />
<img v-else src="../images/image-up.png" alt="" class="center-image" <img v-else src="../images/image-up.png" alt="" class="center-image"
@click="toggleOpinion(index)" /> @click="toggleOpinion(item, index)" />
</div> </div>
</div> </div>
</div> </div>
<div v-if="expandedIndex === index" class="desc"> <div v-if="isOpinionExpanded(item, index)" class="desc"
{{ item.contentZh }} v-html="highlightOpinionText(item.contentZh)"></div>
</div>
<!-- <div class="right"> --> <!-- <div class="right"> -->
<!-- <div class="tag" v-for="(val, idx) in item.hylyList" :key="idx"> <!-- <div class="tag" v-for="(val, idx) in item.hylyList" :key="idx">
{{ val }} {{ val }}
...@@ -193,11 +199,12 @@ ...@@ -193,11 +199,12 @@
</template> </template>
<script setup> <script setup>
import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue" import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SearchContainer from "@/components/SearchContainer.vue"; import SearchContainer from "@/components/SearchContainer.vue";
import { ref, onMounted, computed, defineProps, nextTick } from "vue"; import { ref, onMounted, computed, defineProps } from "vue";
import setChart from "@/utils/setChart"; import { ElMessage } from "element-plus";
import getWordCloudChart from "./utils/worldCloudChart";
import { import {
getThinkTankReportAbstract, getThinkTankReportAbstract,
getThinkTankReportContent, getThinkTankReportContent,
...@@ -209,6 +216,7 @@ import { getChartAnalysis } from "@/api/aiAnalysis/index"; ...@@ -209,6 +216,7 @@ import { getChartAnalysis } from "@/api/aiAnalysis/index";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import "echarts-wordcloud"; import "echarts-wordcloud";
import AiSummary from '@/components/base/Ai/AiSummary/index.vue' import AiSummary from '@/components/base/Ai/AiSummary/index.vue'
import { getPersonSummaryInfo } from "@/api/common/index";
const router = useRouter(); const router = useRouter();
...@@ -235,24 +243,59 @@ const props = defineProps({ ...@@ -235,24 +243,59 @@ const props = defineProps({
const searchOpinions = ref(''); const searchOpinions = ref('');
const escapeHtml = (text) => {
return String(text ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const escapeRegExp = (text) => {
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const highlightOpinionText = (text) => {
const safeText = escapeHtml(text);
const keyword = (searchOpinions.value || "").trim();
if (!keyword) return safeText;
const pattern = new RegExp(`(${escapeRegExp(keyword)})`, "gi");
return safeText.replace(pattern, `<span class="opinion-keyword-highlight">$1</span>`);
};
const handleSearchOpinions = () => { const handleSearchOpinions = () => {
currentPage.value = 1; currentPage.value = 1;
handleGetThinkTankReportViewpoint(); handleGetThinkTankReportViewpoint();
}; };
// 当前展开的核心论点下标(同一时刻只展开一条) /** 可同时展开多条;用 id 区分项,避免翻页后索引与展开状态错位 */
const expandedIndex = ref(null); const expandedOpinionKeys = ref(new Set());
const filteredOpinions = computed(() => majorOpinions.value); const filteredOpinions = computed(() => majorOpinions.value);
const opinionsTotal = computed(() => total.value); const opinionsTotal = computed(() => total.value);
const toggleOpinion = index => { const getOpinionExpandKey = (item, index) => {
if (expandedIndex.value === index) { if (item != null && item.id != null && item.id !== "") {
expandedIndex.value = null; return String(item.id);
}
return `idx-${index}`;
};
const isOpinionExpanded = (item, index) => {
return expandedOpinionKeys.value.has(getOpinionExpandKey(item, index));
};
const toggleOpinion = (item, index) => {
const key = getOpinionExpandKey(item, index);
const next = new Set(expandedOpinionKeys.value);
if (next.has(key)) {
next.delete(key);
} else { } else {
expandedIndex.value = index; next.add(key);
} }
expandedOpinionKeys.value = next;
}; };
const publishTime = computed(() => { const publishTime = computed(() => {
...@@ -277,6 +320,62 @@ const reportAuthors = computed(() => { ...@@ -277,6 +320,62 @@ const reportAuthors = computed(() => {
} }
return []; return [];
}); });
// 点击报告作者头像,跳转到人物主页
// 与核心研究人员逻辑一致:核心依赖 personId,本页面依赖作者的 id(作为 personId 传入)
const handleClickReportAuthor = async (author) => {
const personId = author?.id;
if (!personId) return;
let personTypeList = [];
try {
personTypeList = JSON.parse(window.sessionStorage.getItem("personTypeList") || "[]");
} catch (e) {
personTypeList = [];
}
const params = { personId };
try {
const res = await getPersonSummaryInfo(params);
if (res.code !== 200 || !res.data) return;
const arr = personTypeList.filter((t) => {
const typeIdNum = Number(t.typeId);
const personTypeNum = Number(res.data.personType);
return !Number.isNaN(typeIdNum) && !Number.isNaN(personTypeNum) && typeIdNum === personTypeNum;
});
if (!arr.length) {
ElMessage.warning("找不到当前人员的类型值!");
return;
}
const personTypeName = arr[0]?.typeName || "";
let type = 0;
if (personTypeName === "科技企业领袖") {
type = 1;
} else if (personTypeName === "国会议员") {
type = 2;
} else if (personTypeName === "智库研究人员") {
type = 3;
} else {
ElMessage.warning("找不到当前人员的类型值!");
return;
}
window.sessionStorage.setItem("curTabName", author?.name || "");
const route = router.resolve({
path: "/characterPage",
query: {
type,
personId
}
});
window.open(route.href, "_blank");
} catch (error) {
console.error("点击报告作者头像跳转失败", error);
}
};
const riskSignal = computed(() => { const riskSignal = computed(() => {
const info = props.thinkInfo || {}; const info = props.thinkInfo || {};
return info.riskSignal; return info.riskSignal;
...@@ -345,6 +444,8 @@ const box2Data = ref([ ...@@ -345,6 +444,8 @@ const box2Data = ref([
]); ]);
// 报告关键词云 // 报告关键词云
const box5Data = ref([]); const box5Data = ref([]);
/** 词云子组件不 watch 数据,每次接口成功有数据时递增 key,强制重新挂载以触发 onMounted */
const box5WordCloudKey = ref(0);
//获取科技领域词云 //获取科技领域词云
const handleGetThinkTankReportIndustryCloud = async () => { const handleGetThinkTankReportIndustryCloud = async () => {
...@@ -362,10 +463,9 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -362,10 +463,9 @@ const handleGetThinkTankReportIndustryCloud = async () => {
})); }));
// 该接口数据用于「报告关键词云」 // 该接口数据用于「报告关键词云」
box5Data.value = data; box5Data.value = data;
if (data.length) {
await nextTick(); box5WordCloudKey.value += 1;
const box5Chart = getWordCloudChart(box5Data.value); }
setChart(box5Chart, "box5Chart");
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -462,7 +562,7 @@ const handleGetThinkTankReportViewpoint = async () => { ...@@ -462,7 +562,7 @@ const handleGetThinkTankReportViewpoint = async () => {
total.value = res.data.totalElements || 0; total.value = res.data.totalElements || 0;
// 重置展开状态 // 重置展开状态
expandedIndex.value = null; expandedOpinionKeys.value = new Set();
} }
} catch (error) { } catch (error) {
console.error("获取主要观点error", error); console.error("获取主要观点error", error);
...@@ -492,7 +592,7 @@ onMounted(() => { ...@@ -492,7 +592,7 @@ onMounted(() => {
.wrap { .wrap {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 16px;
padding-bottom: 16px; padding-bottom: 16px;
flex-direction: column; flex-direction: column;
...@@ -520,7 +620,7 @@ onMounted(() => { ...@@ -520,7 +620,7 @@ onMounted(() => {
.box1 { .box1 {
margin-top: 17px; margin-top: 17px;
width: 480px; width: 480px;
height: 486px;
// border: 1px solid rgba(234, 236, 238, 1); // border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px; // border-radius: 10px;
...@@ -528,7 +628,7 @@ onMounted(() => { ...@@ -528,7 +628,7 @@ onMounted(() => {
// background: rgba(255, 255, 255, 1); // background: rgba(255, 255, 255, 1);
.box1-main { .box1-main {
width: 480px; width: 480px;
height: 438px;
.text-box { .text-box {
width: 437px; width: 437px;
...@@ -635,8 +735,8 @@ onMounted(() => { ...@@ -635,8 +735,8 @@ onMounted(() => {
.author-box { .author-box {
width: 437px; width: 437px;
height: auto; height: auto;
/* 改为自适应高度,不要固定 220px */
max-height: 220px;
margin-top: 34px; margin-top: 34px;
margin-left: 18px; margin-left: 18px;
display: grid; display: grid;
...@@ -646,6 +746,7 @@ onMounted(() => { ...@@ -646,6 +746,7 @@ onMounted(() => {
/* 左右间距(同一行) */ /* 左右间距(同一行) */
row-gap: 8px; row-gap: 8px;
/* 上下间距(同一列) */ /* 上下间距(同一列) */
margin-bottom: 38px;
.author-item { .author-item {
width: 213px; width: 213px;
...@@ -659,6 +760,7 @@ onMounted(() => { ...@@ -659,6 +760,7 @@ onMounted(() => {
margin-top: 3px; margin-top: 3px;
margin-left: 3px; margin-left: 3px;
display: inline-block; display: inline-block;
cursor: pointer;
img { img {
...@@ -726,9 +828,9 @@ onMounted(() => { ...@@ -726,9 +828,9 @@ onMounted(() => {
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
#box5Chart { .box5Chart {
width: 100%; width: 418px;
height: 100%; height: 292px;
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
} }
...@@ -738,7 +840,7 @@ onMounted(() => { ...@@ -738,7 +840,7 @@ onMounted(() => {
.box2 { .box2 {
width: 480px; width: 480px;
height: 648px;
// border: 1px solid rgba(234, 236, 238, 1); // border: 1px solid rgba(234, 236, 238, 1);
// 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);
...@@ -749,6 +851,7 @@ onMounted(() => { ...@@ -749,6 +851,7 @@ onMounted(() => {
margin-top: 5px; margin-top: 5px;
margin-left: 23px; margin-left: 23px;
.box2-item { .box2-item {
height: 103px; height: 103px;
width: 100%; width: 100%;
...@@ -834,6 +937,7 @@ onMounted(() => { ...@@ -834,6 +937,7 @@ onMounted(() => {
.box2-btn { .box2-btn {
margin-top: 16px; margin-top: 16px;
margin-bottom: 21px;
margin-left: 23px; margin-left: 23px;
width: 436px; width: 436px;
height: 36px; height: 36px;
...@@ -880,7 +984,7 @@ onMounted(() => { ...@@ -880,7 +984,7 @@ onMounted(() => {
.box3 { .box3 {
width: 1103px; width: 1103px;
height: 890px;
// border: 1px solid rgba(234, 236, 238, 1); // border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px; // border-radius: 10px;
...@@ -888,13 +992,24 @@ onMounted(() => { ...@@ -888,13 +992,24 @@ onMounted(() => {
// background: rgba(255, 255, 255, 1); // background: rgba(255, 255, 255, 1);
.box3-main { .box3-main {
width: 1058px; width: 1058px;
height: 830px;
margin-top: 3px; margin-top: 3px;
margin-left: 22px; margin-left: 22px;
padding-bottom: 22px; padding-bottom: 22px;
:deep(.summary-main) {
margin-bottom: 25px;
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular 常规 */
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
/* 两端对齐 */
}
.box3-top { .box3-top {
width: 1058px; width: 1058px;
...@@ -943,7 +1058,8 @@ onMounted(() => { ...@@ -943,7 +1058,8 @@ onMounted(() => {
.box4 { .box4 {
width: 1103px; width: 1103px;
height: 965px; height: auto;
// border: 1px solid rgba(234, 236, 238, 1); // border: 1px solid rgba(234, 236, 238, 1);
// 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);
...@@ -984,12 +1100,13 @@ onMounted(() => { ...@@ -984,12 +1100,13 @@ onMounted(() => {
.box4-main { .box4-main {
width: 1057px; width: 1057px;
height: auto;
margin: 0 auto; margin: 0 auto;
.box4-main-main { .box4-main-main {
height: 767px; height: auto;
overflow: hidden;
overflow: visible;
.box4-item { .box4-item {
width: 1057px; width: 1057px;
...@@ -998,6 +1115,11 @@ onMounted(() => { ...@@ -998,6 +1115,11 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
border-bottom: 1px solid rgba(234, 236, 238, 1);
&:first-child {
border-top: 1px solid rgba(234, 236, 238, 1);
}
.top-row { .top-row {
display: flex; display: flex;
...@@ -1005,7 +1127,7 @@ onMounted(() => { ...@@ -1005,7 +1127,7 @@ onMounted(() => {
} }
.left { .left {
margin-top: 24px; margin-top: 19px;
margin-left: 15px; margin-left: 15px;
width: 24px; width: 24px;
height: 24px; height: 24px;
...@@ -1021,9 +1143,9 @@ onMounted(() => { ...@@ -1021,9 +1143,9 @@ onMounted(() => {
} }
.center { .center {
min-height: 64px; min-height: 62px;
margin-left: 13px; margin-left: 18px;
display: flex; display: flex;
align-items: center; align-items: center;
// overflow: hidden; // overflow: hidden;
...@@ -1031,14 +1153,14 @@ onMounted(() => { ...@@ -1031,14 +1153,14 @@ onMounted(() => {
// white-space: nowrap; // white-space: nowrap;
.title { .title {
margin-top: 12px;
width: 918px; width: 918px;
// height: 55px; // height: 55px;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
line-height: 26px; line-height: 30px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
...@@ -1101,9 +1223,10 @@ onMounted(() => { ...@@ -1101,9 +1223,10 @@ onMounted(() => {
} }
.desc { .desc {
width: 950px;
margin-top: 22px; padding-top: 22px;
margin-left: 52px; // 24(left) + 13(center margin) + 一点间距 padding-bottom: 23px;
padding-left: 56px; // 24(left) + 13(center margin) + 一点间距
color: rgb(59, 65, 75); color: rgb(59, 65, 75);
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
...@@ -1114,13 +1237,13 @@ onMounted(() => { ...@@ -1114,13 +1237,13 @@ onMounted(() => {
letter-spacing: 0px; letter-spacing: 0px;
text-align: justify; text-align: justify;
/* 两端对齐 */ /* 两端对齐 */
border-top: 1px solid rgba(234, 236, 238, 1);
} }
}
.box4-item {
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
.title :deep(.opinion-keyword-highlight),
.desc :deep(.opinion-keyword-highlight) {
background-color: #fff59d;
}
} }
} }
......
...@@ -120,15 +120,7 @@ ...@@ -120,15 +120,7 @@
</template> </template>
<template v-else> <template v-else>
<div class="box3-chart-row"> <div class="box3-chart-row">
<button v-if="box3LegendShowNav" type="button" class="box3-legend-nav-btn" :disabled="!box3LegendCanPrev"
aria-label="上一页图例" @click="handleBox3LegendPrev">
</button>
<div id="box3Chart" class="box3-chart-canvas"></div> <div id="box3Chart" class="box3-chart-canvas"></div>
<button v-if="box3LegendShowNav" type="button" class="box3-legend-nav-btn" :disabled="!box3LegendCanNext"
aria-label="下一页图例" @click="handleBox3LegendNext">
</button>
</div> </div>
<div class="source"> <div class="source">
<TipTab :text="POLICY_TRACKING_TIP_BOX3" /> <TipTab :text="POLICY_TRACKING_TIP_BOX3" />
...@@ -188,17 +180,11 @@ ...@@ -188,17 +180,11 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<el-checkbox-group <el-checkbox-group class="checkbox-group" v-model="selectedAreaList" @change="handleChangePolicyAreaList">
class="checkbox-group"
v-model="selectedAreaList"
@change="handleChangePolicyAreaList">
<el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_AREA"> <el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_AREA">
{{ POLICY_FILTER_ALL_AREA }} {{ POLICY_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox <el-checkbox class="filter-checkbox" v-for="research in areaList" :key="research.id"
class="filter-checkbox"
v-for="research in areaList"
:key="research.id"
:label="research.id"> :label="research.id">
{{ research.name }} {{ research.name }}
</el-checkbox> </el-checkbox>
...@@ -211,17 +197,12 @@ ...@@ -211,17 +197,12 @@
<div class="title">{{ "涉及部门" }}</div> <div class="title">{{ "涉及部门" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<el-checkbox-group <el-checkbox-group class="checkbox-group" v-model="selectedDepartmentList"
class="checkbox-group"
v-model="selectedDepartmentList"
@change="handleChangePolicyDepartmentList"> @change="handleChangePolicyDepartmentList">
<el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_DEPT"> <el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_DEPT">
{{ POLICY_FILTER_ALL_DEPT }} {{ POLICY_FILTER_ALL_DEPT }}
</el-checkbox> </el-checkbox>
<el-checkbox <el-checkbox class="filter-checkbox" v-for="department in selectableDepartment" :key="department.id"
class="filter-checkbox"
v-for="department in selectableDepartment"
:key="department.id"
:label="department.id"> :label="department.id">
{{ department.name }} {{ department.name }}
</el-checkbox> </el-checkbox>
...@@ -234,18 +215,11 @@ ...@@ -234,18 +215,11 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<el-checkbox-group <el-checkbox-group class="checkbox-group" v-model="selectedYearList" @change="handleChangePolicyYearList">
class="checkbox-group"
v-model="selectedYearList"
@change="handleChangePolicyYearList">
<el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_TIME"> <el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_TIME">
{{ POLICY_FILTER_ALL_TIME }} {{ POLICY_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox <el-checkbox class="filter-checkbox" v-for="year in selectableYears" :key="year" :label="year">
class="filter-checkbox"
v-for="year in selectableYears"
:key="year"
:label="year">
{{ year }} {{ year }}
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
...@@ -260,10 +234,10 @@ ...@@ -260,10 +234,10 @@
</div> </div>
<div class="item-right"> <div class="item-right">
<div> <div>
<div class="title">{{ item.titleZh }}</div> <div class="title" v-html="highlightPolicyText(item.titleZh)"></div>
<div class="info"> <div class="info">
<div class="text"> <div class="text">
{{ item.reportDateDisplay }}·{{ item.contentZh }} <span v-html="highlightPolicyText(`${item.reportDateDisplay}·${item.contentZh}`)"></span>
</div> </div>
<div class="more" @click="toDetail(item)"> <div class="more" @click="toDetail(item)">
...@@ -313,7 +287,7 @@ ...@@ -313,7 +287,7 @@
import { ref, watch, onMounted, computed, nextTick } from "vue"; import { ref, watch, onMounted, computed, nextTick } from "vue";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import getPieChart from "./utils/piechart"; import getPieChart from "./utils/piechart";
import getMultiLineChart, { POLICY_TRACKING_LEGEND_PAGE_SIZE } from "./utils/multiLineChart"; import getMultiLineChart from "./utils/multiLineChart";
import { import {
getThinkPolicyIndustry, getThinkPolicyIndustry,
getThinkPolicyIndustryTotal, getThinkPolicyIndustryTotal,
...@@ -594,41 +568,7 @@ const hasBox3ChartData = computed(() => { ...@@ -594,41 +568,7 @@ const hasBox3ChartData = computed(() => {
); );
}); });
const box3LegendPageIndex = ref(0); /** 研究领域变化趋势:图例不分页,超多时最多三行显示 */
const box3LegendPageCount = computed(() => {
const n = box3Data.value?.data?.length ?? 0;
return Math.max(1, Math.ceil(n / POLICY_TRACKING_LEGEND_PAGE_SIZE));
});
const box3LegendShowNav = computed(() => {
const n = box3Data.value?.data?.length ?? 0;
return n > POLICY_TRACKING_LEGEND_PAGE_SIZE;
});
const box3LegendCanPrev = computed(() => box3LegendPageIndex.value > 0);
const box3LegendCanNext = computed(
() => box3LegendPageIndex.value < box3LegendPageCount.value - 1
);
const handleBox3LegendPrev = () => {
if (box3LegendPageIndex.value <= 0) {
return;
}
box3LegendPageIndex.value -= 1;
renderBox3Chart();
};
const handleBox3LegendNext = () => {
if (box3LegendPageIndex.value >= box3LegendPageCount.value - 1) {
return;
}
box3LegendPageIndex.value += 1;
renderBox3Chart();
};
/** 研究领域变化趋势:图例分页与概览「数量变化趋势」一致 */
const renderBox3Chart = async () => { const renderBox3Chart = async () => {
const v = box3Data.value; const v = box3Data.value;
if ( if (
...@@ -640,22 +580,8 @@ const renderBox3Chart = async () => { ...@@ -640,22 +580,8 @@ const renderBox3Chart = async () => {
) { ) {
return; return;
} }
const totalNames = v.data.length;
const pages = Math.max(1, Math.ceil(totalNames / POLICY_TRACKING_LEGEND_PAGE_SIZE));
if (box3LegendPageIndex.value >= pages) {
box3LegendPageIndex.value = pages - 1;
}
if (box3LegendPageIndex.value < 0) {
box3LegendPageIndex.value = 0;
}
await nextTick(); await nextTick();
const box3Chart = getMultiLineChart( const box3Chart = getMultiLineChart({ title: v.title, data: v.data });
{ title: v.title, data: v.data },
{
legendShowCount: POLICY_TRACKING_LEGEND_PAGE_SIZE,
legendPageIndex: box3LegendPageIndex.value
}
);
setChart(box3Chart, "box3Chart"); setChart(box3Chart, "box3Chart");
}; };
...@@ -729,7 +655,6 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -729,7 +655,6 @@ const handleGetThinkPolicyIndustryChange = async () => {
return; return;
} }
box3Data.value = frontendData; box3Data.value = frontendData;
box3LegendPageIndex.value = 0;
await renderBox3Chart(); await renderBox3Chart();
} else { } else {
box3Data.value = { title: [], data: [] }; box3Data.value = { title: [], data: [] };
...@@ -740,6 +665,19 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -740,6 +665,19 @@ const handleGetThinkPolicyIndustryChange = async () => {
} }
}; };
/** 兼容 getChartAnalysis 返回对象:从 data[0] 提取「解读」文本 */
const getInterpretationTextFromChartResponse = (res) => {
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
return (
first?.["解读"] ||
first?.["interpretation"] ||
first?.["analysis"] ||
first?.["content"] ||
""
);
};
/** 政策追踪-领域分布饼图解读(与概览页 box6 入参一致) */ /** 政策追踪-领域分布饼图解读(与概览页 box6 入参一致) */
const fetchPolicyPtBox1ChartInterpretation = async () => { const fetchPolicyPtBox1ChartInterpretation = async () => {
const list = Array.isArray(box1Data.value) ? box1Data.value : []; const list = Array.isArray(box1Data.value) ? box1Data.value : [];
...@@ -767,17 +705,9 @@ const fetchPolicyPtBox1ChartInterpretation = async () => { ...@@ -767,17 +705,9 @@ const fetchPolicyPtBox1ChartInterpretation = async () => {
})) }))
}; };
try { try {
const text = await getChartAnalysis( const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) });
{ text: JSON.stringify(chartPayload) }, const text = getInterpretationTextFromChartResponse(res);
{ aiContentPolicyPt1.value = text || aiContentPolicyPt1.value || "未返回有效解读内容";
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentPolicyPt1.value = chunk;
}
}
}
);
aiContentPolicyPt1.value = text || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("政策追踪领域分布图表解读请求失败", error); console.error("政策追踪领域分布图表解读请求失败", error);
aiContentPolicyPt1.value = "解读加载失败"; aiContentPolicyPt1.value = "解读加载失败";
...@@ -813,17 +743,9 @@ const fetchPolicyPtBox2ChartInterpretation = async () => { ...@@ -813,17 +743,9 @@ const fetchPolicyPtBox2ChartInterpretation = async () => {
})) }))
}; };
try { try {
const text = await getChartAnalysis( const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) });
{ text: JSON.stringify(chartPayload) }, const text = getInterpretationTextFromChartResponse(res);
{ aiContentPolicyPt2.value = text || aiContentPolicyPt2.value || "未返回有效解读内容";
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentPolicyPt2.value = chunk;
}
}
}
);
aiContentPolicyPt2.value = text || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("政策追踪部门分布图表解读请求失败", error); console.error("政策追踪部门分布图表解读请求失败", error);
aiContentPolicyPt2.value = "解读加载失败"; aiContentPolicyPt2.value = "解读加载失败";
...@@ -867,17 +789,9 @@ const fetchPolicyPtBox3ChartInterpretation = async () => { ...@@ -867,17 +789,9 @@ const fetchPolicyPtBox3ChartInterpretation = async () => {
}) })
}; };
try { try {
const text = await getChartAnalysis( const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) });
{ text: JSON.stringify(chartPayload) }, const text = getInterpretationTextFromChartResponse(res);
{ aiContentPolicyPt3.value = text || aiContentPolicyPt3.value || "未返回有效解读内容";
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentPolicyPt3.value = chunk;
}
}
}
);
aiContentPolicyPt3.value = text || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("政策追踪研究领域趋势图表解读请求失败", error); console.error("政策追踪研究领域趋势图表解读请求失败", error);
aiContentPolicyPt3.value = "解读加载失败"; aiContentPolicyPt3.value = "解读加载失败";
...@@ -889,6 +803,27 @@ const fetchPolicyPtBox3ChartInterpretation = async () => { ...@@ -889,6 +803,27 @@ const fetchPolicyPtBox3ChartInterpretation = async () => {
// 搜索政策建议 // 搜索政策建议
const searchPolicy = ref(""); const searchPolicy = ref("");
const escapeHtml = (text) => {
return String(text ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const escapeRegExp = (text) => {
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const highlightPolicyText = (text) => {
const safeText = escapeHtml(text);
const keyword = (searchPolicy.value || "").trim();
if (!keyword) return safeText;
const pattern = new RegExp(`(${escapeRegExp(keyword)})`, "gi");
return safeText.replace(pattern, '<span class="keyword-highlight">$1</span>');
};
const yearList = ref([ const yearList = ref([
{ {
label: "近一年发布", label: "近一年发布",
...@@ -1507,40 +1442,10 @@ onMounted(() => { ...@@ -1507,40 +1442,10 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
gap: 8px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
.box3-legend-nav-btn {
flex-shrink: 0;
width: 32px;
height: 32px;
margin-top: 6px;
padding: 0;
box-sizing: border-box;
border: 1px solid rgba(220, 223, 230, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
color: rgba(59, 65, 75, 1);
font-size: 20px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.box3-legend-nav-btn:hover:not(:disabled) {
border-color: var(--color-main-primary, #055fc2);
color: var(--color-main-primary, #055fc2);
}
.box3-legend-nav-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.box3-chart-canvas { .box3-chart-canvas {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
...@@ -2128,6 +2033,10 @@ onMounted(() => { ...@@ -2128,6 +2033,10 @@ onMounted(() => {
} }
} }
:deep(.keyword-highlight) {
background-color: #fff59d;
}
.tag-box { .tag-box {
margin-top: 7px; margin-top: 7px;
display: flex; display: flex;
...@@ -2237,4 +2146,8 @@ onMounted(() => { ...@@ -2237,4 +2146,8 @@ onMounted(() => {
height: 8px !important; height: 8px !important;
margin-right: 4px; margin-right: 4px;
} }
:deep(.el-checkbox__label) {
font-size: 16px;
}
</style> </style>
import * as echarts from 'echarts' import * as echarts from 'echarts'
/** 政策追踪「研究领域变化趋势」图例分页:每页条数(与概览数量变化趋势逻辑一致,条数按产品要求为 4) */
export const POLICY_TRACKING_LEGEND_PAGE_SIZE = 4
const colorList = [ const colorList = [
'rgba(5, 95, 194, 1)', 'rgba(5, 95, 194, 1)',
'rgba(19, 168, 168, 1)', 'rgba(19, 168, 168, 1)',
...@@ -35,22 +32,16 @@ const parseRgba = (colorStr) => { ...@@ -35,22 +32,16 @@ const parseRgba = (colorStr) => {
/** /**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} chartInput * @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} chartInput
* @param {{ legendShowCount?: number, legendPageIndex?: number }} [options]
*/ */
const getMultiLineChart = (chartInput, options = {}) => { const getMultiLineChart = (chartInput) => {
const title = chartInput.title const title = chartInput.title
const series = chartInput.data || [] const series = chartInput.data || []
const legendShowCount =
typeof options.legendShowCount === 'number' && options.legendShowCount > 0
? options.legendShowCount
: POLICY_TRACKING_LEGEND_PAGE_SIZE
const rawPageIndex = Number(options.legendPageIndex) || 0
const allNames = series.map((item) => item.name) const allNames = series.map((item) => item.name)
const pageCount = Math.max(1, Math.ceil(allNames.length / legendShowCount)) const lineSize = Math.ceil(allNames.length / 3)
const legendPageIndex = Math.min(Math.max(0, rawPageIndex), pageCount - 1) const legendLine1 = allNames.slice(0, lineSize)
const legendStart = legendPageIndex * legendShowCount const legendLine2 = allNames.slice(lineSize, lineSize * 2)
const legendData = allNames.slice(legendStart, legendStart + legendShowCount) 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
...@@ -91,31 +82,71 @@ const getMultiLineChart = (chartInput, options = {}) => { ...@@ -91,31 +82,71 @@ const getMultiLineChart = (chartInput, options = {}) => {
}, },
/* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */ /* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */
grid: { grid: {
top: 50, top: 92,
right: 10, right: 10,
bottom: 0, bottom: 0,
left: 20, left: 20,
containLabel: true containLabel: true
}, },
legend: { legend: [
show: true, {
type: 'plain', show: true,
data: legendData, type: 'plain',
top: 4, data: legendLine1,
left: 'center', top: 4,
icon: 'circle', left: 'center',
textStyle: { icon: 'circle',
fontFamily: 'Source Han Sans CN', textStyle: {
fontWeight: 400, fontFamily: 'Source Han Sans CN',
fontSize: 14, fontWeight: 400,
lineHeight: 24, fontSize: 14,
letterSpacing: 0, lineHeight: 24,
align: 'left', letterSpacing: 0,
color: 'rgb(95, 101, 108)' align: 'left',
color: 'rgb(95, 101, 108)'
},
itemWidth: 12,
itemHeight: 12
}, },
itemWidth: 12, {
itemHeight: 12 show: legendLine2.length > 0,
}, type: 'plain',
data: legendLine2,
top: 30,
left: 'center',
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,
itemHeight: 12
},
{
show: legendLine3.length > 0,
type: 'plain',
data: legendLine3,
top: 56,
left: 'center',
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,
itemHeight: 12
}
],
color: colorList, color: colorList,
xAxis: [ xAxis: [
{ {
...@@ -142,6 +173,16 @@ const getMultiLineChart = (chartInput, options = {}) => { ...@@ -142,6 +173,16 @@ const getMultiLineChart = (chartInput, options = {}) => {
yAxis: [ yAxis: [
{ {
type: 'value', type: 'value',
name: '数量',
nameLocation: 'end',
nameGap: 20,
nameTextStyle: {
color: 'rgb(132, 136, 142)',
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 11,
padding: [0, 0, 0, -20] // 👈 这个是左移 4px(上、右、下、左)
},
splitNumber: 4, splitNumber: 4,
axisLabel: { axisLabel: {
color: 'rgb(132, 136, 142)', color: 'rgb(132, 136, 142)',
......
...@@ -21,16 +21,17 @@ ...@@ -21,16 +21,17 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchTypeIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchTypeIds)"> :model-value="selectedResearchIds"
全部领域 @change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchTypeList" :key="type.id" v-model="selectedResearchIds" :label="type.id" <el-checkbox v-for="type in researchTypeList" :key="type.id" class="filter-checkbox" :label="type.id">
class="filter-checkbox" @change="handleGetThinkDynamicsReport()">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
<div class="select-time-box"> <div class="select-time-box">
...@@ -39,16 +40,17 @@ ...@@ -39,16 +40,17 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchTimeIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchTimeIds)"> :model-value="selectedResearchTimeIds"
全部时间 @change="handleTimeGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchTimeList" :key="type.id" v-model="selectedResearchTimeIds" <el-checkbox v-for="type in researchTimeList" :key="type.id" class="filter-checkbox" :label="type.id">
:label="type.id" class="filter-checkbox" @change="handleGetThinkDynamicsReport()">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
<!-- <div class="input-main"> <!-- <div class="input-main">
<el-input placeholder="输入作者名字" v-model="author" @input="handleGetThinkDynamicsReport()" /> <el-input placeholder="输入作者名字" v-model="author" @input="handleGetThinkDynamicsReport()" />
...@@ -60,16 +62,17 @@ ...@@ -60,16 +62,17 @@
<div class="title">{{ "听证会部门" }}</div> <div class="title">{{ "听证会部门" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchHearingIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchHearingIds)"> :model-value="selectedResearchHearingIds"
全部部门 @change="handleDeptGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_DEPT">
{{ RESOURCE_FILTER_ALL_DEPT }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchHearingList" :key="type.id" v-model="selectedResearchHearingIds" <el-checkbox v-for="type in researchHearingList" :key="type.id" class="filter-checkbox" :label="type.id">
:label="type.id" class="filter-checkbox" @change="handleGetThinkDynamicsReport()">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
<!-- <div class="input-main"> <!-- <div class="input-main">
<el-input placeholder="输入作者名字" v-model="author" @input="handleGetThinkDynamicsReport()" /> <el-input placeholder="输入作者名字" v-model="author" @input="handleGetThinkDynamicsReport()" />
...@@ -84,11 +87,10 @@ ...@@ -84,11 +87,10 @@
<img class="card-item-img" src="../images/img congress.png" alt="report image" /> <img class="card-item-img" src="../images/img congress.png" alt="report image" />
<div class="card-item-text"> <div class="card-item-text">
<div class="card-item-title"> <div class="card-item-title">
<span v-html="highlightText(item.title)"></span>
{{ item.title }}
</div> </div>
<div class="card-item-time"> <div class="card-item-time">
{{ item.time + ' · ' + item.content }} <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"> {{ item.category }}</div> <div class="card-item-category"> {{ item.category }}</div>
...@@ -113,6 +115,17 @@ ...@@ -113,6 +115,17 @@
</template> </template>
<script setup> <script setup>
import { ref, toRefs, watch, computed } from "vue"; import { ref, toRefs, watch, computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
RESOURCE_FILTER_ALL_DEPT,
RESOURCE_FILTER_EARLIER,
normalizeExclusiveAllOption,
stripAllAreaForRequest,
stripAllTimeForRequest,
stripAllDeptForRequest,
matchesEarlierChineseDate
} from "../../../utils/resourceLibraryFilters";
const props = defineProps({ const props = defineProps({
researchTypeList: { researchTypeList: {
...@@ -154,6 +167,10 @@ const props = defineProps({ ...@@ -154,6 +167,10 @@ const props = defineProps({
currentPage: { currentPage: {
type: Number, type: Number,
default: 1 default: 1
},
searchKeyword: {
type: String,
default: ""
} }
}); });
...@@ -180,69 +197,96 @@ function parseChineseDate(dateStr) { ...@@ -180,69 +197,96 @@ function parseChineseDate(dateStr) {
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
} }
const selectedResearchIds = ref([]); const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([]); const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
const selectedResearchHearingIds = ref([]); const selectedResearchHearingIds = ref([RESOURCE_FILTER_ALL_DEPT]);
const escapeHtml = (text) => {
return String(text ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const escapeRegExp = (text) => {
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const highlightText = (text) => {
const safeText = escapeHtml(text);
const keyword = (props.searchKeyword || "").trim();
if (!keyword) return safeText;
const pattern = new RegExp(`(${escapeRegExp(keyword)})`, "gi");
return safeText.replace(pattern, '<span class="keyword-highlight">$1</span>');
};
watch( watch(
() => props.selectedFilters, () => props.selectedFilters,
val => { val => {
selectedResearchIds.value = val?.researchTypeIds ? [...val.researchTypeIds] : []; selectedResearchIds.value =
selectedResearchTimeIds.value = val?.researchTimeIds ? [...val.researchTimeIds] : []; val?.researchTypeIds?.length > 0
selectedResearchHearingIds.value = val?.researchHearingIds ? [...val.researchHearingIds] : []; ? [...val.researchTypeIds]
: [RESOURCE_FILTER_ALL_AREA];
selectedResearchTimeIds.value =
val?.researchTimeIds?.length > 0
? [...val.researchTimeIds]
: [RESOURCE_FILTER_ALL_TIME];
selectedResearchHearingIds.value =
val?.researchHearingIds?.length > 0
? [...val.researchHearingIds]
: [RESOURCE_FILTER_ALL_DEPT];
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
); );
const buildSelectedFiltersPayload = () => ({ const buildSelectedFiltersPayload = () => ({
researchTypeIds: [...selectedResearchIds.value], researchTypeIds: stripAllAreaForRequest(selectedResearchIds.value),
researchTimeIds: [...selectedResearchTimeIds.value], researchTimeIds: stripAllTimeForRequest(selectedResearchTimeIds.value),
researchHearingIds: [...selectedResearchHearingIds.value] researchHearingIds: stripAllDeptForRequest(selectedResearchHearingIds.value)
}); });
const researchTypeIds = computed(() => (researchTypeList.value || []).map(item => item.id)); const handleAreaGroupChange = (val) => {
const researchTimeIds = computed(() => (researchTimeList.value || []).map(item => item.id)); selectedResearchIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_AREA);
const researchHearingIds = computed(() => (researchHearingList.value || []).map(item => item.id)); handleGetThinkDynamicsReport();
};
const getTargetSelection = ids => { const handleTimeGroupChange = (val) => {
if (ids === researchTypeIds.value) return selectedResearchIds; selectedResearchTimeIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME);
if (ids === researchTimeIds.value) return selectedResearchTimeIds; handleGetThinkDynamicsReport();
return selectedResearchHearingIds;
}; };
const isGroupAllSelected = ids => const handleDeptGroupChange = (val) => {
ids.length > 0 && ids.every(id => getTargetSelection(ids).value.includes(id)); selectedResearchHearingIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_DEPT);
const handleToggleAll = (checked, ids) => {
if (!ids.length) return;
const targetSelection = getTargetSelection(ids);
const nextSelected = new Set(targetSelection.value);
if (checked) {
ids.forEach(id => nextSelected.add(id));
} else {
ids.forEach(id => nextSelected.delete(id));
}
targetSelection.value = [...nextSelected];
handleGetThinkDynamicsReport(); handleGetThinkDynamicsReport();
}; };
const filteredHearingData = computed(() => { const filteredHearingData = computed(() => {
const areaSel = stripAllAreaForRequest(selectedResearchIds.value);
const timeSel = stripAllTimeForRequest(selectedResearchTimeIds.value);
const deptSel = stripAllDeptForRequest(selectedResearchHearingIds.value);
const rangeStart = getDateYearsAgo(selectedYear.value || 1); const rangeStart = getDateYearsAgo(selectedYear.value || 1);
return (hearingData.value || []).filter(item => { return (hearingData.value || []).filter(item => {
const itemDate = parseChineseDate(item.time); const itemDate = parseChineseDate(item.time);
const matchTopRange = itemDate ? itemDate >= rangeStart : true; const matchTopRange = itemDate ? itemDate >= rangeStart : true;
const matchYear = const matchYear =
selectedResearchTimeIds.value.length === 0 || timeSel.length === 0 ||
selectedResearchTimeIds.value.some(year => String(item.time || "").startsWith(year)); timeSel.some(sel => {
if (sel === RESOURCE_FILTER_EARLIER) {
return matchesEarlierChineseDate(item.time);
}
return String(item.time || "").startsWith(String(sel));
});
const matchDepartment = const matchDepartment =
selectedResearchHearingIds.value.length === 0 || deptSel.length === 0 ||
selectedResearchHearingIds.value.some(department => deptSel.some(department =>
String(item.content || "").includes(department) || String(item.title || "").includes(department) String(item.content || "").includes(department) || String(item.title || "").includes(department)
); );
const matchType = const matchType =
selectedResearchIds.value.length === 0 || areaSel.length === 0 ||
selectedResearchIds.value.some(typeId => areaSel.some(typeId =>
String(item.category || "").includes(typeId) || String(item.title || "").includes(typeId) String(item.category || "").includes(String(typeId)) || String(item.title || "").includes(String(typeId))
); );
return matchTopRange && matchYear && matchDepartment && matchType; return matchTopRange && matchYear && matchDepartment && matchType;
}); });
...@@ -466,6 +510,10 @@ const handleToReportDetail = item => { ...@@ -466,6 +510,10 @@ const handleToReportDetail = item => {
} }
} }
:deep(.keyword-highlight) {
background-color: #fff59d;
}
.right { .right {
width: 1224px; width: 1224px;
height: 1377px; height: 1377px;
......
...@@ -21,16 +21,17 @@ ...@@ -21,16 +21,17 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchTypeIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchTypeIds)"> :model-value="selectedResearchIds"
全部领域 @change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchTypeList" :key="type.id" v-model="selectedResearchIds" :label="type.id" <el-checkbox v-for="type in researchTypeList" :key="type.id" class="filter-checkbox" :label="type.id">
class="filter-checkbox" @change="handleGetThinkDynamicsReport()">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
<div class="select-time-box"> <div class="select-time-box">
...@@ -39,16 +40,17 @@ ...@@ -39,16 +40,17 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchTimeIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchTimeIds)"> :model-value="selectedResearchTimeIds"
全部时间 @change="handleTimeGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchTimeList" :key="type.id" v-model="selectedResearchTimeIds" <el-checkbox v-for="type in researchTimeList" :key="type.id" class="filter-checkbox" :label="type.id">
:label="type.id" class="filter-checkbox" @change="handleGetThinkDynamicsReport()">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
<!-- <div class="input-main"> <!-- <div class="input-main">
<el-input placeholder="输入作者名字" v-model="author" @input="handleGetThinkDynamicsReport()" /> <el-input placeholder="输入作者名字" v-model="author" @input="handleGetThinkDynamicsReport()" />
...@@ -63,7 +65,7 @@ ...@@ -63,7 +65,7 @@
<img :src="item.imageUrl" alt="" /> <img :src="item.imageUrl" alt="" />
</div> </div>
<div class="footer-card-title"> <div class="footer-card-title">
{{ item.name }} <span v-html="highlightText(item.name)"></span>
</div> </div>
<div class="footer-card-footer"> <div class="footer-card-footer">
<div class="time">{{ item.times }}</div> <div class="time">{{ item.times }}</div>
...@@ -84,7 +86,14 @@ ...@@ -84,7 +86,14 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, ref, toRefs, watch } from "vue"; import { ref, toRefs, watch } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption,
stripAllAreaForRequest,
stripAllTimeForRequest
} from "../../../utils/resourceLibraryFilters";
const props = defineProps({ const props = defineProps({
researchTypeList: { researchTypeList: {
...@@ -114,6 +123,10 @@ const props = defineProps({ ...@@ -114,6 +123,10 @@ const props = defineProps({
currentPage: { currentPage: {
type: Number, type: Number,
default: 1 default: 1
},
searchKeyword: {
type: String,
default: ""
} }
}); });
...@@ -127,43 +140,58 @@ const emit = defineEmits([ ...@@ -127,43 +140,58 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变 // 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props); const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props);
const selectedResearchIds = ref([]); const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([]); const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
const escapeHtml = (text) => {
return String(text ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const escapeRegExp = (text) => {
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const highlightText = (text) => {
const safeText = escapeHtml(text);
const keyword = (props.searchKeyword || "").trim();
if (!keyword) return safeText;
const pattern = new RegExp(`(${escapeRegExp(keyword)})`, "gi");
return safeText.replace(pattern, '<span class="keyword-highlight">$1</span>');
};
// 父组件更新时同步到子组件
watch( watch(
() => props.selectedFilters, () => props.selectedFilters,
val => { val => {
selectedResearchIds.value = val?.researchTypeIds ? [...val.researchTypeIds] : []; selectedResearchIds.value =
selectedResearchTimeIds.value = val?.researchTimeIds ? [...val.researchTimeIds] : []; val?.researchTypeIds?.length > 0
? [...val.researchTypeIds]
: [RESOURCE_FILTER_ALL_AREA];
selectedResearchTimeIds.value =
val?.researchTimeIds?.length > 0
? [...val.researchTimeIds]
: [RESOURCE_FILTER_ALL_TIME];
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
); );
const buildSelectedFiltersPayload = () => ({ const buildSelectedFiltersPayload = () => ({
researchTypeIds: [...selectedResearchIds.value], researchTypeIds: stripAllAreaForRequest(selectedResearchIds.value),
researchTimeIds: [...selectedResearchTimeIds.value], researchTimeIds: stripAllTimeForRequest(selectedResearchTimeIds.value),
researchHearingIds: [] researchHearingIds: []
}); });
const researchTypeIds = computed(() => (researchTypeList.value || []).map(item => item.id)); const handleAreaGroupChange = (val) => {
const researchTimeIds = computed(() => (researchTimeList.value || []).map(item => item.id)); selectedResearchIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_AREA);
handleGetThinkDynamicsReport();
const getTargetSelection = ids => (ids === researchTypeIds.value ? selectedResearchIds : selectedResearchTimeIds); };
const isGroupAllSelected = ids =>
ids.length > 0 && ids.every(id => getTargetSelection(ids).value.includes(id));
const handleToggleAll = (checked, ids) => { const handleTimeGroupChange = (val) => {
if (!ids.length) return; selectedResearchTimeIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME);
const targetSelection = getTargetSelection(ids);
const nextSelected = new Set(targetSelection.value);
if (checked) {
ids.forEach(id => nextSelected.add(id));
} else {
ids.forEach(id => nextSelected.delete(id));
}
targetSelection.value = [...nextSelected];
handleGetThinkDynamicsReport(); handleGetThinkDynamicsReport();
}; };
...@@ -326,6 +354,10 @@ const handleToReportDetail = item => { ...@@ -326,6 +354,10 @@ const handleToReportDetail = item => {
} }
} }
:deep(.keyword-highlight) {
background-color: #fff59d;
}
.right { .right {
width: 1284px; width: 1284px;
height: 1377px; height: 1377px;
......
...@@ -21,16 +21,17 @@ ...@@ -21,16 +21,17 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchTypeIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchTypeIds)"> :model-value="selectedResearchIds"
全部领域 @change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchTypeList" :key="type.id" v-model="selectedResearchIds" :label="type.id" <el-checkbox v-for="type in researchTypeList" :key="type.id" class="filter-checkbox" :label="type.id">
class="filter-checkbox" @change="handleGetThinkDynamicsReport()">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
<div class="select-time-box"> <div class="select-time-box">
...@@ -39,16 +40,17 @@ ...@@ -39,16 +40,17 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchTimeIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchTimeIds)"> :model-value="selectedResearchTimeIds"
全部时间 @change="handleTimeGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchTimeList" :key="type.id" v-model="selectedResearchTimeIds" <el-checkbox v-for="type in researchTimeList" :key="type.id" class="filter-checkbox" :label="type.id">
:label="type.id" class="filter-checkbox" @change="handleGetThinkDynamicsReport()">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
<!-- <div class="input-main"> <!-- <div class="input-main">
<el-input placeholder="输入作者名字" v-model="author" @input="handleGetThinkDynamicsReport()" /> <el-input placeholder="输入作者名字" v-model="author" @input="handleGetThinkDynamicsReport()" />
...@@ -63,7 +65,7 @@ ...@@ -63,7 +65,7 @@
<img :src="item.imageUrl" alt="" /> <img :src="item.imageUrl" alt="" />
</div> </div>
<div class="footer-card-title"> <div class="footer-card-title">
{{ item.name }} <span v-html="highlightText(item.name)"></span>
</div> </div>
<div class="footer-card-footer"> <div class="footer-card-footer">
<div class="time">{{ item.times }}</div> <div class="time">{{ item.times }}</div>
...@@ -84,7 +86,14 @@ ...@@ -84,7 +86,14 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, ref, toRefs, watch } from "vue"; import { ref, toRefs, watch } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption,
stripAllAreaForRequest,
stripAllTimeForRequest
} from "../../../utils/resourceLibraryFilters";
const props = defineProps({ const props = defineProps({
researchTypeList: { researchTypeList: {
...@@ -114,6 +123,10 @@ const props = defineProps({ ...@@ -114,6 +123,10 @@ const props = defineProps({
currentPage: { currentPage: {
type: Number, type: Number,
default: 1 default: 1
},
searchKeyword: {
type: String,
default: ""
} }
}); });
...@@ -127,43 +140,59 @@ const emit = defineEmits([ ...@@ -127,43 +140,59 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变 // 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props); const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props);
const selectedResearchIds = ref([]); const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([]); const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
const escapeHtml = (text) => {
return String(text ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const escapeRegExp = (text) => {
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const highlightText = (text) => {
const safeText = escapeHtml(text);
const keyword = (props.searchKeyword || "").trim();
if (!keyword) return safeText;
const pattern = new RegExp(`(${escapeRegExp(keyword)})`, "gi");
return safeText.replace(pattern, '<span class="keyword-highlight">$1</span>');
};
// 父组件更新时同步到子组件 // 父组件更新时同步到子组件(接口层无选中时为「全部」)
watch( watch(
() => props.selectedFilters, () => props.selectedFilters,
val => { val => {
selectedResearchIds.value = val?.researchTypeIds ? [...val.researchTypeIds] : []; selectedResearchIds.value =
selectedResearchTimeIds.value = val?.researchTimeIds ? [...val.researchTimeIds] : []; val?.researchTypeIds?.length > 0
? [...val.researchTypeIds]
: [RESOURCE_FILTER_ALL_AREA];
selectedResearchTimeIds.value =
val?.researchTimeIds?.length > 0
? [...val.researchTimeIds]
: [RESOURCE_FILTER_ALL_TIME];
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
); );
const buildSelectedFiltersPayload = () => ({ const buildSelectedFiltersPayload = () => ({
researchTypeIds: [...selectedResearchIds.value], researchTypeIds: stripAllAreaForRequest(selectedResearchIds.value),
researchTimeIds: [...selectedResearchTimeIds.value], researchTimeIds: stripAllTimeForRequest(selectedResearchTimeIds.value),
researchHearingIds: [] researchHearingIds: []
}); });
const researchTypeIds = computed(() => (researchTypeList.value || []).map(item => item.id)); const handleAreaGroupChange = (val) => {
const researchTimeIds = computed(() => (researchTimeList.value || []).map(item => item.id)); selectedResearchIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_AREA);
handleGetThinkDynamicsReport();
const getTargetSelection = ids => (ids === researchTypeIds.value ? selectedResearchIds : selectedResearchTimeIds); };
const isGroupAllSelected = ids =>
ids.length > 0 && ids.every(id => getTargetSelection(ids).value.includes(id));
const handleToggleAll = (checked, ids) => { const handleTimeGroupChange = (val) => {
if (!ids.length) return; selectedResearchTimeIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME);
const targetSelection = getTargetSelection(ids);
const nextSelected = new Set(targetSelection.value);
if (checked) {
ids.forEach(id => nextSelected.add(id));
} else {
ids.forEach(id => nextSelected.delete(id));
}
targetSelection.value = [...nextSelected];
handleGetThinkDynamicsReport(); handleGetThinkDynamicsReport();
}; };
...@@ -326,6 +355,10 @@ const handleToReportDetail = item => { ...@@ -326,6 +355,10 @@ const handleToReportDetail = item => {
} }
} }
:deep(.keyword-highlight) {
background-color: #fff59d;
}
.right { .right {
width: 1284px; width: 1284px;
height: 1377px; height: 1377px;
......
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
<div class="main-header"> <div class="main-header">
<div class="search-box"> <div class="search-box">
<el-input placeholder="搜索智库动态" v-model="searchReport" style="width: 270px" /> <el-input placeholder="搜索智库动态" v-model="searchReport" style="width: 270px"
@keyup.enter="handleGetThinkDynamicsReport()" />
<div class="icon"> <div class="icon">
<img src="./images/search-icon.png" alt="" @click="handleGetThinkDynamicsReport()" /> <img src="./images/search-icon.png" alt="" @click="handleGetThinkDynamicsReport()" />
</div> </div>
...@@ -16,31 +17,9 @@ ...@@ -16,31 +17,9 @@
</div> </div>
<div> <div>
<div class="select-box"> <div class="select-box">
<div class="select-box-time">
<el-select v-model="selectedYear" placeholder="近一年以来" style="width: 120px" :teleported="true"
:placement="'bottom-start'" :popper-options="{
modifiers: [
{
name: 'preventOverflow', // 禁用自动翻转逻辑
options: {
mainAxis: false, // 禁用垂直方向的自动调整
altAxis: false, // 禁用水平方向的自动调整
}
},
{
name: 'flip', // 完全禁用翻转功能
enabled: false
}
]
}">
<el-option @click="handleGetThinkDynamicsReport()" v-for="item in yearList" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
</div>
<div class="select-box-sort"> <div class="select-box-sort">
<el-select v-model="sort" placeholder="倒序" style="width: 120px" :teleported="true" <el-select v-model="sort" placeholder="发布时间" style="width: 120px" :teleported="true"
:placement="'bottom-start'" :popper-options="{ :placement="'bottom-start'" :popper-options="{
modifiers: [ modifiers: [
{ {
...@@ -57,10 +36,9 @@ ...@@ -57,10 +36,9 @@
] ]
}"> }">
<template #prefix> <template #prefix>
<img src="./images/image down.png" class="select-prefix-img" alt="" @click.stop="toggleSortAndFetch()" <img v-if="sort !== true" src="./images/image down.png" class="select-prefix-img" alt=""
:key="true" label="正序" :value="true" v-if="!sort" /> @click.stop="toggleSortAndFetch()" />
<img src="./images/image up.png" class="select-prefix-img" alt="" @click.stop="toggleSortAndFetch()" <img v-else src="./images/image up.png" class="select-prefix-img" alt="" @click.stop="toggleSortAndFetch()" />
:key="true" label="正序" :value="true" v-if="sort" />
</template> </template>
<el-option @click="handleGetThinkDynamicsReport()" :key="true" label="正序" :value="true" /> <el-option @click="handleGetThinkDynamicsReport()" :key="true" label="正序" :value="true" />
<el-option @click="handleGetThinkDynamicsReport()" :key="false" label="倒序" :value="false" /> <el-option @click="handleGetThinkDynamicsReport()" :key="false" label="倒序" :value="false" />
...@@ -72,7 +50,7 @@ ...@@ -72,7 +50,7 @@
<div v-if="isThinkTankReport"> <div v-if="isThinkTankReport">
<ThinkTankReport :research-type-list="researchTypeList" :research-time-list="researchTimeList" <ThinkTankReport :research-type-list="researchTypeList" :research-time-list="researchTimeList"
:key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterList" :total="total" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterList" :total="total"
:current-page="currentPage" @update:selected-filters="handleSelectedFiltersUpdate" :current-page="currentPage" :search-keyword="searchReport" @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" />
</div> </div>
...@@ -80,6 +58,7 @@ ...@@ -80,6 +58,7 @@
<CongressHearing :research-type-list="researchTypeList" :research-time-list="researchTimeList" <CongressHearing :research-type-list="researchTypeList" :research-time-list="researchTimeList"
:key="`国会听证会-${tabResetKey}`" :research-hearing-list="researchHearingList" :selected-filters="selectedFilters" :key="`国会听证会-${tabResetKey}`" :research-hearing-list="researchHearingList" :selected-filters="selectedFilters"
:selected-year="selectedYear" :cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :selected-year="selectedYear" :cur-footer-list="curFooterList" :total="total" :current-page="currentPage"
:search-keyword="searchReport"
:hearing-data="hearingData" @update:selected-filters="handleSelectedFiltersUpdate" :hearing-data="hearingData" @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" />
...@@ -87,7 +66,7 @@ ...@@ -87,7 +66,7 @@
<div> <div>
<SurveyForm v-if="isSurveyForm" :research-type-list="researchTypeList" :research-time-list="researchTimeList" <SurveyForm v-if="isSurveyForm" :research-type-list="researchTypeList" :research-time-list="researchTimeList"
:key="`调查项目-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterList" :total="total" :key="`调查项目-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterList" :total="total"
:current-page="currentPage" @update:selected-filters="handleSelectedFiltersUpdate" :current-page="currentPage" :search-keyword="searchReport" @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" />
</div> </div>
...@@ -113,8 +92,14 @@ import SurveyForm from "./SurveyForm/index.vue" ...@@ -113,8 +92,14 @@ import SurveyForm from "./SurveyForm/index.vue"
import { import {
getThinkDynamicsReportType, getThinkDynamicsReportType,
getHylyList, getHylyList,
getThinkDynamicsReport getThinkTankReport
} from "@/api/thinkTank/overview"; } from "@/api/thinkTank/overview";
import {
RESOURCE_FILTER_EARLIER,
stripAllTimeForRequest,
isSelectionCoveringAllOptions,
getResourceLibraryReportDateRangeFromTimeSelection
} from "../../utils/resourceLibraryFilters";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import ThinkTankReport from "./ThinkTankReport/index.vue"; import ThinkTankReport from "./ThinkTankReport/index.vue";
import CongressHearing from "./CongressHearing/index.vue"; import CongressHearing from "./CongressHearing/index.vue";
...@@ -323,7 +308,7 @@ const createDefaultSelectedFilters = () => ({ ...@@ -323,7 +308,7 @@ const createDefaultSelectedFilters = () => ({
const resetThinkDynamicsState = () => { const resetThinkDynamicsState = () => {
searchReport.value = '' searchReport.value = ''
selectedYear.value = 1 selectedYear.value = 1
sort.value = false sort.value = null
author.value = '' author.value = ''
currentPage.value = 1 currentPage.value = 1
selectedFilters.value = createDefaultSelectedFilters() selectedFilters.value = createDefaultSelectedFilters()
...@@ -375,8 +360,8 @@ const researchTimeList = ref([ ...@@ -375,8 +360,8 @@ const researchTimeList = ref([
name: '2021年', name: '2021年',
}, },
{ {
id: '更早以前', id: RESOURCE_FILTER_EARLIER,
name: '更早以前', name: RESOURCE_FILTER_EARLIER,
} }
]) ])
const researchHearingList = ref([ const researchHearingList = ref([
...@@ -492,35 +477,11 @@ const curFooterList = ref([ ...@@ -492,35 +477,11 @@ const curFooterList = ref([
// } // }
]); ]);
const yearList = ref([
{
label: "近一年发布",
value: 1
},
{
label: "近两年发布",
value: 2
},
{
label: "近三年发布",
value: 3
}
]);
const selectedYear = ref(1); const selectedYear = ref(1);
const sort = ref(false); /** null:界面显示「发布时间」占位且默认按发布时间倒序;true 正序;false 倒序(显式选中),与政策追踪一致 */
//获取当前时间x年前的日期 const sort = ref(null);
function getDateYearsAgo(years) {
// 获取当前日期
const currentDate = new Date();
// 计算指定年数之前的日期
const pastDate = new Date(currentDate.getFullYear() - years, currentDate.getMonth(), currentDate.getDate());
// 格式化日期为 "YYYY-MM-DD" 的形式
const year = pastDate.getFullYear();
const month = String(pastDate.getMonth() + 1).padStart(2, "0"); // 月份从0开始,需要加1
const day = String(pastDate.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
//获取智库研究类型 //获取智库研究类型
const handleGetThinkDynamicsReportType = async () => { const handleGetThinkDynamicsReportType = async () => {
try { try {
...@@ -550,9 +511,9 @@ const handleGetHylyList = async () => { ...@@ -550,9 +511,9 @@ const handleGetHylyList = async () => {
}; };
const toggleSortAndFetch = async () => { const toggleSortAndFetch = async () => {
sort.value = !sort.value sort.value = sort.value === true ? false : true;
await handleGetThinkDynamicsReport() await handleGetThinkDynamicsReport();
} };
const currentPage = ref(1); const currentPage = ref(1);
// 处理页码改变事件 // 处理页码改变事件
const handleCurrentChange = page => { const handleCurrentChange = page => {
...@@ -569,50 +530,62 @@ function arrayToString(arr) { ...@@ -569,50 +530,62 @@ function arrayToString(arr) {
}, ""); }, "");
} }
function buildThinkDynamicsYears(selectedYears) {
const allYearIds = researchTimeList.value.map(item => item.id)
if (!selectedYears?.length || selectedYears.length === allYearIds.length) {
return null
}
return arrayToString(selectedYears)
}
// 获取智库动态报告,payload 为子组件筛选变更时传入的当前选中值,避免时序导致拿到旧值 // 获取智库动态报告,payload 为子组件筛选变更时传入的当前选中值,避免时序导致拿到旧值
const handleGetThinkDynamicsReport = async (payload) => { const handleGetThinkDynamicsReport = async (payload) => {
const nextFilters = payload && typeof payload === 'object' const nextFilters = payload && typeof payload === "object"
? { ? {
researchTypeIds: payload.researchTypeIds ? [...payload.researchTypeIds] : [], researchTypeIds: payload.researchTypeIds ? [...payload.researchTypeIds] : [],
researchTimeIds: payload.researchTimeIds ? [...payload.researchTimeIds] : [], researchTimeIds: payload.researchTimeIds ? [...payload.researchTimeIds] : [],
researchHearingIds: payload.researchHearingIds ? [...payload.researchHearingIds] : [] researchHearingIds: payload.researchHearingIds ? [...payload.researchHearingIds] : []
} }
: selectedFilters.value; : selectedFilters.value;
if (payload && typeof payload === 'object') { if (payload && typeof payload === "object") {
selectedFilters.value = nextFilters; selectedFilters.value = nextFilters;
currentPage.value = 1; currentPage.value = 1;
} }
// 国会听证会走本地数据,与资源库 /thinkTankOverview/report 无关
if (!isThinkTankReport.value && !isSurveyForm.value) {
return;
}
try { try {
const parmas = { const strippedTime = stripAllTimeForRequest(nextFilters.researchTimeIds || []);
id: router.currentRoute._value.params.id, const allTimeIds = (researchTimeList.value || []).map((x) => x.id);
startDate: getDateYearsAgo(selectedYear.value ?? 1), const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
parmas: { strippedTime,
searchText: searchReport.value, allTimeIds
authorName: author.value ? author.value : null, );
sortFun: sort.value ?? true, const areas = nextFilters.researchTypeIds || [];
currentPage: currentPage.value - 1, const allAreaIds = (researchTypeList.value || []).map((a) => a.id);
pageSize: 12, const domainIds = isSelectionCoveringAllOptions(areas, allAreaIds)
researchTypeIds: arrayToString(nextFilters.researchTypeIds) === '' ? null : arrayToString(nextFilters.researchTypeIds), ? ""
years: buildThinkDynamicsYears(nextFilters.researchTimeIds) : arrayToString(areas);
} const keyword = (searchReport.value || "").trim();
const thinkTankId = router.currentRoute._value.params?.id;
const params = {
pageNum: currentPage.value,
pageSize: 12,
sortFun: sort.value === true,
domainIds,
startDate,
endDate
};
if (isSurveyForm.value) {
params.category = "调查项目";
}
if (thinkTankId) {
params.thinkTankId = thinkTankId;
}
if (keyword) {
params.keyword = keyword;
} }
const res = await getThinkDynamicsReport(parmas); const res = await getThinkTankReport(params);
console.log("智库动态报告", res); console.log("智库动态报告", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
curFooterList.value = res.data.content curFooterList.value = res.data.content;
total.value = res.data.totalElements total.value = res.data.totalElements;
} else if (res.code === 500 && res.message === '未找到对应筛选的报告') { } else if (res.code === 500 && res.message === "未找到对应筛选的报告") {
curFooterList.value = [] curFooterList.value = [];
total.value = 0 total.value = 0;
} }
} catch (error) { } catch (error) {
console.error("获取智库动态报告error", error); console.error("获取智库动态报告error", error);
......
...@@ -201,7 +201,8 @@ ...@@ -201,7 +201,8 @@
<div class="box3-main-right"> <div class="box3-main-right">
<div class="box3-right-item" v-for="(item, index) in box3RightData" :key="index"> <div class="box3-right-item" v-for="(item, index) in box3RightData" :key="index">
<div class="icon" @click="handleClickPerson(item)"> <div class="icon" @click="handleClickPerson(item)">
<img :src="item.imageUrl ? item.imageUrl : DefaultIcon1" alt="" /> <img :src="item.imageUrl ? item.imageUrl : DefaultIcon1" alt=""
@error="() => { if (item.imageUrl) item.imageUrl = null; }" />
</div> </div>
<div class="info"> <div class="info">
<div class="info-header"> <div class="info-header">
...@@ -223,17 +224,26 @@ ...@@ -223,17 +224,26 @@
<div class="info"><img src="./images/image-exclamation.png"></div> <div class="info"><img src="./images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> <div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div>
</div> </div>
<div class="middle">
<div class="middle-text">{{ "共" }}{{ personTotal }}{{ "名核心研究人员" }}</div>
<div class="page">
<div class="image" @click="handlePersonPrevPage"><img src="./images/left-page-btn.png" alt="" /></div>
<div class="page-text">{{ personPageText }}</div>
<div class="image" @click="handlePersonNextPage"><img src="./images/right-page-btn.png" alt="" /></div>
</div>
</div>
<div class="box3-main-bottom"> <div class="box3-main-bottom">
<div class="box3-right-item" v-for="(item, index) in box3RightData" :key="index"> <div class="box3-right-item" v-for="(item, index) in personList" :key="index">
<div class="icon" @click="handleClickPerson(item)"> <div class="icon" @click="handleClickPerson(item)">
<img :src="item.imageUrl ? item.imageUrl : DefaultIcon1" alt="" /> <img :src="item.avatarUrl ? item.avatarUrl : DefaultIcon1" alt=""
@error="() => { if (item.avatarUrl) item.avatarUrl = null; }" />
</div> </div>
<div class="info"> <div class="info">
<div class="info-header"> <div class="info-header">
<div class="name">{{ item.name }}</div> <div class="name">{{ item.name }}</div>
<div class="position">{{ item.positionTitle ?? '全球研究人才' }}</div> <div class="position">{{ item.positionTitle ?? '全球研究人才' }}</div>
</div> </div>
<div class="info-footer">{{ item.describe }}</div> <div class="info-footer">{{ item.introduction }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -245,14 +255,14 @@ ...@@ -245,14 +255,14 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick } from "vue"; import { ref, onMounted, nextTick, computed } from "vue";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import getPieChart from "./utils/piechart"; import getPieChart from "./utils/piechart";
import getTreeMapChart from "./utils/treeMapChart"; import getTreeMapChart from "./utils/treeMapChart";
import { import {
getThinkTankInfoBasic, getThinkTankInfoBasic,
getThinkTankInfoBranch, getThinkTankInfoBranch,
getThinkTankFundsTotal,
getThinkTankFundsSource, getThinkTankFundsSource,
getThinkTankResearchAreae, getThinkTankResearchAreae,
getThinkTankPerson getThinkTankPerson
...@@ -262,7 +272,7 @@ import { useRouter } from "vue-router"; ...@@ -262,7 +272,7 @@ import { useRouter } from "vue-router";
import DefaultIcon1 from '@/assets/icons/default-icon1.png' import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import { getPersonSummaryInfo } from "@/api/common/index"; import { getPersonSummaryInfo } from "@/api/common/index";
const totalPersonNumber = ref(0)
const router = useRouter(); const router = useRouter();
import InfoImg from "./images/img.png"; import InfoImg from "./images/img.png";
import Icon1 from "./images/icon1.png"; import Icon1 from "./images/icon1.png";
...@@ -341,9 +351,9 @@ const handleGetThinkTankInfoBranch = async () => { ...@@ -341,9 +351,9 @@ const handleGetThinkTankInfoBranch = async () => {
// 经费来源 // 经费来源
const box1LeftData = ref({ const box1LeftData = ref({
total: "4.358亿美元", totalJe: 0,
gov: "3.271亿美元", zfJe: 0,
other: "1.087亿美元" otherJ: 0
}); });
const box1ChartData = ref([ const box1ChartData = ref([
...@@ -387,10 +397,19 @@ const box1ChartData = ref([ ...@@ -387,10 +397,19 @@ const box1ChartData = ref([
const handleGetThinkTankFundsTotal = async () => { const handleGetThinkTankFundsTotal = async () => {
try { try {
const res = await getThinkTankFundsTotal(router.currentRoute._value.params.id); const res = await getThinkTankFundsSource(router.currentRoute._value.params.id);
console.log("获取经费来源统计", res); console.log("获取经费来源统计", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
box1LeftData.value = res.data // 后端字段已更新:totalAmount/usGovernmentAmount/pentagonContractorAmount
// 前端卡片仍使用 totalJe/zfJe/otherJ 字段名,因此做兼容映射(不改样式)
const totalJe = Number(res.data.totalAmount ?? res.data.totalJe ?? 0);
const zfJe = Number(res.data.usGovernmentAmount ?? res.data.zfJe ?? 0);
const otherJ = Number(res.data.pentagonContractorAmount ?? res.data.otherJ ?? 0);
return box1LeftData.value = {
totalJe: totalJe || 0, // 为空就用 0
zfJe: zfJe || 0, // 为空就用 0
otherJ: otherJ || 0 // 为空就用 0
};
} }
} catch (error) { } catch (error) {
console.error("获取经费来源统计error", error); console.error("获取经费来源统计error", error);
...@@ -404,10 +423,11 @@ const handleGetThinkTankFundsSource = async () => { ...@@ -404,10 +423,11 @@ const handleGetThinkTankFundsSource = async () => {
console.log("获取经费来源", res); console.log("获取经费来源", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const topList = Array.isArray(res.data) ? res.data.slice(0, 7) : [] // 后端字段已改为 sources;机构名优先 institution,其次 secondInstitution
const topList = Array.isArray(res.data.sources) ? res.data.sources : []
const data = topList.map(item => ({ const data = topList.map(item => ({
name: item.institution, name: item.institution || item.secondInstitution || "未知机构",
value: item.amount, value: Number(item.amount) || 0,
})) }))
box1ChartData.value = data box1ChartData.value = data
const box1Chart = getPieChart(box1ChartData.value); const box1Chart = getPieChart(box1ChartData.value);
...@@ -423,24 +443,30 @@ const handleGetThinkTankFundsSource = async () => { ...@@ -423,24 +443,30 @@ const handleGetThinkTankFundsSource = async () => {
const LABEL_OFFSET_UP = 3 + LABEL_BOX_HEIGHT / 2 const LABEL_OFFSET_UP = 3 + LABEL_BOX_HEIGHT / 2
const dataList = box1ChartData.value const dataList = box1ChartData.value
const total = dataList.reduce((s, d) => s + (d.value || 0), 0) || 1 const total = dataList.reduce((s, d) => s + (d.value || 0), 0) || 1
const formatDeptName = (name) => {
const text = String(name || "");
const chars = Array.from(text);
return chars.length > 9 ? `${chars.slice(0, 9).join("")}...` : text;
};
box1Chart.series[0].label = { box1Chart.series[0].label = {
...box1Chart.series[0].label, ...box1Chart.series[0].label,
alignTo: 'edge', alignTo: 'edge',
offset: [0, -LABEL_OFFSET_UP], offset: [0, -LABEL_OFFSET_UP],
formatter(params) { formatter(params) {
const valueYi = (params.data.value || 0) / 1000000 const valueYi = (params.data.value || 0) / 100000000
const percent = Math.round(params.percent || 0) const percent = params.percent || 0
const valueStr = `${Math.round(valueYi)}亿 ${percent}%` const valueStr = `${valueYi.toFixed(3)}亿 ${percent}%`
let cumulative = 0 let cumulative = 0
for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 0 for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 0
const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360 const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360
const angleNorm = ((centerAngle % 360) + 360) % 360 const angleNorm = ((centerAngle % 360) + 360) % 360
// 左侧仅 270–360° 及跨圈:部门左、经费右;其余(含 180–270° 右下两条):经费左、部门右 // 左侧仅 270–360° 及跨圈:部门左、经费右;其余(含 180–270° 右下两条):经费左、部门右
const isLeftLabel = (angleNorm >= 270 && angleNorm < 360) || centerAngle >= 360 const isLeftLabel = (angleNorm >= 270 && angleNorm < 360) || centerAngle >= 360
const deptName = formatDeptName(params.data.name);
if (isLeftLabel) { if (isLeftLabel) {
return `{nameL|${params.data.name}}{valueR|${valueStr}}` return `{nameL|${deptName}}{valueR|${valueStr}}`
} }
return `{valueL|${valueStr}}{nameR|${params.data.name}}` return `{valueL|${valueStr}}{nameR|${deptName}}`
}, },
lineHeight: LABEL_BOX_HEIGHT, lineHeight: LABEL_BOX_HEIGHT,
rich: { rich: {
...@@ -604,8 +630,8 @@ const handleBox3Chart = () => { ...@@ -604,8 +630,8 @@ const handleBox3Chart = () => {
delete box3Chart.series[0].itemStyle; delete box3Chart.series[0].itemStyle;
box3Chart.series[0].sort = false; box3Chart.series[0].sort = false;
// 图表充满 box3-main-top,左右各留 12px // 图表充满 box3-main-top,左右各留 12px
box3Chart.series[0].left = 12; box3Chart.series[0].left = 0;
box3Chart.series[0].right = 12; box3Chart.series[0].right = 0;
box3Chart.series[0].top = 0; box3Chart.series[0].top = 0;
box3Chart.series[0].bottom = 0; box3Chart.series[0].bottom = 0;
// 方块内文字与区域标题统一为黑色,方块内文字加 1px 白色描边(等效 CSS text-stroke: 1px #fff) // 方块内文字与区域标题统一为黑色,方块内文字加 1px 白色描边(等效 CSS text-stroke: 1px #fff)
...@@ -621,57 +647,62 @@ const handleBox3Chart = () => { ...@@ -621,57 +647,62 @@ const handleBox3Chart = () => {
}; };
box3Chart.series[0].upperLabel = { box3Chart.series[0].upperLabel = {
...box3Chart.series[0].upperLabel, ...box3Chart.series[0].upperLabel,
color: 'rgb(51,51,51)' color: 'rgb(51,51,51)',
fontSize: 16,
fontWeight: 700,
backgroundColor: 'rgb(247,248,249)',
lineHeight: 16,
height: 16,
padding: 0
}; };
setChart(box3Chart, "box3Chart"); setChart(box3Chart, "box3Chart");
}; };
const box3RightData = ref([ const PERSON_PAGE_SIZE = 10;
{ const personCurrentPage = ref(1);
img: Icon1, const box3RightData = ref({
name: "杰森·马西尼 ​", content: [],
postion: "兰德公司总裁兼首席执行官", totalElements: 0
desc: "曾任职于哈佛大学经济学院、美国财政部" });
}, const personList = computed(() => (Array.isArray(box3RightData.value?.content) ? box3RightData.value.content : []));
{ const personTotal = computed(() => Number(box3RightData.value?.totalElements || 0));
img: Icon2, const personTotalPages = computed(() => (personTotal.value > 0 ? Math.ceil(personTotal.value / PERSON_PAGE_SIZE) : 1));
name: "安德鲁·R·霍恩 ​", const personPageText = computed(() => `${personCurrentPage.value}/${personTotalPages.value}`);
postion: "高级副总裁,研究与分析",
desc: "曾任职于白宫科技政策顾问办公室"
},
{
img: Icon3,
name: "杰森·马西尼 ​",
postion: "兰德公司总裁兼首席执行官",
desc: "日籍研究院,毕业于斯坦福大学"
},
{
img: Icon4,
name: "梅丽莎·罗 ​",
postion: "副总裁,全球研究人才",
desc: "曾任职于美国国防部"
},
{
img: Icon5,
name: "安妮塔·钱德拉 ​ ​",
postion: "副总裁兼主任,兰德社会和经济福祉",
desc: "曾任职于哈佛大学经济学院、美国商务部"
}
]);
const handleGetThinkPerson = async () => { const handleGetThinkPerson = async (page = personCurrentPage.value) => {
try { try {
const safePage = Math.max(1, Number(page) || 1);
const params = {
currentPage: safePage,
pageSize: PERSON_PAGE_SIZE,
thinkTankId: router.currentRoute._value.params.id
}
const res = await getThinkTankPerson(params);
const res = await getThinkTankPerson(router.currentRoute._value.params.id);
console.log("核心研究人员", res); console.log("核心研究人员", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
box3RightData.value = res.data box3RightData.value = {
content: Array.isArray(res.data.content) ? res.data.content : [],
totalElements: Number(res.data.totalElements || 0)
}
// 以后端返回页码为准;未返回时使用请求页码
personCurrentPage.value = Number(res.data.currentPage || res.data.pageNum || safePage);
} }
} catch (error) { } catch (error) {
console.error("获取核心研究人员error", error); console.error("获取核心研究人员error", error);
} }
}; };
const handlePersonPrevPage = () => {
if (personCurrentPage.value <= 1) return;
handleGetThinkPerson(personCurrentPage.value - 1);
};
const handlePersonNextPage = () => {
if (personCurrentPage.value >= personTotalPages.value) return;
handleGetThinkPerson(personCurrentPage.value + 1);
};
// 点击人物头像,跳转到人物主页 // 点击人物头像,跳转到人物主页
const handleClickPerson = async item => { const handleClickPerson = async item => {
...@@ -1238,10 +1269,11 @@ onMounted(() => { ...@@ -1238,10 +1269,11 @@ onMounted(() => {
.box3-main-top { .box3-main-top {
width: 1100px; width: 1100px;
height: 372px; height: 379px;
box-sizing: border-box; box-sizing: border-box;
padding-left: 35px; padding-left: 35px;
padding-top: 7px;
#box3Chart { #box3Chart {
width: 1035px; width: 1035px;
...@@ -1287,15 +1319,67 @@ onMounted(() => { ...@@ -1287,15 +1319,67 @@ onMounted(() => {
} }
} }
.middle {
width: 1027px;
height: 32px;
margin-left: 43px;
display: flex;
justify-content: space-between;
margin-top: 24px;
.middle-text {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: rgb(95, 101, 108);
}
.page {
height: 24px;
display: flex;
margin-top: 4px;
.page-text {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: center;
color: rgb(5, 95, 194);
margin: 1px 8px;
}
.image {
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
}
.box3-main-bottom { .box3-main-bottom {
margin-left: 28px; margin-left: 35px;
margin-top: 24px; margin-top: 24px;
width: 536px; width: 1044px;
height: 326px; height: 352px;
display: flex;
flex-wrap: wrap;
column-gap: 44px;
row-gap: 8px;
align-content: flex-start;
.box3-right-item { .box3-right-item {
display: flex; display: flex;
width: 520px; width: 500px;
height: 64px; height: 64px;
align-items: center; align-items: center;
...@@ -1314,7 +1398,7 @@ onMounted(() => { ...@@ -1314,7 +1398,7 @@ onMounted(() => {
} }
.info { .info {
width: 459px; width: calc(100% - 61px);
margin-left: 13px; margin-left: 13px;
.info-header { .info-header {
...@@ -1322,6 +1406,7 @@ onMounted(() => { ...@@ -1322,6 +1406,7 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
.name { .name {
width: 219px;
height: 24px; height: 24px;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
...@@ -1329,9 +1414,13 @@ onMounted(() => { ...@@ -1329,9 +1414,13 @@ onMounted(() => {
font-weight: 700; font-weight: 700;
line-height: 24px; line-height: 24px;
letter-spacing: 1px; letter-spacing: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.position { .position {
width: 219px;
height: 22px; height: 22px;
color: var(--color-main-active); color: var(--color-main-active);
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
...@@ -1340,6 +1429,9 @@ onMounted(() => { ...@@ -1340,6 +1429,9 @@ onMounted(() => {
line-height: 22px; line-height: 22px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: right; text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
} }
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
}"> }">
<template #prefix> <template #prefix>
<img src="../assets/images/sort-asc.png" class="select-prefix-img" alt="" @click.stop="toggleSort()" <img src="../assets/images/sort-asc.png" class="select-prefix-img" alt="" @click.stop="toggleSort()"
:key="true" label="正序" :value="true" v-if="sort === true" /> :key="true" label="正序" :value="true" v-if="sort !== false" />
<img src="../assets/images/sort-desc.png" class="select-prefix-img" alt="" @click.stop="toggleSort()" <img src="../assets/images/sort-desc.png" class="select-prefix-img" alt="" @click.stop="toggleSort()"
:key="false" label="倒序" :value="false" v-if="sort === false" /> :key="false" label="倒序" :value="false" v-if="sort === false" />
</template> </template>
...@@ -191,7 +191,7 @@ const handleGetThinkTankList = async () => { ...@@ -191,7 +191,7 @@ const handleGetThinkTankList = async () => {
total.value = 0; total.value = 0;
} }
}; };
// 初始为 null:el-select 显示 placeholder;但排序仍按“正序”规则(见 sortedCardList) // 默认显示 placeholder「报告数量」,但前缀图标显示正序
const sort = ref(null); const sort = ref(null);
const toggleSort = () => { const toggleSort = () => {
sort.value = sort.value === false ? true : false sort.value = sort.value === false ? true : false
......
...@@ -7,15 +7,21 @@ ...@@ -7,15 +7,21 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox v-model="checkAllModel" class="all-checkbox" @change="emit('check-all-change', $event)"> class="checkbox-group"
全部领域 :model-value="selectedAreaList"
@change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="research in areaList" :key="research.id" v-model="selectedAreaListModel" <el-checkbox
:label="research.id" @change="emit('checked-area-change')" class="filter-checkbox"> v-for="research in areaList"
:key="research.id"
class="filter-checkbox"
:label="research.id">
{{ research.name }} {{ research.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
...@@ -25,17 +31,21 @@ ...@@ -25,17 +31,21 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox v-model="checkAllTimeModel" class="all-checkbox" class="checkbox-group"
@change="emit('check-all-time-change', $event)"> :model-value="selectedPubTimeList"
全部时间 @change="handleTimeGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="time in pubTimeList" :key="time.id" v-model="selectedPubTimeListModel" :label="time.id" <el-checkbox
class="filter-checkbox" @change="emit('checked-area-time-change')"> v-for="time in pubTimeList"
:key="time.id"
class="filter-checkbox"
:label="time.id">
{{ time.name }} {{ time.name }}
</el-checkbox> </el-checkbox>
</el-checkbox-group>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -68,56 +78,39 @@ ...@@ -68,56 +78,39 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue"; import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters";
const props = defineProps({ defineProps({
checkAll: { type: Boolean, default: false },
isIndeterminate: { type: Boolean, default: false },
areaList: { type: Array, default: () => [] }, areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] }, selectedAreaList: { type: Array, default: () => [] },
checkAllTime: { type: Boolean, default: false },
isIndeterminateTime: { type: Boolean, default: false },
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 }
}); });
const emit = defineEmits([ const emit = defineEmits([
"update:checkAll",
"update:selectedAreaList", "update:selectedAreaList",
"check-all-change",
"checked-area-change",
"update:checkAllTime",
"update:selectedPubTimeList", "update:selectedPubTimeList",
"check-all-time-change", "filter-change",
"checked-area-time-change",
"report-click", "report-click",
"page-change" "page-change"
]); ]);
const checkAllModel = computed({ const handleAreaGroupChange = (val) => {
get: () => props.checkAll, emit("update:selectedAreaList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_AREA));
set: val => emit("update:checkAll", val) emit("filter-change");
}); };
const selectedAreaListModel = computed({ const handleTimeGroupChange = (val) => {
get: () => props.selectedAreaList, emit("update:selectedPubTimeList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME));
set: val => emit("update:selectedAreaList", val) emit("filter-change");
}); };
const checkAllTimeModel = computed({
get: () => props.checkAllTime,
set: val => emit("update:checkAllTime", val)
});
const selectedPubTimeListModel = computed({
get: () => props.selectedPubTimeList,
set: val => emit("update:selectedPubTimeList", val)
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
<template> <template>
<!-- 调查项目:结构/样式与智库报告一致,但组件独立,避免互相影响 --> <!-- 调查项目:与智库报告相同的「全部」互斥逻辑 -->
<div class="home-main-footer-main"> <div class="home-main-footer-main">
<div class="left"> <div class="left">
<div class="select-box"> <div class="select-box">
...@@ -8,15 +8,21 @@ ...@@ -8,15 +8,21 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox v-model="checkAllModel" class="all-checkbox" @change="emit('check-all-change', $event)"> class="checkbox-group"
全部领域 :model-value="selectedAreaList"
@change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="research in areaList" :key="research.id" v-model="selectedAreaListModel" <el-checkbox
:label="research.id" @change="emit('checked-area-change')" class="filter-checkbox"> v-for="research in areaList"
:key="research.id"
class="filter-checkbox"
:label="research.id">
{{ research.name }} {{ research.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
...@@ -26,16 +32,21 @@ ...@@ -26,16 +32,21 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox v-model="checkAllTimeModel" class="all-checkbox" class="checkbox-group"
@change="emit('check-all-time-change', $event)"> :model-value="selectedPubTimeList"
全部时间 @change="handleTimeGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox v-model="selectedPubTimeListModel" v-for="time in pubTimeList" :key="time.id" :label="time.id" <el-checkbox
class="filter-checkbox" @change="emit('checked-area-time-change')"> v-for="time in pubTimeList"
:key="time.id"
class="filter-checkbox"
:label="time.id">
{{ time.name }} {{ time.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
</div> </div>
...@@ -68,56 +79,39 @@ ...@@ -68,56 +79,39 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue"; import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters";
const props = defineProps({ defineProps({
checkAll: { type: Boolean, default: false },
isIndeterminate: { type: Boolean, default: false },
areaList: { type: Array, default: () => [] }, areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] }, selectedAreaList: { type: Array, default: () => [] },
checkAllTime: { type: Boolean, default: false },
isIndeterminateTime: { type: Boolean, default: false },
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 }
}); });
const emit = defineEmits([ const emit = defineEmits([
"update:checkAll",
"update:selectedAreaList", "update:selectedAreaList",
"check-all-change",
"checked-area-change",
"update:checkAllTime",
"update:selectedPubTimeList", "update:selectedPubTimeList",
"check-all-time-change", "filter-change",
"checked-area-time-change",
"report-click", "report-click",
"page-change" "page-change"
]); ]);
const checkAllModel = computed({ const handleAreaGroupChange = (val) => {
get: () => props.checkAll, emit("update:selectedAreaList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_AREA));
set: val => emit("update:checkAll", val) emit("filter-change");
}); };
const selectedAreaListModel = computed({
get: () => props.selectedAreaList,
set: val => emit("update:selectedAreaList", val)
});
const checkAllTimeModel = computed({ const handleTimeGroupChange = (val) => {
get: () => props.checkAllTime, emit("update:selectedPubTimeList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME));
set: val => emit("update:checkAllTime", val) emit("filter-change");
}); };
const selectedPubTimeListModel = computed({
get: () => props.selectedPubTimeList,
set: val => emit("update:selectedPubTimeList", val)
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -7,16 +7,21 @@ ...@@ -7,16 +7,21 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchTypeIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchTypeIds)"> :model-value="selectedResearchIds"
全部领域 @change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchTypeList" :key="type.id" v-model="selectedResearchIds" :label="type.id" <el-checkbox
class="filter-checkbox" @change="handleFilterChange"> v-for="type in researchTypeList"
:key="type.id"
class="filter-checkbox"
:label="type.id">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
...@@ -26,16 +31,21 @@ ...@@ -26,16 +31,21 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchTimeIds)" class="checkbox-group"
@change="val => handleToggleAll(val, researchTimeIds)"> :model-value="selectedResearchTimeIds"
全部时间 @change="handleTimeGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchTimeList" :key="type.id" v-model="selectedResearchTimeIds" <el-checkbox
:label="type.id" class="filter-checkbox" @change="handleFilterChange"> v-for="type in researchTimeList"
:key="type.id"
class="filter-checkbox"
:label="type.id">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
...@@ -45,16 +55,21 @@ ...@@ -45,16 +55,21 @@
<div class="title">{{ "听证会部门" }}</div> <div class="title">{{ "听证会部门" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group hearing-grid"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isGroupAllSelected(researchHearingIds)" class="checkbox-group hearing-grid"
@change="val => handleToggleAll(val, researchHearingIds)"> :model-value="selectedResearchHearingIds"
全部部门 @change="handleDeptGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_DEPT">
{{ RESOURCE_FILTER_ALL_DEPT }}
</el-checkbox> </el-checkbox>
<el-checkbox v-for="type in researchHearingList" :key="type.id" v-model="selectedResearchHearingIds" <el-checkbox
:label="type.id" class="filter-checkbox" @change="handleFilterChange"> v-for="type in researchHearingList"
:key="type.id"
class="filter-checkbox"
:label="type.id">
{{ type.name }} {{ type.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
</div> </div>
...@@ -76,8 +91,7 @@ ...@@ -76,8 +91,7 @@
class="card-open-image" /> class="card-open-image" />
</div> </div>
<div class="card-item-category"> <div class="card-item-category">
<AreaTag :key="index" :tagName="item.category"> <AreaTag :key="`cat-${item.id}`" :tagName="item.category" />
</AreaTag>
</div> </div>
</div> </div>
</div> </div>
...@@ -101,6 +115,18 @@ ...@@ -101,6 +115,18 @@
<script setup> <script setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import AreaTag from "@/components/base/AreaTag/index.vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
RESOURCE_FILTER_ALL_DEPT,
RESOURCE_FILTER_EARLIER,
normalizeExclusiveAllOption,
stripAllAreaForRequest,
stripAllTimeForRequest,
stripAllDeptForRequest,
matchesEarlierChineseDate
} from "../utils/resourceLibraryFilters";
const props = defineProps({ const props = defineProps({
researchTypeList: { type: Array, default: () => [] }, researchTypeList: { type: Array, default: () => [] },
...@@ -112,11 +138,10 @@ const emit = defineEmits(["report-click"]); ...@@ -112,11 +138,10 @@ const emit = defineEmits(["report-click"]);
const pageSize = 10; const pageSize = 10;
const currentPage = ref(1); const currentPage = ref(1);
const selectedResearchIds = ref([]); const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([]); const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
const selectedResearchHearingIds = ref([]); const selectedResearchHearingIds = ref([RESOURCE_FILTER_ALL_DEPT]);
// 概览页暂无独立接口时,先使用一份静态数据(结构与智库动态保持一致)
const hearingData = ref([ const hearingData = ref([
{ id: 1, title: "美国国会听证会:人工智能与国家安全", content: "美中经济与安全审查委员会", category: "人工智能", time: "2025年7月8日" }, { id: 1, title: "美国国会听证会:人工智能与国家安全", content: "美中经济与安全审查委员会", category: "人工智能", time: "2025年7月8日" },
{ id: 2, title: "美国国会听证会:先进制造供应链韧性", content: "国会-行政部门中国委员会", category: "先进制造", time: "2025年6月15日" }, { id: 2, title: "美国国会听证会:先进制造供应链韧性", content: "国会-行政部门中国委员会", category: "先进制造", time: "2025年6月15日" },
...@@ -138,46 +163,44 @@ const researchHearingList = ref([ ...@@ -138,46 +163,44 @@ const researchHearingList = ref([
{ id: "美国商务部", name: "美国商务部" }, { id: "美国商务部", name: "美国商务部" },
]); ]);
const researchTypeIds = computed(() => (props.researchTypeList || []).map(item => item.id)); const handleAreaGroupChange = (val) => {
const researchTimeIds = computed(() => (props.researchTimeList || []).map(item => item.id)); selectedResearchIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_AREA);
const researchHearingIds = computed(() => (researchHearingList.value || []).map(item => item.id)); currentPage.value = 1;
};
const getTargetSelection = ids => { const handleTimeGroupChange = (val) => {
if (ids === researchTypeIds.value) return selectedResearchIds; selectedResearchTimeIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME);
if (ids === researchTimeIds.value) return selectedResearchTimeIds; currentPage.value = 1;
return selectedResearchHearingIds;
}; };
const isGroupAllSelected = ids => const handleDeptGroupChange = (val) => {
ids.length > 0 && ids.every(id => getTargetSelection(ids).value.includes(id)); selectedResearchHearingIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_DEPT);
currentPage.value = 1;
const handleToggleAll = (checked, ids) => {
if (!ids.length) return;
const targetSelection = getTargetSelection(ids);
const nextSelected = new Set(targetSelection.value);
if (checked) {
ids.forEach(id => nextSelected.add(id));
} else {
ids.forEach(id => nextSelected.delete(id));
}
targetSelection.value = [...nextSelected];
handleFilterChange();
}; };
const filteredHearingData = computed(() => { const filteredHearingData = computed(() => {
const areaSel = stripAllAreaForRequest(selectedResearchIds.value);
const timeSel = stripAllTimeForRequest(selectedResearchTimeIds.value);
const deptSel = stripAllDeptForRequest(selectedResearchHearingIds.value);
return (hearingData.value || []).filter(item => { return (hearingData.value || []).filter(item => {
const matchYear = const matchYear =
selectedResearchTimeIds.value.length === 0 || timeSel.length === 0 ||
selectedResearchTimeIds.value.some(year => String(item.time || "").startsWith(year)); timeSel.some(sel => {
if (sel === RESOURCE_FILTER_EARLIER) {
return matchesEarlierChineseDate(item.time);
}
return String(item.time || "").startsWith(String(sel));
});
const matchDepartment = const matchDepartment =
selectedResearchHearingIds.value.length === 0 || deptSel.length === 0 ||
selectedResearchHearingIds.value.some(department => deptSel.some(department =>
String(item.content || "").includes(department) || String(item.title || "").includes(department) String(item.content || "").includes(department) || String(item.title || "").includes(department)
); );
const matchType = const matchType =
selectedResearchIds.value.length === 0 || areaSel.length === 0 ||
selectedResearchIds.value.some(typeId => areaSel.some(typeId =>
String(item.category || "").includes(typeId) || String(item.title || "").includes(typeId) String(item.category || "").includes(String(typeId)) || String(item.title || "").includes(String(typeId))
); );
return matchYear && matchDepartment && matchType; return matchYear && matchDepartment && matchType;
}); });
...@@ -189,10 +212,6 @@ const displayList = computed(() => { ...@@ -189,10 +212,6 @@ const displayList = computed(() => {
return list.slice(start, start + pageSize); return list.slice(start, start + pageSize);
}); });
const handleFilterChange = () => {
currentPage.value = 1;
};
const handlePageChange = page => { const handlePageChange = page => {
currentPage.value = page; currentPage.value = page;
}; };
......
...@@ -7,16 +7,21 @@ ...@@ -7,16 +7,21 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isAllSelected(typeIds)" class="checkbox-group"
@change="val => toggleAll(val, typeIds)"> :model-value="selectedTypeIds"
全部领域 @change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox class="filter-checkbox" v-for="t in (researchTypeList || [])" :key="t.id" <el-checkbox
v-model="selectedTypeIds" :label="t.id" @change="emitChange"> class="filter-checkbox"
v-for="t in (researchTypeList || [])"
:key="t.id"
:label="t.id">
{{ t.name }} {{ t.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
...@@ -26,16 +31,21 @@ ...@@ -26,16 +31,21 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <el-checkbox-group
<el-checkbox class="filter-checkbox" :model-value="isAllSelected(yearIds)" class="checkbox-group"
@change="val => toggleAll(val, yearIds)"> :model-value="selectedYearIds"
全部时间 @change="handleYearGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox class="filter-checkbox" v-for="y in (researchTimeList || [])" :key="y.id" <el-checkbox
v-model="selectedYearIds" :label="y.id" @change="emitChange"> class="filter-checkbox"
v-for="y in (researchTimeList || [])"
:key="y.id"
:label="y.id">
{{ y.name }} {{ y.name }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
</div> </div>
...@@ -95,10 +105,17 @@ ...@@ -95,10 +105,17 @@
</template> </template>
<script setup> <script setup>
import { computed, ref } from "vue"; import { ref } from "vue";
import AreaTag from "@/components/base/AreaTag/index.vue"; import AreaTag from "@/components/base/AreaTag/index.vue";
import {
const props = defineProps({ RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption,
stripAllAreaForRequest,
stripAllTimeForRequest
} from "../utils/resourceLibraryFilters";
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: () => [] },
...@@ -109,29 +126,25 @@ const props = defineProps({ ...@@ -109,29 +126,25 @@ const props = defineProps({
const emit = defineEmits(["filter-change", "page-change", "item-click"]); const emit = defineEmits(["filter-change", "page-change", "item-click"]);
const selectedTypeIds = ref([]); const selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedYearIds = ref([]); const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]);
const typeIds = computed(() => (props.researchTypeList || []).map(i => i.id)); const emitFilterToParent = () => {
const yearIds = computed(() => (props.researchTimeList || []).map(i => i.id));
const isAllSelected = ids => ids.length > 0 && ids.every(id => {
const target = ids === typeIds.value ? selectedTypeIds.value : selectedYearIds.value
return target.includes(id)
});
const toggleAll = (checked, ids) => {
const target = ids === typeIds.value ? selectedTypeIds : selectedYearIds
target.value = checked ? [...ids] : []
emitChange()
}
const emitChange = () => {
emit("filter-change", { emit("filter-change", {
researchTypeIds: [...selectedTypeIds.value], researchTypeIds: stripAllAreaForRequest(selectedTypeIds.value),
researchTimeIds: [...selectedYearIds.value], researchTimeIds: stripAllTimeForRequest(selectedYearIds.value),
}) });
} };
const handleAreaGroupChange = (val) => {
selectedTypeIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_AREA);
emitFilterToParent();
};
const handleYearGroupChange = (val) => {
selectedYearIds.value = normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME);
emitFilterToParent();
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -197,15 +197,7 @@ ...@@ -197,15 +197,7 @@
</template> </template>
<template v-else> <template v-else>
<div class="box5-chart-row"> <div class="box5-chart-row">
<button v-if="box5LegendShowNav" type="button" class="box5-legend-nav-btn"
:disabled="!box5LegendCanPrev" aria-label="上一页图例" @click="handleBox5LegendPrev">
</button>
<div id="box5Chart" class="box5-chart-canvas"></div> <div id="box5Chart" class="box5-chart-canvas"></div>
<button v-if="box5LegendShowNav" type="button" class="box5-legend-nav-btn"
:disabled="!box5LegendCanNext" aria-label="下一页图例" @click="handleBox5LegendNext">
</button>
</div> </div>
<div class="source"> <div class="source">
<TipTab :text="'智库报告数量变化趋势,数据来源:美国各智库官网'" /> <TipTab :text="'智库报告数量变化趋势,数据来源:美国各智库官网'" />
...@@ -342,17 +334,19 @@ ...@@ -342,17 +334,19 @@
</div> </div>
</div> </div>
<div class="select-box"> <div class="select-box">
<div class="paixu-btn" @click="handleSwithSort()"> <el-select v-model="resourceLibrarySortModel" class="resource-library-sort-select" placeholder="发布时间"
<div class="icon1"> style="width: 120px" :teleported="true" placement="bottom-start"
<img v-if="activeSort" src="@/assets/icons/shengxu1.png" alt="" /> :popper-options="resourceLibrarySortPopperOptions" @change="handleResourceLibrarySortChange">
<img v-else src="@/assets/icons/jiangxu1.png" alt="" /> <template #prefix>
</div> <img v-if="resourceLibrarySortModel !== true"
<div class="text">{{ "发布时间" }}</div> src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png"
<div class="icon2"> class="resource-library-sort-prefix-img" alt="" @click.stop="toggleResourceLibrarySortPrefix" />
<img v-if="activeSort" src="@/assets/icons/shengxu2.png" alt="" /> <img v-else src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image up.png"
<img v-else src="@/assets/icons/jiangxu2.png" alt="" /> class="resource-library-sort-prefix-img" alt="" @click.stop="toggleResourceLibrarySortPrefix" />
</div> </template>
</div> <el-option :key="'resource-lib-sort-asc'" label="正序" :value="true" />
<el-option :key="'resource-lib-sort-desc'" label="倒序" :value="false" />
</el-select>
</div> </div>
<!-- <el-select v-model="sort" placeholder="发布时间" style="width: 120px; margin-left: 8px"> <!-- <el-select v-model="sort" placeholder="发布时间" style="width: 120px; margin-left: 8px">
<el-option @click="handleGetetThinkTankReport()" :key="true" label="正序" :value="true" /> <el-option @click="handleGetetThinkTankReport()" :key="true" label="正序" :value="true" />
...@@ -360,25 +354,17 @@ ...@@ -360,25 +354,17 @@
</el-select> --> </el-select> -->
</div> </div>
<div class="resource-library-content"> <div class="resource-library-content">
<HomeMainFooterMain v-if="activeCate === '智库报告'" v-model:checkAll="checkAll" <HomeMainFooterMain v-if="activeCate === '智库报告'" :area-list="areaList"
:is-indeterminate="isIndeterminate" :area-list="areaList" v-model:selectedAreaList="selectedAreaList" v-model:selectedAreaList="selectedAreaList" :pub-time-list="pubTimeList"
@check-all-change="handleCheckAllChange" @checked-area-change="handleCheckedAreaChange()" v-model:selectedPubTimeList="selectedPubTimeList" @filter-change="handleThinkTankReportFilterChange"
v-model:checkAllTime="checkAllTime" :is-indeterminate-time="isIndeterminateTime"
:pub-time-list="pubTimeList" v-model:selectedPubTimeList="selectedPubTimeList"
@check-all-time-change="handleCheckAllChangeTime" @checked-area-time-change="handleCheckedAreaChangeTime()"
:cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :cur-footer-list="curFooterList" :total="total" :current-page="currentPage"
@report-click="handleToReportDetail" @page-change="handleCurrentChange" /> @report-click="handleToReportDetail" @page-change="handleCurrentChange" />
<HomeMainFooterSurvey v-else-if="activeCate === '调查项目'" v-model:checkAll="surveyCheckAll" <HomeMainFooterSurvey v-else-if="activeCate === '调查项目'" :area-list="areaList"
:is-indeterminate="surveyIsIndeterminate" :area-list="areaList" v-model:selectedAreaList="surveySelectedAreaList" :pub-time-list="pubTimeList"
v-model:selectedAreaList="surveySelectedAreaList" @check-all-change="handleSurveyCheckAllChange" v-model:selectedPubTimeList="surveySelectedPubTimeList" @filter-change="handleSurveyFilterChange"
@checked-area-change="handleSurveyCheckedAreaChange()" v-model:checkAllTime="surveyCheckAllTime" :cur-footer-list="surveyFooterList" :total="surveyTotal" :current-page="surveyCurrentPage"
:is-indeterminate-time="surveyIsIndeterminateTime" :pub-time-list="pubTimeList" @report-click="handleToReportDetail" @page-change="handleSurveyCurrentChange" />
v-model:selectedPubTimeList="surveySelectedPubTimeList"
@check-all-time-change="handleSurveyCheckAllChangeTime"
@checked-area-time-change="handleSurveyCheckedAreaChangeTime()" :cur-footer-list="surveyFooterList"
:total="surveyTotal" :current-page="surveyCurrentPage" @report-click="handleToReportDetail"
@page-change="handleSurveyCurrentChange" />
<ThinkTankCongressHearingOverview v-else-if="activeCate === '国会听证会'" :key="`congress-${resourceTabResetKey}`" <ThinkTankCongressHearingOverview v-else-if="activeCate === '国会听证会'" :key="`congress-${resourceTabResetKey}`"
:research-type-list="areaList" :research-time-list="pubTimeList" @report-click="handleToReportDetail" /> :research-type-list="areaList" :research-time-list="pubTimeList" @report-click="handleToReportDetail" />
...@@ -423,7 +409,7 @@ import { ...@@ -423,7 +409,7 @@ import {
getThinkTankReportRemarks getThinkTankReportRemarks
} from "@/api/thinkTank/overview"; } from "@/api/thinkTank/overview";
import { getPersonSummaryInfo } from "@/api/common/index"; import { getPersonSummaryInfo } from "@/api/common/index";
import getMultiLineChart, { MULTILINE_LEGEND_SHOW_COUNT } from "./utils/multiLineChart"; import getMultiLineChart from "./utils/multiLineChart";
import getPieChart from "./utils/piechart"; import getPieChart from "./utils/piechart";
import getSankeyChart from "./utils/sankey"; import getSankeyChart from "./utils/sankey";
import { getChartAnalysis } from "@/api/aiAnalysis/index"; import { getChartAnalysis } from "@/api/aiAnalysis/index";
...@@ -437,6 +423,16 @@ import News5 from "./assets/images/news5.png"; ...@@ -437,6 +423,16 @@ import News5 from "./assets/images/news5.png";
import AiButton from '@/components/base/Ai/AiButton/index.vue' import AiButton from '@/components/base/Ai/AiButton/index.vue'
import AiPane from '@/components/base/Ai/AiPane/index.vue' import AiPane from '@/components/base/Ai/AiPane/index.vue'
import TipTab from "./TipTab/index.vue"; import TipTab from "./TipTab/index.vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
RESOURCE_FILTER_EARLIER,
stripAllAreaForRequest,
stripAllTimeForRequest,
buildNumericYearsQueryString,
isSelectionCoveringAllOptions,
getResourceLibraryReportDateRangeFromTimeSelection
} from "./utils/resourceLibraryFilters";
import Message1 from "./assets/images/message-icon1.png"; import Message1 from "./assets/images/message-icon1.png";
import Message2 from "./assets/images/message-icon2.png"; import Message2 from "./assets/images/message-icon2.png";
...@@ -1009,63 +1005,17 @@ const box5ChartDisplayData = computed(() => { ...@@ -1009,63 +1005,17 @@ const box5ChartDisplayData = computed(() => {
}; };
}); });
const box5LegendPageIndex = ref(0);
const box5LegendPageCount = computed(() => {
const n = box5ChartDisplayData.value?.data?.length ?? 0;
return Math.max(1, Math.ceil(n / MULTILINE_LEGEND_SHOW_COUNT));
});
const box5LegendShowNav = computed(() => {
const n = box5ChartDisplayData.value?.data?.length ?? 0;
return n > MULTILINE_LEGEND_SHOW_COUNT;
});
const box5LegendCanPrev = computed(() => box5LegendPageIndex.value > 0);
const box5LegendCanNext = computed(
() => box5LegendPageIndex.value < box5LegendPageCount.value - 1
);
const handleBox5LegendPrev = () => {
if (box5LegendPageIndex.value <= 0) {
return;
}
box5LegendPageIndex.value -= 1;
renderBox5Chart();
};
const handleBox5LegendNext = () => {
if (box5LegendPageIndex.value >= box5LegendPageCount.value - 1) {
return;
}
box5LegendPageIndex.value += 1;
renderBox5Chart();
};
const renderBox5Chart = () => { const renderBox5Chart = () => {
const chartInput = box5ChartDisplayData.value; const chartInput = box5ChartDisplayData.value;
if (!chartInput.title.length || !chartInput.data.length) { if (!chartInput.title.length || !chartInput.data.length) {
return; return;
} }
const totalNames = chartInput.data.length; const box5Chart = getMultiLineChart(chartInput);
const pages = Math.max(1, Math.ceil(totalNames / MULTILINE_LEGEND_SHOW_COUNT));
if (box5LegendPageIndex.value >= pages) {
box5LegendPageIndex.value = pages - 1;
}
if (box5LegendPageIndex.value < 0) {
box5LegendPageIndex.value = 0;
}
const box5Chart = getMultiLineChart(chartInput, {
legendShowCount: MULTILINE_LEGEND_SHOW_COUNT,
legendPageIndex: box5LegendPageIndex.value
});
setChart(box5Chart, "box5Chart"); setChart(box5Chart, "box5Chart");
}; };
const handleBox5AreaChange = () => { const handleBox5AreaChange = () => {
aiContentBox5.value = ""; aiContentBox5.value = "";
box5LegendPageIndex.value = 0;
renderBox5Chart(); renderBox5Chart();
}; };
...@@ -1073,12 +1023,24 @@ const handleBox5 = async year => { ...@@ -1073,12 +1023,24 @@ const handleBox5 = async year => {
const y = year != null ? Number(year) : box5selectetedYear.value; const y = year != null ? Number(year) : box5selectetedYear.value;
box5selectetedYear.value = y; box5selectetedYear.value = y;
box5selectetedArea.value = "全部领域"; box5selectetedArea.value = "全部领域";
box5LegendPageIndex.value = 0;
await handleGetThinkTankPolicyIndustryChange(getBox5YearDateRange(y)); await handleGetThinkTankPolicyIndustryChange(getBox5YearDateRange(y));
renderBox5Chart(); renderBox5Chart();
aiContentBox5.value = ""; aiContentBox5.value = "";
}; };
/** 兼容 getChartAnalysis 返回对象:从 data[0] 提取「解读」文本 */
const getInterpretationTextFromChartResponse = (res) => {
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
return (
first?.["解读"] ||
first?.["interpretation"] ||
first?.["analysis"] ||
first?.["content"] ||
""
);
};
/** 请求 box5 折线图解读并更新 aiContentBox5(支持流式 SSE) */ /** 请求 box5 折线图解读并更新 aiContentBox5(支持流式 SSE) */
const fetchBox5ChartInterpretation = async () => { const fetchBox5ChartInterpretation = async () => {
const v = box5ChartDisplayData.value; const v = box5ChartDisplayData.value;
...@@ -1108,17 +1070,9 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -1108,17 +1070,9 @@ const fetchBox5ChartInterpretation = async () => {
}) })
}; };
try { try {
const text = await getChartAnalysis( const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) });
{ text: JSON.stringify(chartPayload) }, const text = getInterpretationTextFromChartResponse(res);
{ aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentBox5.value = chunk;
}
}
}
);
aiContentBox5.value = text || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("图表解读请求失败", error); console.error("图表解读请求失败", error);
aiContentBox5.value = "解读加载失败"; aiContentBox5.value = "解读加载失败";
...@@ -1247,17 +1201,9 @@ const fetchBox6ChartInterpretation = async () => { ...@@ -1247,17 +1201,9 @@ const fetchBox6ChartInterpretation = async () => {
})) }))
}; };
try { try {
const text = await getChartAnalysis( const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) });
{ text: JSON.stringify(chartPayload) }, const text = getInterpretationTextFromChartResponse(res);
{ aiContentBox6.value = text || aiContentBox6.value || "未返回有效解读内容";
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentBox6.value = chunk;
}
}
}
);
aiContentBox6.value = text || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("领域分布图表解读请求失败", error); console.error("领域分布图表解读请求失败", error);
aiContentBox6.value = "解读加载失败"; aiContentBox6.value = "解读加载失败";
...@@ -1473,17 +1419,9 @@ const fetchBox7ChartInterpretation = async () => { ...@@ -1473,17 +1419,9 @@ const fetchBox7ChartInterpretation = async () => {
})) }))
}; };
try { try {
const text = await getChartAnalysis( const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) });
{ text: JSON.stringify(chartPayload) }, const text = getInterpretationTextFromChartResponse(res);
{ aiContentBox7.value = text || aiContentBox7.value || "未返回有效解读内容";
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentBox7.value = chunk;
}
}
}
);
aiContentBox7.value = text || "未返回有效解读内容";
} catch (error) { } catch (error) {
console.error("智库资金流向图表解读请求失败", error); console.error("智库资金流向图表解读请求失败", error);
aiContentBox7.value = "解读加载失败"; aiContentBox7.value = "解读加载失败";
...@@ -1578,46 +1516,37 @@ const resourceTabResetKey = ref(0) ...@@ -1578,46 +1516,37 @@ const resourceTabResetKey = ref(0)
const resetResourceTabCommon = () => { const resetResourceTabCommon = () => {
// 统一回到“刷新后刚加载出来的状态” // 统一回到“刷新后刚加载出来的状态”
// 分页归 1,排序归默认(false) // 分页归 1,排序占位「发布时间」且默认倒序(null),与智库动态一致
currentPage.value = 1 currentPage.value = 1
sort.value = false sort.value = null
surveyCurrentPage.value = 1 surveyCurrentPage.value = 1
surveySort.value = false surveySort.value = null
policyCurrentPage.value = 1 policyCurrentPage.value = 1
policySort.value = false policySort.value = null
congressResourceSort.value = null
// 强制重置那些在子组件内部维护状态的组件(国会听证会/政策建议筛选) // 强制重置那些在子组件内部维护状态的组件(国会听证会/政策建议筛选)
resourceTabResetKey.value += 1 resourceTabResetKey.value += 1
} }
const resetThinkTankReportFilters = () => { const resetThinkTankReportFilters = () => {
checkAll.value = false selectedAreaList.value = [RESOURCE_FILTER_ALL_AREA];
isIndeterminate.value = true selectedPubTimeList.value = [RESOURCE_FILTER_ALL_TIME];
selectedAreaList.value = [] };
checkAllTime.value = false
isIndeterminateTime.value = true
selectedPubTimeList.value = [""]
}
const resetSurveyFilters = () => { const resetSurveyFilters = () => {
surveyCheckAll.value = false surveySelectedAreaList.value = [RESOURCE_FILTER_ALL_AREA];
surveyIsIndeterminate.value = true surveySelectedPubTimeList.value = [RESOURCE_FILTER_ALL_TIME];
surveySelectedAreaList.value = [] };
surveyCheckAllTime.value = false
surveyIsIndeterminateTime.value = true
surveySelectedPubTimeList.value = [""]
}
const resetPolicyFilters = () => { const resetPolicyFilters = () => {
// 默认“不过滤领域”:domainIds 传 null,由后端返回全部领域 // 与资源库勾选一致:子组件 remount 后为「全部领域 + 全部时间」,此处用空数组表示不按具体 id 过滤(接口侧走全量/默认年)
policySelectedTypeIds.value = [] policySelectedTypeIds.value = [];
// 默认选择最新一年,保证 /thinkTankOverview/policy 能返回数据(后端通常需要 years) policySelectedYearIds.value = [];
policySelectedYearIds.value = pubTimeList.value?.length ? [pubTimeList.value[0].id] : [] };
}
const handleClickCate = cate => { const handleClickCate = cate => {
activeCate.value = cate; activeCate.value = cate;
...@@ -1672,30 +1601,11 @@ const handleToSocialDetail = item => { ...@@ -1672,30 +1601,11 @@ const handleToSocialDetail = item => {
}); });
window.open(route.href, "_blank"); window.open(route.href, "_blank");
}; };
const checkAll = ref(false); const selectedAreaList = ref([RESOURCE_FILTER_ALL_AREA]);
const isIndeterminate = ref(true);
const selectedAreaList = ref([]);
const handleCheckAllChange = val => { /** 资源库-智库报告:筛选变更(与政策追踪相同的「全部」互斥后)重置页码并请求 */
// console.log(val, "handleCheckAllChange"); const handleThinkTankReportFilterChange = () => {
if (val) { currentPage.value = 1;
isIndeterminate.value = false;
selectedAreaList.value.length !== areaList.value.length
? (selectedAreaList.value = areaList.value.map(obj => obj.id))
: "";
} else {
selectedAreaList.value = [];
}
// selectedAreaList.value = val ? areaList : []
// isIndeterminate.value = false
handleGetetThinkTankReport();
};
const handleCheckedAreaChange = () => {
// console.log(selectedAreaList.value, "handleCheckedAreaChange");
console.log(selectedAreaList.value, "当前选中的领域");
selectedAreaList.value.length !== areaList.value.length
? (isIndeterminate.value = true)
: ((checkAll.value = true), (isIndeterminate.value = false));
handleGetetThinkTankReport(); handleGetetThinkTankReport();
}; };
...@@ -1719,58 +1629,77 @@ const pubTimeList = ref([ ...@@ -1719,58 +1629,77 @@ const pubTimeList = ref([
{ {
id: 2021, id: 2021,
name: "2021" name: "2021"
},
{
id: RESOURCE_FILTER_EARLIER,
name: RESOURCE_FILTER_EARLIER
} }
// {
// id: "更早时间",
// name: "更早时间"
// }
]); ]);
const selectedPubTimeList = ref([""]); const selectedPubTimeList = ref([RESOURCE_FILTER_ALL_TIME]);
const checkAllTime = ref(false); /** null:占位「发布时间」且默认倒序;true 正序;false 倒序(显式),与智库动态一致 */
const isIndeterminateTime = ref(true); const sort = ref(null);
const sort = ref(false);
const activeSort = computed(() => { /** 国会听证会无列表接口排序,仅保持与动态相同的排序控件展示与交互 */
if (activeCate.value === "调查项目") return surveySort.value; const congressResourceSort = ref(null);
if (activeCate.value === "政策建议") return policySort.value;
// 国会听证会目前不走排序;默认使用智库报告排序状态 const resourceLibrarySortPopperOptions = {
return sort.value; modifiers: [
{ name: "preventOverflow", options: { mainAxis: false, altAxis: false } },
{ name: "flip", enabled: false }
]
};
const resourceLibrarySortModel = computed({
get() {
if (activeCate.value === "调查项目") {
return surveySort.value;
}
if (activeCate.value === "政策建议") {
return policySort.value;
}
if (activeCate.value === "国会听证会") {
return congressResourceSort.value;
}
return sort.value;
},
set(v) {
if (activeCate.value === "调查项目") {
surveySort.value = v;
} else if (activeCate.value === "政策建议") {
policySort.value = v;
} else if (activeCate.value === "国会听证会") {
congressResourceSort.value = v;
} else {
sort.value = v;
}
}
}); });
const handleSwithSort = () => {
const handleResourceLibrarySortChange = () => {
if (activeCate.value === "调查项目") { if (activeCate.value === "调查项目") {
surveySort.value = !surveySort.value;
handleGetThinkTankSurvey(); handleGetThinkTankSurvey();
return; } else if (activeCate.value === "政策建议") {
}
if (activeCate.value === "政策建议") {
policySort.value = !policySort.value;
handleGetThinkTankPolicyAdvice(); handleGetThinkTankPolicyAdvice();
} else if (activeCate.value === "国会听证会") {
return; return;
} else {
handleGetetThinkTankReport();
} }
sort.value = !sort.value;
handleGetetThinkTankReport();
}; };
const handleCheckAllChangeTime = val => { const toggleResourceLibrarySortPrefix = () => {
// console.log(val, "handleCheckAllChange"); if (activeCate.value === "调查项目") {
if (val) { surveySort.value = surveySort.value === true ? false : true;
isIndeterminateTime.value = false; handleGetThinkTankSurvey();
selectedPubTimeList.value.length !== pubTimeList.value.length } else if (activeCate.value === "政策建议") {
? (selectedPubTimeList.value = pubTimeList.value.map(obj => obj.id)) policySort.value = policySort.value === true ? false : true;
: ""; handleGetThinkTankPolicyAdvice();
} else if (activeCate.value === "国会听证会") {
congressResourceSort.value = congressResourceSort.value === true ? false : true;
} else { } else {
selectedPubTimeList.value = []; sort.value = sort.value === true ? false : true;
handleGetetThinkTankReport();
} }
// selectedAreaList.value = val ? areaList : []
// isIndeterminate.value = false
handleGetetThinkTankReport();
};
const handleCheckedAreaChangeTime = () => {
// console.log(selectedAreaList.value, "handleCheckedAreaChange");
console.log(selectedPubTimeList.value, "当前选中的时间");
selectedPubTimeList.value.length !== pubTimeList.value.length
? (isIndeterminateTime.value = true)
: ((checkAllTime.value = true), (isIndeterminateTime.value = false));
handleGetetThinkTankReport();
}; };
const curFooterList = ref([ const curFooterList = ref([
...@@ -1851,52 +1780,15 @@ const currentPage = ref(1); ...@@ -1851,52 +1780,15 @@ const currentPage = ref(1);
const total = ref(0); const total = ref(0);
// ===== 调查项目:独立状态(不影响智库报告)===== // ===== 调查项目:独立状态(不影响智库报告)=====
const surveyCheckAll = ref(false); const surveySelectedAreaList = ref([RESOURCE_FILTER_ALL_AREA]);
const surveyIsIndeterminate = ref(true); const surveySelectedPubTimeList = ref([RESOURCE_FILTER_ALL_TIME]);
const surveySelectedAreaList = ref([]); const surveySort = ref(null);
const surveySelectedPubTimeList = ref([""]);
const surveyCheckAllTime = ref(false);
const surveyIsIndeterminateTime = ref(true);
const surveySort = ref(false);
const surveyFooterList = ref([]); const surveyFooterList = ref([]);
const surveyCurrentPage = ref(1); const surveyCurrentPage = ref(1);
const surveyTotal = ref(0); const surveyTotal = ref(0);
const handleSurveyCheckAllChange = val => { const handleSurveyFilterChange = () => {
if (val) { surveyCurrentPage.value = 1;
surveyIsIndeterminate.value = false;
surveySelectedAreaList.value.length !== areaList.value.length
? (surveySelectedAreaList.value = areaList.value.map(obj => obj.id))
: "";
} else {
surveySelectedAreaList.value = [];
}
handleGetThinkTankSurvey();
};
const handleSurveyCheckedAreaChange = () => {
surveySelectedAreaList.value.length !== areaList.value.length
? (surveyIsIndeterminate.value = true)
: ((surveyCheckAll.value = true), (surveyIsIndeterminate.value = false));
handleGetThinkTankSurvey();
};
const handleSurveyCheckAllChangeTime = val => {
if (val) {
surveyIsIndeterminateTime.value = false;
surveySelectedPubTimeList.value.length !== pubTimeList.value.length
? (surveySelectedPubTimeList.value = pubTimeList.value.map(obj => obj.id))
: "";
} else {
surveySelectedPubTimeList.value = [];
}
handleGetThinkTankSurvey();
};
const handleSurveyCheckedAreaChangeTime = () => {
surveySelectedPubTimeList.value.length !== pubTimeList.value.length
? (surveyIsIndeterminateTime.value = true)
: ((surveyCheckAllTime.value = true), (surveyIsIndeterminateTime.value = false));
handleGetThinkTankSurvey(); handleGetThinkTankSurvey();
}; };
...@@ -1906,12 +1798,24 @@ const handleSurveyCurrentChange = page => { ...@@ -1906,12 +1798,24 @@ const handleSurveyCurrentChange = page => {
}; };
const handleGetThinkTankSurvey = async () => { const handleGetThinkTankSurvey = async () => {
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
stripAllTimeForRequest(surveySelectedPubTimeList.value),
(pubTimeList.value || []).map((x) => x.id)
);
const params = { const params = {
pageNum: surveyCurrentPage.value, pageNum: surveyCurrentPage.value,
pageSize: 12, pageSize: 12,
sortFun: surveySort.value, sortFun: surveySort.value === true,
researchTypeIds: arrayToString(surveySelectedAreaList.value), domainIds: (() => {
years: arrayToString(surveySelectedPubTimeList.value), const areas = stripAllAreaForRequest(surveySelectedAreaList.value);
const allAreaIds = (areaList.value || []).map((a) => a.id);
if (isSelectionCoveringAllOptions(areas, allAreaIds)) {
return "";
}
return arrayToString(areas);
})(),
startDate,
endDate,
category: "调查项目" category: "调查项目"
}; };
try { try {
...@@ -1932,12 +1836,14 @@ const handleGetThinkTankSurvey = async () => { ...@@ -1932,12 +1836,14 @@ const handleGetThinkTankSurvey = async () => {
const policyFooterList = ref([]); const policyFooterList = ref([]);
const policyCurrentPage = ref(1); const policyCurrentPage = ref(1);
const policyTotal = ref(0); const policyTotal = ref(0);
const policySort = ref(false); const policySort = ref(null);
const policySelectedTypeIds = ref([]); const policySelectedTypeIds = ref([]);
const policySelectedYearIds = ref([]); const policySelectedYearIds = ref([]);
const handlePolicyFilterChange = payload => { const handlePolicyFilterChange = payload => {
policySelectedTypeIds.value = payload?.researchTypeIds ? [...payload.researchTypeIds] : []; // 子组件 ThinkTankPolicyAdviceOverview 传 researchTypeIds(与智库报告筛选一致)
const typeIds = payload?.researchTypeIds ?? payload?.domainIds;
policySelectedTypeIds.value = Array.isArray(typeIds) ? [...typeIds] : [];
policySelectedYearIds.value = payload?.researchTimeIds ? [...payload.researchTimeIds] : []; policySelectedYearIds.value = payload?.researchTimeIds ? [...payload.researchTimeIds] : [];
policyCurrentPage.value = 1; policyCurrentPage.value = 1;
handleGetThinkTankPolicyAdvice(); handleGetThinkTankPolicyAdvice();
...@@ -1983,21 +1889,32 @@ const handleGetThinkTankPolicyAdvice = async () => { ...@@ -1983,21 +1889,32 @@ const handleGetThinkTankPolicyAdvice = async () => {
policyFooterList.value = mock.slice(start, start + pageSize); policyFooterList.value = mock.slice(start, start + pageSize);
}; };
const fallbackYear = pubTimeList.value?.length ? String(pubTimeList.value[0].id) : null const strippedPolicyYears = stripAllTimeForRequest(policySelectedYearIds.value);
const yearsStr = arrayToString(policySelectedYearIds.value) const allPubTimeIds = (pubTimeList.value || []).map((x) => x.id);
// 领域:如果用户还没选领域,则默认使用“全部领域”的 id 列表 /** 与智库报告一致:仅「全部时间」或选满所有具体年份 → 不按 years 狭义过滤 */
const effectiveDomainIds = policySelectedTypeIds.value.length const isPolicyAllTime =
? policySelectedTypeIds.value strippedPolicyYears.length === 0 ||
: (areaList.value || []).map(obj => obj.id) (allPubTimeIds.length > 0 && isSelectionCoveringAllOptions(strippedPolicyYears, allPubTimeIds));
const domainIdsStr = arrayToString(effectiveDomainIds) const typeIdsForApi = stripAllAreaForRequest(policySelectedTypeIds.value);
// 领域:未选具体领域(仅「全部领域」)时传全量 id 串
const effectiveDomainIds = typeIdsForApi.length
? typeIdsForApi
: (areaList.value || []).map(obj => obj.id);
const domainIdsStr = arrayToString(effectiveDomainIds);
const params = { const params = {
currentPage: policyCurrentPage.value - 1, currentPage: policyCurrentPage.value - 1,
pageSize: 7, pageSize: 7,
sortFun: policySort.value, sortFun: policySort.value === true
years: yearsStr === "" ? fallbackYear : yearsStr
}; };
// 全部时间:不传 years,避免误用首项年份(如仅查 2025)
if (!isPolicyAllTime) {
const yearsStr = buildNumericYearsQueryString(strippedPolicyYears);
if (yearsStr) {
params.years = yearsStr;
}
}
// 领域:始终传 domainIds;如果用户没选,就传“全部领域”的 id 串 // 领域:始终传 domainIds;如果用户没选,就传“全部领域”的 id 串
params.domainIds = domainIdsStr || null params.domainIds = domainIdsStr || null;
try { try {
const res = await getThinkTankOverviewPolicy(params); const res = await getThinkTankOverviewPolicy(params);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
...@@ -2050,12 +1967,24 @@ function arrayToString(arr) { ...@@ -2050,12 +1967,24 @@ function arrayToString(arr) {
//获取智库报告 //获取智库报告
const handleGetetThinkTankReport = async () => { const handleGetetThinkTankReport = async () => {
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
stripAllTimeForRequest(selectedPubTimeList.value),
(pubTimeList.value || []).map((x) => x.id)
);
const params = { const params = {
pageNum: currentPage.value, pageNum: currentPage.value,
pageSize: 12, pageSize: 12,
sortFun: sort.value, sortFun: sort.value === true,
researchTypeIds: arrayToString(selectedAreaList.value), domainIds: (() => {
years: arrayToString(selectedPubTimeList.value) const areas = stripAllAreaForRequest(selectedAreaList.value);
const allAreaIds = (areaList.value || []).map((a) => a.id);
if (isSelectionCoveringAllOptions(areas, allAreaIds)) {
return "";
}
return arrayToString(areas);
})(),
startDate,
endDate
}; };
try { try {
const res = await getThinkTankReport(params); const res = await getThinkTankReport(params);
...@@ -2505,7 +2434,7 @@ onMounted(async () => { ...@@ -2505,7 +2434,7 @@ onMounted(async () => {
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
position: relative; position: relative;
z-index: 9999999;
overflow: visible; overflow: visible;
.card { .card {
...@@ -2519,12 +2448,12 @@ onMounted(async () => { ...@@ -2519,12 +2448,12 @@ onMounted(async () => {
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
position: relative; // 让 red-info 按当前 card 自身定位 position: relative; // 让 red-info 按当前 card 自身定位
z-index: 9999998;
&:hover { &:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
z-index: 9999999; z-index: 9;
} }
.red-info { .red-info {
...@@ -2549,7 +2478,7 @@ onMounted(async () => { ...@@ -2549,7 +2478,7 @@ onMounted(async () => {
padding: 2px 8px; padding: 2px 8px;
/* 左右留空隙,更美观 */ /* 左右留空隙,更美观 */
white-space: nowrap; white-space: nowrap;
z-index: 10000000; z-index: 10;
} }
.card-header { .card-header {
...@@ -3602,40 +3531,10 @@ onMounted(async () => { ...@@ -3602,40 +3531,10 @@ onMounted(async () => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
gap: 8px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
.box5-legend-nav-btn {
flex-shrink: 0;
width: 32px;
height: 32px;
margin-top: 6px;
padding: 0;
box-sizing: border-box;
border: 1px solid rgba(220, 223, 230, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
color: rgba(59, 65, 75, 1);
font-size: 20px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.box5-legend-nav-btn:hover:not(:disabled) {
border-color: var(--color-main-primary, #055fc2);
color: var(--color-main-primary, #055fc2);
}
.box5-legend-nav-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.box5-chart-canvas { .box5-chart-canvas {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
...@@ -4220,56 +4119,29 @@ onMounted(async () => { ...@@ -4220,56 +4119,29 @@ onMounted(async () => {
box-sizing: border-box; box-sizing: border-box;
padding: 5px 0; padding: 5px 0;
.paixu-btn { .resource-library-sort-select {
display: flex;
width: 120px;
height: 32px; height: 32px;
box-sizing: border-box; box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
border: 1px solid rgba(230, 231, 232, 1); border: 1px solid rgb(230, 231, 232);
border-radius: 4px; border-radius: 4px;
background: rgba(255, 255, 255, 1); background: rgb(255, 255, 255);
&:hover { .resource-library-sort-prefix-img {
background: var(--color-bg-hover); width: 8px;
} height: 8px;
margin-right: 4px;
cursor: pointer; cursor: pointer;
.icon1 {
width: 11px;
height: 14px;
margin-top: 10px;
margin-left: 9px;
img {
width: 100%;
height: 100%;
}
} }
.text { :deep(.el-select__wrapper) {
height: 19px; min-height: 30px;
color: rgba(95, 101, 108, 1); box-shadow: none;
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: 0px;
text-align: left;
margin-top: 7px;
margin-left: 9px;
} }
.icon2 { :deep(.el-select-dropdown) {
width: 10px; left: 0 !important;
height: 5px; top: 100% !important;
margin-top: 5px; transform: none !important;
margin-left: 13px;
img {
width: 100%;
height: 100%;
}
} }
} }
} }
......
import * as echarts from 'echarts'; import * as echarts from 'echarts';
/** 图例分页:每页展示的图例项数量(box5 数量变化趋势) */
export const MULTILINE_LEGEND_SHOW_COUNT = 11;
/** /**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data * @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data
* @param {{ legendShowCount?: number, legendPageIndex?: number }} [options]
*/ */
const getMultiLineChart = (data, options = {}) => { const getMultiLineChart = (data) => {
// 提取标题和系列数据 // 提取标题和系列数据
const title = data.title const title = data.title
const series = data.data const series = data.data
const legendShowCount =
typeof options.legendShowCount === 'number' && options.legendShowCount > 0
? options.legendShowCount
: MULTILINE_LEGEND_SHOW_COUNT
const rawPageIndex = Number(options.legendPageIndex) || 0
const allNames = series.map((item) => item.name) const allNames = series.map((item) => item.name)
const pageCount = Math.max(1, Math.ceil(allNames.length / legendShowCount)) const legendSplitAt = Math.ceil(allNames.length / 2)
const legendPageIndex = Math.min(Math.max(0, rawPageIndex), pageCount - 1) const legendFirstLine = allNames.slice(0, legendSplitAt)
const legendStart = legendPageIndex * legendShowCount const legendSecondLine = allNames.slice(legendSplitAt)
const legendData = allNames.slice(legendStart, legendStart + legendShowCount)
// 定义配色数组 // 定义配色数组
const colorList = [ const colorList = [
...@@ -101,28 +91,46 @@ const getMultiLineChart = (data, options = {}) => { ...@@ -101,28 +91,46 @@ const getMultiLineChart = (data, options = {}) => {
}, },
/* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */ /* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */
grid: { grid: {
top: '28%', top: '34%',
right: '5%', right: '3%',
bottom: '5%', bottom: '5%',
left: '5%', left: '2%',
containLabel: true containLabel: true
}, },
legend: { legend: [
show: true, {
type: 'plain', show: true,
data: legendData, type: 'plain',
top: 8, data: legendFirstLine,
left: 'center', top: 8,
icon: 'circle', left: 'center',
textStyle: { icon: 'circle',
fontFamily: 'Source Han Sans CN', // 字体 textStyle: {
fontWeight: 400, // 字重值(Regular对应400) fontFamily: 'Source Han Sans CN',
fontSize: 14, // 字号 fontWeight: 400,
lineHeight: 24, // 行高 fontSize: 14,
letterSpacing: 0, // 字间距 lineHeight: 24,
align: 'left' // 文本左对齐 letterSpacing: 0,
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: colorList, // 使用预设的配色数组 color: colorList, // 使用预设的配色数组
xAxis: [ xAxis: [
{ {
......
/** 与政策追踪侧筛选文案保持一致 */
export const RESOURCE_FILTER_ALL_AREA = "全部领域";
export const RESOURCE_FILTER_ALL_TIME = "全部时间";
export const RESOURCE_FILTER_ALL_DEPT = "全部部门";
/**
* 「全部」与具体项互斥;空选时回到「全部」
* (同市场准入/政策追踪 resourceLibrary normalizeWithAll)
*/
export function normalizeExclusiveAllOption(val, allLabel) {
if (!Array.isArray(val)) {
return [allLabel];
}
if (val.includes(allLabel) && val.length > 1) {
if (val[val.length - 1] === allLabel) {
return [allLabel];
}
return val.filter((item) => item !== allLabel);
}
if (val.length === 0) {
return [allLabel];
}
return val;
}
/** 发请求前去掉「全部领域」占位 */
export function stripAllAreaForRequest(arr) {
return (Array.isArray(arr) ? arr : []).filter((id) => id !== RESOURCE_FILTER_ALL_AREA);
}
/** 发请求前去掉「全部时间」占位 */
export function stripAllTimeForRequest(arr) {
return (Array.isArray(arr) ? arr : []).filter(
(id) => id !== RESOURCE_FILTER_ALL_TIME && id !== "" && id != null
);
}
/** 发请求前去掉「全部部门」占位 */
export function stripAllDeptForRequest(arr) {
return (Array.isArray(arr) ? arr : []).filter((id) => id !== RESOURCE_FILTER_ALL_DEPT);
}
/**
* 已选中的具体项是否覆盖选项全集(与仅选「全部领域/全部时间」等价:请求层不按子项狭义过滤)
* @param {unknown[]} selectedStrippedIds 已去掉「全部」占位后的选中 id
* @param {unknown[]} allOptionIds 当前列表全部可选项 id(与勾选框 label 一致)
*/
export function isSelectionCoveringAllOptions(selectedStrippedIds, allOptionIds) {
if (!Array.isArray(selectedStrippedIds) || !Array.isArray(allOptionIds) || allOptionIds.length === 0) {
return false;
}
if (selectedStrippedIds.length !== allOptionIds.length) {
return false;
}
const setSel = new Set(selectedStrippedIds);
return allOptionIds.every((id) => setSel.has(id));
}
/** 与政策追踪一致:勾选「更早」表示 2000~2020 自然年 */
export const RESOURCE_FILTER_EARLIER = "更早";
export const RESOURCE_YEAR_EARLIER_START = 2000;
export const RESOURCE_YEAR_EARLIER_END = 2020;
/** 展开「更早」为逐年数字(概览 report 接口 years 多为数字年) */
export function expandEarlierNumericYears() {
const out = [];
for (let y = RESOURCE_YEAR_EARLIER_START; y <= RESOURCE_YEAR_EARLIER_END; y += 1) {
out.push(y);
}
return out;
}
/**
* 概览资源库:去掉「全部时间」后的选中项 → years 查询串(含「更早」则展开为 2000…2020)
*/
export function buildNumericYearsQueryString(strippedTimeIds) {
const list = Array.isArray(strippedTimeIds) ? strippedTimeIds : [];
const yearsSet = new Set();
for (const id of list) {
if (id === RESOURCE_FILTER_EARLIER) {
expandEarlierNumericYears().forEach((y) => yearsSet.add(y));
} else if (id !== null && id !== undefined && id !== "") {
const n = Number(id);
if (!Number.isNaN(n)) {
yearsSet.add(n);
}
}
}
return [...yearsSet].sort((a, b) => b - a).join(",");
}
const DYNAMICS_YEAR_LABEL_RE = /^(\d{4})年$/;
/**
* 智库动态:时间为「2025年」等字符串 id → years 查询串(「更早」展开为 2000…2020)
*/
export function buildDynamicsYearsQueryString(strippedTimeIds) {
const list = Array.isArray(strippedTimeIds) ? strippedTimeIds : [];
const yearsSet = new Set();
for (const id of list) {
if (id === RESOURCE_FILTER_EARLIER) {
expandEarlierNumericYears().forEach((y) => yearsSet.add(y));
continue;
}
const m = String(id).match(DYNAMICS_YEAR_LABEL_RE);
if (m) {
yearsSet.add(Number(m[1]));
}
}
return [...yearsSet].sort((a, b) => b - a).join(",");
}
/**
* 中文日期串如「2020年5月1日」是否落在「更早」区间(与政策追踪矩形树一致)
*/
export function matchesEarlierChineseDate(timeStr) {
const m = String(timeStr || "").match(/^(\d{4})年/);
if (!m) {
return false;
}
const y = Number(m[1]);
return y >= RESOURCE_YEAR_EARLIER_START && y <= RESOURCE_YEAR_EARLIER_END;
}
/** 与政策追踪「仅全部时间」一致:固定起止(结束日按产品要求) */
export const RESOURCE_REPORT_ALL_TIME_START = "2000-01-01";
export const RESOURCE_REPORT_ALL_TIME_END = "2025-12-31";
function getResourceReportDateYearsAgo(years) {
const currentDate = new Date();
const pastDate = new Date(currentDate.getFullYear() - years, currentDate.getMonth(), currentDate.getDate());
const y = pastDate.getFullYear();
const month = String(pastDate.getMonth() + 1).padStart(2, "0");
const day = String(pastDate.getDate()).padStart(2, "0");
return `${y}-${month}-${day}`;
}
function getResourceReportTodayYmd() {
const d = new Date();
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}`;
}
/**
* 资源库 /thinkTankOverview/report:由发布时间多选(数字年 +「更早」)推导 startDate/endDate,语义对齐政策追踪 getPolicyListDateRangeFromYearList。
* - 仅「全部时间」或选满全部具体年份 → 2000-01-01 ~ 2025-12-31
* - 单选/多选自然年 +「更早」→ 取最小年 01-01 与最大年 12-31 包络(「更早」为 2000~2020)
* - 无法解析时的兜底:近 relativeYearsAgo 年至今天
*
* @param {unknown[]} strippedTimeIds stripAllTimeForRequest 后的选中项
* @param {unknown[]} allTimeOptionIds 当前 pubTimeList 全部 id(用于判断是否选满 = 全部时间)
* @param {number} [relativeYearsAgo=1] 兜底用「近 N 年」
* @returns {{ startDate: string, endDate: string }}
*/
export function getResourceLibraryReportDateRangeFromTimeSelection(
strippedTimeIds,
allTimeOptionIds,
relativeYearsAgo = 1
) {
const stripped = Array.isArray(strippedTimeIds) ? strippedTimeIds : [];
const allIds = Array.isArray(allTimeOptionIds) ? allTimeOptionIds : [];
const isAllTime =
stripped.length === 0 ||
(allIds.length > 0 && isSelectionCoveringAllOptions(stripped, allIds));
if (isAllTime) {
return {
startDate: RESOURCE_REPORT_ALL_TIME_START,
endDate: RESOURCE_REPORT_ALL_TIME_END,
};
}
let minY = null;
let maxY = null;
const set = new Set(stripped);
if (set.has(RESOURCE_FILTER_EARLIER)) {
minY = RESOURCE_YEAR_EARLIER_START;
maxY = RESOURCE_YEAR_EARLIER_END;
}
const yearLabelCnRe = /^(\d{4})年$/;
for (const id of set) {
if (id === RESOURCE_FILTER_EARLIER) {
continue;
}
let yNum = Number(id);
if (Number.isNaN(yNum)) {
const m = String(id).match(yearLabelCnRe);
if (m) {
yNum = Number(m[1]);
}
}
if (!Number.isNaN(yNum)) {
minY = minY == null ? yNum : Math.min(minY, yNum);
maxY = maxY == null ? yNum : Math.max(maxY, yNum);
}
}
if (minY == null || maxY == null) {
const y = Number(relativeYearsAgo) > 0 ? Number(relativeYearsAgo) : 1;
return {
startDate: getResourceReportDateYearsAgo(y),
endDate: getResourceReportTodayYmd(),
};
}
return {
startDate: `${minY}-01-01`,
endDate: `${maxY}-12-31`,
};
}
...@@ -6,7 +6,7 @@ const getSankeyChart = (nodes, links) => { ...@@ -6,7 +6,7 @@ const getSankeyChart = (nodes, links) => {
type: 'sankey', type: 'sankey',
layout: 'none', layout: 'none',
left: '2%', left: '2%',
right: '18%', right: '15%',
top: '5%', top: '5%',
bottom: '5%', bottom: '5%',
emphasis: { emphasis: {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论