提交 82f15736 authored 作者: 朱政's avatar 朱政

feat:查看全部智库,智库报告-报告原文,智库报告-政策追踪样式与功能开发

上级 20ebbfed
......@@ -10,6 +10,13 @@ export function getThinkTankList() {
})
}
export function getAllThinkTankList(params) {
return request({
method: 'GET',
url: '/api/thinkTankOverview/thinkTanks/page',
params: params
})
}
//智库概览:获取智库发布
export function getNewReport() {
return request({
......@@ -299,9 +306,26 @@ export function getThinkTankReportPolicy(params) {
//获取相关政策动态
export function getThinkTankReportPolicyAction(params) {
const {
reportId,
currentPage,
pageSize,
keyword = "",
orgIds = "",
// 新增:按科技领域 / 标签过滤
industryName = ""
} = params;
return request({
method: 'GET',
url: `/api/thinkTankReport/policyAction/${params}`,
url: `/api/thinkTankReport/policyDetail/${reportId}`,
params: {
currentPage,
pageSize,
keyword,
// 后端按标签过滤使用的字段
industryName,
orgIds,
}
})
}
......
......@@ -3,6 +3,7 @@ const thinkTank = () => import('@/views/thinkTank/index.vue')
const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue')
const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue')
const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue')
const allThinkTank= () => import('@/views/thinkTank/allThinkTank/index.vue')
const thinktankRoutes = [
// 智库系统的主要路由
......@@ -40,6 +41,14 @@ const thinktankRoutes = [
// title: "报告原文"
// }
},
{
path: "/thinkTank/allThinkTank",
name: "allThinkTank",
component: allThinkTank,
// meta: {
// title: "报告原文"
// }
},
]
......
......@@ -5,9 +5,8 @@
<div class="home-content">
<div class="home-content-header">
<SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef"
placeholder="搜索科技法案" :containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true"
defaultBillSearchType="federal" />
<SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef" placeholder="搜索科技法案"
:containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true" defaultBillSearchType="federal" />
</div>
<DivideHeader id="position1" class="divide1" :titleText="'最新动态'"></DivideHeader>
......@@ -28,8 +27,8 @@
</div>
</div>
<div class="box1-main" style="display: block">
<el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000"
arrow="never" indicator-position="none" @change="handleCarouselChange">
<el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000" arrow="never"
indicator-position="none" @change="handleCarouselChange">
<el-carousel-item v-for="(bill, billIndex) in hotBillList" :key="billIndex">
<div class="carousel-content" style="display: flex; height: 100%">
<div class="box1-main-left">
......@@ -37,8 +36,7 @@
{{ bill.billName }}
</div>
<div class="box1-main-left-info">
<AreaTag v-for="(item, index) in bill.hylyList" :key="index"
:tagName="item.industryName">
<AreaTag v-for="(item, index) in bill.hylyList" :key="index" :tagName="item.industryName">
</AreaTag>
</div>
<div class="box1-main-left-info1">
......@@ -56,18 +54,15 @@
</div>
</div>
<div class="box1-main-left-info2">
<div class="info2-item" v-for="(item, index) in bill.dyqkList"
:key="index">
<div class="time-line"
v-if="index !== bill.dyqkList.length - 1"></div>
<div class="info2-item" v-for="(item, index) in bill.dyqkList" :key="index">
<div class="time-line" v-if="index !== bill.dyqkList.length - 1"></div>
<div class="item-icon">
<img src="./assets/images/info2-icon.png" alt="" />
</div>
<div class="item-time" :class="{ itemTimeActive: index === 0 }">
{{ item.actionDate }}
</div>
<div class="item-title"
:class="{ itemTitleActive: index === 0 }">
<div class="item-title" :class="{ itemTitleActive: index === 0 }">
{{ item.actionContentCn }}
</div>
</div>
......@@ -94,23 +89,15 @@
</el-carousel>
</div>
</overviewMainBox>
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal"
@item-click="handleClickToDetailO" riskLevel="signalLevel" postDate="signalTime"
name="signalTitle" />
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" @item-click="handleClickToDetailO"
riskLevel="signalLevel" postDate="signalTime" name="signalTitle" />
</div>
<DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center">
<NewsList
:newsList="newsList"
img="newsImage"
title="newsTitle"
from="from"
content="newsContent"
/>
<MessageBubble :messageList="messageList" imageUrl="personImage"
@more-click="handleToSocialDetail" @person-click="handleClickToCharacter" name="personName"
content="remarks" source="orgName" />
<NewsList :newsList="newsList" img="newsImage" title="newsTitle" from="from" content="newsContent" />
<MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail"
@person-click="handleClickToCharacter" name="personName" content="remarks" source="orgName" />
</div>
<DivideHeader id="position3" class="divide3" :titleText="'数据总览'"></DivideHeader>
......@@ -1783,7 +1770,7 @@ onUnmounted(() => {
margin-top: 21px;
height: 450px;
display: flex;
gap:16px;
gap: 16px;
.box3 {
width: 792px;
......@@ -2074,6 +2061,7 @@ onUnmounted(() => {
.overview-card--double {
width: calc(#{$overview-single-width} * 2 + #{$overview-card-gap});
}
.box5-main,
.box6-main,
.box7-main,
......
......@@ -591,6 +591,7 @@
</template>
<script setup>
//这是一个备注
import NewsList from "@/components/base/newsList/index.vue";
import RiskSignal from "@/components/base/RiskSignal/index.vue";
import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue";
......
......@@ -62,7 +62,7 @@
</div>
<div class="text" @click="toReport()">{{ "报告原文" }}</div>
</div>
<div class="btn">
<div class="btn" @click="handleDownloadDocument">
<div class="icon">
<img src="./images/btn-icon3.png" alt="" />
</div>
......@@ -89,7 +89,7 @@ import { ref, onMounted } from "vue";
import ReportAnalysis from "./reportAnalysis/index.vue";
import PolicyTracking from "./policyTracking/index.vue";
import { getThinkTankReportSummary, getThinkTankReportRelated } from "@/api/thinkTank/overview";
import { getThinkTankReportSummary, getThinkTankReportRelated, getThinkTankReportcontentUrl } from "@/api/thinkTank/overview";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute();
......@@ -163,6 +163,56 @@ const goToOfficialWebsite = () => {
window.open(url, "_blank");
};
// 文档下载:先拉取 PDF 地址,再中英文都下载(与报告原文页相同方式)
const downloadOnePdf = async (url, filename) => {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/pdf" },
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
};
const handleDownloadDocument = async () => {
try {
const { ElMessage } = await import("element-plus");
const res = await getThinkTankReportcontentUrl(router.currentRoute._value.params.id);
if (res.code !== 200 || !res.data) {
ElMessage.warning("暂无下载链接");
return;
}
const urlZh = res.data.content ? String(res.data.content).split("#")[0] : "";
const urlEn = res.data.contentEn ? String(res.data.contentEn).split("#")[0] : "";
if (!urlZh && !urlEn) {
ElMessage.warning("暂无下载链接");
return;
}
const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-");
if (urlZh) {
await downloadOnePdf(urlZh, `${baseName}_中文.pdf`);
}
if (urlEn) {
if (urlZh) await new Promise(r => setTimeout(r, 300));
await downloadOnePdf(urlEn, `${baseName}_英文.pdf`);
}
ElMessage.success(urlZh && urlEn ? "已下载中文、英文两份 PDF" : "下载成功");
} catch (error) {
console.error("文档下载失败:", error);
try {
const { ElMessage } = await import("element-plus");
ElMessage.error("PDF 下载失败,请稍后重试");
} catch (_) { }
}
};
</script>
<style lang="scss" scoped>
......
<template>
<div class="wrap">
<div class="left">
<div class="wrap" ref="wrapRef">
<div class="box1">
<AnalysisBox title="政策建议相关情况" :showAllBtn="false">
<AnalysisBox title="政策建议相关情况" :showAllBtn="true">
<div class="box1-main">
<div class="box1-item" v-for="(item, index) in box1Data" :key="index">
<div class="left">
{{ index + 1 }}
<div class="search-box">
<el-input placeholder="搜索政策建议" v-model="searchOpinions" @keyup.enter="handleSearchOpinions" />
<div class="icon">
<img src="../images/Line_Search.png" alt="" @click="handleSearchOpinions" />
</div>
<div class="center">
<div class="title">{{ item.content }}</div>
<div class="tag-box">
<div class="tag" v-for="(val, idx) in item.tagList" :key="idx">
{{ val }}
</div>
<div class="tag-box">
<span class="tag" :class="{ 'tag-active': !activeTag }" @click="handleTagClick('')">
<div class="tag-text">{{ "全部" }}</div>
</span>
<span class="tag" v-for="(tag, index) in allTags" :key="tag + index"
:class="{ 'tag-active': activeTag === tag }" @click="handleTagClick(tag)">
<span class="tag-text"> {{ tag }} ({{ tagCountMap[tag] || 0 }} 项)</span>
</span>
</div>
<div class="file-box">
<div class="file-item" v-for="(vall, idxx) in item.fileList" :key="idxx">
<div class="file-item-left">法案</div>
<div class="file-item-center">{{ vall.relationBillsList }}</div>
<div class="file-item-right">
<img src="@/assets/images/icon-right-circle.png" alt="" />
<div class="item-box">
<div class="item" v-for="(item, index) in filteredOpinions" :key="item.id || index"
:class="{ 'item-active': index === activeItemIndex }" @click="
() => {
activeItemIndex = index;
box1DataItem = item;
console.log('当前选中政策建议:', item);
}
">
<!-- 序号 -->
<div class="item-left">{{ index + 1 }}</div>
<!-- 标题中与搜索关键词相同的文字高亮显示 -->
<div class="item-right" v-html="highlightKeyword(item.titleZh)"></div>
<!-- 仅在选中项时显示右侧竖线 -->
<div class="item-right-solid" v-if="index === activeItemIndex"></div>
</div>
</div>
<div class="file-item" v-for="(vall, idxx) in item.fileList" :key="idxx">
<div class="file-item-left">政令</div>
<div class="file-item-center">{{ vall.relationAdList }}</div>
<div class="file-item-right">
<img src="@/assets/images/icon-right-circle.png" alt="" />
<div class="box4-main-footer">
<div class="info">{{ opinionsTotal }}条核心论点</div>
<div class="page-box">
<el-pagination :page-size="pageSize" background layout="prev, pager, next" :total="opinionsTotal"
@current-change="handleCurrentChange" :current-page="currentPage" />
</div>
</div>
</div>
<div class="right" v-if="box1DataItem">
<div class="right-header">
<div class="header-text">
<div class="left-text">
{{ "政策建议详情" }}
</div>
<!-- <div class="right">
<div class="right-text">
<div class="text">
{{ "查看智库原文" }}
{{ "跳转原文" }}
</div>
<div class="icon">
<img src="@/assets/icons/open.png" alt="" />
<div class="right-header-image">
<img src="../images/image-open2.png" alt="" @click="toReport()" />
</div>
</div> -->
</div>
</div>
</AnalysisBox>
</div>
<div class="right-content" v-html="box1DataItem ? highlightKeyword(box1DataItem.contentZh || '') : ''">
</div>
<div class="info-group">
<div class="info-content">
<div class="info-item">
<div class="info-text">{{ "相关领域:" }}</div>
<div class="info-right" v-if="box1DataItem && box1DataItem.domains">
<div class="tag-box">
<div class="tag" v-for="(item, index) in box1DataItem.domains" :key="index" v-show="item">{{
item.industryName
}}</div>
</div>
</div>
</div>
<div class="info-item" v-if="box1DataItem">
<div class="info-text">{{ "遏制手段:" }}</div>
<div class="info-right">
<div class="tag-box">
<div class="tag">{{ "移民政策" }}</div>
</div>
</div>
</div>
<div class="info-item">
<div class="info-text">{{ "涉及部门:" }}</div>
<div class="info-right" v-for="(item, index) in box1DataItem.organizations" :key="index"
v-show="item">
<div class="info-right-image">
<img :src="item.logoUrl" />
</div>
<div class="info-right-text">
{{ item.orgName }}
</div>
</div>
<div class="box2">
<AnalysisBox title="相关政策动态" :showAllBtn="false">
<div class="box2-main">
<div class="box2-item" v-for="(item, index) in box2Data" :key="index" @click="handleToNewsAnalysis(item)">
<div class="box2-item-left">
<div class="point">
<img src="@/assets/images/dot.png" alt="" />
</div>
<div class="line" v-if="index !== box2Data.length - 1"></div>
</div>
<div class="box2-item-center">
<div class="title">{{ item.newsTitle }}</div>
<div class="content">{{ item.newsContent }}</div>
</div>
<div class="box2-item-right">
<div class="time">{{ item.newsDate }}</div>
<div class="img-box">
<img :src="item.newsImage?item.newsImage:DefaultNewsImg" alt="" />
<div class="right-footer-title">
<div class="footer-title-text">{{ "相关政策" }}</div>
</div>
<div class="right-footer-box">
<div class="relatedBills" v-for="(item, index) in box1DataItem.relatedBills" :key="index" v-show="item">
<div class="tag">{{ "政令" }}</div>
<div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedBills-content">{{ item.name }}</div>
<div class="footer-image"> <img src="../images/image-right.png" alt="" /></div>
</div>
<div class="relatedAdministrativeOrders" v-for="(item, index) in box1DataItem.relatedAdministrativeOrders"
v-show="item" :key="index">
<div class="tag">{{ "政令" }}</div>
<div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedAdministrativeOrders-content">{{ item.content }}</div>
<div class="footer-image"> <img src="../images/image-right.png" alt="" /></div>
</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted, computed, nextTick } from "vue";
import {
getThinkTankReportPolicy,
getThinkTankReportPolicyAction
......@@ -83,238 +136,185 @@ import {
import { useRouter } from "vue-router";
import DefaultNewsImg from '@/assets/images/default-icon-news.png'
const router = useRouter();
// 政策建议相关情况
const box1Data = ref([
{
id: 1,
title: "示例标题1",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 2,
title: "示例标题2",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 3,
title: "示例标题3",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 4,
title: "示例标题4",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 5,
title: "示例标题5",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 6,
title: "示例标题6",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 7,
title: "示例标题7",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 8,
title: "示例标题8",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 9,
title: "示例标题9",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
},
{
id: 10,
title: "示例标题10",
tagList: ["人才交流", "移民政策"],
fileList: [
{
type: "法案",
name: "2024《重塑美国人口结构法案》"
},
{
type: "法案",
name: "2025《开放人才法案》"
}
]
}
]);
const handleGetThinkTankReportPolicy = async () => {
try {
const res = await getThinkTankReportPolicy(router.currentRoute._value.params.id);
console.log("政策建议相关情况", res);
if (res.code === 200 && res.data) {
box1Data.value = res.data
const searchOpinions = ref("");
// 政策建议相关情况(当前页/当前筛选展示的数据)
const box1Data = ref([]);
// 全量数据(用于标签统计与前端筛选分页)
const allBox1Data = ref([]);
// 每个标签在全部数据中的出现次数(基于全部数据,非当前页)
const tagCountMap = ref({});
// 标签列表(基于当前全量数据 + 搜索条件)
const allTags = ref([]);
// 当前选中的标签(空字符串表示“全部”)
const activeTag = ref("");
// 当前选中的条目索引(相对于当前页,默认选中第一个)
const activeItemIndex = ref(0);
const pageSize = ref(10);
// 当前选中 item 的数据
const box1DataItem = ref(null);
// 整个页面容器,用于分页后滚回 wrap 顶部
const wrapRef = ref(null);
const reportUrl = ref("");
// 总条数基于当前筛选条件(activeTag)
const opinionsTotal = computed(() => {
if (!activeTag.value) {
return allBox1Data.value.length;
}
return allBox1Data.value.filter(item =>
(item.domains || []).some(d => d?.industryName === activeTag.value)
).length;
});
const currentPage = ref(1);
const handleCurrentChange = page => {
currentPage.value = page;
activeItemIndex.value = 0;
updatePageData();
// 使用页面滚动轴滚动到最顶部
nextTick(() => {
scrollToWrapTop()
});
};
const scrollToWrapTop = () => {
const el = wrapRef.value;
if (!el) return;
// 向上查找最近的可滚动父元素
let node = el.parentElement;
while (node && node !== document.body && node !== document.documentElement) {
if (node.scrollHeight > node.clientHeight + 1) {
const top = node.scrollTop + el.getBoundingClientRect().top - node.getBoundingClientRect().top - 400;
node.scrollTo({ top, behavior: "smooth" });
return;
}
} catch (error) {
console.error("获取政策建议相关情况rror", error);
node = node.parentElement;
}
// 兜底:全局滚动
window.scrollTo({
top: el.getBoundingClientRect().top + window.scrollY - 40,
behavior: "smooth"
});
};
const toReport = () => {
// 相关政策动态
const box2Data = ref([
// {
// title: "这是示例标题a",
// content: "这是示例内容",
// time: "2025-08-30",
// img: 1
// },
// {
// title: "这是示例标题a",
// content: "这是示例内容",
// time: "2025-08-30",
// img: 1
// },
// {
// title: "这是示例标题a",
// content: "这是示例内容",
// time: "2025-08-30",
// img: 1
// },
// {
// title: "这是示例标题a",
// content: "这是示例内容",
// time: "2025-08-30",
// img: 1
// },
// {
// title: "这是示例标题a",
// content: "这是示例内容",
// time: "2025-08-30",
// img: 1
// },
// {
// title: "这是示例标题a",
// content: "这是示例内容",
// time: "2025-08-30",
// img: 1
// }
]);
// 点击新闻条目,跳转到新闻分析页
const handleToNewsAnalysis = news => {
console.log(news)
const route = router.resolve({
path: "/newsAnalysis",
query: {
newsId: news.newsId
name: "ReportOriginal",
params: {
id: router.currentRoute._value.params.id
}
});
window.open(route.href, "_blank");
};
// 当前页展示的数据
const filteredOpinions = computed(() => box1Data.value);
// 处理标签点击(通过接口按标签过滤)
const handleTagClick = tag => {
activeTag.value = tag;
currentPage.value = 1;
// 切换标签只在前端重算分页,并重置选中行
activeItemIndex.value = 0;
updatePageData();
};
const handleSearchOpinions = () => {
// 搜索时默认切回“全部”标签
activeTag.value = "";
currentPage.value = 1;
activeItemIndex.value = 0;
handleGetThinkTankReportPolicyAction();
};
// 高亮标题中与搜索关键词相同的文字
const highlightKeyword = text => {
if (!text) return "";
const keyword = (searchOpinions.value || "").trim();
if (!keyword) return text;
// 转义正则特殊字符
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const reg = new RegExp(escaped, "gi");
return text.replace(reg, match => `<span class="highlight-keyword">${match}</span>`);
};
// 根据当前全量数据更新 tag 及数量
const updateTagsFromAllData = () => {
const set = new Set();
const map = {};
allBox1Data.value.forEach(item => {
(item.domains || []).forEach(d => {
if (!d?.industryName) return;
const name = d.industryName;
set.add(name);
map[name] = (map[name] || 0) + 1;
});
});
allTags.value = Array.from(set);
tagCountMap.value = map;
};
const handleGetThinkTankReportPolicyAction = async () => {
try {
const res = await getThinkTankReportPolicyAction(router.currentRoute._value.params.id);
const params = {
reportId: router.currentRoute._value.params.id,
// 这里请求全量数据,前端自行分页
currentPage: 0,
pageSize: 1000,
keyword: (searchOpinions.value || "").trim(),
// 仍将标签传给后端,若后端支持则为额外兜底过滤
industryName: activeTag.value || undefined
};
const res = await getThinkTankReportPolicyAction(params);
console.log("相关政策动态", res);
if (res.code === 200 && res.data) {
box2Data.value = res.data
// 每次请求先清空全量与当前页数据
allBox1Data.value = [];
box1Data.value = [];
if (res && res.code === 200 && res.data && Array.isArray(res.data.content)) {
const content = res.data.content;
allBox1Data.value = content;
console.log(allBox1Data.value, "allBox1Data");
// 基于当前搜索结果更新 tag 和数量
updateTagsFromAllData();
// 拉取到全量后,根据当前筛选与页码更新本页数据,默认选中第一页第一条
activeItemIndex.value = 0;
updatePageData();
} else {
// 非 200 / 数据结构异常时,清空标签和数量
allTags.value = [];
tagCountMap.value = {};
console.warn("相关政策动态接口返回非 200:", res && res.code);
}
} catch (error) {
console.error("获取相关政策动态rror", error);
console.error("获取相关政策动态 error", error);
allBox1Data.value = [];
box1Data.value = [];
allTags.value = [];
tagCountMap.value = {};
}
};
// 按当前标签与页码从全量数据中截取一页
const updatePageData = () => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
let list = allBox1Data.value;
if (activeTag.value) {
list = list.filter(item =>
(item.domains || []).some(d => d?.industryName === activeTag.value)
);
}
const pageList = list.slice(start, end);
box1Data.value = pageList;
// 如当前页有数据且当前选中索引超出范围,则默认选中当前页第一条
if (pageList.length > 0) {
if (activeItemIndex.value < 0 || activeItemIndex.value >= pageList.length) {
activeItemIndex.value = 0;
}
// 同步当前选中 item 数据
box1DataItem.value = pageList[activeItemIndex.value];
console.log("当前选中政策建议:", box1DataItem.value);
} else {
activeItemIndex.value = -1;
box1DataItem.value = null;
}
};
onMounted(async () => {
handleGetThinkTankReportPolicy()
handleGetThinkTankReportPolicyAction()
});
</script>
......@@ -324,73 +324,51 @@ onMounted(async () => {
display: flex;
justify-content: center;
gap: 16px;
height: 100%;
height: 1573px;
padding-bottom: 16px;
.box-header {
width: 100%;
height: 50px;
display: flex;
position: relative;
.header-left {
margin-top: 18px;
width: 8px;
height: 20px;
border-radius: 0 4px 4px 0;
background: var(--color-main-active);
}
.box1 {
margin-top: 19px;
width: 1600px;
height: 1173px;
.title {
margin-left: 14px;
margin-top: 14px;
height: 26px;
line-height: 26px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
}
.box1-main {
margin-top: 8px;
height: 1097px;
padding-left: 21px;
padding-right: 50px;
padding-bottom: 21px;
gap: 55px;
display: flex;
.header-btn-box {
position: absolute;
top: 15px;
right: 83px;
.left {
width: 506px;
gap: 12px;
display: flex;
justify-content: flex-end;
gap: 8px;
flex-direction: column;
.btn {
height: 28px;
padding: 0 8px;
.search-box {
display: flex;
width: 100%;
height: 32px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 28px;
}
position: relative;
.btnActive {
color: var(--color-main-active);
border: 1px solid var(--color-main-active);
}
}
.header-right {
.icon {
width: 16px;
height: 16px;
cursor: pointer;
position: absolute;
top: 14px;
right: 12px;
right: 8px;
top: 8px;
display: flex;
justify-content: flex-end;
gap: 4px;
.icon {
width: 28px;
height: 28px;
img {
width: 100%;
......@@ -398,222 +376,264 @@ onMounted(async () => {
}
}
}
}
.left {
height: 100%;
.tag-box {
width: 506px;
gap: 8px;
display: flex;
height: 28px;
.box1 {
margin-top: 16px;
width: 1104px;
height: 100%;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
.box1-main {
width: 1056px;
min-height: 738px;
max-height: 1280px;
padding-bottom: 20px;
margin: 0 auto;
overflow: hidden;
overflow-y: auto;
.box1-item {
align-items: center;
// height: 128px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
display: flex;
position: relative;
.left {
width: 28px;
.tag {
height: 28px;
border-radius: 14px;
background: rgba(231, 243, 255, 1);
margin-left: 16px;
margin-top: 16px;
text-align: center;
line-height: 28px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
letter-spacing: 0px;
border: 1px solid rgb(230, 231, 232);
border-radius: 4px;
padding: 0px 8px;
display: flex;
cursor: pointer;
&.tag-active {
border-color: rgb(5, 95, 194);
background-color: rgb(231, 243, 255);
.tag-text {
color: rgb(5, 95, 194);
}
}
.center {
width: 850px;
margin-left: 18px;
.title {
margin-top: 16px;
// height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
line-height: 24px;
.tag-text {
font-size: 16px;
line-height: 28px;
letter-spacing: 0px;
text-align: left;
font-family: "Source Han Sans CN";
color: rgb(59, 65, 75);
font-weight: 400;
align-items: center;
display: flex;
}
}
}
.tag-box {
.item-box {
width: 506px;
height: 100%;
border-top: 1px solid rgb(234, 236, 238);
.item {
width: 506px;
display: flex;
margin-top: 7px;
gap: 8px;
gap: 18px;
padding-top: 16px;
padding-left: 15px;
padding-right: 22px;
padding-bottom: 16px;
border-bottom: 1px solid rgb(234, 236, 238);
cursor: pointer;
position: relative;
.tag {
height: 22px;
padding: 0 8px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: rgba(247, 248, 249, 1);
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
&.item-active {
background-color: rgb(246, 250, 255);
border: 1px solid rgb(185, 220, 255);
letter-spacing: 0px;
text-align: left;
.item-right {
color: rgb(5, 95, 194);
font-weight: 700;
}
}
.file-box {
margin-top: 7px;
display: flex;
gap: 8px;
.file-item {
height: 32px;
padding: 0 8px;
border-radius: 4px;
background: rgba(246, 250, 255, 1);
display: flex;
.item-left {
margin-top: 3px;
width: 24px;
height: 27px;
display: inline-flex;
align-items: center;
.file-item-left {
height: 22px;
padding: 0 4px;
border-radius: 4px;
justify-content: center;
border-radius: 50%;
background: rgba(231, 243, 255, 1);
line-height: 22px;
text-align: center;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-family: "Source Han Sans CN";
font-size: 14px;
font-weight: 400;
letter-spacing: 0px;
text-align: left;
/* 关键:不要被 flex 压缩 */
flex: 0 0 27px;
}
.file-item-center {
margin-left: 12px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
.item-right {
flex: 1;
width: auto;
color: rgba(59, 65, 75, 1);
font-family: "Source Han Sans CN";
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 30px;
letter-spacing: 0px;
text-align: left;
}
.file-item-right {
margin-left: 12px;
width: 20px;
height: 20px;
img {
width: 100%;
height: 100%;
.item-right-solid {
position: absolute;
right: 0;
top: 16px;
bottom: 16px;
width: 4px;
background-color: rgb(5, 95, 194);
}
}
}
.box4-main-footer {
display: flex;
justify-content: space-between;
}
}
.right {
position: absolute;
top: 16px;
right: 11px;
width: 968px;
.right-header {
height: 54px;
width: 968px;
display: flex;
border-top: 1px solid rgb(234, 236, 238);
border-bottom: 1px solid rgb(234, 236, 238);
margin-bottom: 16px;
.header-text {
width: 968px;
height: 24px;
justify-content: flex-end;
align-items: center;
gap: 9px;
margin-top: 16px;
justify-content: space-between;
display: flex;
.text {
.right-text {
height: 24px;
font-family: Microsoft YaHei;
font-size: 16px;
color: var(--color-main-active);
display: flex;
.text {
color: rgb(5, 95, 194);
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular */
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
letter-spacing: 0;
text-align: justify;
/* 两端对齐 */
}
.icon {
width: 16px;
height: 16px;
.right-header-image {
width: 12px;
height: 12px;
display: inline-block;
margin-left: 6px;
margin-top: 4.5px;
img {
width: 100%;
height: 100%;
}
}
object-fit: contain;
/* 保证图片不变形 */
}
}
}
.box1-footer {
height: 50px;
display: flex;
justify-content: space-between;
box-sizing: border-box;
padding: 20px;
.info {
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: 0px;
.left-text {
height: 24px;
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-weight: 700;
/* Bold 粗体 */
font-size: 18px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
}
}
}
.right-content {
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular */
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
margin-bottom: 16px;
}
.box2 {
.info-group {
width: 968px;
height: 160px;
border-top: 1px solid rgb(234, 236, 238);
border-bottom: 1px solid rgb(234, 236, 238);
.info-content {
width: 968px;
height: 128px;
margin-top: 16px;
width: 480px;
height: 790px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
.box2-main {
margin: 0 auto;
margin-top: 15px;
width: 459px;
height: 654px;
overflow: hidden;
.box2-item {
height: 109px;
display: flex;
flex-direction: column;
gap: 16px;
.box2-item-left {
.point {
margin-left: 14px;
width: 10px;
height: 10px;
.info-item {
height: 32px;
display: flex;
align-items: center;
justify-content: flex-start;
.info-text {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: justify;
}
.info-right {
height: 24px;
margin-left: 8px;
display: flex;
.tag-box {
height: 24px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 8px;
.tag {
border-radius: 4px;
height: 24px;
background-color: rgb(231, 243, 255);
padding: 1px 8px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(5, 95, 194);
}
}
.info-right-image {
height: 20px;
width: 20px;
margin-right: 4px;
margin-top: 2px;
img {
width: 100%;
......@@ -621,68 +641,89 @@ onMounted(async () => {
}
}
.line {
width: 2px;
height: 109px;
background: #ccc;
margin-left: 18px;
.info-right-text {
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
}
}
.box2-item-center {
margin-left: 11px;
}
.title {
width: 314px;
.right-footer-title {
width: 968px;
height: 56px;
border-bottom: 1px solid rgb(234, 236, 238);
display: flex;
.footer-title-text {
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
margin-top: 16px;
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
width: 314px;
height: 48px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
overflow: hidden;
}
}
.box2-item-right {
margin-left: 18px;
.right-footer-box {
width: 968px;
gap: 13px;
display: flex;
flex-direction: column;
.time {
height: 22px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 14px;
.relatedBills {
width: 968px;
height: 48px;
border: 1px solid rgb(231, 243, 255);
background-color: rgb(246, 250, 255);
padding: 11px 16px;
display: flex;
.tag {
border-radius: 4px;
height: 24px;
background-color: rgb(231, 243, 255);
padding: 1px 8px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
letter-spacing: 0;
text-align: left;
color: rgb(5, 95, 194);
margin-right: 4px;
}
.relatedBills-content {
height: 24px;
margin-left: 8px;
margin-left: 8px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
/* Regular 对应字重 */
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
color: rgb(5, 95, 194);
}
.img-box {
margin-top: 14px;
width: 78px;
height: 50px;
border-radius: 2px;
.footer-image {
margin-top: 2px;
width: 20px;
height: 20px;
margin-left: auto;
img {
width: 100%;
......@@ -690,36 +731,49 @@ onMounted(async () => {
}
}
}
}
}
.box2-footer {
margin: 10px auto;
width: 108px;
height: 32px;
.relatedAdministrativeOrders {
width: 968px;
height: 48px;
border: 1px solid rgb(231, 243, 255);
background-color: rgb(246, 250, 255);
padding: 11px 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
&:hover {
background: var(--color-bg-hover);
.relatedAdministrativeOrders-content {
height: 24px;
margin-left: 8px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
/* Regular 对应字重 */
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
color: rgb(5, 95, 194);
}
.text {
color: var(--color-main-active);
height: 22px;
font-family: Microsoft YaHei;
font-size: 14px;
.tag {
border-radius: 4px;
height: 24px;
background-color: rgb(231, 243, 255);
padding: 1px 8px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
letter-spacing: 0;
text-align: left;
color: rgb(5, 95, 194);
margin-right: 4px;
}
.icon {
width: 16px;
height: 16px;
.footer-image {
margin-top: 2px;
width: 20px;
height: 20px;
margin-left: auto;
img {
width: 100%;
......@@ -728,5 +782,27 @@ onMounted(async () => {
}
}
}
}
}
}
}
/* 搜索关键词高亮(v-html 渲染,需要 :deep 才能作用到插入的 DOM) */
:deep(.highlight-keyword) {
background-color: #fff3b0;
}
:deep(.analysis-box-wrapper .wrapper-header) {
height: 54px !important;
display: flex;
align-items: center;
.header-title>div {
line-height: 54px;
}
}
</style>
\ No newline at end of file
......@@ -1129,5 +1129,10 @@ onMounted(() => {
:deep(.analysis-box-wrapper .wrapper-header) {
height: 54px !important;
display: flex;
align-items: center;
.header-title > div {
line-height: 54px;
}
}
</style>
......@@ -456,11 +456,10 @@ const handleGetThinkPolicyIndustryChange = async () => {
frontendData.data.push(industryData);
});
box3Data.value = frontendData;
let box3Chart = getMultiLineChart(
// 传入全部行业数据,支持 13 条线同时展示
const box3Chart = getMultiLineChart(
box3Data.value.title,
box3Data.value.data[0].value,
box3Data.value.data[1].value,
box3Data.value.data[2].value
box3Data.value.data
);
setChart(box3Chart, "box3Chart");
}
......
import * as echarts from 'echarts'
import { size, split } from 'lodash'
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
// data: [{ name: string, value: number[] }, ...]
const getMultiLineChart = (dataX, seriesData) => {
return {
tooltip: {
trigger: 'axis',
......@@ -71,32 +72,12 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
}
}
],
series: [
{
name: '人工智能',
type: 'line',
emphasis: {
focus: 'series'
},
data: dataY1
},
{
name: '集成电路',
series: (seriesData || []).map(item => ({
name: item.name,
type: 'line',
emphasis: {
focus: 'series'
},
data: dataY2
},
{
name: '量子科技',
type: 'line',
emphasis: {
focus: 'series'
},
data: dataY3
}
]
emphasis: { focus: 'series' },
data: item.value
}))
}
}
......
<template>
<div class="home-wrapper">
<div class="home-main" ref="containerRef">
<div class="home-top-bg"></div>
<div class="to-back">
<div class="to-back-btn" @click="goToAllThinkTank">
<div class="back-image">
<img src="../assets/images/arrow-left.png" />
</div>
<div class="back-text">
{{ "返回" }}
</div>
</div>
</div>
<div class="item-box">
<div class="box-header">
<div class="box-header-text">
<div class="box-title">{{ "美国科技智库" }}</div>
<div class="number-btn">
<div class="number-text">
{{ total }}{{ "家" }}
</div>
</div>
</div>
<div class="select-box">
<div class="search-box">
<el-input placeholder="搜索政策建议" v-model="searchPolicy">
<template #suffix>
<img src="../assets/images/Line_Search.png" class="search-icon" alt="搜索">
</template>
</el-input>
</div>
<div class="select-box-sort">
<el-select v-model="sort" placeholder="报告数量" :teleported="true" :placement="'bottom-start'"
:popper-options="{
modifiers: [
{
name: 'preventOverflow', // 禁用自动翻转逻辑
options: {
mainAxis: false, // 禁用垂直方向的自动调整
altAxis: false, // 禁用水平方向的自动调整
}
},
{
name: 'flip', // 完全禁用翻转功能
enabled: false
}
]
}">
<template #prefix>
<img src="../assets/images/sort-asc.png" class="select-prefix-img" alt=""
@click.stop="toggleSortAndFetch()" :key="true" label="正序" :value="true" v-if="sort" />
<img src="../assets/images/sort-desc.png" class="select-prefix-img" alt=""
@click.stop="toggleSortAndFetch()" :key="true" label="倒序" :value="true" v-if="!sort" />
</template>
<el-option @click="handleGetThinkDynamicsReport()" :key="true" label="正序" :value="true" />
<el-option @click="handleGetThinkDynamicsReport()" :key="false" label="倒序" :value="false" />
</el-select>
</div>
</div>
</div>
<div class="title-info-bar">
<div class="title-info">
<div class="info">
<img src="../assets/images/info.png" />
</div>
<div class="title">
{{ "近期美国智库机构发布涉华报告数量汇总" }}
</div>
</div>
</div>
<div class="all-item">
<div class="item-card" v-for="(item, index) in sortedCardList" :key="item.id || index"
@click="handleClick(item)">
<div class="item-header">
<div class="item-header-image">
<img :src=item.logo alt="" />
</div>
<div class="item-header-text">
<div class="text">{{ item.reportNumber }}{{ "篇报告" }}</div>
</div>
</div>
<div class="item-middle">
<div class="think-tank-name">{{ item.name }}</div>
<div class="country">{{ item.country }}</div>
</div>
<div class="item-content">
{{ item.desc }}
</div>
<div class="tag-box">
<AreaTag v-for="(val, idx) in item.tagList" :key="idx" :tagName="val.industryName"></AreaTag>
</div>
</div>
</div>
<div class="page-box">
<div class="page-change">
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
layout="prev, pager, next" background @current-change="handleCurrentChange" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
import { getAllThinkTankList } from "@/api/thinkTank/overview"
import { useRouter } from 'vue-router';
import router from "@/router";
const cardList = ref([]);
// 按 reportNumber 从大到小排序,保证从左到右从上到下排列
const sortedCardList = computed(() => {
return [...cardList.value].sort((a, b) => {
const an = Number(a?.reportNumber ?? 0);
const bn = Number(b?.reportNumber ?? 0);
return bn - an;
});
});
// el-pagination 是 1-based
const currentPage = ref(1)
const pageSize = ref(15)
const total = ref(0)
const handleCurrentChange = page => {
currentPage.value = page;
handleGetThinkTankList()
};
const routerTo = useRouter()
// 跳转到全部智库页面
const goToAllThinkTank = () => {
// 替换为你的实际路由路径
routerTo.push('/thinkTank');
};
const handleGetThinkTankList = async () => {
try {
const res = await getAllThinkTankList({
// 后端通常是 0-based,这里做一次转换
currentPage: currentPage.value - 1,
pageSize: pageSize.value
});
console.log("智库列表", res);
if (res.code === 200 && res.data) {
const list = res.data?.content || [];
total.value = res.data.totalElements;
cardList.value = list.map(item => ({
...item,
id: item.id,
logo: item.imageUrl,
name: item.name,
tagList: item.tags,
country: item.country,
desc: item.describe,
rank: item.id,
reportNumber: item.reportNumber
}));
}
} catch (error) {
console.error("获取智库列表error", error);
}
};
const sort = ref(null);
const searchPolicy = ref("");
const handleClick = tank => {
console.log(tank);
window.sessionStorage.setItem("curTabName", tank.name + "概览");
// router.push({ name: "ThinkTankDetail", params: { id: tank.id } });
if (!tank.id) {
ElMessage.warning("当前智库id为空,无法进入详情页");
return;
}
const curRoute = router.resolve({ name: "ThinkTankDetail", params: { id: tank.id, name: tank.name } });
window.open(curRoute.href, "_blank");
};
onMounted(async () => {
handleGetThinkTankList();
});
</script>
<style lang="scss" scoped>
.home-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow-y: hidden;
.home-main {
position: relative;
width: 100%;
height: 100%;
overflow-y: auto;
background-size: 100% 100%;
display: flex;
flex-direction: column;
align-items: center;
.home-top-bg {
background:
url("../assets/images/background.png"),
linear-gradient(180deg, rgba(229, 241, 254, 1) 0%, rgba(246, 251, 255, 0) 30%);
background-size: 100% 100%;
position: absolute;
width: 100%;
height: 100%;
z-index: -100;
top: -64px;
}
.to-back {
width: 100%;
height: 80px;
box-sizing: border-box;
min-height: 80px;
position: relative;
.to-back-btn {
position: absolute;
left: calc(50% - 76px - 1600px/2 - 20px - 33px);
/* 核心公式 */
width: 92px;
height: 38px;
border-radius: 32px;
background-color: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(25, 69, 130, 0.1);
display: flex;
padding-left: 20px;
margin-top: 24px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
.back-text {
height: 30px;
margin-top: 3px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: center;
color: rgb(95, 101, 108);
}
.back-image {
width: 16px;
height: 16px;
margin-top: 11px;
margin-right: 4px;
img {
width: 100%;
height: 100%;
}
}
}
}
.item-box {
width: 1600px;
height: 962px;
box-sizing: border-box;
min-height: 962px;
.box-header {
width: 1600px;
height: 62px;
display: flex;
justify-content: space-between;
.box-header-text {
height: 62px;
display: flex;
.box-title {
font-family: "YouSheBiaoTiHei";
font-weight: 400;
font-size: 48px;
line-height: 62px;
letter-spacing: 0;
text-align: left;
color: rgb(34, 41, 52);
margin-left: 2px;
}
.number-btn {
height: 35px;
border-radius: 50px;
background-color: rgb(5, 95, 194);
display: flex;
padding: 2px 16px;
margin-top: 13.5px;
margin-left: 12px;
.number-text {
font-family: "YouSheBiaoTiHei";
font-weight: 400;
font-size: 24px;
line-height: 31px;
letter-spacing: 0;
text-align: left;
color: rgb(255, 255, 255);
}
}
}
.select-box {
height: 32px;
margin-top: 15px;
display: flex;
gap: 12px;
.search-box {
width: 160px;
height: 32px;
border: 1px solid rgb(230, 231, 232);
background-color: rgb(255, 255, 255);
border-radius: 4px;
:deep(.el-input__wrapper) {
height: 32px;
border: 1px solid rgb(230, 231, 232);
background-color: #fff;
border-radius: 4px;
box-shadow: none;
padding: 0 10px;
}
.search-icon {
width: 16px;
height: 16px;
cursor: pointer;
}
}
.select-box-sort {
.select-prefix-img {
width: 6.72px;
height: 14px;
margin-right: 10px;
}
}
:deep(.el-select-dropdown) {
left: 0 !important;
/* 强制下拉框左对齐选择框 */
top: 100% !important;
/* 强制下拉框在选择框正下方 */
transform: none !important;
/* 禁用默认的位移变换 */
}
}
}
.title-info-bar {
width: 1600px;
height: 36px;
margin-top: 14px;
display: flex;
.title-info {
height: 24px;
margin-top: 6px;
display: flex;
.info {
width: 16px;
height: 16px;
margin-top: 4px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
}
}
.title {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 18px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
}
}
}
.all-item {
width: 1600px;
height: 716px;
margin-top: 30px;
display: flex;
gap: 16px;
flex-wrap: wrap;
/* 👉 加上这一行就自动换行了! */
.item-card {
width: 307px;
height: 228px;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
background-color: rgba(255, 255, 255, 0.65);
border: 1px solid rgb(255, 255, 255);
border-radius: 10px;
padding-left: 20px;
padding-top: 17px;
padding-bottom: 21px;
.item-header {
width: 287px;
height: 36px;
display: flex;
.item-header-image {
width: 36px;
height: 36px;
img {
width: 100%;
height: 100%;
}
}
.item-header-text {
height: 32px;
width: 180px;
margin-top: 2px;
margin-left: 71px;
background: linear-gradient(270deg, rgba(5, 95, 194, 0.1), rgba(5, 95, 194, 0) 100%);
.text {
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 18px;
line-height: 24px;
letter-spacing: 0;
text-align: right;
color: rgb(5, 95, 194);
width: 136px;
height: 24px;
margin-top: 4px;
margin-left: 26px;
}
}
}
.item-middle {
height: 24px;
display: flex;
justify-content: space-between;
margin-top: 11px;
.think-tank-name {
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
color: rgb(59, 65, 75);
height: 24px;
}
.country {
color: rgb(132, 136, 142);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: right;
height: 22px;
margin-right: 21px;
}
}
.item-content {
width: 266px;
height: 66px;
margin-top: 13px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: justify;
color: rgb(95, 101, 108);
/* 👇 下面这 4 行是核心:控制 3 行溢出显示 ... */
display: -webkit-box;
-webkit-line-clamp: 3;
/* 限制 3 行 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.tag-box {
margin-top: 14px;
margin-right: 21px;
display: flex;
gap: 8px;
height: 24px;
display: flex;
flex-wrap: wrap;
/* 允许换行 */
gap: 8px;
overflow: auto;
/* 根据内容自动调整高度 */
// max-width: calc(3 * 40px + 2 * 8px);
/* 每行 3 列,每个 tag 最大宽度 200px,gap 8px */
}
}
}
.page-box {
height: 32px;
margin-top: 36px;
display: flex;
justify-content: center;
}
}
}
}
:deep(.el-select) {
width: 160px;
height: 32px;
}
</style>
\ No newline at end of file
......@@ -42,9 +42,18 @@
<div class="item-footer">热点科技领域</div>
</div>
</div> -->
<div class="title-info-bar">
<div class="title-info">
<div class="info">
<img src="./assets/images/info.png" />
</div>
<div class="title">
{{ "近期美国智库机构发布涉华报告数量汇总" }}
</div>
</div>
</div>
<div class="home-main-header-card-box">
<div class="card" v-for="(item, index) in cardList" :key="index" @click="handleClick(item)">
<div class="card" v-for="(item, index) in sortedCardList" :key="index" @click="handleClick(item)">
<div class="card-header">
<div class="icon">
<img :src="item.logo" alt="" />
......@@ -52,9 +61,8 @@
<!-- <div class="rank" :class="{ rank1: item.rank === 1, rank2: item.rank === 2, rank3: item.rank === 3 }">
{{ "No." + (index + 1) }}
</div> -->
<div class="rank"
:class="{ 'rank-number-one': index === 0, 'rank-number-two': index === 1, 'rank-number-three': index === 2, 'rank-number-four': index === 3, 'rank-number-five': index === 4 }">
<div class="number"> {{ "No." + (index + 1) }}</div>
<div class="rank">
<div class=" number">{{ item.reportNumber }} {{ "篇报告" }}</div>
</div>
</div>
......@@ -74,9 +82,12 @@
<AreaTag v-for="(val, idx) in item.tagList" :key="idx" :tagName="val.industryName"></AreaTag>
</div>
</div>
<div class="card">
<div class="card" @click="goToAllThinkTank()">
<div class="more">
{{ "查看全部智库 >" }}
{{ "查看全部智库" }}{{ "(" }}{{ totalAllItem }}{{ ")" }}{{ "家" }}
<div class="blue-right">
<img src="./assets/images/blue-right.png" alt="" />
</div>
</div>
</div>
</div>
......@@ -170,7 +181,8 @@
</div>
</div>
<div class="box5-main">
<div id="box5Chart"></div>
<el-empty v-if="!hasBox5ChartData" description="暂无数据" :image-size="100" />
<div id="box5Chart" v-else></div>
<div class="source">
<div class="info"><img src="./assets/images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div>
......@@ -331,6 +343,7 @@ import {
getThinkTankPolicyIndustryChange,
getThinkTankPolicyIndustry,
getThinkTankDonation,
getAllThinkTankList,
getThinkTankHot,
getNewReport,
getHylyList,
......@@ -378,8 +391,34 @@ import Box1Img from "./assets/images/box1-img.png";
import Box1Logo from "./assets/images/box1-logo.png";
import { setCanvasCreator } from "echarts/core";
import { ElMessage } from "element-plus";
import { useRouter } from 'vue-router';
const containerRef = ref(null);
const statCountInfo = ref([]);
const pageSize = ref(15)
const totalAllItem = ref(0)
const handleGetAllThinkTankList = async () => {
try {
const res = await getAllThinkTankList({
// 后端通常是 0-based,这里做一次转换
currentPage: currentPage.value - 1,
pageSize: pageSize.value
});
console.log("智库列表", res);
if (res.code === 200 && res.data) {
totalAllItem.value = res.data.totalElements;
}
} catch (error) {
console.error("获取智库列表error", error);
}
};
const routerTo = useRouter()
// 跳转到全部智库页面
const goToAllThinkTank = () => {
// 替换为你的实际路由路径
routerTo.push('/thinkTank/allThinkTank');
};
const getStatCountInfo = async () => {
......@@ -446,6 +485,13 @@ const cardList = ref([
// ]
// }
]);
const sortedCardList = computed(() => {
return [...cardList.value].sort((a, b) => {
const an = Number(a?.reportNumber ?? 0);
const bn = Number(b?.reportNumber ?? 0);
return bn - an;
});
});
// 获取智库列表
const handleGetThinkTankList = async () => {
try {
......@@ -729,6 +775,11 @@ const box5Data = ref({
// }
// ]
});
const hasBox5ChartData = computed(() => {
const v = box5Data.value;
if (!v || typeof v !== "object") return false;
return Array.isArray(v.title) && v.title.length > 0 && Array.isArray(v.data) && v.data.length > 0;
});
//获取当前时间x年前的日期
function getDateYearsAgo(years) {
......@@ -814,7 +865,7 @@ const handleGetThinkTankPolicyIndustryChange = async date => {
});
box5Data.value = result;
} else {
box5Data.value = [];
box5Data.value = { title: [], data: [] };
}
} catch (error) {
console.error("获取政策建议趋势分布error", error);
......@@ -1739,6 +1790,7 @@ const handleSearch = () => {
onMounted(async () => {
handleGetThinkTankList();
handleGetAllThinkTankList()
await getStatCountInfo();
// 定义一个定时器,每隔2秒轮播一次
setInterval(() => {
......@@ -2009,8 +2061,43 @@ onMounted(async () => {
}
}
.home-main-header-card-box {
.title-info-bar {
width: 1600px;
height: 36px;
margin-top: 64px;
display: flex;
.title-info {
height: 24px;
margin-top: 6px;
display: flex;
.info {
width: 16px;
height: 16px;
margin-top: 4px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
}
}
.title {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 18px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
}
}
}
.home-main-header-card-box {
margin-top: 30px;
display: flex;
justify-content: space-between;
......@@ -2050,77 +2137,26 @@ onMounted(async () => {
}
.rank {
width: 100px;
width: 180px;
height: 32px;
background: linear-gradient(270deg, rgba(5, 95, 194, 0.1), rgba(5, 95, 194, 0) 100%);
.number {
line-height: 32px;
text-align: left;
font-family: "YouSheBiaoTiHei";
font-size: 24px;
font-weight: 400;
line-height: 31px;
letter-spacing: 0px;
overflow: hidden;
height: 31px;
}
}
// text-overflow: ellipsis;
// white-space: nowrap;
.rank-number-one {
background: linear-gradient(90deg, rgba(206, 79, 81, 0), rgba(206, 79, 81, 0.3) 100%);
color: rgb(206, 79, 81);
padding-left: 33px;
}
.rank-number-two {
background: linear-gradient(270deg, rgba(255, 172, 77, 0.3), rgba(255, 172, 77, 0) 100%);
color: rgb(255, 149, 77);
padding-left: 23px;
}
.rank-number-three {
background: linear-gradient(270deg, rgba(255, 197, 61, 0.3), rgba(255, 197, 61, 0) 100%);
color: rgba(255, 197, 61, 1);
padding-left: 23px;
}
.rank-number-four {
background: linear-gradient(270deg, rgba(10, 87, 166, 0.3), rgba(10, 87, 166, 0) 100%);
color: rgba(5, 95, 194, 1);
padding-left: 23px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 18px;
line-height: 24px;
letter-spacing: 0;
text-align: right;
width: 136px;
margin-top: 4px;
margin-left: 26px;
color: rgb(5, 95, 194);
}
.rank-number-five {
background: linear-gradient(270deg, rgba(10, 87, 166, 0.3), rgba(10, 87, 166, 0) 100%);
color: rgba(5, 95, 194, 1);
padding-left: 23px;
}
.rank1 {
background: linear-gradient(270deg, rgba(206, 79, 81, 0.3), rgba(206, 79, 81, 0) 100%);
color: rgba(206, 79, 81, 1);
}
.rank2 {
background: linear-gradient(270deg, rgba(255, 172, 77, 0.3), rgba(255, 172, 77, 0) 100%);
color: rgba(255, 149, 77, 1);
}
.rank3 {
background: linear-gradient(270deg, rgba(255, 197, 61, 0.3), rgba(255, 197, 61, 0) 100%);
color: rgba(255, 197, 61, 1);
}
}
.card-title {
......@@ -2252,7 +2288,7 @@ onMounted(async () => {
.more {
margin: 103px auto;
height: 22px;
height: 24px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 16px;
......@@ -2261,6 +2297,22 @@ onMounted(async () => {
letter-spacing: 1px;
text-align: center;
cursor: pointer;
display: flex;
justify-content: center;
/* 👈 水平居中 */
.blue-right {
width: 16px;
height: 16px;
margin-top: 5px;
margin-left: 8px;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
}
}
......
......@@ -3,23 +3,20 @@
<div class="header">
<div class="header-top">
<div class="header-top-left">
<img :src="thinkInfo.contentUrl" alt="" />
<img :src="thinkInfo.imageUrl" alt="" />
<div>
<div class="title">{{ thinkInfo.name }}</div>
<div class="en-title">
{{ thinkInfo.ename }}
</div>
<div style="display: flex;">
<div class="tag-box" v-for="value in thinkInfo.tags">
<div class="tag">{{ value.industryName }}</div>
</div>
</div>
</div>
</div>
<div class="header-top-right">
<div class="image-name-box">
<div class="image"> <img :src=thinkInfo.thinkTankLogoUrl alt="" /></div>
<div class="name">{{ thinkInfo.thinkTankName }}</div>
</div>
<div class="time">{{ thinkInfo.times }}</div>
</div>
</div>
......@@ -30,19 +27,55 @@
<div style=" margin-top: 17px;">
智库报告原文
</div>
<div class="btn-box">
<div class="translate">
<div class="switch">
<el-switch v-model="valueSwitch" />
</div>
<div class="translate-image">
<img class="translate-icon" src="../ReportDetail/images/image-translate.png" alt=""
style="width: 16px; height: 16px; max-width: 16px; max-height: 16px; display: block; object-fit: contain;" />
</div>
<div class="translate-text">{{ "显示原文" }}</div>
</div>
<div class="btn" @click="handleDownload">
<div class="icon">
<img src="../ReportDetail/images/image-pdf.png" alt="" />
</div>
<div class="text">{{ "下载" }}</div>
</div>
<div class="btn search-btn" @click="showSearchInput = !showSearchInput">
<div class="icon">
<img src="../ReportDetail/images/Line_Search.png" alt="" />
</div>
<div class="text">{{ "查找" }}</div>
</div>
<div v-if="showSearchInput" class="search-input-wrap">
<el-input
v-model="searchKeywordText"
placeholder="输入关键词查找页码"
clearable
size="small"
style="width: 200px;"
@keyup.enter="handleSearchInPdf"
/>
<el-button type="primary" size="small" @click="handleSearchInPdf">查找</el-button>
</div>
</div>
</div>
<div class="report-box">
<iframe :src="reportUrlWithPage" width="50%" height="100%">
</iframe>
<iframe :src="reportUrlEnWithPage" width="50%" height="100%">
</iframe>
<!-- <pdf :pdfUrl="reportUrl" style="width: 48%;" />
<pdf :pdfUrl="reportUrlEn" style="width: 48%;" /> -->
<!-- <iframe src="https://www.rand.org/pubs/research_reports/RRA3572-1.html" width="100%" height="600px"
frameborder="0" allowfullscreen></iframe> -->
<pdf
v-if="valueSwitch && reportUrlEnWithPage"
ref="leftPdfRef"
:pdfUrl="reportUrlEnWithPage"
class="pdf-pane"
/>
<pdf
v-if="reportUrlWithPage"
ref="rightPdfRef"
:pdfUrl="reportUrlWithPage"
:class="['pdf-pane', { 'pdf-pane-full': !valueSwitch }]"
/>
</div>
</div>
</div>
......@@ -78,7 +111,83 @@ const buildPdfPageUrl = url => {
const reportUrlWithPage = computed(() => buildPdfPageUrl(reportUrl.value))
const reportUrlEnWithPage = computed(() => buildPdfPageUrl(reportUrlEn.value))
const valueSwitch = ref(true)
const showSearchInput = ref(false)
const searchKeywordText = ref('')
const leftPdfRef = ref(null)
const rightPdfRef = ref(null)
const handleSearchInPdf = async () => {
const keyword = searchKeywordText.value?.trim()
if (!keyword) return
const leftPdf = leftPdfRef.value
const rightPdf = rightPdfRef.value
let page = 0
let targetRef = null
if (leftPdf && typeof leftPdf.searchKeyword === 'function') {
page = await leftPdf.searchKeyword(keyword)
if (page) targetRef = leftPdf
}
if (!page && rightPdf && typeof rightPdf.searchKeyword === 'function') {
page = await rightPdf.searchKeyword(keyword)
if (page) targetRef = rightPdf
}
if (page && targetRef && typeof targetRef.goToPage === 'function') {
targetRef.goToPage(page)
} else {
try {
const { ElMessage } = await import('element-plus')
ElMessage.warning('未找到包含该关键词的页面')
} catch (_) {}
}
}
// 下载:中英文都下载,与政令原文页相同的 fetch → blob → a 标签触发下载
const downloadOnePdf = async (url, filename) => {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/pdf' },
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(blobUrl)
}
const handleDownload = async () => {
const urlZh = reportUrl.value ? String(reportUrl.value).split('#')[0] : ''
const urlEn = reportUrlEn.value ? String(reportUrlEn.value).split('#')[0] : ''
if (!urlZh && !urlEn) {
try {
const { ElMessage } = await import('element-plus')
ElMessage.warning('暂无下载链接')
} catch (_) {}
return
}
const baseName = (thinkInfo.value?.name || '报告原文').replace(/[/\\?%*:|"<>]/g, '-')
const { ElMessage } = await import('element-plus')
try {
if (urlZh) {
await downloadOnePdf(urlZh, `${baseName}_中文.pdf`)
}
if (urlEn) {
if (urlZh) await new Promise(r => setTimeout(r, 300))
await downloadOnePdf(urlEn, `${baseName}_英文.pdf`)
}
if (urlZh || urlEn) {
ElMessage.success(urlZh && urlEn ? '已下载中文、英文两份 PDF' : '下载成功')
}
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('PDF 下载失败,请稍后重试')
}
}
// 获取报告全局信息
const handleGetThinkTankReportSummary = async () => {
......@@ -121,36 +230,34 @@ onMounted(async () => {
<style lang="scss" scoped>
.wrap {
width: 100%;
height: 100%;
overflow: hidden;
overflow-y: auto;
.header {
width: 100%;
height: 88px;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99;
z-index: 99999;
overflow: hidden;
.header-top {
margin: 0 auto;
margin-top: 20px;
margin-left: 160px;
margin-bottom: 20px;
width: 1600px;
display: flex;
justify-content: space-between;
margin-right: 160px;
.header-top-left {
display: flex;
img {
width: 72px;
height: 88px;
width: 44px;
height: 54px;
}
.title {
......@@ -166,6 +273,7 @@ onMounted(async () => {
}
.en-title {
margin-top: 4px;
margin-left: 20px;
height: 24px;
color: rgba(95, 101, 108, 1);
......@@ -182,26 +290,24 @@ onMounted(async () => {
display: flex;
gap: 8px;
margin-left: 20px;
.tag {
height: 26px;
padding: 0 8px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
}
.header-top-right {
display: flex;
flex-direction: column;
text-align: right;
align-items: flex-end;
.image-name-box {
width: 118px;
height: 24px;
gap: 6px;
text-align: right;
display: flex;
justify-content: flex-end;
.name {
height: 24px;
color: rgba(95, 101, 108, 1);
......@@ -211,6 +317,23 @@ onMounted(async () => {
line-height: 24px;
letter-spacing: 0px;
text-align: right;
}
.image {
width: 16px;
height: 16px;
margin-top: 5px;
img {
width: 100%;
height: 100%;
}
}
}
.time {
......@@ -281,25 +404,118 @@ onMounted(async () => {
}
}
}
}
.main {
margin: 0 auto;
background: rgb(255, 255, 255);
width: 1600px;
height: 928px;
overflow: hidden;
border: 1px, solid, rgb(234, 236, 238);
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
.main-header {
height: 64px;
/* box-sizing: border-box; */
border-bottom: 1px solid rgb(234, 236, 238);
background: rgb(255, 255, 255);
margin: 0 70px;
color: rgba(59, 65, 75, 1);
font-family: "Source Han Sans CN";
font-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
width: 1456px;
text-align: left;
display: flex;
justify-content: space-between;
.btn-box {
display: flex;
gap: 12px;
align-items: center;
gap: 8px;
.translate {
display: flex;
flex-wrap: nowrap;
align-items: center;
height: 24px;
margin-right: 16px;
:deep(.el-switch) {
width: 22px !important;
height: 14px !important;
margin-bottom: 5px;
margin-right: 8px;
}
:deep(.el-switch__core) {
width: 22px !important;
height: 14px !important;
min-width: 22px !important;
}
:deep(.el-switch__button),
:deep(.el-switch__action) {
width: 10px !important;
height: 10px !important;
}
/* 打开时圆球从左边移到最右边:轨道 22px - 圆球 10px = 12px */
:deep(.el-switch.is-checked .el-switch__button),
:deep(.el-switch.is-checked .el-switch__action) {
transform: translateX(6px) !important;
}
.translate-image {
display: flex;
width: 16px;
height: 16px;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
.translate-text {
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
}
.btn {
width: 120px;
height: 36px;
width: 88px;
height: 32px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.icon {
width: 16px;
height: 16px;
display: inline-flex;
margin-top: 8px;
margin-left: 16px;
img {
width: 100%;
......@@ -308,61 +524,49 @@ onMounted(async () => {
}
.text {
width: 66px;
height: 22px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
margin-top: 4px;
width: 32px;
height: 24px;
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-size: 16px;
font-weight: 400;
line-height: 22px;
line-height: 24px;
letter-spacing: 0px;
text-align: center;
text-align: left;
}
}
.btn1 {
border-radius: 6px;
background: var(--color-main-active);
.text {
color: rgba(255, 255, 255, 1);
}
.search-btn {
cursor: pointer;
}
.search-input-wrap {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: 4px;
}
}
}
.main {
margin: 0 auto;
background: #ffffff;
width: 1600px;
height: 1025px;
overflow: hidden;
.report-box {
margin-left: 70px;
width: 1456px;
height: 881px;
display: flex;
overflow-y: auto;
/* 右侧统一滚动条,控制两侧原文+译文一起滚动 */
overflow-x: hidden;
}
.main-header {
height: 64px;
/* box-sizing: border-box; */
border-bottom: 1px solid rgb(234, 236, 238);
border-top: 1px solid rgb(234, 236, 238);
/* box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); */
background: rgb(255, 255, 255);
margin: 0 70px;
color: rgba(59, 65, 75, 1);
line-height: 64px;
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
.pdf-pane {
width: 50%;
height: 100%;
}
.report-box {
.pdf-pane-full {
width: 100%;
height: 750px;
overflow: auto;
display: flex;
}
}
}
......
<template>
<div class="pdf-viewer">
<canvas ref="pdfCanvas"></canvas>
<canvas
v-for="page in pageCount"
:key="page"
:ref="el => setCanvasRef(page, el)"
></canvas>
<div v-if="loading" class="loading">加载中...</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { ref, onMounted, nextTick } from 'vue';
import * as pdfjsLib from 'pdfjs-dist';
......@@ -24,37 +28,98 @@ export default {
}
},
setup(props) {
const pdfCanvas = ref(null);
// 非响应式的 canvas 映射,避免触发布局递归更新
const canvasMap = {};
const pageCount = ref(0);
const loading = ref(true);
const pdfDocRef = ref(null);
onMounted(async () => {
const setCanvasRef = (page, el) => {
if (!el) return;
canvasMap[page] = el;
};
const parsePdfUrl = (pdfUrl) => {
if (!pdfUrl || typeof pdfUrl !== 'string') return '';
const [urlPart] = pdfUrl.split('#');
return urlPart;
}
const renderPdf = async (pdfUrl) => {
const url = parsePdfUrl(pdfUrl)
if (!url) return
loading.value = true
pdfDocRef.value = null
try {
const loadingTask = pdfjsLib.getDocument(props.pdfUrl);
const loadingTask = pdfjsLib.getDocument(url);
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1); // 加载第一页
pdfDocRef.value = pdf
pageCount.value = pdf.numPages;
const viewport = page.getViewport({ scale: 1.5 });
const context = pdfCanvas.value.getContext('2d');
// 等待 canvas 按 pageCount 渲染出来
await nextTick();
for (let p = 1; p <= pdf.numPages; p++) {
const pdfPage = await pdf.getPage(p);
const viewport = pdfPage.getViewport({ scale: 1.5 });
const canvas = canvasMap[p];
if (!canvas) continue;
const context = canvas.getContext('2d');
const renderContext = {
canvasContext: context,
viewport: viewport
};
pdfCanvas.value.width = viewport.width;
pdfCanvas.value.height = viewport.height;
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render(renderContext).promise;
await pdfPage.render(renderContext).promise;
}
} catch (error) {
console.error('加载 PDF 出错:', error);
} finally {
loading.value = false;
}
});
}
/** 在 PDF 中查找关键词,返回首次出现的页码(1-based),未找到返回 0 */
const searchKeyword = async (keyword) => {
const doc = pdfDocRef.value
if (!doc || !keyword || !String(keyword).trim()) return 0
const k = String(keyword).trim()
const num = doc.numPages
for (let p = 1; p <= num; p++) {
const page = await doc.getPage(p)
const content = await page.getTextContent()
const text = (content.items || []).map(it => it.str || '').join('')
if (text.includes(k)) return p
}
return 0
}
/** 滚动到指定页码(1-based)对应的 canvas */
const goToPage = (pageNum) => {
const canvas = canvasMap[pageNum]
if (canvas && typeof canvas.scrollIntoView === 'function') {
canvas.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
// 首次挂载后再根据当前 url 渲染,避免 canvas 还没准备好
onMounted(() => {
if (props.pdfUrl) {
renderPdf(props.pdfUrl)
}
})
return {
pdfCanvas,
loading
};
pageCount,
setCanvasRef,
loading,
searchKeyword,
goToPage
}
}
};
</script>
......@@ -63,12 +128,13 @@ export default {
.pdf-viewer {
position: relative;
width: 100%;
height: 800px;
/* 高度由内容决定,让外层容器控制滚动 */
}
canvas {
width: 100%;
height: 100%;
height: auto;
display: block;
}
.loading {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论