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

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

上级 20ebbfed
...@@ -10,6 +10,13 @@ export function getThinkTankList() { ...@@ -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() { export function getNewReport() {
return request({ return request({
...@@ -299,9 +306,26 @@ export function getThinkTankReportPolicy(params) { ...@@ -299,9 +306,26 @@ export function getThinkTankReportPolicy(params) {
//获取相关政策动态 //获取相关政策动态
export function getThinkTankReportPolicyAction(params) { export function getThinkTankReportPolicyAction(params) {
return request({ const {
method: 'GET', reportId,
url: `/api/thinkTankReport/policyAction/${params}`, currentPage,
pageSize,
keyword = "",
orgIds = "",
// 新增:按科技领域 / 标签过滤
industryName = ""
} = params;
return request({
method: 'GET',
url: `/api/thinkTankReport/policyDetail/${reportId}`,
params: {
currentPage,
pageSize,
keyword,
// 后端按标签过滤使用的字段
industryName,
orgIds,
}
}) })
} }
......
...@@ -3,6 +3,7 @@ const thinkTank = () => import('@/views/thinkTank/index.vue') ...@@ -3,6 +3,7 @@ const thinkTank = () => import('@/views/thinkTank/index.vue')
const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue') const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue')
const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue') const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue')
const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue') const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue')
const allThinkTank= () => import('@/views/thinkTank/allThinkTank/index.vue')
const thinktankRoutes = [ const thinktankRoutes = [
// 智库系统的主要路由 // 智库系统的主要路由
...@@ -40,6 +41,14 @@ const thinktankRoutes = [ ...@@ -40,6 +41,14 @@ const thinktankRoutes = [
// title: "报告原文" // title: "报告原文"
// } // }
}, },
{
path: "/thinkTank/allThinkTank",
name: "allThinkTank",
component: allThinkTank,
// meta: {
// title: "报告原文"
// }
},
] ]
......
...@@ -5,9 +5,8 @@ ...@@ -5,9 +5,8 @@
<div class="home-content"> <div class="home-content">
<div class="home-content-header"> <div class="home-content-header">
<SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef" <SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef" placeholder="搜索科技法案"
placeholder="搜索科技法案" :containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true" :containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true" defaultBillSearchType="federal" />
defaultBillSearchType="federal" />
</div> </div>
<DivideHeader id="position1" class="divide1" :titleText="'最新动态'"></DivideHeader> <DivideHeader id="position1" class="divide1" :titleText="'最新动态'"></DivideHeader>
...@@ -28,8 +27,8 @@ ...@@ -28,8 +27,8 @@
</div> </div>
</div> </div>
<div class="box1-main" style="display: block"> <div class="box1-main" style="display: block">
<el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000" <el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000" arrow="never"
arrow="never" indicator-position="none" @change="handleCarouselChange"> indicator-position="none" @change="handleCarouselChange">
<el-carousel-item v-for="(bill, billIndex) in hotBillList" :key="billIndex"> <el-carousel-item v-for="(bill, billIndex) in hotBillList" :key="billIndex">
<div class="carousel-content" style="display: flex; height: 100%"> <div class="carousel-content" style="display: flex; height: 100%">
<div class="box1-main-left"> <div class="box1-main-left">
...@@ -37,8 +36,7 @@ ...@@ -37,8 +36,7 @@
{{ bill.billName }} {{ bill.billName }}
</div> </div>
<div class="box1-main-left-info"> <div class="box1-main-left-info">
<AreaTag v-for="(item, index) in bill.hylyList" :key="index" <AreaTag v-for="(item, index) in bill.hylyList" :key="index" :tagName="item.industryName">
:tagName="item.industryName">
</AreaTag> </AreaTag>
</div> </div>
<div class="box1-main-left-info1"> <div class="box1-main-left-info1">
...@@ -56,18 +54,15 @@ ...@@ -56,18 +54,15 @@
</div> </div>
</div> </div>
<div class="box1-main-left-info2"> <div class="box1-main-left-info2">
<div class="info2-item" v-for="(item, index) in bill.dyqkList" <div class="info2-item" v-for="(item, index) in bill.dyqkList" :key="index">
:key="index"> <div class="time-line" v-if="index !== bill.dyqkList.length - 1"></div>
<div class="time-line"
v-if="index !== bill.dyqkList.length - 1"></div>
<div class="item-icon"> <div class="item-icon">
<img src="./assets/images/info2-icon.png" alt="" /> <img src="./assets/images/info2-icon.png" alt="" />
</div> </div>
<div class="item-time" :class="{ itemTimeActive: index === 0 }"> <div class="item-time" :class="{ itemTimeActive: index === 0 }">
{{ item.actionDate }} {{ item.actionDate }}
</div> </div>
<div class="item-title" <div class="item-title" :class="{ itemTitleActive: index === 0 }">
:class="{ itemTitleActive: index === 0 }">
{{ item.actionContentCn }} {{ item.actionContentCn }}
</div> </div>
</div> </div>
...@@ -94,25 +89,17 @@ ...@@ -94,25 +89,17 @@
</el-carousel> </el-carousel>
</div> </div>
</overviewMainBox> </overviewMainBox>
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" <RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" @item-click="handleClickToDetailO"
@item-click="handleClickToDetailO" riskLevel="signalLevel" postDate="signalTime" riskLevel="signalLevel" postDate="signalTime" name="signalTitle" />
name="signalTitle" />
</div> </div>
<DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader> <DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center"> <div class="center-center">
<NewsList <NewsList :newsList="newsList" img="newsImage" title="newsTitle" from="from" content="newsContent" />
:newsList="newsList" <MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail"
img="newsImage" @person-click="handleClickToCharacter" name="personName" content="remarks" source="orgName" />
title="newsTitle"
from="from"
content="newsContent"
/>
<MessageBubble :messageList="messageList" imageUrl="personImage"
@more-click="handleToSocialDetail" @person-click="handleClickToCharacter" name="personName"
content="remarks" source="orgName" />
</div> </div>
<DivideHeader id="position3" class="divide3" :titleText="'数据总览'"></DivideHeader> <DivideHeader id="position3" class="divide3" :titleText="'数据总览'"></DivideHeader>
<div class="center-footer"> <div class="center-footer">
<OverviewCard class="overview-card--double box5" title="涉华法案数量变化趋势" :icon="box5HeaderIcon"> <OverviewCard class="overview-card--double box5" title="涉华法案数量变化趋势" :icon="box5HeaderIcon">
...@@ -1783,7 +1770,7 @@ onUnmounted(() => { ...@@ -1783,7 +1770,7 @@ onUnmounted(() => {
margin-top: 21px; margin-top: 21px;
height: 450px; height: 450px;
display: flex; display: flex;
gap:16px; gap: 16px;
.box3 { .box3 {
width: 792px; width: 792px;
...@@ -2074,6 +2061,7 @@ onUnmounted(() => { ...@@ -2074,6 +2061,7 @@ onUnmounted(() => {
.overview-card--double { .overview-card--double {
width: calc(#{$overview-single-width} * 2 + #{$overview-card-gap}); width: calc(#{$overview-single-width} * 2 + #{$overview-card-gap});
} }
.box5-main, .box5-main,
.box6-main, .box6-main,
.box7-main, .box7-main,
......
...@@ -591,6 +591,7 @@ ...@@ -591,6 +591,7 @@
</template> </template>
<script setup> <script setup>
//这是一个备注
import NewsList from "@/components/base/newsList/index.vue"; import NewsList from "@/components/base/newsList/index.vue";
import RiskSignal from "@/components/base/RiskSignal/index.vue"; import RiskSignal from "@/components/base/RiskSignal/index.vue";
import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue"; import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue";
......
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
</div> </div>
<div class="text" @click="toReport()">{{ "报告原文" }}</div> <div class="text" @click="toReport()">{{ "报告原文" }}</div>
</div> </div>
<div class="btn"> <div class="btn" @click="handleDownloadDocument">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon3.png" alt="" /> <img src="./images/btn-icon3.png" alt="" />
</div> </div>
...@@ -89,7 +89,7 @@ import { ref, onMounted } from "vue"; ...@@ -89,7 +89,7 @@ import { ref, onMounted } from "vue";
import ReportAnalysis from "./reportAnalysis/index.vue"; import ReportAnalysis from "./reportAnalysis/index.vue";
import PolicyTracking from "./policyTracking/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"; import { useRoute, useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
...@@ -163,6 +163,56 @@ const goToOfficialWebsite = () => { ...@@ -163,6 +163,56 @@ const goToOfficialWebsite = () => {
window.open(url, "_blank"); 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -1129,5 +1129,10 @@ onMounted(() => { ...@@ -1129,5 +1129,10 @@ onMounted(() => {
:deep(.analysis-box-wrapper .wrapper-header) { :deep(.analysis-box-wrapper .wrapper-header) {
height: 54px !important; height: 54px !important;
display: flex;
align-items: center;
.header-title > div {
line-height: 54px;
}
} }
</style> </style>
...@@ -456,11 +456,10 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -456,11 +456,10 @@ const handleGetThinkPolicyIndustryChange = async () => {
frontendData.data.push(industryData); frontendData.data.push(industryData);
}); });
box3Data.value = frontendData; box3Data.value = frontendData;
let box3Chart = getMultiLineChart( // 传入全部行业数据,支持 13 条线同时展示
const box3Chart = getMultiLineChart(
box3Data.value.title, box3Data.value.title,
box3Data.value.data[0].value, box3Data.value.data
box3Data.value.data[1].value,
box3Data.value.data[2].value
); );
setChart(box3Chart, "box3Chart"); setChart(box3Chart, "box3Chart");
} }
......
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { size, split } from 'lodash' import { size, split } from 'lodash'
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => { // data: [{ name: string, value: number[] }, ...]
const getMultiLineChart = (dataX, seriesData) => {
return { return {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
...@@ -71,32 +72,12 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => { ...@@ -71,32 +72,12 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
} }
} }
], ],
series: [ series: (seriesData || []).map(item => ({
{ name: item.name,
name: '人工智能', type: 'line',
type: 'line', emphasis: { focus: 'series' },
emphasis: { data: item.value
focus: 'series' }))
},
data: dataY1
},
{
name: '集成电路',
type: 'line',
emphasis: {
focus: 'series'
},
data: dataY2
},
{
name: '量子科技',
type: 'line',
emphasis: {
focus: 'series'
},
data: dataY3
}
]
} }
} }
......
差异被折叠。
...@@ -42,9 +42,18 @@ ...@@ -42,9 +42,18 @@
<div class="item-footer">热点科技领域</div> <div class="item-footer">热点科技领域</div>
</div> </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="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="card-header">
<div class="icon"> <div class="icon">
<img :src="item.logo" alt="" /> <img :src="item.logo" alt="" />
...@@ -52,9 +61,8 @@ ...@@ -52,9 +61,8 @@
<!-- <div class="rank" :class="{ rank1: item.rank === 1, rank2: item.rank === 2, rank3: item.rank === 3 }"> <!-- <div class="rank" :class="{ rank1: item.rank === 1, rank2: item.rank === 2, rank3: item.rank === 3 }">
{{ "No." + (index + 1) }} {{ "No." + (index + 1) }}
</div> --> </div> -->
<div class="rank" <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">{{ item.reportNumber }} {{ "篇报告" }}</div>
<div class="number"> {{ "No." + (index + 1) }}</div>
</div> </div>
</div> </div>
...@@ -74,9 +82,12 @@ ...@@ -74,9 +82,12 @@
<AreaTag v-for="(val, idx) in item.tagList" :key="idx" :tagName="val.industryName"></AreaTag> <AreaTag v-for="(val, idx) in item.tagList" :key="idx" :tagName="val.industryName"></AreaTag>
</div> </div>
</div> </div>
<div class="card"> <div class="card" @click="goToAllThinkTank()">
<div class="more"> <div class="more">
{{ "查看全部智库 >" }} {{ "查看全部智库" }}{{ "(" }}{{ totalAllItem }}{{ ")" }}{{ "家" }}
<div class="blue-right">
<img src="./assets/images/blue-right.png" alt="" />
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -170,7 +181,8 @@ ...@@ -170,7 +181,8 @@
</div> </div>
</div> </div>
<div class="box5-main"> <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="source">
<div class="info"><img src="./assets/images/image-exclamation.png"></div> <div class="info"><img src="./assets/images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> <div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div>
...@@ -331,6 +343,7 @@ import { ...@@ -331,6 +343,7 @@ import {
getThinkTankPolicyIndustryChange, getThinkTankPolicyIndustryChange,
getThinkTankPolicyIndustry, getThinkTankPolicyIndustry,
getThinkTankDonation, getThinkTankDonation,
getAllThinkTankList,
getThinkTankHot, getThinkTankHot,
getNewReport, getNewReport,
getHylyList, getHylyList,
...@@ -378,8 +391,34 @@ import Box1Img from "./assets/images/box1-img.png"; ...@@ -378,8 +391,34 @@ import Box1Img from "./assets/images/box1-img.png";
import Box1Logo from "./assets/images/box1-logo.png"; import Box1Logo from "./assets/images/box1-logo.png";
import { setCanvasCreator } from "echarts/core"; import { setCanvasCreator } from "echarts/core";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useRouter } from 'vue-router';
const containerRef = ref(null); const containerRef = ref(null);
const statCountInfo = ref([]); 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 () => { const getStatCountInfo = async () => {
...@@ -446,6 +485,13 @@ const cardList = ref([ ...@@ -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 () => { const handleGetThinkTankList = async () => {
try { try {
...@@ -729,6 +775,11 @@ const box5Data = ref({ ...@@ -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年前的日期 //获取当前时间x年前的日期
function getDateYearsAgo(years) { function getDateYearsAgo(years) {
...@@ -814,7 +865,7 @@ const handleGetThinkTankPolicyIndustryChange = async date => { ...@@ -814,7 +865,7 @@ const handleGetThinkTankPolicyIndustryChange = async date => {
}); });
box5Data.value = result; box5Data.value = result;
} else { } else {
box5Data.value = []; box5Data.value = { title: [], data: [] };
} }
} catch (error) { } catch (error) {
console.error("获取政策建议趋势分布error", error); console.error("获取政策建议趋势分布error", error);
...@@ -1739,6 +1790,7 @@ const handleSearch = () => { ...@@ -1739,6 +1790,7 @@ const handleSearch = () => {
onMounted(async () => { onMounted(async () => {
handleGetThinkTankList(); handleGetThinkTankList();
handleGetAllThinkTankList()
await getStatCountInfo(); await getStatCountInfo();
// 定义一个定时器,每隔2秒轮播一次 // 定义一个定时器,每隔2秒轮播一次
setInterval(() => { setInterval(() => {
...@@ -2009,8 +2061,43 @@ onMounted(async () => { ...@@ -2009,8 +2061,43 @@ onMounted(async () => {
} }
} }
.home-main-header-card-box { .title-info-bar {
width: 1600px;
height: 36px;
margin-top: 64px; 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; display: flex;
justify-content: space-between; justify-content: space-between;
...@@ -2050,77 +2137,26 @@ onMounted(async () => { ...@@ -2050,77 +2137,26 @@ onMounted(async () => {
} }
.rank { .rank {
width: 100px; width: 180px;
height: 32px; height: 32px;
background: linear-gradient(270deg, rgba(5, 95, 194, 0.1), rgba(5, 95, 194, 0) 100%);
.number { .number {
line-height: 32px; font-family: "Source Han Sans CN";
text-align: left; font-weight: 700;
font-family: "YouSheBiaoTiHei"; font-size: 18px;
font-size: 24px; line-height: 24px;
font-weight: 400; letter-spacing: 0;
line-height: 31px; text-align: right;
letter-spacing: 0px; width: 136px;
overflow: hidden; margin-top: 4px;
margin-left: 26px;
height: 31px; color: rgb(5, 95, 194);
} }
} }
// 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;
}
.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 { .card-title {
...@@ -2252,7 +2288,7 @@ onMounted(async () => { ...@@ -2252,7 +2288,7 @@ onMounted(async () => {
.more { .more {
margin: 103px auto; margin: 103px auto;
height: 22px; height: 24px;
color: rgba(5, 95, 194, 1); color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
font-size: 16px; font-size: 16px;
...@@ -2261,6 +2297,22 @@ onMounted(async () => { ...@@ -2261,6 +2297,22 @@ onMounted(async () => {
letter-spacing: 1px; letter-spacing: 1px;
text-align: center; text-align: center;
cursor: pointer; 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;
}
}
} }
} }
} }
......
<template> <template>
<div class="pdf-viewer"> <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 v-if="loading" class="loading">加载中...</div>
</div> </div>
</template> </template>
<script> <script>
import { ref, onMounted } from 'vue'; import { ref, onMounted, nextTick } from 'vue';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
...@@ -24,37 +28,98 @@ export default { ...@@ -24,37 +28,98 @@ export default {
} }
}, },
setup(props) { setup(props) {
const pdfCanvas = ref(null); // 非响应式的 canvas 映射,避免触发布局递归更新
const canvasMap = {};
const pageCount = ref(0);
const loading = ref(true); 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 { try {
const loadingTask = pdfjsLib.getDocument(props.pdfUrl); const loadingTask = pdfjsLib.getDocument(url);
const pdf = await loadingTask.promise; 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 }); // 等待 canvas 按 pageCount 渲染出来
const context = pdfCanvas.value.getContext('2d'); await nextTick();
const renderContext = {
canvasContext: context,
viewport: viewport
};
pdfCanvas.value.width = viewport.width; for (let p = 1; p <= pdf.numPages; p++) {
pdfCanvas.value.height = viewport.height; 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
};
await page.render(renderContext).promise; canvas.width = viewport.width;
canvas.height = viewport.height;
await pdfPage.render(renderContext).promise;
}
} catch (error) { } catch (error) {
console.error('加载 PDF 出错:', error); console.error('加载 PDF 出错:', error);
} finally { } finally {
loading.value = false; 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 { return {
pdfCanvas, pageCount,
loading setCanvasRef,
}; loading,
searchKeyword,
goToPage
}
} }
}; };
</script> </script>
...@@ -63,12 +128,13 @@ export default { ...@@ -63,12 +128,13 @@ export default {
.pdf-viewer { .pdf-viewer {
position: relative; position: relative;
width: 100%; width: 100%;
height: 800px; /* 高度由内容决定,让外层容器控制滚动 */
} }
canvas { canvas {
width: 100%; width: 100%;
height: 100%; height: auto;
display: block;
} }
.loading { .loading {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论