提交 09b8b813 authored 作者: 付康's avatar 付康

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

feat:智库样式功能开发 查看合并请求 !193
VITE_BASE_API= '/api' VITE_BASE_API= '/api'
# 图表解读等 /aiAnalysis 代理服务密钥(勿提交到公开仓库时可改为本地 .env.local)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
# 线上地址 # 线上地址
VITE_BASE_API= '/api' VITE_BASE_API= '/api'
# 图表解读等 AI 服务(与部署环境一致时填写)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
...@@ -51,6 +51,14 @@ service.interceptors.request.use(config => { ...@@ -51,6 +51,14 @@ service.interceptors.request.use(config => {
config.headers['token'] = token config.headers['token'] = token
// config.headers['Authorization'] = `Bearer ${token}` // 如果后端需要Bearer格式可以使用这个 // config.headers['Authorization'] = `Bearer ${token}` // 如果后端需要Bearer格式可以使用这个
} }
// 图表解读等 AI 分析服务(Vite 代理 /aiAnalysis)需要 X-API-Key
const reqUrl = String(config.url || '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) {
config.headers['X-API-Key'] = aiApiKey
}
}
return config return config
}, error => { }, error => {
console.log(error) console.log(error)
...@@ -82,8 +90,14 @@ service.interceptors.response.use( ...@@ -82,8 +90,14 @@ service.interceptors.response.use(
// 重复请求触发的取消不提示错误 // 重复请求触发的取消不提示错误
if (isCanceledError) return Promise.reject(error) if (isCanceledError) return Promise.reject(error)
// 处理token过期或无效的情况 // 处理token过期或无效的情况(排除 AI 分析服务:其 401 多为 API Key 问题)
if (error.response && (error.response.status === 401 || error.response.status === 403)) { const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
!isAiAnalysisRequest
) {
ElMessage({ ElMessage({
message: 'Token已过期,请重新登录', message: 'Token已过期,请重新登录',
type: 'error', type: 'error',
......
...@@ -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({
...@@ -28,13 +35,19 @@ export function getThinkTankRiskSignal() { ...@@ -28,13 +35,19 @@ export function getThinkTankRiskSignal() {
}) })
} }
// 政策建议趋势分布 /**
* 政策建议趋势分布(数量变化趋势)
* @param {{ startDate: string, endDate: string }} params - 如 2024-01-01 ~ 2024-12-31
*/
export function getThinkTankPolicyIndustryChange(params) { export function getThinkTankPolicyIndustryChange(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/thinkTankOverview/policyIndustryChange/${params}`, url: `/api/thinkTankOverview/policyIndustryChange`,
params: {
}) startDate: params.startDate,
endDate: params.endDate
}
});
} }
// 政策建议领域分布 // 政策建议领域分布
...@@ -113,10 +126,44 @@ export function getThinkDynamicsReportType() { ...@@ -113,10 +126,44 @@ export function getThinkDynamicsReportType() {
//智库动态:获取智库报告 //智库动态:获取智库报告
export function getThinkDynamicsReport(params) { export function getThinkDynamicsReport(params) {
const safe = params || {}
// 兼容两种调用方式:
// 1) { id, startDate, authorName, currentPage, pageSize, researchTypeIds, searchText, sortFun, years }
// 2) { id, startDate, parmas: { authorName, currentPage, pageSize, researchTypeIds, searchText, sortFun, years } }
const inner = safe.parmas && typeof safe.parmas === 'object' ? safe.parmas : {}
const id = safe.id
const startDate = safe.startDate
const authorName = inner.authorName ?? safe.authorName ?? ''
const currentPage = inner.currentPage ?? safe.currentPage ?? 1
const pageSize = inner.pageSize ?? safe.pageSize ?? 10
const researchTypeIds = inner.researchTypeIds ?? safe.researchTypeIds ?? ''
const searchText = inner.searchText ?? safe.searchText ?? ''
const sortFun = inner.sortFun ?? safe.sortFun ?? false
const years = inner.years ?? safe.years ?? null
const query = { currentPage, pageSize, sortFun }
// 仅在有值时才传,避免后端按空值筛选
if (authorName) query.authorName = authorName
if (researchTypeIds) query.researchTypeIds = researchTypeIds
if (searchText) query.searchText = searchText
if (years !== null && years !== undefined && years !== '') query.years = years
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankInfo/report/${params.id}/${params.startDate}`, url: `/api/thinkTankInfo/report/${id}/${startDate}`,
params: params.parmas params: query
})
}
// 智库领域观点分析(流式)
// [POST] 8.140.26.4:10029/report-domain-view-analysis
export function postReportDomainViewAnalysis(data) {
return request({
method: 'POST',
// 开发环境走 Vite 同源代理,避免浏览器跨域(见 vite.config.js:/intelligent-api -> 8.140.26.4:10029)
url: '/intelligent-api/report-domain-view-analysis',
data
}) })
} }
...@@ -152,15 +199,31 @@ export function getThinkPolicyIndustryChange(params) { ...@@ -152,15 +199,31 @@ export function getThinkPolicyIndustryChange(params) {
}) })
} }
//获取智库政策 /**
* 获取智库政策(政策追踪列表)
* GET /api/thinkTankInfo/policy
* Query: thinkTankId, startDate, endDate, orgIds, domainIds(科技领域/智库领域,逗号分隔 id), pageNum, pageSize, sortField, sortOrder, sortFun, reportId 等
*/
export function getThinkPolicy(params) { export function getThinkPolicy(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankInfo/policy/${params.id}/${params.startDate}`, url: '/api/thinkTankInfo/policy',
params params
}) })
} }
/**
* 政府机构字典(政策追踪-涉及部门筛选项)
* GET /api/commonDict/gov/agency
* @returns {Promise<{ code: number, data: Array<{ id: string, name: string }> }>}
*/
export function getGovAgencyDict() {
return request({
method: 'GET',
url: '/api/commonDict/gov/agency'
})
}
//智库百科基本信息 //智库百科基本信息
export function getThinkTankInfoBasic(params) { export function getThinkTankInfoBasic(params) {
return request({ return request({
...@@ -299,9 +362,26 @@ export function getThinkTankReportPolicy(params) { ...@@ -299,9 +362,26 @@ export function getThinkTankReportPolicy(params) {
//获取相关政策动态 //获取相关政策动态
export function getThinkTankReportPolicyAction(params) { export function getThinkTankReportPolicyAction(params) {
const {
reportId,
currentPage,
pageSize,
keyword = "",
orgIds = "",
// 新增:按科技领域 / 标签过滤
industryName = ""
} = params;
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankReport/policyAction/${params}`, url: `/api/thinkTankReport/policyDetail/${reportId}`,
params: {
currentPage,
pageSize,
keyword,
// 后端按标签过滤使用的字段
industryName,
orgIds,
}
}) })
} }
......
...@@ -24,17 +24,19 @@ const props = defineProps({ ...@@ -24,17 +24,19 @@ const props = defineProps({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tip-wrapper{ .tip-wrapper {
width: 100%; width: 100%;
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 22px; height: 22px;
.icon{
.icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
img{
img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
......
...@@ -3,6 +3,8 @@ const thinkTank = () => import('@/views/thinkTank/index.vue') ...@@ -3,6 +3,8 @@ 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 MultiThinkTankViewAnalysis= () => import('@/views/thinkTank/MultiThinkTankViewAnalysis/index.vue')
const thinktankRoutes = [ const thinktankRoutes = [
// 智库系统的主要路由 // 智库系统的主要路由
...@@ -36,9 +38,19 @@ const thinktankRoutes = [ ...@@ -36,9 +38,19 @@ const thinktankRoutes = [
path: "/thinkTank/reportOriginal/:id", path: "/thinkTank/reportOriginal/:id",
name: "ReportOriginal", name: "ReportOriginal",
component: ReportOriginal, component: ReportOriginal,
// meta: {
// title: "报告原文" },
// } {
path: "/thinkTank/allThinkTank",
name: "allThinkTank",
component: allThinkTank,
},
{
path: "/thinkTank/MultiThinkTankViewAnalysis/:id",
name: "MultiThinkTankViewAnalysis",
component: MultiThinkTankViewAnalysis,
}, },
] ]
......
...@@ -9,6 +9,10 @@ const setChart = (option, chartId) => { ...@@ -9,6 +9,10 @@ const setChart = (option, chartId) => {
chartDom.removeAttribute("_echarts_instance_"); chartDom.removeAttribute("_echarts_instance_");
let chart = echarts.init(chartDom); let chart = echarts.init(chartDom);
chart.setOption(option); chart.setOption(option);
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout(() => {
chart.resize();
}, 0);
return chart; return chart;
}; };
......
...@@ -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>
<div class="committee-cards-section"> <div class="committee-cards-section">
...@@ -55,8 +54,8 @@ ...@@ -55,8 +54,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">
...@@ -64,8 +63,7 @@ ...@@ -64,8 +63,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">
...@@ -83,18 +81,15 @@ ...@@ -83,18 +81,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>
...@@ -121,9 +116,8 @@ ...@@ -121,9 +116,8 @@
</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>
......
...@@ -701,6 +701,7 @@ ...@@ -701,6 +701,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";
......
<template>
<div class="analysis-box-wrapper" :style="{ width: width ? width : '100%', height: height ? height : '100%' }">
<div class="wrapper-header">
<div class="header-icon"></div>
<div class="header-title">
<div v-if="title">{{ title }}</div>
<slot v-else name="custom-title"></slot>
</div>
<div class="header-btn" v-if="!showAllBtn">
<slot name="header-btn"></slot>
</div>
<div class="header-btn1" v-else>
<slot name="header-btn"></slot>
</div>
<div class="header-right">
<div class="text-one" :class="{ 'is-active': activeView === 'consensus' }"
@click="handleTabChange('consensus')">
{{ "共识观点" }}
</div>
<div class="text-two" :class="{ 'is-active': activeView === 'divergence' }"
@click="handleTabChange('divergence')">
{{ "分歧观点" }}
</div>
<div class="header-right-btn" @click="handleSave" v-if="showAllBtn">
<img src="@/assets/icons/box-header-icon1.png" alt="">
</div>
<div class="header-right-btn" @click="handleCollect">
<img src="@/assets/icons/box-header-icon3.png" alt="">
</div>
</div>
</div>
<div class="wrapper-main">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { ref, computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
height: {
type: String,
default: ''
},
showAllBtn: {
type: Boolean,
default: true
},
// 当业务功能尚未实现时,点击右上角图标仅弹出统一提示
devTip: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['save', 'download', 'collect', 'tab-change'])
// 共识/分歧:单选,默认共识观点
const activeView = ref('consensus')
const handleTabChange = type => {
activeView.value = type
emit('tab-change', type)
}
const handleSave = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
}
ElMessage.success('保存当前内容')
// emit('save')
}
const handleDownload = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
}
ElMessage.success('下载当前内容')
// emit('download')
}
const handleCollect = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
}
ElMessage.success('收藏当前内容')
// emit('collect')
}
</script>
<style lang="scss" scoped>
.analysis-box-wrapper {
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);
.wrapper-header {
height: 45px;
display: flex;
padding-right: 14px;
align-items: center;
box-sizing: border-box;
.header-icon {
width: 8px;
height: 20px;
background: var(--color-main-active);
border-radius: 0 4px 4px 0;
margin-right: 14px;
}
.header-title {
flex: auto;
width: 20px;
// color: var(--color-main-active);
// font-family: Source Han Sans CN;
// font-size: 20px;
// font-weight: 700;
// line-height: 26px;
// letter-spacing: 0px;
height: 100%;
&>div {
height: 100%;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 20px;
line-height: 45px;
font-weight: 700;
}
}
// .header-btn {
// display: flex;
// justify-content: flex-end;
// gap: 8px;
// }
// .header-btn1 {
// position: absolute;
// top: 14px;
// right: 116px;
// }
.header-right {
height: 28px;
display: flex;
justify-content: flex-end;
gap: 4px;
.text-one {
border: 1px solid rgb(230, 231, 232);
width: 88px;
height: 32px;
border-radius: 4px;
background-color: rgb(255, 255, 255);
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.is-active {
background-color: rgb(246, 250, 255);
color: rgb(5, 95, 194);
font-weight: 700;
border: 1px solid rgb(5, 95, 194);
}
}
.text-two {
border: 1px solid rgb(230, 231, 232);
width: 88px;
height: 32px;
border-radius: 4px;
background-color: rgb(255, 255, 255);
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.is-active {
background-color: rgb(246, 250, 255);
color: rgb(5, 95, 194);
font-weight: 700;
border: 1px solid rgb(5, 95, 194);
}
}
.header-right-btn {
width: 28px;
height: 28px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
}
.wrapper-main {
height: calc(100% - 45px);
overflow: hidden;
// overflow-y: auto;
padding: 5px auto;
}
}
</style>
<template>
<div class="overview-main-box-wrapper"
:style="{ width: width ? width : '1064px', height: height ? height : '450px' }">
<div class="overview-main-box-header">
<div class="header-left">
<div class="header-icon">
<slot name="header-icon"></slot>
</div>
<div class="header-title">{{ title }}</div>
</div>
<div class="header-right" @click="handleClickToDetail()">
{{ "查看详情 >" }}
</div>
</div>
<div class="wrapper-main">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const emit = defineEmits(['toDetail'])
const props = defineProps({
title: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
height: {
type: String,
default: ''
}
})
const handleClickToDetail = () => {
emit('toDetail')
}
</script>
<style lang="scss" scoped>
.overview-main-box-wrapper {
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);
position: relative;
.overview-main-box-header {
height: 48px;
border-bottom: 1px solid rgba(240, 242, 244, 1);
display: flex;
justify-content: space-between;
box-sizing: border-box;
.header-left {
display: flex;
.header-icon {
width: 24px;
height: 24px;
margin-top: 12px;
margin-left: 17px;
}
.header-title {
margin-left: 19px;
height: 48px;
padding: 0 16px;
background: var(--color-main-active);
color: #fff;
font-family: Source Han Sans CN;
font-size: 20px;
font-weight: 700;
line-height: 48px;
text-align: center;
}
}
.header-right {
margin-right: 27px;
margin-top: 12px;
height: 24px;
color: rgba(20, 89, 187, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
cursor: pointer;
}
}
.wrapper-main {
height: calc(100% - 48px);
overflow: hidden;
// position: relative;
}
}
</style>
<template>
<div class="overview-normal-box-wrapper"
:style="{ width: width ? width : '1064px', height: height ? height : '460px' }">
<div class="overview-normal-box-header">
<div class="header-left">
<div class="header-icon">
<slot name="header-icon"></slot>
</div>
<div class="header-title">{{ title }}</div>
</div>
<div class="header-right">
<slot name="header-right"></slot>
</div>
</div>
<div class="wrapper-main">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
height: {
type: String,
default: ''
}
})
</script>
<style lang="scss" scoped>
.overview-normal-box-wrapper {
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);
position: relative;
.overview-normal-box-header {
height: 48px;
border-bottom: 1px solid rgba(240, 242, 244, 1);
display: flex;
justify-content: space-between;
box-sizing: border-box;
.header-left {
display: flex;
.header-icon {
width: 24px;
height: 24px;
margin-top: 14px;
margin-left: 19px;
}
.header-title {
margin-left: 17px;
height: 48px;
color: var(--color-main-active);
font-family: Source Han Sans CN;
font-size: 20px;
font-weight: 700;
line-height: 48px;
text-align: center;
}
}
.header-right {
height: 48px;
margin-right: 28px;
}
}
.wrapper-main {
height: calc(100% - 48px);
overflow: hidden;
}
}
</style>
<template>
<div class="wrap">
<div class="header">
<div class="header-content">
<div class="header-left">
{{ "多智库报告观点汇聚分析" }}
</div>
<div class="header-right">
</div>
</div>
</div>
<div class="box">
<div class="box1">
<AnalysisBox title="选择智库报告" :showAllBtn="true">
<div class="box1-main" v-if="isChoseItem">
<div class="box1-header">
<div class="header-select">
<div class="select-Domain">
<el-select v-model="selectedAreaList" placeholder="全部领域" :teleported="true"
:placement="'bottom-start'" :popper-options="{
modifiers: [
{
name: 'preventOverflow', // 禁用自动翻转逻辑
options: {
mainAxis: false, // 禁用垂直方向的自动调整
altAxis: false, // 禁用水平方向的自动调整
}
},
{
name: 'flip', // 完全禁用翻转功能
enabled: false
}
]
}" @change="handleCheckedAreaChange">
<el-option label="全部领域" :value="''" />
<el-option v-for="item in areaList" :key="item.id" :label="item.name" :value="item.id">
{{ item.name }}
</el-option>
</el-select>
</div>
<div class="search-box">
<el-input placeholder="搜索智库报告" v-model="searchPolicy" @keyup.enter="handleSearchPolicy">
<template #suffix>
<img src="../assets/images/Line_Search.png" class="search-icon" alt="搜索"
@click="handleSearchPolicy">
</template>
</el-input>
</div>
</div>
<div class="footer-select">
<div class="select-thinkTank">
<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 }
]
}">
<el-option label="全部智库" :value="''" />
<el-option label="兰德公司" :value="'1'" />
</el-select>
</div>
<div class="select-time">
<el-select v-model="selectedYears" placeholder="近一年" :teleported="true" :placement="'bottom-start'"
:popper-options="{
modifiers: [
{
name: 'preventOverflow', // 禁用自动翻转逻辑
options: {
mainAxis: false, // 禁用垂直方向的自动调整
altAxis: false, // 禁用水平方向的自动调整
}
},
{
name: 'flip', // 完全禁用翻转功能
enabled: false
}
]
}" @change="handleTimeChange">
<el-option v-for="item in yearsOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="footer-text">{{ total }}{{ "篇报告" }}</div>
</div>
</div>
<div class="box1-middle">
<div class="box1-item" v-for="(item, index) in curFooterList" :key="item.id">
<div class="left">
<img :src=item.imageUrl alt="">
</div>
<div class="right">
<div class="right-header"> {{ item.name }}</div>
<div class="right-footer">
<div class="time">{{ item.times }}</div>
<div class="text-image">
<div class="image">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="text">
{{ item.thinkTankName }}
</div>
</div>
</div>
</div>
<div class="image-change">
<img src="../assets/images/plus.png" alt="" v-if="!selectedIds.has(item.id)"
@click="toggleSelected(item.id, item)" />
<img src="../assets/images/Minus.png" alt="" v-else @click="toggleSelected(item.id, item)" />
</div>
</div>
</div>
<div class="page-box">
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :page-count="pageCount" background
layout="prev, pager, next" :total="total" @current-change="handleCurrentChange" />
</div>
<div class="box1-footer" :class="{ 'is-disabled': !canProceed }" @click="handleChoseItem">
<div class="text">{{ "已选择" }}{{ selectedIds.size }}{{ "篇报告" }}</div>
<div class="image"><img src="../assets/images/right-arrow.png" alt="" /></div>
</div>
</div>
<div class="box1-main-analysis" v-if="!isChoseItem">
<div class="box1-header-analysis">
<div class="left-text-analysis" @click="handleBack">{{ "< 返回选择" }}</div>
<div class="right-text-analysis">{{ "共选择" }}{{ selectedReportList.length }}{{ "篇智库报告" }}</div>
</div>
<div class="box1-middle-analysis">
<div class="box1-item" v-for="(item, index) in selectedReportList" :key="item.id || index">
<div class="left">
<img :src=item.imageUrl alt="">
</div>
<div class="right">
<div class="right-header"> {{ item.name }}</div>
<div class="right-footer">
<div class="time">{{ item.times }}</div>
<div class="text-image">
<div class="image">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="text">
{{ item.thinkTankName }}
</div>
</div>
</div>
</div>
<div class="image-change">
<img src="../assets/images/plus.png" alt="" v-if="!selectedIds.has(item.id)"
@click="toggleSelected(item.id, item)" />
<img src="../assets/images/Minus.png" alt="" v-else @click="toggleSelected(item.id, item)" />
</div>
</div>
</div>
<div class="box1-footer-analysis" :class="{ 'is-disabled': !canProceed }" @click="handleAnalysis">
<div class="text-analysis">{{ "开始分析" }}</div>
<div class="image-analysis"><img src="../assets/images/right-arrow-white.png" alt="" /></div>
</div>
</div>
</AnalysisBox>
</div>
<div class="box2">
<AnalysisBox title="共识观点列表" :showAllBtn="true" v-if="isBox2">
<div class="box2-main">
<div class="empty-image">
<img src="../assets/images/empty-image.png" alt="" />
</div>
</div>
</AnalysisBox>
<AnalysisResultBox title="核心观点分析" :showAllBtn="false" v-if="!isBox2" @tab-change="handleOpinionTabChange">
<div class="box2-main-Consensus" v-if="activeOpinionTab === 'consensus'">
<div class="box2-main-Consensus-header">
<div class="tech-development-view">{{ "科技发展观点 1条" }}</div>
<div class="criticism-view">{{ "对我打压观点 1条" }}</div>
<div class="thinkTank-view">
<div class="thinkTank-image">
<img src="../assets/images/rand-image.png" alt="" />
</div>
{{ "兰德公司 2条" }}
</div>
</div>
<div class="box2-main-Consensus-content">
<template v-for="(item, index) in mockConsensusList" :key="item.id">
<div class="Consensus-item">
<div class="Consensus-item-number">{{ index + 1 }}</div>
<div class="Consensus-item-title">{{ item.title }}</div>
<div class="Consensus-item-right">{{ `${item.reportCount}篇报告提及` }}</div>
<div class="Consensus-item-image" @click="toggleConsensusItem(item.id)">
<img src="../assets/images/down.png" alt="" v-if="!openConsensusIds.has(item.id)" />
<img src="../assets/images/up.png" alt="" v-else />
</div>
</div>
<div class="Consensus-expand" v-if="openConsensusIds.has(item.id)">
<div class="Consensus-expand-row">
<div class="Consensus-expand-title">
{{ item.childrenTitle }}
</div>
<div class="Consensus-expand-subtitle">
<div class="Consensus-expand-image">
<img src="../assets/images/rand-image.png" alt="" />
</div>
{{ item.thinkTankName }}
</div>
</div>
<div class="Consensus-expand-content">
{{ item.childrenContent }}
</div>
<div class="Consensus-expand-badge"
:class="{ 'is-tech-development': item.view === '科技发展观点', 'is-criticism': item.view === '对我打压观点' }">
{{ item.view }}
</div>
</div>
</template>
</div>
</div>
<div class="box2-main-Differences" v-else>
<div class="box2-main-Differences-content">
<template v-for="(item, index) in mockDifferenceList" :key="item.id">
<div class="Differences-item">
<div class="Differences-item-number">{{ index + 1 }}</div>
<div class="Differences-item-title">
<div class="Differences-item-title-left"> {{ item.divergent_views[0].view }}</div>
<div class="Differences-item-title-right">{{ item.divergent_views[1].view }}</div>
</div>
<div class="Differences-item-image" @click="toggleDifferencesItem(item.id)">
<img src="../assets/images/down.png" alt="" v-if="!openDifferencesIds.has(item.id)" />
<img src="../assets/images/up.png" alt="" v-else />
</div>
</div>
<div class="Differences-expand" v-if="openDifferencesIds.has(item.id)">
<div class="Differences-expand-content">
<div class="Differences-expand-top-content">
<div class="Differences-expand-top-content-left">
<div class="content-left-title">{{ item.divergent_views[0].Consensus.title }}</div>
<div class="content-left-img-name">
<div class="content-left-img">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="content-left-name">
{{ item.divergent_views[0].Consensus.institution }}
</div>
</div>
<div class="content-left-text">{{ item.divergent_views[0].Consensus.quote }}</div>
</div>
<div class="Differences-expand-top-content-right">
<div class="content-right-title">{{ item.divergent_views[1].Differences.title }}</div>
<div class="content-right-img-name">
<div class="content-right-img">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="content-right-name">
{{ item.divergent_views[1].Differences.institution }}
</div>
</div>
<div class="content-right-text">{{ item.divergent_views[1].Differences.quote }}</div>
</div>
</div>
<div class="Differences-expand-view">
<div class="Differences-expand-view-left">
<div class="left-tag">{{ item.divergent_views[0].Consensus.tag }}</div>
</div>
<div class="Differences-expand-view-right">
<div class="right-tag">{{ item.divergent_views[1].Differences.tag }}</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</AnalysisResultBox>
</div>
</div>
</div>
</template>
<script setup>
import router from '@/router';
import { onMounted, ref, computed, reactive, nextTick } from "vue";
import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue"
import AnalysisResultBox from "./boxBackground/analysisBox.vue"
import { getThinkTankReport, getHylyList, getThinkDynamicsReport, postReportDomainViewAnalysis } from "@/api/thinkTank/overview";
const sort = ref("");
const searchPolicy = ref("");
const isBox2 = ref(true)
const activeOpinionTab = ref('consensus')
const handleOpinionTabChange = type => {
activeOpinionTab.value = type
}
const mockConsensusList = ref([
{
id: 'consensus-1',
title: '应加强美国本土芯片制造能力,减少对亚洲供应链的依赖。',
reportCount: 1,
childrenTitle: "《中美冲突、竞争与合作的动因:通用人工智能(AGI)诱发的五大国家安全难题》",
thinkTankName: "兰德公司",
childrenContent: "虽然表面上与西方私营企业相似,但中国私营企业对政府政策的约束更紧密,且更易获得政府资源。这些概念差异意味着试图改变中国经济将非常困难,中国私营企业可能需要区别于西方企业。",
view: "科技发展观点"
},
{
id: 'consensus-2',
title: '人工智能领域核心风险来源于美国技术领先地位的丧失。',
reportCount: 1,
childrenTitle: "《人工智能将在未来五年如何影响中美关系?》",
thinkTankName: "兰德公司",
childrenContent: "人工智能已成为大国竞争的核心领域,美国若在算法研发、算力基础设施、数据资源等方面失去领先优势,将直接影响军事优势、经济竞争力与全球话语权。未来五年是技术格局定型的关键期,保持技术代差是维护美国国家安全的重要前提。",
view: "对我打压观点"
},
])
const mockDifferenceList = ref([
{
"id": 1,
"divergent_views": [
{
"view": "人工智能领域核心风险来源于美国技术领先地位的丧失",
"Consensus": {
"institution": "兰德科技智库",
"title": "《中美冲突、竞争与合作的动因:通用人工智能(AGI)诱导…》",
"quote": "联邦政府应优先考虑如何防止美国技术领先地位的丧失,并将其列为最根本的战略风险。",
"tag": "对我打压观点"
}
},
{
"view": "人工智能领域核心风险来源于AI技术本身固有的安全难题",
"Differences": {
"institution": "布鲁金斯学会",
"title": "《人工智能将在未来五年如何影响中美关系?》",
"quote": "目前人工智能领域最大的风险源于AI技术本身固有的安全难题(如智能体失控)以及中美在AGI竞赛中因误判而引发冲突的不稳定性。因此,联邦政府应该通过构建合作机制与规范来管理风险。",
"tag": "科技发展观点"
}
}
]
}
])
const openConsensusIds = ref(new Set())
const toggleConsensusItem = id => {
const set = new Set(openConsensusIds.value)
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
openConsensusIds.value = set
}
const openDifferencesIds = ref(new Set())
const toggleDifferencesItem = id => {
const set = new Set(openDifferencesIds.value)
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
openDifferencesIds.value = set
}
const domainName = computed(() => {
const id = selectedAreaList.value
if (!id) return '全部领域'
const hit = Array.isArray(areaList.value) ? areaList.value.find(i => String(i.id) === String(id)) : null
return hit?.name || '全部领域'
})
const domainViewAnalysisRes = ref(null)
// 近N年发布(用于 startDate)
const selectedYears = ref(5);
const yearsOptions = [
{ label: "近一年", value: 1 },
{ label: "近两年", value: 2 },
{ label: "近三年", value: 3 },
{ label: "近四年", value: 4 },
{ label: "近五年", value: 5 },
];
// 单选:选中领域 id;空字符串表示“全部领域”
const selectedAreaList = ref('');
const selectedPubTimeList = ref([""]);
const currentPage = ref(1);
const curFooterList = ref([
]);
const isChoseItem = ref(true)
const areaList = ref([
// {
// id: "全部领域",
// name: "全部领域"
// },
// "通信网络",
// "量子科技",
// "能源"
]);
const handleChoseItem = () => {
if (!canProceed.value) return
isChoseItem.value = false
}
const handleAnalysis = () => {
if (!canProceed.value) return
isBox2.value = false
// 每次进入“开始分析”默认回到共识观点,避免上次状态残留导致内容错位
activeOpinionTab.value = 'consensus'
handlePostReportDomainViewAnalysis()
}
const handleBack = () => {
isChoseItem.value = true
isBox2.value = true
// 返回选择时也重置,确保下次进入分析展示一致
activeOpinionTab.value = 'consensus'
}
const pageSize = 10;
const total = ref(0);
// total 为 0 时也固定显示 1 页
const pageCount = computed(() => {
const t = Number(total.value || 0);
return t > 0 ? Math.ceil(t / pageSize) : 1;
});
// 已选择的报告 id(每条 item 独立控制 plus/minus,支持多条同时变化)
const selectedIds = ref(new Set());
const selectedReportMap = ref(new Map());
const selectedReportList = computed(() => Array.from(selectedReportMap.value.values()));
// 选择/开始分析都要求至少选择 2 篇报告
const canProceed = computed(() => selectedIds.value.size >= 2)
const toggleSelected = (id, item) => {
const set = selectedIds.value;
const map = selectedReportMap.value;
if (set.has(id)) {
set.delete(id);
map.delete(id);
} else {
set.add(id);
if (item) {
map.set(id, item);
}
}
selectedIds.value = new Set(set); // 触发响应式更新
selectedReportMap.value = new Map(map); // 触发响应式更新
};
const normalizeViewList = raw => {
const list = Array.isArray(raw) ? raw : []
return list
.filter(v => v && (v.view_id || v.viewId || v.id))
.map(v => ({
view_id: v.view_id ?? v.viewId ?? v.id,
view_text: v.view_text ?? v.viewText ?? v.text ?? ''
}))
}
const handlePostReportDomainViewAnalysis = async () => {
try {
// 临时:先按接口文档示例写死,确保链路打通
const payload = {
domain: '人工智能',
report_view_list: [
{
report_id: 'Rand_RRA2877-1',
view_list: [
{
view_id: 35959,
view_text:
"We assess that metaverse technologies—that is, AI systems, immersive technologies, and enabling digital technologies—will continue to be integrated across new markets and create new demands for immersive experiences. We anticipate this will drive an increased TAV. We also assess that the metaverse concept is at an inflection point. As a result, uncertainties exist in assessing how the metaverse will expand, how large it will become, and whether it will become an expansive virtual world that directly competes with the physical world. We do assess that by the end of our study’s ten-year horizon, the conceptualization of this internet-like, immersive environment with virtual spaces will probably feel functionally more like an enhanced version of the IoT today than the metaverse that some have aspirationally described."
}
]
},
{
report_id: 'Rand_RRA3124-1',
view_list: [
{
view_id: 35826,
view_text:
"But even successful generalization to other present-day models cannot guarantee that the method will generalize well to future models, which may differ from the models of today in unexpected ways"
}
]
}
]
}
const res = await postReportDomainViewAnalysis(payload)
console.log('智库领域观点分析接口返回', res)
domainViewAnalysisRes.value = res
} catch (e) {
console.error('智库领域观点分析接口调用失败', e)
domainViewAnalysisRes.value = null
}
}
function arrayToString(value) {
// 支持 array / string / null
if (Array.isArray(value)) {
return value.reduce((acc, item) => {
if (item !== null && item !== undefined && item !== "") {
return acc === "" ? item : acc + "," + item;
}
return acc;
}, "");
}
if (value === null || value === undefined || value === "") return "";
return String(value);
}
const handleCheckedAreaChange = () => {
// 领域筛选变化时,从第一页重新加载
console.log(selectedAreaList.value, "当前选中的领域");
currentPage.value = 1;
handleGetetThinkTankReport(1);
};
const handleSearchPolicy = () => {
currentPage.value = 1;
handleGetetThinkTankReport(1);
};
const handleTimeChange = () => {
currentPage.value = 1;
handleGetetThinkTankReport(1);
};
const handleCurrentChange = page => {
console.log(page, "pagepagepage");
currentPage.value = page;
handleGetetThinkTankReport(page);
};
//获取行业领域字典
// getHylyList
const handleGetHylyList = async () => {
try {
const res = await getHylyList();
console.log("行业领域字典", res);
if (res.code === 200 && res.data) {
areaList.value = res.data;
}
} catch (error) {
console.error("获取行业领域字典error", error);
}
};
//获取智库报告
const handleGetetThinkTankReport = async (page = currentPage.value) => {
const id = router.currentRoute?.value?.params?.id || "";
const getDateYearsAgo = years => {
const d = new Date();
d.setFullYear(d.getFullYear() - Number(years || 1));
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
};
const params = {
id,
startDate: getDateYearsAgo(selectedYears.value),
// 不传 authorName(避免后端按空字符串筛选)
currentPage: Number(page) - 1, // 1-based
pageSize: pageSize,
researchTypeIds: arrayToString(selectedAreaList.value),
searchText: (searchPolicy.value || "").trim(),
};
try {
// 先清空,避免视觉上看起来没变化
curFooterList.value = [];
const res = await getThinkDynamicsReport(params);
console.log("智库报告", res);
if (res.code === 200 && res.data) {
curFooterList.value = res.data.content;
total.value = res.data.totalElements;
} else {
// 无数据:清空列表、总数置 0、分页回到 1
curFooterList.value = [];
total.value = 0;
currentPage.value = 1;
}
} catch (error) {
console.error("获取智库报告error", error);
curFooterList.value = [];
total.value = 0;
currentPage.value = 1;
}
};
onMounted(async () => {
handleGetetThinkTankReport();
handleGetHylyList()
});
</script>
<style lang="scss" scoped>
.wrap {
display: flex;
flex-direction: column;
align-items: center;
.header {
height: 64px;
width: 100%;
background-color: rgb(255, 255, 255);
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
border-bottom: 1px solid #EAECEE;
display: flex;
flex-direction: column;
align-items: center;
.header-content {
width: 1600px;
height: 64px;
justify-content: space-between;
display: flex;
.header-left {
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 20px;
line-height: 26px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
margin-top: 18px;
}
}
}
.box {
display: flex;
gap: 16px;
margin-top: 16px;
width: 1600px;
height: 1094px;
.box1 {
width: 480px;
height: 920px;
display: flex;
.box1-main {
height: 866px;
width: 100%;
padding-left: 23px;
padding-bottom: 18px;
padding-right: 22px;
display: flex;
flex-direction: column;
.box1-header {
width: 435px;
height: 68px;
margin-top: 2px;
display: flex;
flex-direction: column;
gap: 12px;
.header-select {
width: 435px;
height: 32px;
display: flex;
gap: 8px;
.select-Domain {
width: 150px;
height: 32px;
:deep(.el-select__placeholder) {
color: rgb(95, 101, 108) !important;
}
}
.search-box {
width: 277px;
height: 32px;
border: 1px solid rgb(230, 231, 232);
border-radius: 4px;
.search-icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
}
}
.footer-select {
width: 435px;
height: 24px;
display: flex;
.select-thinkTank {
width: 107px;
height: 24px;
:deep(.el-input__wrapper) {
box-shadow: none !important;
border: none !important;
outline: none !important;
}
:deep(.el-input__wrapper.is-focus) {
box-shadow: none !important;
border: none !important;
outline: none !important;
}
:deep(.el-select__wrapper) {
box-shadow: none !important;
border: none !important;
outline: none !important;
height: 24px !important;
line-height: 24px !important;
min-height: 24px !important;
}
:deep(.el-input__inner) {
height: 24px !important;
line-height: 24px !important;
min-height: 24px !important;
/* 文字垂直居中 */
}
:deep(.el-select__placeholder) {
color: rgb(95, 101, 108) !important;
}
}
.select-time {
width: 97px;
height: 24px;
:deep(.el-input__wrapper) {
box-shadow: none !important;
border: none !important;
outline: none !important;
}
:deep(.el-input__wrapper.is-focus) {
box-shadow: none !important;
border: none !important;
outline: none !important;
}
:deep(.el-select__wrapper) {
box-shadow: none !important;
border: none !important;
outline: none !important;
height: 24px !important;
line-height: 24px !important;
min-height: 24px !important;
}
:deep(.el-select__placeholder) {
color: rgb(95, 101, 108) !important;
}
}
.footer-text {
height: 24px;
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
margin-left: auto;
}
:deep(.el-input__wrapper) {
box-shadow: none !important;
border: none !important;
}
}
}
.box1-middle {
width: 435px;
height: 656px;
margin-top: 12px;
display: flex;
flex-direction: column;
overflow-y: auto;
/* 垂直方向自动滚动条 */
overflow-x: hidden;
/* 禁止横向滚动 */
.box1-item {
height: 100px;
width: 435px;
border-top: 1px solid rgb(234, 236, 238);
border-bottom: 1px solid rgb(234, 236, 238);
flex-shrink: 0;
display: flex;
/* 防止item被挤压变形 */
.left {
width: 54px;
height: 72px;
margin-top: 14px;
margin-left: 9px;
img {
width: 100%;
height: 100%;
}
}
.right {
width: 320px;
height: 72px;
margin-top: 12px;
margin-left: 8px;
display: flex;
flex-direction: column;
.right-header {
width: 320px;
height: 48px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 700;
/* Bold */
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
/* 两端对齐 */
display: -webkit-box;
-webkit-line-clamp: 2;
/* 限制 2 行 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.right-footer {
width: 320px;
height: 22px;
margin-top: 4px;
display: flex;
justify-content: space-between;
.time {
height: 22px;
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular */
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
/* 左对齐 */
color: rgb(132, 136, 142);
}
.text-image {
height: 22px;
display: flex;
.image {
width: 16px;
height: 16px;
margin-top: 3px;
margin-right: 4px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 22px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
}
}
}
}
.image-change {
width: 16px;
height: 16px;
margin-top: 17px;
margin-left: 14px;
img {
width: 100%;
height: 100%;
}
}
}
}
.page-box {
margin-top: 21px;
align-items: center;
justify-content: center;
display: flex;
}
.box1-footer {
width: 435px;
height: 36px;
margin-top: 21px;
border-radius: 6px;
border: 1px solid #EAECEE;
display: flex;
cursor: pointer;
.text {
color: rgb(59, 65, 75);
font-family: "Microsoft YaHei";
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0px;
margin-top: 7px;
margin-left: 154px;
}
.image {
width: 13px;
height: 8px;
margin-top: 14px;
margin-left: 8px;
display: inline-block;
img {
width: 100%;
height: 100%;
vertical-align: top;
}
}
&.is-disabled {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}
}
}
.box1-main-analysis {
height: 866px;
width: 100%;
padding-left: 23px;
padding-bottom: 18px;
padding-right: 22px;
display: flex;
flex-direction: column;
.box1-header-analysis {
width: 435px;
height: 24px;
margin-top: 1px;
display: flex;
display: flex;
justify-content: space-between;
.left-text-analysis {
height: 24px;
font-family: "Source Han Sans CN";
font-weight: 400;
/* 字重:Regular */
font-size: 16px;
/* 字号 */
line-height: 24px;
/* 行高 */
letter-spacing: 0px;
/* 字间距 */
text-align: left;
/* 左对齐 */
color: rgb(59, 65, 75);
cursor: pointer;
}
.right-text-analysis {
height: 24px;
font-family: "Source Han Sans CN";
font-weight: 400;
/* 字重:Regular */
font-size: 16px;
/* 字号 */
line-height: 24px;
/* 行高 */
letter-spacing: 0px;
/* 字间距 */
text-align: left;
/* 左对齐 */
color: rgb(95, 101, 108);
}
}
.box1-middle-analysis {
margin-top: 12px;
width: 435px;
height: 757px;
flex-direction: column;
overflow-y: auto;
/* 垂直方向自动滚动条 */
overflow-x: hidden;
/* 禁止横向滚动 */
.box1-item {
height: 100px;
width: 435px;
border-top: 1px solid rgb(234, 236, 238);
border-bottom: 1px solid rgb(234, 236, 238);
flex-shrink: 0;
display: flex;
/* 防止item被挤压变形 */
.left {
width: 54px;
height: 72px;
margin-top: 14px;
margin-left: 9px;
img {
width: 100%;
height: 100%;
}
}
.right {
width: 320px;
height: 72px;
margin-top: 12px;
margin-left: 8px;
display: flex;
flex-direction: column;
.right-header {
width: 320px;
height: 48px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 700;
/* Bold */
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
/* 两端对齐 */
display: -webkit-box;
-webkit-line-clamp: 2;
/* 限制 2 行 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.right-footer {
width: 320px;
height: 22px;
margin-top: 4px;
display: flex;
justify-content: space-between;
.time {
height: 22px;
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular */
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
/* 左对齐 */
color: rgb(132, 136, 142);
}
.text-image {
height: 22px;
display: flex;
.image {
width: 16px;
height: 16px;
margin-top: 3px;
margin-right: 4px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 22px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
}
}
}
}
.image-change {
width: 16px;
height: 16px;
margin-top: 17px;
margin-left: 14px;
img {
width: 100%;
height: 100%;
}
}
}
}
.box1-footer-analysis {
width: 435px;
height: 36px;
margin-top: 18px;
border-radius: 6px;
background-color: rgb(5, 95, 194);
display: flex;
cursor: pointer;
.text-analysis {
color: rgb(255, 255, 255);
font-family: "Microsoft YaHei";
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0px;
margin-top: 7px;
margin-left: 175.5px;
}
.image-analysis {
width: 13px;
height: 8px;
margin-top: 14px;
margin-left: 8px;
display: inline-block;
img {
width: 100%;
height: 100%;
vertical-align: top;
}
}
&.is-disabled {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}
}
}
}
.box2 {
width: 1104px;
height: 920px;
.box2-main {
height: 866px;
width: 100%;
display: flex;
justify-content: center;
.empty-image {
width: 289px;
height: 215px;
margin-top: 276px;
justify-content: center;
display: flex;
img {
width: 100%;
height: 100%;
}
}
}
.box2-main-Consensus {
height: 866px;
width: 100%;
display: flex;
padding-top: 4px;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 18px;
display: flex;
flex-direction: column;
.box2-main-Consensus-header {
width: 1056px;
height: 34px;
display: flex;
flex-direction: row;
gap: 12px;
.tech-development-view {
height: 34px;
padding: 2px 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(231, 243, 255);
border-radius: 20px;
color: rgb(5, 95, 194);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
/* 两端对齐 */
}
.criticism-view {
height: 34px;
padding: 2px 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(250, 237, 237);
border-radius: 20px;
color: rgb(206, 79, 81);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
/* 两端对齐 */
}
.thinkTank-view {
height: 34px;
padding: 2px 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(255, 255, 255);
border: 1px solid rgb(234, 236, 238);
border-radius: 20px;
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
.thinkTank-image {
width: 16px;
height: 16px;
margin-right: 10px;
margin-bottom: 0px;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
/* 两端对齐 */
}
.box2-main-Consensus-content {
width: 1056px;
overflow-y: auto;
margin-top: 16px;
height: 794px;
display: flex;
flex-direction: column;
overflow-x: hidden;
.Consensus-item {
width: 1056px;
min-height: 62px;
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: flex-start;
border-top: 1px solid rgb(234, 236, 238);
padding: 16px 15px;
}
.Consensus-item:last-child {
border-bottom: 1px solid rgb(234, 236, 238);
}
.Consensus-item:has(+ .Consensus-expand) {
border-bottom: 1px solid rgb(234, 236, 238);
margin-bottom: 0;
}
/* 展开时:只让“上方标题行”字重变粗 */
.Consensus-item:has(+ .Consensus-expand) .Consensus-item-title {
font-weight: 700;
}
.Consensus-item-number {
flex: 0 0 24px;
width: 24px;
height: 24px;
background-color: rgb(231, 243, 255);
color: rgb(5, 95, 194);
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: center;
margin-top: 3px;
}
.Consensus-item-title {
flex: 1;
min-width: 0;
min-height: 30px;
margin-left: 18px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
color: rgb(59, 65, 75);
}
.Consensus-item-right {
flex: 0 0 auto;
width: 89px;
height: 30px;
margin-left: 18px;
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
}
.Consensus-item-image {
flex: 0 0 auto;
width: 14px;
height: 24px;
margin-left: 18px;
img {
width: 100%;
height: 100%;
display: block;
}
}
.Consensus-expand {
width: 1056px;
box-sizing: border-box;
border-top: none;
padding: 16px 50px 24px 58px;
}
.Consensus-expand:last-child {
border-bottom: 1px solid rgb(234, 236, 238);
}
.Consensus-expand-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.Consensus-expand-image {
width: 16px;
height: 16px;
margin-top: 3px;
margin-right: 4px;
img {
width: 100%;
height: 100%;
display: block;
}
}
.Consensus-expand-badge {
margin-top: 24px;
width: 112px;
height: 28px;
padding: 2px 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.Consensus-expand-badge.is-tech-development {
background-color: rgb(231, 243, 255);
color: rgb(5, 95, 194);
}
.Consensus-expand-badge.is-criticism {
background-color: rgb(250, 237, 237);
color: rgb(206, 79, 81);
}
.Consensus-expand-title {
width: 834px;
height: 30px;
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
}
.Consensus-expand-subtitle {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
color: rgb(95, 101, 108);
display: flex;
}
.Consensus-expand-content {
margin-top: 4px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
color: rgb(95, 101, 108);
padding-bottom: 16px;
border-bottom: 1px solid rgb(234, 236, 238);
}
}
}
.box2-main-Differences {
height: 866px;
width: 100%;
display: flex;
padding-top: 4px;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 18px;
display: flex;
flex-direction: column;
.box2-main-Differences-content {
width: 1056px;
overflow-y: auto;
display: flex;
flex-direction: column;
overflow-x: hidden;
.Differences-item {
width: 1056px;
min-height: 62px;
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: flex-start;
border-top: 1px solid rgb(234, 236, 238);
padding-bottom: 16px;
padding-top: 16px;
padding-left: 15px;
padding-right: 9px;
}
.Differences-item:last-child {
border-bottom: 1px solid rgb(234, 236, 238);
}
.Differences-item:has(+ .Differences-expand) {
border-bottom: 1px solid rgb(234, 236, 238);
margin-bottom: 0;
}
.Differences-item-number {
flex: 0 0 24px;
width: 24px;
height: 24px;
background-color: rgb(231, 243, 255);
color: rgb(5, 95, 194);
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: center;
margin-top: 3px;
}
.Differences-item-title {
width: 958px;
flex: 1;
min-width: 0;
min-height: 30px;
margin-left: 18px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
color: rgb(59, 65, 75);
display: flex;
gap: 36px
}
.Differences-item:has(+ .Differences-expand) .Differences-item-title {
font-weight: 700;
}
.Differences-item-title-right {
min-height: 30px;
width: 461px;
}
.Differences-item-title-left {
min-height: 30px;
width: 461px;
}
.Differences-item-right {
flex: 0 0 auto;
width: 89px;
height: 30px;
margin-left: 18px;
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
}
.Differences-item-image {
flex: 0 0 auto;
width: 14px;
height: 24px;
margin-left: 18px;
img {
width: 100%;
height: 100%;
display: block;
}
}
.Differences-expand {
width: 1056px;
box-sizing: border-box;
border-top: none;
padding: 22px 50px 24px 58px;
display: flex;
}
.Differences-expand:last-child {
border-bottom: 1px solid rgb(234, 236, 238);
}
.Differences-expand-content {
width: 948px;
display: flex;
flex-direction: column;
}
.Differences-expand-top-content {
width: 948px;
display: flex;
gap: 44px
}
.Differences-expand-top-content-left {
width: 452px;
display: flex;
flex-direction: column;
}
.content-left-title {
width: 452px;
height: 24px;
display: flex;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-weight: 400 !important;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.content-left-img-name {
width: 452px;
height: 22px;
display: flex;
margin-top: 8px;
}
.content-left-img {
width: 16px;
height: 16px;
margin-top: 3px;
margin-right: 4px;
img {
width: 100%;
height: 100%;
display: block;
}
}
.content-left-name {
height: 22px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
}
.content-left-text {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
color: rgb(95, 101, 108);
margin-top: 8px;
}
.content-right-title {
width: 452px;
height: 24px;
display: flex;
/* 关键:超出宽度显示 ... */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-weight: 400 !important;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.content-right-img-name {
width: 452px;
height: 22px;
display: flex;
margin-top: 8px;
}
.content-right-img {
width: 16px;
height: 16px;
margin-top: 3px;
margin-right: 4px;
img {
width: 100%;
height: 100%;
display: block;
}
}
.content-right-name {
height: 22px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
}
.Differences-expand-top-content-right {
width: 452px;
display: flex;
flex-direction: column;
}
.content-right-text {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
color: rgb(95, 101, 108);
margin-top: 8px;
}
.Differences-expand-view {
width: 948px;
height: 36px;
display: flex;
margin-top: 10px;
gap: 44px
}
.Differences-expand-view-left {
width: 452px;
height: 36px;
}
.left-tag {
width: 112px;
height: 28px;
color: rgb(206, 79, 81);
background-color: rgb(250, 237, 237);
border-radius: 4px;
margin-top: 8px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
display: flex;
align-items: center;
justify-content: center;
}
.Differences-expand-view-right {
width: 452px;
height: 36px;
}
.right-tag {
width: 112px;
height: 28px;
color: rgb(5, 95, 194);
background-color: rgb(231, 243, 255);
border-radius: 4px;
margin-top: 8px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
}
: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
...@@ -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>
......
<template> <template>
<div class="wrap"> <div class="wrap" ref="wrapRef">
<div class="left">
<div class="box1"> <div class="box1">
<AnalysisBox title="政策建议相关情况" :showAllBtn="false"> <AnalysisBox title="政策建议相关情况" :showAllBtn="true">
<div class="box1-main"> <div class="box1-main">
<div class="box1-item" v-for="(item, index) in box1Data" :key="index">
<div class="left"> <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>
<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>
<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>
<div class="file-box"> <div class="item-box">
<div class="file-item" v-for="(vall, idxx) in item.fileList" :key="idxx"> <div class="item" v-for="(item, index) in filteredOpinions" :key="item.id || index"
<div class="file-item-left">法案</div> :class="{ 'item-active': index === activeItemIndex }" @click="
<div class="file-item-center">{{ vall.relationBillsList }}</div> () => {
<div class="file-item-right"> activeItemIndex = index;
<img src="@/assets/images/icon-right-circle.png" alt="" /> 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> </div>
<div class="file-item" v-for="(vall, idxx) in item.fileList" :key="idxx"> <div class="box4-main-footer">
<div class="file-item-left">政令</div> <div class="info">{{ opinionsTotal }}条核心论点</div>
<div class="file-item-center">{{ vall.relationAdList }}</div> <div class="page-box">
<div class="file-item-right"> <el-pagination :page-size="pageSize" background layout="prev, pager, next" :total="opinionsTotal"
<img src="@/assets/images/icon-right-circle.png" alt="" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
</div> </div>
<div class="right" v-if="box1DataItem">
<div class="right-header">
<div class="header-text">
<div class="left-text">
{{ "政策建议详情" }}
</div> </div>
<!-- <div class="right"> <div class="right-text">
<div class="text"> <div class="text">
{{ "查看智库原文" }} {{ "跳转原文" }}
</div> </div>
<div class="icon"> <div class="right-header-image">
<img src="@/assets/icons/open.png" alt="" /> <img src="../images/image-open2.png" alt="" @click="toReport()" />
</div> </div>
</div> -->
</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> </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>
<div class="line" v-if="index !== box2Data.length - 1"></div>
</div> </div>
<div class="box2-item-center">
<div class="title">{{ item.newsTitle }}</div>
<div class="content">{{ item.newsContent }}</div>
</div> </div>
<div class="box2-item-right"> <div class="right-footer-title">
<div class="time">{{ item.newsDate }}</div> <div class="footer-title-text">{{ "相关政策" }}</div>
<div class="img-box"> </div>
<img :src="item.newsImage?item.newsImage:DefaultNewsImg" alt="" /> <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>
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted, computed, nextTick } from "vue";
import { import {
getThinkTankReportPolicy, getThinkTankReportPolicy,
getThinkTankReportPolicyAction getThinkTankReportPolicyAction
...@@ -83,238 +136,185 @@ import { ...@@ -83,238 +136,185 @@ import {
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import DefaultNewsImg from '@/assets/images/default-icon-news.png' import DefaultNewsImg from '@/assets/images/default-icon-news.png'
const router = useRouter(); const router = useRouter();
// 政策建议相关情况 const searchOpinions = ref("");
const box1Data = ref([ // 政策建议相关情况(当前页/当前筛选展示的数据)
{ const box1Data = ref([]);
id: 1, // 全量数据(用于标签统计与前端筛选分页)
title: "示例标题1", const allBox1Data = ref([]);
tagList: ["人才交流", "移民政策"], // 每个标签在全部数据中的出现次数(基于全部数据,非当前页)
fileList: [ const tagCountMap = ref({});
{ // 标签列表(基于当前全量数据 + 搜索条件)
type: "法案", const allTags = ref([]);
name: "2024《重塑美国人口结构法案》" // 当前选中的标签(空字符串表示“全部”)
}, const activeTag = ref("");
{ // 当前选中的条目索引(相对于当前页,默认选中第一个)
type: "法案", const activeItemIndex = ref(0);
name: "2025《开放人才法案》" const pageSize = ref(10);
} // 当前选中 item 的数据
] const box1DataItem = ref(null);
}, // 整个页面容器,用于分页后滚回 wrap 顶部
{ const wrapRef = ref(null);
id: 2, const reportUrl = ref("");
title: "示例标题2",
tagList: ["人才交流", "移民政策"], // 总条数基于当前筛选条件(activeTag)
fileList: [ const opinionsTotal = computed(() => {
{ if (!activeTag.value) {
type: "法案", return allBox1Data.value.length;
name: "2024《重塑美国人口结构法案》" }
}, return allBox1Data.value.filter(item =>
{ (item.domains || []).some(d => d?.industryName === activeTag.value)
type: "法案", ).length;
name: "2025《开放人才法案》" });
} const currentPage = ref(1);
] const handleCurrentChange = page => {
}, currentPage.value = page;
{ activeItemIndex.value = 0;
id: 3, updatePageData();
title: "示例标题3", // 使用页面滚动轴滚动到最顶部
tagList: ["人才交流", "移民政策"], nextTick(() => {
fileList: [ scrollToWrapTop()
{ });
type: "法案", };
name: "2024《重塑美国人口结构法案》" const scrollToWrapTop = () => {
}, const el = wrapRef.value;
{ if (!el) return;
type: "法案",
name: "2025《开放人才法案》" // 向上查找最近的可滚动父元素
} 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;
id: 4, node.scrollTo({ top, behavior: "smooth" });
title: "示例标题4", return;
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
} }
} catch (error) { node = node.parentElement;
console.error("获取政策建议相关情况rror", error);
} }
// 兜底:全局滚动
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({ const route = router.resolve({
path: "/newsAnalysis", name: "ReportOriginal",
query: { params: {
newsId: news.newsId id: router.currentRoute._value.params.id
} }
}); });
window.open(route.href, "_blank"); 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 () => { const handleGetThinkTankReportPolicyAction = async () => {
try { 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); 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) { } 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 () => { onMounted(async () => {
handleGetThinkTankReportPolicy()
handleGetThinkTankReportPolicyAction() handleGetThinkTankReportPolicyAction()
}); });
</script> </script>
...@@ -324,73 +324,51 @@ onMounted(async () => { ...@@ -324,73 +324,51 @@ onMounted(async () => {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 16px; gap: 16px;
height: 100%; height: 1573px;
padding-bottom: 16px; padding-bottom: 16px;
.box-header { .box1 {
width: 100%; margin-top: 19px;
height: 50px; width: 1600px;
display: flex; height: 1173px;
position: relative;
.header-left {
margin-top: 18px;
width: 8px;
height: 20px;
border-radius: 0 4px 4px 0;
background: var(--color-main-active);
}
.title { .box1-main {
margin-left: 14px; margin-top: 8px;
margin-top: 14px; height: 1097px;
height: 26px; padding-left: 21px;
line-height: 26px; padding-right: 50px;
color: var(--color-main-active); padding-bottom: 21px;
font-family: Microsoft YaHei; gap: 55px;
font-size: 20px; display: flex;
font-weight: 700;
}
.header-btn-box { .left {
position: absolute; width: 506px;
top: 15px; gap: 12px;
right: 83px;
display: flex; display: flex;
justify-content: flex-end; flex-direction: column;
gap: 8px;
.btn { .search-box {
height: 28px; display: flex;
padding: 0 8px; width: 100%;
height: 32px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1); border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px; border-radius: 4px;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
color: rgba(59, 65, 75, 1); position: relative;
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 28px;
}
.btnActive {
color: var(--color-main-active);
border: 1px solid var(--color-main-active);
}
}
.header-right { .icon {
width: 16px;
height: 16px;
cursor: pointer;
position: absolute; position: absolute;
top: 14px; right: 8px;
right: 12px; top: 8px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 4px;
.icon {
width: 28px;
height: 28px;
img { img {
width: 100%; width: 100%;
...@@ -398,222 +376,264 @@ onMounted(async () => { ...@@ -398,222 +376,264 @@ onMounted(async () => {
} }
} }
} }
}
.left { .tag-box {
height: 100%; 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 { .tag {
width: 28px;
height: 28px; height: 28px;
border-radius: 14px;
background: rgba(231, 243, 255, 1); border: 1px solid rgb(230, 231, 232);
margin-left: 16px; border-radius: 4px;
margin-top: 16px;
text-align: center; padding: 0px 8px;
line-height: 28px; display: flex;
color: var(--color-main-active); cursor: pointer;
font-family: Microsoft YaHei;
font-size: 14px; &.tag-active {
font-weight: 400; border-color: rgb(5, 95, 194);
letter-spacing: 0px; background-color: rgb(231, 243, 255);
.tag-text {
color: rgb(5, 95, 194);
}
} }
.center {
width: 850px;
margin-left: 18px;
.title { .tag-text {
margin-top: 16px; font-size: 16px;
// height: 24px; line-height: 28px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
line-height: 24px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; 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; display: flex;
margin-top: 7px; gap: 18px;
gap: 8px; 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 { &.item-active {
height: 22px; background-color: rgb(246, 250, 255);
padding: 0 8px; border: 1px solid rgb(185, 220, 255);
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;
letter-spacing: 0px; .item-right {
text-align: left; color: rgb(5, 95, 194);
font-weight: 700;
} }
} }
.file-box { .item-left {
margin-top: 7px; margin-top: 3px;
display: flex; width: 24px;
gap: 8px; height: 27px;
display: inline-flex;
.file-item {
height: 32px;
padding: 0 8px;
border-radius: 4px;
background: rgba(246, 250, 255, 1);
display: flex;
align-items: center; align-items: center;
justify-content: center;
.file-item-left { border-radius: 50%;
height: 22px;
padding: 0 4px;
border-radius: 4px;
background: rgba(231, 243, 255, 1); background: rgba(231, 243, 255, 1);
line-height: 22px;
text-align: center;
color: var(--color-main-active); color: var(--color-main-active);
font-family: Microsoft YaHei; font-family: "Source Han Sans CN";
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; /* 关键:不要被 flex 压缩 */
flex: 0 0 27px;
} }
.file-item-center { .item-right {
margin-left: 12px; flex: 1;
color: var(--color-main-active); width: auto;
font-family: Microsoft YaHei; color: rgba(59, 65, 75, 1);
font-family: "Source Han Sans CN";
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 30px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
} }
.file-item-right { .item-right-solid {
margin-left: 12px; position: absolute;
width: 20px; right: 0;
height: 20px; top: 16px;
bottom: 16px;
img { width: 4px;
width: 100%; background-color: rgb(5, 95, 194);
height: 100%;
} }
} }
} }
.box4-main-footer {
display: flex;
justify-content: space-between;
} }
} }
.right { .right {
position: absolute; width: 968px;
top: 16px;
right: 11px; .right-header {
height: 54px;
width: 968px;
display: flex; 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; height: 24px;
justify-content: flex-end; margin-top: 16px;
align-items: center; justify-content: space-between;
gap: 9px; display: flex;
.text { .right-text {
height: 24px; height: 24px;
font-family: Microsoft YaHei; display: flex;
font-size: 16px;
color: var(--color-main-active); .text {
color: rgb(5, 95, 194);
font-family: "Source Han Sans CN";
font-weight: 400; font-weight: 400;
/* Regular */
font-size: 16px;
line-height: 24px; line-height: 24px;
letter-spacing: 0px; letter-spacing: 0;
text-align: right; text-align: justify;
/* 两端对齐 */
} }
.icon { .right-header-image {
width: 16px; width: 12px;
height: 16px; height: 12px;
display: inline-block;
margin-left: 6px;
margin-top: 4.5px;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} object-fit: contain;
} /* 保证图片不变形 */
} }
} }
} }
.box1-footer { .left-text {
height: 50px; height: 24px;
display: flex; color: rgb(59, 65, 75);
justify-content: space-between; font-family: "Source Han Sans CN";
box-sizing: border-box; font-weight: 700;
padding: 20px; /* Bold 粗体 */
font-size: 18px;
.info { line-height: 24px;
color: rgba(132, 136, 142, 1); letter-spacing: 0;
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: 0px;
text-align: left; 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; 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; display: flex;
flex-direction: column;
gap: 16px;
.box2-item-left { .info-item {
.point { height: 32px;
margin-left: 14px; display: flex;
width: 10px; align-items: center;
height: 10px; 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 { img {
width: 100%; width: 100%;
...@@ -621,68 +641,89 @@ onMounted(async () => { ...@@ -621,68 +641,89 @@ onMounted(async () => {
} }
} }
.line { .info-right-text {
width: 2px; color: rgb(95, 101, 108);
height: 109px; font-family: "Source Han Sans CN";
background: #ccc; font-weight: 400;
margin-left: 18px; font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
} }
} }
.box2-item-center { }
margin-left: 11px;
.title { .right-footer-title {
width: 314px; width: 968px;
height: 56px;
border-bottom: 1px solid rgb(234, 236, 238);
display: flex;
.footer-title-text {
height: 24px; height: 24px;
color: rgba(59, 65, 75, 1); margin-top: 16px;
font-family: Microsoft YaHei; color: rgb(59, 65, 75);
font-size: 16px; font-family: "Source Han Sans CN";
font-weight: 700; font-weight: 700;
line-height: 24px; font-size: 18px;
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;
line-height: 24px; line-height: 24px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: justify; text-align: justify;
overflow: hidden;
} }
} }
.box2-item-right { .right-footer-box {
margin-left: 18px; width: 968px;
gap: 13px;
display: flex;
flex-direction: column;
.time { .relatedBills {
height: 22px; width: 968px;
color: rgba(95, 101, 108, 1); height: 48px;
font-family: Microsoft YaHei; border: 1px solid rgb(231, 243, 255);
font-size: 14px; 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-weight: 400;
font-size: 14px;
line-height: 22px; line-height: 22px;
letter-spacing: 0px; letter-spacing: 0;
text-align: right; text-align: left;
overflow: hidden; color: rgb(5, 95, 194);
text-overflow: ellipsis; margin-right: 4px;
white-space: nowrap; }
.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 { .footer-image {
margin-top: 14px; margin-top: 2px;
width: 78px; width: 20px;
height: 50px; height: 20px;
border-radius: 2px; margin-left: auto;
img { img {
width: 100%; width: 100%;
...@@ -690,36 +731,49 @@ onMounted(async () => { ...@@ -690,36 +731,49 @@ onMounted(async () => {
} }
} }
} }
}
}
.box2-footer { .relatedAdministrativeOrders {
margin: 10px auto; width: 968px;
width: 108px; height: 48px;
height: 32px; border: 1px solid rgb(231, 243, 255);
background-color: rgb(246, 250, 255);
padding: 11px 16px;
display: flex; 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 { .tag {
color: var(--color-main-active); border-radius: 4px;
height: 22px; height: 24px;
font-family: Microsoft YaHei; background-color: rgb(231, 243, 255);
font-size: 14px; padding: 1px 8px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400; font-weight: 400;
font-size: 14px;
line-height: 22px; line-height: 22px;
letter-spacing: 0px; letter-spacing: 0;
text-align: left;
color: rgb(5, 95, 194);
margin-right: 4px;
} }
.icon { .footer-image {
width: 16px; margin-top: 2px;
height: 16px; width: 20px;
height: 20px;
margin-left: auto;
img { img {
width: 100%; width: 100%;
...@@ -728,5 +782,27 @@ onMounted(async () => { ...@@ -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> </style>
\ No newline at end of file
...@@ -37,18 +37,23 @@ ...@@ -37,18 +37,23 @@
<div class="author-title">报告作者:</div> <div class="author-title">报告作者:</div>
<div class="author-content"> <div class="author-content">
<template v-if="Array.isArray(reportAuthors) && reportAuthors.length"> <template v-if="Array.isArray(reportAuthors) && reportAuthors.length">
<span v-for="(author, idx) in reportAuthors" :key="idx"> <span v-if="reportAuthors.length === 1">
{{ author.name }} {{ reportAuthors[0].name }}
<span v-if="idx < reportAuthors.length - 1"></span>
</span> </span>
<!-- 多个作者:显示第一个 + 等 -->
<span v-else>
{{ reportAuthors[0].name }}
</span>
</template> </template>
</div> </div>
</div> </div>
</div> </div>
<div class="author-box" v-for="(author, idx) in reportAuthors" :key="idx" <div class="author-box">
<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">
<div class="author-item"> <div class="image"><img :src="author.avatar" alt="" /></div>
<div class="image"><img :src="author.avatar" :alt="reportAuthors[0].name" /></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>
...@@ -58,6 +63,13 @@ ...@@ -58,6 +63,13 @@
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="box5">
<AnalysisBox title="报告关键词云" :showAllBtn="true">
<div class="box5-main">
<div id="box5Chart"></div>
</div>
</AnalysisBox>
</div>
<div class="box2"> <div class="box2">
<!-- <div class="box-header"> <!-- <div class="box-header">
<div class="header-left"></div> <div class="header-left"></div>
...@@ -94,7 +106,7 @@ ...@@ -94,7 +106,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box2-btn"> <div class="box2-btn" @click="goToAllThinkTank">
<div class="btn-text"> <div class="btn-text">
多智库报告观点汇聚分析 多智库报告观点汇聚分析
</div> </div>
...@@ -109,14 +121,12 @@ ...@@ -109,14 +121,12 @@
<div class="box3"> <div class="box3">
<AnalysisBox title="报告摘要" :showAllBtn="true"> <AnalysisBox title="报告摘要" :showAllBtn="true">
<div class="box3-main"> <div class="box3-main">
<div class="box3-top"> <AiSummary>
<div class="top-title"> <template #summary-content>
<div class="title-image"> {{ box1Data }}
<img src="../images/title-image.png" alt="" /> </template>
</div> </AiSummary>
</div>
</div>
<div class="box3-text"> {{ box1Data }}</div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -185,7 +195,7 @@ ...@@ -185,7 +195,7 @@
<script setup> <script setup>
import WarningPane from "@/components/base/WarningPane/index.vue" import WarningPane from "@/components/base/WarningPane/index.vue"
import SearchContainer from "@/components/SearchContainer.vue"; import SearchContainer from "@/components/SearchContainer.vue";
import { ref, onMounted, computed, defineProps } from "vue"; import { ref, onMounted, computed, defineProps, nextTick } from "vue";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import getWordCloudChart from "./utils/worldCloudChart"; import getWordCloudChart from "./utils/worldCloudChart";
import { import {
...@@ -197,8 +207,20 @@ import { ...@@ -197,8 +207,20 @@ import {
} from "@/api/thinkTank/overview"; } from "@/api/thinkTank/overview";
import { getChartAnalysis } from "@/api/aiAnalysis/index"; import { getChartAnalysis } from "@/api/aiAnalysis/index";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter();
import "echarts-wordcloud"; import "echarts-wordcloud";
import AiSummary from '@/components/base/Ai/AiSummary/index.vue'
const router = useRouter();
const goToAllThinkTank = () => {
const thinkTankId = props?.thinkInfo?.thinkTankId || props?.thinkInfo?.id;
const route = router.resolve({
name: "MultiThinkTankViewAnalysis",
params: { id: thinkTankId }
});
window.open(route.href, "_blank");
};
const props = defineProps({ const props = defineProps({
thinkInfo: { thinkInfo: {
...@@ -321,6 +343,8 @@ const box2Data = ref([ ...@@ -321,6 +343,8 @@ const box2Data = ref([
// value: 89 // value: 89
// } // }
]); ]);
// 报告关键词云
const box5Data = ref([]);
//获取科技领域词云 //获取科技领域词云
const handleGetThinkTankReportIndustryCloud = async () => { const handleGetThinkTankReportIndustryCloud = async () => {
...@@ -332,19 +356,22 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -332,19 +356,22 @@ const handleGetThinkTankReportIndustryCloud = async () => {
const res = await getThinkTankReportIndustryCloud(params); const res = await getThinkTankReportIndustryCloud(params);
console.log("科技领域词云", res); console.log("科技领域词云", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const data = []; const data = (res.data || []).map(item => ({
res.data.map(item => {
data.push({
name: item.clause, name: item.clause,
value: item.count value: item.count
}); }));
box2Data.value = data; // 该接口数据用于「报告关键词云」
const box2Chart = getWordCloudChart(box2Data.value); box5Data.value = data;
setChart(box2Chart, "box2Chart");
}); await nextTick();
const box5Chart = getWordCloudChart(box5Data.value);
setChart(box5Chart, "box5Chart");
} else {
box5Data.value = [];
} }
} catch (error) { } catch (error) {
console.error("获取科技领域词云error", error); console.error("获取科技领域词云error", error);
box5Data.value = [];
} }
}; };
//涉及科技领域 //涉及科技领域
...@@ -575,7 +602,7 @@ onMounted(() => { ...@@ -575,7 +602,7 @@ onMounted(() => {
} }
.author { .author {
height: 24px;
display: flex; display: flex;
gap: 4px; gap: 4px;
...@@ -607,9 +634,18 @@ onMounted(() => { ...@@ -607,9 +634,18 @@ onMounted(() => {
.author-box { .author-box {
width: 437px; width: 437px;
height: 220px; height: auto;
/* 改为自适应高度,不要固定 220px */
max-height: 220px;
margin-top: 34px; margin-top: 34px;
margin-left: 18px; margin-left: 18px;
display: grid;
grid-template-columns: 1fr 1fr;
/* 两列等宽 */
column-gap: 4px;
/* 左右间距(同一行) */
row-gap: 8px;
/* 上下间距(同一列) */
.author-item { .author-item {
width: 213px; width: 213px;
...@@ -646,6 +682,9 @@ onMounted(() => { ...@@ -646,6 +682,9 @@ onMounted(() => {
letter-spacing: 0; letter-spacing: 0;
text-align: left; text-align: left;
color: rgb(59, 65, 75); color: rgb(59, 65, 75);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
.author-position { .author-position {
...@@ -658,6 +697,9 @@ onMounted(() => { ...@@ -658,6 +697,9 @@ onMounted(() => {
letter-spacing: 0; letter-spacing: 0;
text-align: left; text-align: left;
color: rgb(95, 101, 108); color: rgb(95, 101, 108);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
} }
...@@ -669,6 +711,30 @@ onMounted(() => { ...@@ -669,6 +711,30 @@ onMounted(() => {
} }
.box5 {
width: 480px;
height: 415px;
.box5-main {
width: 480px;
height: 361px;
padding-left: 31px;
padding-right: 32px;
padding-top: 26px;
padding-bottom: 43px;
display: flex;
box-sizing: border-box;
overflow: hidden;
#box5Chart {
width: 100%;
height: 100%;
margin: 0 auto;
overflow: hidden;
}
}
}
.box2 { .box2 {
width: 480px; width: 480px;
...@@ -814,7 +880,7 @@ onMounted(() => { ...@@ -814,7 +880,7 @@ onMounted(() => {
.box3 { .box3 {
width: 1103px; width: 1103px;
height: 545px; height: 890px;
// border: 1px solid rgba(234, 236, 238, 1); // border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px; // border-radius: 10px;
...@@ -822,9 +888,10 @@ onMounted(() => { ...@@ -822,9 +888,10 @@ onMounted(() => {
// background: rgba(255, 255, 255, 1); // background: rgba(255, 255, 255, 1);
.box3-main { .box3-main {
width: 1058px; width: 1058px;
height: 466px; height: 830px;
margin-top: 3px; margin-top: 3px;
margin-left: 22px; margin-left: 22px;
padding-bottom: 22px;
...@@ -1129,5 +1196,11 @@ onMounted(() => { ...@@ -1129,5 +1196,11 @@ 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>
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
const getWordCloudChart = (data) => { const getWordCloudChart = (data) => {
const option = { const option = {
width: 417,
height: 292,
grid: { grid: {
left: 0, left: 0,
top: 0, top: 0,
...@@ -11,7 +13,13 @@ const getWordCloudChart = (data) => { ...@@ -11,7 +13,13 @@ const getWordCloudChart = (data) => {
series: [ series: [
{ {
type: "wordCloud", type: "wordCloud",
shape: "circle", // // 让词云渲染区域严格贴合容器
left: "center",
top: "center",
width: "100%",
height: "100%",
// 使用矩形词云更容易铺满容器且减少留白
shape: "rect", // ✅ 矩形 = 文字排版最整齐、最居中
// 其他形状你可以使用形状路径 // 其他形状你可以使用形状路径
// 或者自定义路径 // 或者自定义路径
// shape: 'circle' // 圆形(默认) // shape: 'circle' // 圆形(默认)
...@@ -22,22 +30,16 @@ const getWordCloudChart = (data) => { ...@@ -22,22 +30,16 @@ const getWordCloudChart = (data) => {
// shape: 'pentagon' // 五边形 // shape: 'pentagon' // 五边形
// shape: 'star' // 星形 // shape: 'star' // 星形
// shape: 'cardioid' // 心形 // shape: 'cardioid' // 心形
gridSize: 30, // 网格大小,影响词间距。 // 网格越大越稀疏,越容易产生留白;这里进一步调小以便更贴合容器
sizeRange: [10, 25], // 定义词云中文字大小的范围 gridSize: 5,
// 适当放大最大字号,提升填充度(同时避免太大溢出)
sizeRange: [16, 24],
rotationRange: [0, 0], rotationRange: [0, 0],
rotationStep: 15, rotationStep: 15,
drawOutOfBound: false, // 是否超出画布 drawOutOfBound: false,
layoutAnimation: false,
// 字体 // 字体
textStyle: { textStyle: {
// normal: {
// color: function () {
// return 'rgb(' + [
// Math.round(Math.random() * 160),
// Math.round(Math.random() * 160),
// Math.round(Math.random() * 160)
// ].join(',') + ')';
// }
// },
color: function () { color: function () {
let colors = [ let colors = [
"rgba(189, 33, 33, 1)", "rgba(189, 33, 33, 1)",
...@@ -49,12 +51,12 @@ const getWordCloudChart = (data) => { ...@@ -49,12 +51,12 @@ const getWordCloudChart = (data) => {
]; ];
return colors[parseInt(Math.random() * colors.length)]; return colors[parseInt(Math.random() * colors.length)];
}, },
textAlign: "center", // ✅ 文字自身水平居中
emphasis: { emphasis: {
shadowBlur: 5, shadowBlur: 5,
shadowColor: "#333", shadowColor: "#333",
}, },
}, },
// 设置词云数据
data: data, data: data,
}, },
], ],
......
...@@ -6,21 +6,35 @@ ...@@ -6,21 +6,35 @@
<div class="icon"> <div class="icon">
<img src="./images/box-header-icon1.png" alt="" /> <img src="./images/box-header-icon1.png" alt="" />
</div> </div>
<div class="title">{{ "政策建议领域分布" }}</div> <div class="title">{{ "领域分布情况" }}</div>
<!-- <div class="box-header-right">{{ "查看数据源 >" }}</div> --> <!-- <div class="box-header-right">{{ "查看数据源 >" }}</div> -->
<div class="select-box"> <div class="select-box">
<el-select v-model="box1SelectYear" placeholder="选择时间" style="width: 100px"> <el-select v-model="box1SelectYear" placeholder="选择时间" style="width: 100px"
<el-option v-for="(item, index) in box1YearList" :key="index" :label="item.label + '年'" @change="handleGetThinkPolicyIndustry">
:value="item.value" @click="handleGetThinkPolicyIndustry()" /> <el-option v-for="item in box1YearList" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box-main"> <div class="box-main" :class="{ 'chart-main--empty': !hasBox1ChartData }">
<template v-if="!hasBox1ChartData">
<el-empty class="policy-tracking-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div id="box1Chart"></div> <div id="box1Chart"></div>
<div class="source"> <div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div> <TipTab :text="POLICY_TRACKING_TIP_BOX1" />
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div>
</div> </div>
<div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentPolicyPt1"
@mouseenter="handleSwitchAiContentShowPolicyPt1(true)">
<AiButton />
</div>
<div class="content-box" v-if="isShowAiContentPolicyPt1"
@mouseleave="handleSwitchAiContentShowPolicyPt1(false)">
<AiPane :aiContent="aiContentPolicyPt1" />
</div>
</div>
</template>
</div> </div>
</div> </div>
<div class="box1 box"> <div class="box1 box">
...@@ -28,21 +42,35 @@ ...@@ -28,21 +42,35 @@
<div class="icon"> <div class="icon">
<img src="./images/box-header-icon2.png" alt="" /> <img src="./images/box-header-icon2.png" alt="" />
</div> </div>
<div class="title">{{ "政策建议涉及部门分布" }}</div> <div class="title">{{ "部门分布情况" }}</div>
<!-- <div class="box-header-right">{{ "查看数据源 >" }}</div> --> <!-- <div class="box-header-right">{{ "查看数据源 >" }}</div> -->
<div class="select-box"> <div class="select-box">
<el-select v-model="box2SelectYear" placeholder="选择时间" style="width: 100px"> <el-select v-model="box2SelectYear" placeholder="选择时间" style="width: 100px"
<el-option v-for="(item, index) in box2YearList" :key="index" :label="item.label + '年'" @change="handleGetPolicyAdviceDeptDistribution">
:value="item.value" @click="handleGetPolicyAdviceDeptDistribution()" /> <el-option v-for="item in box2YearList" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box-main"> <div class="box-main" :class="{ 'chart-main--empty': !hasBox2ChartData }">
<template v-if="!hasBox2ChartData">
<el-empty class="policy-tracking-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div id="box2Chart"></div> <div id="box2Chart"></div>
<div class="source"> <div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div> <TipTab :text="POLICY_TRACKING_TIP_BOX2" />
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> </div>
<div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentPolicyPt2"
@mouseenter="handleSwitchAiContentShowPolicyPt2(true)">
<AiButton />
</div> </div>
<div class="content-box" v-if="isShowAiContentPolicyPt2"
@mouseleave="handleSwitchAiContentShowPolicyPt2(false)">
<AiPane :aiContent="aiContentPolicyPt2" />
</div>
</div>
</template>
</div> </div>
</div> </div>
<!-- <div class="box2 box"> <!-- <div class="box2 box">
...@@ -61,7 +89,7 @@ ...@@ -61,7 +89,7 @@
</el-select> </el-select>
</div> </div>
<div id="box2Chart"> <div id="box2Chart">
<div class="box2-item" v-for="(item, index) in box2Data" :key="index"> <div class="box2-item" v-for="(item, index) in box2TotalData" :key="index">
<div class="icon" :style="{ background: item.color }"></div> <div class="icon" :style="{ background: item.color }"></div>
<div class="name">{{ item.industry }}</div> <div class="name">{{ item.industry }}</div>
<el-progress :show-text="false" :color="box2ColorList[index]" style="width: 170px" stroke-width="8" <el-progress :show-text="false" :color="box2ColorList[index]" style="width: 170px" stroke-width="8"
...@@ -77,38 +105,60 @@ ...@@ -77,38 +105,60 @@
<div class="icon"> <div class="icon">
<img src="./images/box-header-icon2.png" alt="" /> <img src="./images/box-header-icon2.png" alt="" />
</div> </div>
<div class="title">{{ "热门研究方向变化趋势" }}</div> <div class="title">{{ "研究领域变化趋势" }}</div>
<!-- <div class="box-header-right">{{ "查看数据源 >" }}</div> --> <!-- <div class="box-header-right">{{ "查看数据源 >" }}</div> -->
<div class="select-box"> <div class="select-box">
<el-select v-model="box3SelectMonths" placeholder="选择时间" style="width: 100px"> <el-select v-model="box3SelectMonths" placeholder="选择时间" style="width: 100px"
<el-option v-for="item in box3MonthsList" :key="item.value" :label="item.label + '年'" :value="item.value" @change="handleGetThinkPolicyIndustryChange">
@click="handleGetThinkPolicyIndustryChange()" /> <el-option v-for="item in box3MonthsList" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box3-main"> <div class="box3-main" :class="{ 'chart-main--empty': !hasBox3ChartData }">
<div id="box3Chart"></div> <template v-if="!hasBox3ChartData">
<el-empty class="policy-tracking-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<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>
<button v-if="box3LegendShowNav" type="button" class="box3-legend-nav-btn" :disabled="!box3LegendCanNext"
aria-label="下一页图例" @click="handleBox3LegendNext">
</button>
</div>
<div class="source"> <div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div> <TipTab :text="POLICY_TRACKING_TIP_BOX3" />
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div>
</div> </div>
<div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentPolicyPt3"
@mouseenter="handleSwitchAiContentShowPolicyPt3(true)">
<AiButton />
</div>
<div class="content-box" v-if="isShowAiContentPolicyPt3"
@mouseleave="handleSwitchAiContentShowPolicyPt3(false)">
<AiPane :aiContent="aiContentPolicyPt3" />
</div>
</div>
</template>
</div> </div>
</div> </div>
</div> </div>
<div class="bottom"> <div class="bottom">
<div class="bottom-header"> <div class="bottom-header">
<div class="search-box"> <div class="search-box" @keyup.enter="handleGetThinkPolicy()">
<el-input placeholder="搜索政策建议" v-model="searchPolicy" style="width: 270px" /> <el-input placeholder="搜索政策建议" v-model="searchPolicy" style="width: 270px" />
<div class="icon"> <div class="icon">
<img src="./images/search-icon.png" alt="" /> <img src="./images/search-icon.png" alt="" @click="handleGetThinkPolicy()" />
</div> </div>
</div> </div>
<div class="select-box"> <div class="select-box">
<el-select v-model="selectedYear" placeholder="选择时间" style="width: 120px" @click="handleGetThinkPolicy()">
<el-option v-for="item in yearList" :key="item.value" :label="item.label" :value="item.value" /> <el-select class="select-box-sort" v-model="sort" placeholder="发布时间" style="width: 120px" :teleported="true"
</el-select>
<el-select class="select-box-sort" v-model="sort" placeholder="倒序" style="width: 120px" :teleported="true"
:placement="'bottom-start'" :popper-options="{ :placement="'bottom-start'" :popper-options="{
modifiers: [ modifiers: [
{ {
...@@ -122,7 +172,7 @@ ...@@ -122,7 +172,7 @@
] ]
}"> }">
<template #prefix> <template #prefix>
<img v-if="!sort" src="../thinkDynamics/images/image down.png" class="select-prefix-img" alt="" /> <img v-if="sort !== true" src="../thinkDynamics/images/image down.png" class="select-prefix-img" alt="" />
<img v-else src="../thinkDynamics/images/image up.png" class="select-prefix-img" alt="" /> <img v-else src="../thinkDynamics/images/image up.png" class="select-prefix-img" alt="" />
</template> </template>
<el-option @click="handleGetThinkPolicy()" :key="true" label="正序" :value="true" /> <el-option @click="handleGetThinkPolicy()" :key="true" label="正序" :value="true" />
...@@ -138,17 +188,44 @@ ...@@ -138,17 +188,44 @@
<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-for="(item, index) in areaList" :key="index" v-model="selectedAreaList" :label="item" class="checkbox-group"
class="filter-checkbox"> v-model="selectedAreaList"
{{ item }} @change="handleChangePolicyAreaList">
</el-checkbox> --> <el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_AREA">
<el-checkbox class="filter-checkbox" label="全部领域"></el-checkbox> {{ POLICY_FILTER_ALL_AREA }}
<el-checkbox class="filter-checkbox" v-for="research in areaList" :key="research.id" </el-checkbox>
v-model="selectedAreaList" :label="research.id"> <el-checkbox
class="filter-checkbox"
v-for="research in areaList"
:key="research.id"
:label="research.id">
{{ research.name }} {{ research.name }}
</el-checkbox> </el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="select-box-department">
<div class="select-box-header">
<div class="icon"></div>
<div class="title">{{ "涉及部门" }}</div>
</div> </div>
<div class="select-main">
<el-checkbox-group
class="checkbox-group"
v-model="selectedDepartmentList"
@change="handleChangePolicyDepartmentList">
<el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_DEPT">
{{ POLICY_FILTER_ALL_DEPT }}
</el-checkbox>
<el-checkbox
class="filter-checkbox"
v-for="department in selectableDepartment"
:key="department.id"
:label="department.id">
{{ department.name }}
</el-checkbox>
</el-checkbox-group>
</div> </div>
</div> </div>
<div class="select-box-publish"> <div class="select-box-publish">
...@@ -157,49 +234,58 @@ ...@@ -157,49 +234,58 @@
<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-for="(item, index) in areaList" :key="index" v-model="selectedAreaList" :label="item" class="checkbox-group"
class="filter-checkbox"> v-model="selectedYearList"
{{ item }} @change="handleChangePolicyYearList">
</el-checkbox> --> <el-checkbox class="filter-checkbox" :label="POLICY_FILTER_ALL_TIME">
<el-checkbox class="filter-checkbox" label="全部领域"></el-checkbox> {{ POLICY_FILTER_ALL_TIME }}
<el-checkbox class="filter-checkbox" v-for="year in selectableYears" :key="year" </el-checkbox>
v-model="selectedAreaList" :label="year"> <el-checkbox
class="filter-checkbox"
v-for="year in selectableYears"
:key="year"
:label="year">
{{ year }} {{ year }}
</el-checkbox> </el-checkbox>
</div> </el-checkbox-group>
</div> </div>
</div> </div>
</div> </div>
<div class="right"> <div class="right">
<div class="right-main"> <div class="right-main">
<div class="right-main-item" v-for="(item, index) in policyList" :key="index"> <div class="right-main-item" v-for="item in policyList" :key="item.id">
<div class="item-left"> <div class="item-left">
<img :src="item.imageUrl" alt="" /> <img :src="item.imageUrl" alt="" />
</div> </div>
<div class="item-right"> <div class="item-right">
<div> <div>
<div class="title">{{ item.content }}</div> <div class="title">{{ item.titleZh }}</div>
<div class="info"> <div class="info">
{{ item.times }} · {{ item.name }} <div class="text">
{{ item.reportDateDisplay }}·{{ item.contentZh }}
</div>
<div class="more" @click="toDetail(item)"> <div class="more" @click="toDetail(item)">
<img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" /> <img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" />
</div> </div>
</div> </div>
<div class="tag-box"> <div class="tag-box">
<AreaTag v-for="(tag, idx) in item.tagList" :key="idx" :tagName="tag"></AreaTag> <AreaTag v-for="(tag, idx) in item.tagList" :key="idx" :tagName="tag"></AreaTag>
</div> </div>
<div class="file-box"> <div class="file-box">
<div class="file" v-for="(file, idxx) in item.relationBillsList" :key="idxx"> <div class="file" v-for="(file, idxx) in item.relatedBills" :key="`${file.id}-${idxx}`">
<div class="type">法案</div> <div class="type">法案</div>
<div class="title">{{ file.billName }}</div> <div class="title">{{ file.name }}</div>
<div class="more"> <div class="more">
<img src="./images/arrow-right.png" alt="" /> <img src="./images/arrow-right.png" alt="" />
</div> </div>
</div> </div>
<div class="file" v-for="(file, idxx) in item.relationAdList" :key="idxx"> <div class="file" v-for="(file, idxx) in item.relatedAdministrativeOrders"
:key="file.bcId != null ? String(file.bcId) + '-' + idxx : idxx">
<div class="type">政令</div> <div class="type">政令</div>
<div class="title">{{ file.adName }}</div> <div class="title">{{ file.content }}</div>
<div class="more"> <div class="more">
<img src="./images/arrow-right.png" alt="" /> <img src="./images/arrow-right.png" alt="" />
</div> </div>
...@@ -213,7 +299,7 @@ ...@@ -213,7 +299,7 @@
<div class="right-footer"> <div class="right-footer">
<div class="info">共{{ total }}条政策建议</div> <div class="info">共{{ total }}条政策建议</div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total" <el-pagination :page-size="10" background layout="prev, pager, next" :total="total"
@current-change="handleCurrentChange" :current-page="currentPage" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -224,22 +310,71 @@ ...@@ -224,22 +310,71 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, onMounted } 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 from "./utils/multiLineChart"; import getMultiLineChart, { POLICY_TRACKING_LEGEND_PAGE_SIZE } from "./utils/multiLineChart";
import { import {
getThinkPolicyIndustry, getThinkPolicyIndustry,
getThinkPolicyIndustryTotal, getThinkPolicyIndustryTotal,
getThinkPolicyIndustryChange, getThinkPolicyIndustryChange,
getHylyList, getHylyList,
getThinkPolicy, getThinkPolicy,
getPolicyAdviceDeptDistribution getPolicyAdviceDeptDistribution,
getGovAgencyDict
} from "@/api/thinkTank/overview"; } from "@/api/thinkTank/overview";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import TipTab from "@/views/thinkTank/TipTab/index.vue";
import defaultNewsIcon from "@/assets/icons/default-icon-news.png";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
/** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */
const POLICY_TRACKING_TIP_BOX1 =
"智库报告中政策建议的领域分布情况,数据来源:美国兰德公司官网";
const POLICY_TRACKING_TIP_BOX2 =
"智库报告中政策建议涉及领域分布情况,数据来源:美国兰德公司官网";
const POLICY_TRACKING_TIP_BOX3 =
"智库报告热门研究领域变化趋势,数据来源:美国兰德公司官网";
/** 筛选「全部」项文案,与市场准入概览-资源库复选逻辑一致 */
const POLICY_FILTER_ALL_AREA = "全部领域";
const POLICY_FILTER_ALL_TIME = "全部时间";
const POLICY_FILTER_ALL_DEPT = "全部部门";
const isShowAiContentPolicyPt1 = ref(false);
const aiContentPolicyPt1 = ref("");
const isPolicyPt1InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt1 = (val) => {
isShowAiContentPolicyPt1.value = val;
if (val) {
fetchPolicyPtBox1ChartInterpretation();
}
};
const isShowAiContentPolicyPt2 = ref(false);
const aiContentPolicyPt2 = ref("");
const isPolicyPt2InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt2 = (val) => {
isShowAiContentPolicyPt2.value = val;
if (val) {
fetchPolicyPtBox2ChartInterpretation();
}
};
const isShowAiContentPolicyPt3 = ref(false);
const aiContentPolicyPt3 = ref("");
const isPolicyPt3InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt3 = (val) => {
isShowAiContentPolicyPt3.value = val;
if (val) {
fetchPolicyPtBox3ChartInterpretation();
}
};
// import Img1 from "./images/img1.png"; // import Img1 from "./images/img1.png";
// import Img2 from "./images/img2.png"; // import Img2 from "./images/img2.png";
// import Img3 from "./images/img3.png"; // import Img3 from "./images/img3.png";
...@@ -284,11 +419,18 @@ const box1Data = ref([ ...@@ -284,11 +419,18 @@ const box1Data = ref([
// } // }
]); ]);
const relationBillsList = ref([{ billName: "2025《人工智能安全与评估法案》" }]) /** 与智库首页数量变化趋势一致:有数据才展示图表与数据来源 */
const relationAdList = ref([{ adName: "2025《人工智能安全与评估法案》" }]) const hasBox1ChartData = computed(
() => Array.isArray(box1Data.value) && box1Data.value.length > 0
);
const box1SelectYear = ref("2025"); const box1SelectYear = ref("2025");
const box1YearList = ref([ const box1YearList = ref([
{
label: "2026",
value: "2026"
},
{ {
label: "2025", label: "2025",
value: "2025" value: "2025"
...@@ -300,10 +442,16 @@ const box1YearList = ref([ ...@@ -300,10 +442,16 @@ const box1YearList = ref([
{ {
label: "2023", label: "2023",
value: "2023" value: "2023"
} },
{
label: "2022",
value: "2022"
},
]); ]);
const selectableYears = ref(["2025年", "2024年", "2023年", "2022年", "2021年", "更早"]); const selectableYears = ref(["2025年", "2024年", "2023年", "2022年", "2021年", "更早"]);
const selectableDepartment = ref([]);
const handleGetThinkPolicyIndustry = async () => { const handleGetThinkPolicyIndustry = async () => {
aiContentPolicyPt1.value = "";
try { try {
const parmas = { const parmas = {
id: router.currentRoute._value.params.id, id: router.currentRoute._value.params.id,
...@@ -313,21 +461,41 @@ const handleGetThinkPolicyIndustry = async () => { ...@@ -313,21 +461,41 @@ const handleGetThinkPolicyIndustry = async () => {
console.log("提出建议领域分布", res); console.log("提出建议领域分布", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const list = Array.isArray(res.data) ? res.data.slice(0, 7) : []; const list = Array.isArray(res.data) ? res.data.slice(0, 7) : [];
if (!list.length) {
box1Data.value = [];
return;
}
const data = list.map(item => ({ const data = list.map(item => ({
name: item.industry, name: item.industry,
value: item.amount, value: item.amount,
percent: item.percent percent: item.percent
})); }));
box1Data.value = data; box1Data.value = data;
/* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */
await nextTick();
const box1Chart = getPieChart(box1Data.value); const box1Chart = getPieChart(box1Data.value);
setChart(box1Chart, "box1Chart"); setChart(box1Chart, "box1Chart");
} else {
box1Data.value = [];
} }
} catch (error) { } catch (error) {
console.error("获取提出建议领域分布error", error); console.error("获取提出建议领域分布error", error);
box1Data.value = [];
} }
}; };
// 政策建议涉及部门分布(饼图)
const box2Data = ref([]);
const hasBox2ChartData = computed(
() => Array.isArray(box2Data.value) && box2Data.value.length > 0
);
/** 仅「相关政策领域分布」注释区块用;勿写入 box2Data,否则与部门分布接口并行会覆盖饼图数据导致空白 */
const box2TotalData = ref([]);
const handleGetPolicyAdviceDeptDistribution = async () => { const handleGetPolicyAdviceDeptDistribution = async () => {
aiContentPolicyPt2.value = "";
try { try {
const parmas = { const parmas = {
id: router.currentRoute._value.params.id, id: router.currentRoute._value.params.id,
...@@ -335,7 +503,12 @@ const handleGetPolicyAdviceDeptDistribution = async () => { ...@@ -335,7 +503,12 @@ const handleGetPolicyAdviceDeptDistribution = async () => {
}; };
const res = await getPolicyAdviceDeptDistribution(parmas); const res = await getPolicyAdviceDeptDistribution(parmas);
console.log("政策建议涉及部门分布", res); console.log("政策建议涉及部门分布", res);
if (res.code === 200 && res.data && Array.isArray(res.data.series)) { if (
res.code === 200 &&
res.data &&
Array.isArray(res.data.series) &&
res.data.series.length > 0
) {
// 接口新结构:data.series 为 [{ name, value, percent, extra }] // 接口新结构:data.series 为 [{ name, value, percent, extra }]
const list = res.data.series.slice(0, 7); const list = res.data.series.slice(0, 7);
box2Data.value = list.map(item => ({ box2Data.value = list.map(item => ({
...@@ -344,19 +517,26 @@ const handleGetPolicyAdviceDeptDistribution = async () => { ...@@ -344,19 +517,26 @@ const handleGetPolicyAdviceDeptDistribution = async () => {
percent: item.percent percent: item.percent
})); }));
await nextTick();
const box2Chart = getPieChart(box2Data.value); const box2Chart = getPieChart(box2Data.value);
setChart(box2Chart, "box2Chart"); setChart(box2Chart, "box2Chart");
} else {
box2Data.value = [];
} }
} catch (error) { } catch (error) {
console.error("获取政策建议涉及部门分布 error", error); console.error("获取政策建议涉及部门分布 error", error);
box2Data.value = [];
} }
}; };
// 相关政策领域分布 // 相关政策领域分布(旧模块用 box2Data,勿与部门分布混用)
const box2Data = ref([]);
const box2ColorList = ref(["#CE4F51", "#055FC2", "#13A8A8", "#722ED1", "#FA8C16", "#69B1FF"]); const box2ColorList = ref(["#CE4F51", "#055FC2", "#13A8A8", "#722ED1", "#FA8C16", "#69B1FF"]);
const box2SelectYear = ref("2025"); const box2SelectYear = ref("2025");
const box2YearList = ref([ const box2YearList = ref([
{
label: "2026",
value: "2026"
},
{ {
label: "2025", label: "2025",
value: "2025" value: "2025"
...@@ -368,7 +548,11 @@ const box2YearList = ref([ ...@@ -368,7 +548,11 @@ const box2YearList = ref([
{ {
label: "2023", label: "2023",
value: "2023" value: "2023"
} },
{
label: "2022",
value: "2022"
},
]); ]);
const handleGetThinkPolicyIndustryTotal = async () => { const handleGetThinkPolicyIndustryTotal = async () => {
...@@ -380,30 +564,107 @@ const handleGetThinkPolicyIndustryTotal = async () => { ...@@ -380,30 +564,107 @@ const handleGetThinkPolicyIndustryTotal = async () => {
const res = await getThinkPolicyIndustryTotal(parmas); const res = await getThinkPolicyIndustryTotal(parmas);
console.log("相关政策领域分布", res); console.log("相关政策领域分布", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
box2Data.value = res.data; box2TotalData.value = Array.isArray(res.data) ? res.data : [];
} else {
box2TotalData.value = [];
} }
} catch (error) { } catch (error) {
console.error("获取相关政策领域分布error", error); console.error("获取相关政策领域分布error", error);
box2TotalData.value = [];
} }
}; };
// 热门研究方向变化趋势 // 热门研究方向变化趋势
const box3Data = ref({ const box3Data = ref({
// title: ["2024-Q1", "2024-Q2", "2024-Q3", "2024-Q4"], title: [],
// data: [ data: []
// { });
// name: "新能源",
// value: [1,3,4,6] const hasBox3ChartData = computed(() => {
// }, const v = box3Data.value;
// { if (!v || typeof v !== "object") {
// name: "人工智能", return false;
// value: [1,3,4,2] }
// }, const title = v.title;
// ... const data = v.data;
// ] return (
Array.isArray(title) &&
title.length > 0 &&
Array.isArray(data) &&
data.length > 0
);
}); });
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 v = box3Data.value;
if (
!v ||
!Array.isArray(v.title) ||
v.title.length === 0 ||
!Array.isArray(v.data) ||
v.data.length === 0
) {
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();
const box3Chart = getMultiLineChart(
{ title: v.title, data: v.data },
{
legendShowCount: POLICY_TRACKING_LEGEND_PAGE_SIZE,
legendPageIndex: box3LegendPageIndex.value
}
);
setChart(box3Chart, "box3Chart");
};
const box3SelectMonths = ref("2025"); const box3SelectMonths = ref("2025");
const box3MonthsList = ref([ const box3MonthsList = ref([
{
label: "2026",
value: "2026"
},
{ {
label: "2025", label: "2025",
value: "2025" value: "2025"
...@@ -415,10 +676,15 @@ const box3MonthsList = ref([ ...@@ -415,10 +676,15 @@ const box3MonthsList = ref([
{ {
label: "2023", label: "2023",
value: "2023" value: "2023"
} },
{
label: "2022",
value: "2022"
},
]); ]);
const handleGetThinkPolicyIndustryChange = async () => { const handleGetThinkPolicyIndustryChange = async () => {
aiContentPolicyPt3.value = "";
try { try {
const parmas = { const parmas = {
id: router.currentRoute._value.params.id, id: router.currentRoute._value.params.id,
...@@ -426,7 +692,7 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -426,7 +692,7 @@ const handleGetThinkPolicyIndustryChange = async () => {
}; };
const res = await getThinkPolicyIndustryChange(parmas); const res = await getThinkPolicyIndustryChange(parmas);
console.log("热门研究方向变化趋势", res); console.log("热门研究方向变化趋势", res);
if (res.code === 200 && res.data) { if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
// 提取所有季度信息 // 提取所有季度信息
const quarters = res.data.map(item => item.yearQuarter); const quarters = res.data.map(item => item.yearQuarter);
quarters.sort(); // 按时间排序 quarters.sort(); // 按时间排序
...@@ -434,9 +700,11 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -434,9 +700,11 @@ const handleGetThinkPolicyIndustryChange = async () => {
// 提取所有行业名称 // 提取所有行业名称
const industries = new Set(); const industries = new Set();
res.data.forEach(item => { res.data.forEach(item => {
if (Array.isArray(item.industryList)) {
item.industryList.forEach(industry => { item.industryList.forEach(industry => {
industries.add(industry.industry); industries.add(industry.industry);
}); });
}
}); });
// 构建前端数据结构 // 构建前端数据结构
...@@ -449,25 +717,175 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -449,25 +717,175 @@ const handleGetThinkPolicyIndustryChange = async () => {
name: industry, name: industry,
value: quarters.map(quarter => { value: quarters.map(quarter => {
const quarterData = res.data.find(q => q.yearQuarter === quarter); const quarterData = res.data.find(q => q.yearQuarter === quarter);
const industryAmount = quarterData?.industryList.find(i => i.industry === industry)?.amount || 0; const industryAmount =
quarterData?.industryList?.find(i => i.industry === industry)?.amount || 0;
return industryAmount; return industryAmount;
}) })
}; };
frontendData.data.push(industryData); frontendData.data.push(industryData);
}); });
if (!frontendData.data.length) {
box3Data.value = { title: [], data: [] };
return;
}
box3Data.value = frontendData; box3Data.value = frontendData;
let box3Chart = getMultiLineChart( box3LegendPageIndex.value = 0;
box3Data.value.title, await renderBox3Chart();
box3Data.value.data[0].value, } else {
box3Data.value.data[1].value, box3Data.value = { title: [], data: [] };
box3Data.value.data[2].value
);
setChart(box3Chart, "box3Chart");
} }
} catch (error) { } catch (error) {
console.error("获取热门研究方向变化趋势error", error); console.error("获取热门研究方向变化趋势error", error);
box3Data.value = { title: [], data: [] };
}
};
/** 政策追踪-领域分布饼图解读(与概览页 box6 入参一致) */
const fetchPolicyPtBox1ChartInterpretation = async () => {
const list = Array.isArray(box1Data.value) ? box1Data.value : [];
if (!list.length) {
aiContentPolicyPt1.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentPolicyPt1.value &&
aiContentPolicyPt1.value !== "解读生成中…" &&
aiContentPolicyPt1.value !== "解读加载失败" &&
aiContentPolicyPt1.value !== "暂无图表数据";
if (hasValidContent || isPolicyPt1InterpretLoading.value) {
return;
}
isPolicyPt1InterpretLoading.value = true;
aiContentPolicyPt1.value = "解读生成中…";
const chartPayload = {
type: "饼图",
name: "领域分布情况",
year: box1SelectYear.value,
data: list.map((item) => ({
name: item.name,
value: item.value
}))
};
try {
const text = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentPolicyPt1.value = chunk;
}
}
}
);
aiContentPolicyPt1.value = text || "未返回有效解读内容";
} catch (error) {
console.error("政策追踪领域分布图表解读请求失败", error);
aiContentPolicyPt1.value = "解读加载失败";
} finally {
isPolicyPt1InterpretLoading.value = false;
}
};
/** 政策追踪-部门分布饼图解读 */
const fetchPolicyPtBox2ChartInterpretation = async () => {
const list = Array.isArray(box2Data.value) ? box2Data.value : [];
if (!list.length) {
aiContentPolicyPt2.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentPolicyPt2.value &&
aiContentPolicyPt2.value !== "解读生成中…" &&
aiContentPolicyPt2.value !== "解读加载失败" &&
aiContentPolicyPt2.value !== "暂无图表数据";
if (hasValidContent || isPolicyPt2InterpretLoading.value) {
return;
}
isPolicyPt2InterpretLoading.value = true;
aiContentPolicyPt2.value = "解读生成中…";
const chartPayload = {
type: "饼图",
name: "部门分布情况",
year: box2SelectYear.value,
data: list.map((item) => ({
name: item.name,
value: item.value
}))
};
try {
const text = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentPolicyPt2.value = chunk;
}
}
}
);
aiContentPolicyPt2.value = text || "未返回有效解读内容";
} catch (error) {
console.error("政策追踪部门分布图表解读请求失败", error);
aiContentPolicyPt2.value = "解读加载失败";
} finally {
isPolicyPt2InterpretLoading.value = false;
}
};
/** 政策追踪-研究领域变化趋势折线图解读(与概览页 box5 结构一致) */
const fetchPolicyPtBox3ChartInterpretation = async () => {
const v = box3Data.value;
if (
!v ||
!Array.isArray(v.title) ||
v.title.length === 0 ||
!Array.isArray(v.data) ||
v.data.length === 0
) {
aiContentPolicyPt3.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentPolicyPt3.value &&
aiContentPolicyPt3.value !== "解读生成中…" &&
aiContentPolicyPt3.value !== "解读加载失败" &&
aiContentPolicyPt3.value !== "暂无图表数据";
if (hasValidContent || isPolicyPt3InterpretLoading.value) {
return;
}
isPolicyPt3InterpretLoading.value = true;
aiContentPolicyPt3.value = "解读生成中…";
const chartPayload = {
type: "折线图",
name: "研究领域变化趋势",
data: v.title.map((label, i) => {
const point = { period: label };
v.data.forEach((s) => {
point[s.name] = s.value[i] ?? 0;
});
return point;
})
};
try {
const text = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentPolicyPt3.value = chunk;
}
}
}
);
aiContentPolicyPt3.value = text || "未返回有效解读内容";
} catch (error) {
console.error("政策追踪研究领域趋势图表解读请求失败", error);
aiContentPolicyPt3.value = "解读加载失败";
} finally {
isPolicyPt3InterpretLoading.value = false;
} }
}; };
// 搜索政策建议 // 搜索政策建议
const searchPolicy = ref(""); const searchPolicy = ref("");
...@@ -488,9 +906,11 @@ const yearList = ref([ ...@@ -488,9 +906,11 @@ const yearList = ref([
const selectedYear = ref(1); const selectedYear = ref(1);
// 科技领域 // 科技领域 / 涉及部门 / 发布时间(含「全部」项,与市场准入概览资源库互斥逻辑一致)
const areaList = ref([]); const areaList = ref([]);
const selectedAreaList = ref([]); const selectedDepartmentList = ref([POLICY_FILTER_ALL_DEPT]);
const selectedAreaList = ref([POLICY_FILTER_ALL_AREA]);
const selectedYearList = ref([POLICY_FILTER_ALL_TIME]);
//获取科技领域 //获取科技领域
const handleGetHylyList = async () => { const handleGetHylyList = async () => {
...@@ -499,11 +919,77 @@ const handleGetHylyList = async () => { ...@@ -499,11 +919,77 @@ const handleGetHylyList = async () => {
console.log("智库研究类型信息", res.data); console.log("智库研究类型信息", res.data);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
areaList.value = res.data; areaList.value = res.data;
console.log(areaList.value)
} }
} catch (error) { } catch (error) {
console.error("获取研究类型error", error); console.error("获取研究类型error", error);
} }
}; };
/** 涉及部门:GET /api/commonDict/gov/agency */
const handleGetGovAgencyList = async () => {
try {
const res = await getGovAgencyDict();
console.log(res, " 涉及部门")
if (res.code === 200 && Array.isArray(res.data)) {
selectableDepartment.value = res.data.map((row) => ({
id: String(row.id),
name: row.name
}));
} else {
selectableDepartment.value = [];
}
} catch (error) {
console.error("获取政府机构字典 error", error);
selectableDepartment.value = [];
}
};
/** 科技领域:与「全部领域」互斥;空选时回到「全部领域」 */
const handleChangePolicyAreaList = (val) => {
if (val.includes(POLICY_FILTER_ALL_AREA) && val.length > 1) {
if (val[val.length - 1] === POLICY_FILTER_ALL_AREA) {
selectedAreaList.value = [POLICY_FILTER_ALL_AREA];
} else {
selectedAreaList.value = val.filter((item) => item !== POLICY_FILTER_ALL_AREA);
}
} else if (val.length === 0) {
selectedAreaList.value = [POLICY_FILTER_ALL_AREA];
}
currentPage.value = 1;
handleGetThinkPolicy();
};
/** 涉及部门:与「全部部门」互斥;空选时回到「全部部门」 */
const handleChangePolicyDepartmentList = (val) => {
if (val.includes(POLICY_FILTER_ALL_DEPT) && val.length > 1) {
if (val[val.length - 1] === POLICY_FILTER_ALL_DEPT) {
selectedDepartmentList.value = [POLICY_FILTER_ALL_DEPT];
} else {
selectedDepartmentList.value = val.filter((item) => item !== POLICY_FILTER_ALL_DEPT);
}
} else if (val.length === 0) {
selectedDepartmentList.value = [POLICY_FILTER_ALL_DEPT];
}
currentPage.value = 1;
handleGetThinkPolicy();
};
/** 发布时间:与「全部时间」互斥;空选时回到「全部时间」 */
const handleChangePolicyYearList = (val) => {
if (val.includes(POLICY_FILTER_ALL_TIME) && val.length > 1) {
if (val[val.length - 1] === POLICY_FILTER_ALL_TIME) {
selectedYearList.value = [POLICY_FILTER_ALL_TIME];
} else {
selectedYearList.value = val.filter((item) => item !== POLICY_FILTER_ALL_TIME);
}
} else if (val.length === 0) {
selectedYearList.value = [POLICY_FILTER_ALL_TIME];
}
currentPage.value = 1;
handleGetThinkPolicy();
};
const policyList = ref([ const policyList = ref([
// { // {
// img: Img1, // img: Img1,
...@@ -584,8 +1070,119 @@ function getDateMonthsAgo(months) { ...@@ -584,8 +1070,119 @@ function getDateMonthsAgo(months) {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
/** 当天 YYYY-MM-DD,作政策列表 endDate */
function 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 POLICY_YEAR_EARLIER_START = 2000;
/** 「更早」对应自然年上界(含) */
const POLICY_YEAR_EARLIER_END = 2020;
const POLICY_YEAR_LABEL_RE = /^(\d{4})年$/;
/** 勾选「全部时间」时的固定起止(结束日按产品要求写死) */
const POLICY_ALL_TIME_START_DATE = "2000-01-01";
const POLICY_ALL_TIME_END_DATE = "2025-12-31";
/**
* 根据发布时间多选(如「2025年」「更早」)推导列表查询 startDate / endDate
* - 仅选「2025年」→ 2025-01-01 ~ 2025-12-31
* - 多选多个自然年 → 取最小年 01-01 与最大年 12-31 的包络
* - 「更早」→ 2000-01-01 ~ 2020-12-31,可与具体年份合并为并集区间
* - 仅「全部时间」→ 2000-01-01 ~ 2025-12-31(写死)
* - 未选任何项(无「全部时间」)→ 与顶部「近一年/两年/三年」一致
*/
function getPolicyListDateRangeFromYearList(labels, relativeYearYears) {
const raw = Array.isArray(labels) ? labels.filter(Boolean) : [];
const hasAllTime = raw.includes(POLICY_FILTER_ALL_TIME);
const set = new Set(raw);
set.delete(POLICY_FILTER_ALL_TIME);
if (set.size === 0) {
if (hasAllTime) {
return { startDate: POLICY_ALL_TIME_START_DATE, endDate: POLICY_ALL_TIME_END_DATE };
}
const years = Number(relativeYearYears) > 0 ? Number(relativeYearYears) : 1;
return { startDate: getDateYearsAgo(years), endDate: getTodayYmd() };
}
let minY = null;
let maxY = null;
if (set.has("更早")) {
minY = POLICY_YEAR_EARLIER_START;
maxY = POLICY_YEAR_EARLIER_END;
}
for (const lab of set) {
const m = String(lab).match(POLICY_YEAR_LABEL_RE);
if (!m) {
continue;
}
const y = Number(m[1]);
minY = minY == null ? y : Math.min(minY, y);
maxY = maxY == null ? y : Math.max(maxY, y);
}
if (minY == null || maxY == null) {
const years = Number(relativeYearYears) > 0 ? Number(relativeYearYears) : 1;
return { startDate: getDateYearsAgo(years), endDate: getTodayYmd() };
}
return {
startDate: `${minY}-01-01`,
endDate: `${maxY}-12-31`
};
}
/** 将接口 reportDate(如 2025-12-28T08:00:00)格式化为 2025年12月28日(按字符串日期部分,避免时区偏移) */
function formatPolicyReportDateToCn(isoStr) {
if (!isoStr) {
return "";
}
const m = String(isoStr).match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
if (!m) {
return "";
}
return `${m[1]}${Number(m[2])}${Number(m[3])}日`;
}
/** 接口 domains 转为标签文案列表 */
function normalizePolicyDomains(domains) {
if (!Array.isArray(domains)) {
return [];
}
return domains
.map((d) => {
if (typeof d === "string") {
return d;
}
return d?.name ?? d?.industryName ?? d?.domainName ?? "";
})
.filter(Boolean);
}
/** 列表行映射:左侧图为 organizations[0].logoUrl,文案与法案/政令用接口字段名 */
function mapPolicyRowToView(row) {
const orgs = Array.isArray(row.organizations) ? row.organizations : [];
const logoUrl = orgs[0]?.logoUrl || defaultNewsIcon;
const reportDateDisplay = formatPolicyReportDateToCn(row.reportDate);
return {
...row,
titleZh: row.titleZh ?? row.title ?? "",
contentZh: row.contentZh ?? row.content ?? "",
reportDateDisplay,
imageUrl: logoUrl,
tagList: normalizePolicyDomains(row.domains),
relatedBills: Array.isArray(row.relatedBills) ? row.relatedBills : [],
relatedAdministrativeOrders: Array.isArray(row.relatedAdministrativeOrders)
? row.relatedAdministrativeOrders
: [],
reportId: row.reportId ?? row.id
};
}
const toDetail = item => { const toDetail = item => {
window.sessionStorage.setItem("curTabName", item.content); window.sessionStorage.setItem("curTabName", item.contentZh ?? item.content ?? "");
const route = router.resolve({ const route = router.resolve({
name: "ReportDetail", name: "ReportDetail",
params: { params: {
...@@ -596,10 +1193,11 @@ const toDetail = item => { ...@@ -596,10 +1193,11 @@ const toDetail = item => {
}; };
const total = ref(0); const total = ref(0);
const sort = ref(false); /** null:界面显示「发布时间」占位且默认按发布时间倒序;true 正序;false 倒序(显式选中) */
const sort = ref(null);
const handleSwithSort = () => { const handleSwithSort = () => {
sort.value = !sort.value; sort.value = sort.value === true ? false : true;
handleGetThinkPolicy()(); handleGetThinkPolicy();
}; };
const currentPage = ref(1); const currentPage = ref(1);
...@@ -611,32 +1209,58 @@ const handleCurrentChange = page => { ...@@ -611,32 +1209,58 @@ const handleCurrentChange = page => {
const handleGetThinkPolicy = async () => { const handleGetThinkPolicy = async () => {
try { try {
const thinkTankId = router.currentRoute._value.params.id;
const domainIds = selectedAreaList.value
.filter((id) => id != null && id !== "" && id !== POLICY_FILTER_ALL_AREA)
.map((id) => String(id))
.join(",");
const orgIds = selectedDepartmentList.value
.filter((id) => id != null && id !== "" && id !== POLICY_FILTER_ALL_DEPT)
.map((id) => String(id))
.join(",");
const { startDate, endDate } = getPolicyListDateRangeFromYearList(
selectedYearList.value,
selectedYear.value
);
const parmas = { const parmas = {
id: router.currentRoute._value.params.id, thinkTankId,
startDate: getDateYearsAgo(selectedYear.value), startDate,
sortFun: sort.value, endDate,
currentPage: currentPage.value, pageNum: currentPage.value,
pageSize: 10, pageSize: 10,
researchTypeIds: selectedAreaList.value.join(",")
sortOrder: sort.value === true ? "asc" : "desc",
sortFun: sort.value === true,
keyword: searchPolicy.value
}; };
if (domainIds) {
parmas.domainIds = domainIds;
}
if (orgIds) {
parmas.orgIds = orgIds;
}
const res = await getThinkPolicy(parmas); const res = await getThinkPolicy(parmas);
console.log("智库政策", res); console.log("智库政策", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
policyList.value = res.data.content.map(item => ({ const list = Array.isArray(res.data.content) ? res.data.content : [];
...item, policyList.value = list.map((row) => mapPolicyRowToView(row));
relationBillsList: relationBillsList.value, total.value = res.data.totalElements ?? res.data.total ?? 0;
relationAdList: relationAdList.value } else {
})); // 业务失败(如 code 500、data 为 null)时清空列表,避免仍展示上一次请求的数据
total.value = res.data.totalElements; policyList.value = [];
total.value = 0;
} }
} catch (error) { } catch (error) {
console.error("获取智库政策error", error); console.error("获取智库政策error", error);
policyList.value = [];
total.value = 0;
} }
}; };
watch( watch(
() => selectedAreaList.value, () => selectedYear.value,
val => { () => {
currentPage.value = 1;
handleGetThinkPolicy(); handleGetThinkPolicy();
} }
); );
...@@ -646,6 +1270,7 @@ onMounted(() => { ...@@ -646,6 +1270,7 @@ onMounted(() => {
handleGetThinkPolicyIndustryTotal(); handleGetThinkPolicyIndustryTotal();
handleGetThinkPolicyIndustryChange(); handleGetThinkPolicyIndustryChange();
handleGetHylyList(); handleGetHylyList();
handleGetGovAgencyList();
handleGetThinkPolicy(); handleGetThinkPolicy();
handleGetPolicyAdviceDeptDistribution(); handleGetPolicyAdviceDeptDistribution();
}); });
...@@ -656,6 +1281,11 @@ onMounted(() => { ...@@ -656,6 +1281,11 @@ onMounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
.policy-tracking-el-empty {
padding: 0;
margin: 0;
}
.top { .top {
height: 420px; height: 420px;
width: 1600px; width: 1600px;
...@@ -675,7 +1305,8 @@ onMounted(() => { ...@@ -675,7 +1305,8 @@ onMounted(() => {
.box-header { .box-header {
width: 520px; width: 520px;
height: 48px; height: 48px;
border-bottom: 1px solid rgba(234, 236, 238, 1); box-sizing: border-box;
border-bottom: 1px solid rgba(240, 242, 244, 1);
display: flex; display: flex;
position: relative; position: relative;
...@@ -695,8 +1326,8 @@ onMounted(() => { ...@@ -695,8 +1326,8 @@ onMounted(() => {
margin-top: 11px; margin-top: 11px;
margin-left: 18px; margin-left: 18px;
height: 26px; height: 26px;
color: rgba(5, 95, 194, 1); color: rgba(20, 89, 187, 1);
font-family: Microsoft YaHei; font-family: "Source Han Sans CN";
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
line-height: 26px; line-height: 26px;
...@@ -798,113 +1429,151 @@ onMounted(() => { ...@@ -798,113 +1429,151 @@ onMounted(() => {
} }
} }
/* 与智库概览 box6-main 一致:主区高度 372、底边距留给 TipTab + AI */
.box-main { .box-main {
height: 372px;
width: 520px; width: 520px;
height: 372px;
box-sizing: border-box; box-sizing: border-box;
padding: 24px 24px 64px 24px;
position: relative;
padding: 24px 24px 0; &.chart-main--empty {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
#box1Chart { :deep(.el-empty__image) {
height: 309px; margin-bottom: 0;
width: 472px;
} }
#box2Chart {
height: 309px;
width: 472px;
} }
#box3Chart { #box1Chart,
height: 309px; #box2Chart {
width: 472px; width: 472px;
height: 284px;
} }
.source { .source {
margin: 0 auto; position: absolute;
margin-top: 10px; bottom: 21px;
left: 50%;
/* 上下0,左右自动居中 */ transform: translateX(-50%);
width: 370px; width: 350px;
height: 22px; height: 22px;
display: flex; display: flex;
.info {
width: 16px;
height: 16px;
margin-top: 3px;
img {
width: 100%;
height: 100%;
} }
.chart-box {
position: absolute;
right: 0;
bottom: 18px;
.btn-box {
width: 74px;
height: 28px;
} }
.text { .content-box {
font-family: "Source Han Sans CN"; width: 520px;
font-weight: 400; position: absolute;
font-size: 14px; right: 0;
line-height: 22px; bottom: -18px;
letter-spacing: 0px;
text-align: left;
color: rgb(132, 136, 142);
margin-left: 8px;
} }
} }
} }
.box3-main { .box3-main {
height: 372px;
width: 520px; width: 520px;
height: 372px;
box-sizing: border-box;
padding: 24px 24px 64px 24px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-sizing: border-box;
padding-top: 24px;
&.chart-main--empty {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
overflow: visible;
:deep(.el-empty__image) {
margin-bottom: 0;
}
}
.box3-chart-row {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
width: 100%;
box-sizing: border-box;
}
#box3Chart { .box3-legend-nav-btn {
height: 300px; flex-shrink: 0;
width: 520px; 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;
} }
.source { .box3-legend-nav-btn:hover:not(:disabled) {
margin: 0 auto; border-color: var(--color-main-primary, #055fc2);
margin-top: 10px; color: var(--color-main-primary, #055fc2);
}
/* 上下0,左右自动居中 */ .box3-legend-nav-btn:disabled {
width: 370px; opacity: 0.35;
height: 22px; cursor: not-allowed;
display: flex; }
.info { .box3-chart-canvas {
width: 16px; flex: 1;
height: 16px; min-width: 0;
margin-top: 3px; width: 472px;
height: 284px;
}
img { .source {
width: 100%; position: absolute;
height: 100%; bottom: 21px;
left: 50%;
transform: translateX(-50%);
width: 350px;
height: 22px;
display: flex;
} }
.chart-box {
position: absolute;
right: 0;
bottom: 18px;
.btn-box {
width: 74px;
height: 28px;
} }
.text { .content-box {
font-family: "Source Han Sans CN"; width: 520px;
font-weight: 400; position: absolute;
font-size: 14px; right: 0;
line-height: 22px; bottom: -18px;
letter-spacing: 0px;
text-align: left;
color: rgb(132, 136, 142);
margin-left: 8px;
} }
} }
} }
...@@ -1134,6 +1803,120 @@ onMounted(() => { ...@@ -1134,6 +1803,120 @@ onMounted(() => {
} }
} }
.select-box-department {
margin-top: 16px;
.select-box-header {
display: flex;
.title {
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
}
}
.select-main {
margin-top: 12px;
}
.checkbox-group {
display: grid;
gap: 8px;
margin-left: 24px;
.filter-checkbox {
height: 24px;
margin: 0;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
}
.paixu-btn {
display: flex;
width: 120px;
height: 32px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
&:hover {
background: var(--color-bg-hover);
}
cursor: pointer;
.icon1 {
width: 11px;
height: 14px;
margin-top: 10px;
margin-left: 9px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 19px;
color: rgba(95, 101, 108, 1);
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 {
width: 10px;
height: 5px;
margin-top: 5px;
margin-left: 13px;
img {
width: 100%;
height: 100%;
}
}
}
.icon {
margin-top: 4px;
width: 8px;
height: 16px;
background: var(--color-main-active);
border-radius: 0 4px 4px 0;
}
.title {
color: rgba(5, 95, 194, 1);
margin-left: 17px;
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
}
}
.select-box-publish { .select-box-publish {
margin-top: 16px; margin-top: 16px;
...@@ -1251,7 +2034,7 @@ onMounted(() => { ...@@ -1251,7 +2034,7 @@ onMounted(() => {
.right { .right {
width: 1224px; width: 1224px;
max-height: 1670px; min-height: 1670px;
margin-bottom: 20px; margin-bottom: 20px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1); border: 1px solid rgba(234, 236, 238, 1);
...@@ -1277,6 +2060,7 @@ onMounted(() => { ...@@ -1277,6 +2060,7 @@ onMounted(() => {
padding-left: 37px; padding-left: 37px;
padding-right: 36px; padding-right: 36px;
width: calc(100% + 37px - 36px); width: calc(100% + 37px - 36px);
height: 153px;
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
display: flex; display: flex;
...@@ -1287,6 +2071,7 @@ onMounted(() => { ...@@ -1287,6 +2071,7 @@ onMounted(() => {
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
} }
} }
...@@ -1298,7 +2083,7 @@ onMounted(() => { ...@@ -1298,7 +2083,7 @@ onMounted(() => {
.title { .title {
// height: 24px; // height: 24px;
color: rgba(59, 65, 75, 1); color: rgb(59, 65, 75);
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
...@@ -1310,6 +2095,10 @@ onMounted(() => { ...@@ -1310,6 +2095,10 @@ onMounted(() => {
.info { .info {
margin-top: 7px; margin-top: 7px;
height: 22px; height: 22px;
display: flex;
.text {
color: rgba(95, 101, 108, 1); color: rgba(95, 101, 108, 1);
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
font-size: 14px; font-size: 14px;
...@@ -1317,7 +2106,14 @@ onMounted(() => { ...@@ -1317,7 +2106,14 @@ onMounted(() => {
line-height: 22px; line-height: 22px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
display: flex; width: 1020px;
white-space: nowrap;
/* 强制不换行,保持一行 */
overflow: hidden;
/* 超出隐藏 */
text-overflow: ellipsis;
/* 超出显示 ... */
}
.more { .more {
width: 16px; width: 16px;
......
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { size, split } from 'lodash'
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => { /** 政策追踪「研究领域变化趋势」图例分页:每页条数(与概览数量变化趋势逻辑一致,条数按产品要求为 4) */
export const POLICY_TRACKING_LEGEND_PAGE_SIZE = 4
const colorList = [
'rgba(5, 95, 194, 1)',
'rgba(19, 168, 168, 1)',
'rgba(250, 140, 22, 1)',
'rgba(114, 46, 209, 1)',
'rgba(115, 209, 61, 1)',
'rgba(206, 79, 81, 1)',
'rgba(145, 202, 255, 1)',
'rgba(95, 101, 108, 1)',
'rgba(250, 84, 28, 1)',
'rgba(47, 84, 235, 1)',
'rgba(64, 150, 255, 1)',
'rgba(34, 41, 52, 1)',
'rgba(173, 198, 255, 1)',
'rgba(255, 169, 64, 1)'
]
const parseRgba = (colorStr) => {
const match = colorStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/)
if (match) {
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
a: parseFloat(match[4])
}
}
return { r: 0, g: 0, b: 0, a: 1 }
}
/**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} chartInput
* @param {{ legendShowCount?: number, legendPageIndex?: number }} [options]
*/
const getMultiLineChart = (chartInput, options = {}) => {
const title = chartInput.title
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 pageCount = Math.max(1, Math.ceil(allNames.length / legendShowCount))
const legendPageIndex = Math.min(Math.max(0, rawPageIndex), pageCount - 1)
const legendStart = legendPageIndex * legendShowCount
const legendData = allNames.slice(legendStart, legendStart + legendShowCount)
const xCount = Array.isArray(title) ? title.length : 0
const labelFontSize = xCount > 8 ? 10 : xCount > 5 ? 11 : 12
const labelRotate = xCount > 6 ? 28 : 0
const echartsSeries = series.map((item, index) => {
const baseColor =
item.color ||
colorList[index % colorList.length] ||
`rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)`
const { r, g, b } = parseRgba(baseColor)
return {
name: item.name,
type: 'line',
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `rgba(${r}, ${g}, ${b}, 0.3)` },
{ offset: 1, color: `rgba(${r}, ${g}, ${b}, 0)` }
])
},
symbolSize: 8,
emphasis: { focus: 'series' },
data: item.value
}
})
return { return {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
...@@ -12,91 +89,77 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => { ...@@ -12,91 +89,77 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
} }
} }
}, },
/* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */
grid: { grid: {
top: '15%', top: 50,
right: '5%', right: 10,
bottom: '5%', bottom: 0,
left: '5%', left: 20,
containLabel: true containLabel: true
}, },
legend: { legend: {
icon:'circle',
show: true, show: true,
top: 10, type: 'plain',
left:'10%', data: legendData,
top: 4,
left: 'center',
icon: 'circle',
textStyle: { textStyle: {
fontSize: 16,
fontFamily: 'Source Han Sans CN', fontFamily: 'Source Han Sans CN',
fontWeight: 400, fontWeight: 400,
fontSize: 14,
lineHeight: 24, lineHeight: 24,
letterSpacing: 0, letterSpacing: 0,
align: 'left', align: 'left',
color:'rgb(95, 101, 108)' color: 'rgb(95, 101, 108)'
}, },
itemWidth:12, itemWidth: 12,
itemHeight:12, itemHeight: 12
}, },
color: colorList,
xAxis: [ xAxis: [
{ {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: dataX, data: title,
axisLine: { axisLine: {
lineStyle: { lineStyle: {
color: 'rgb(231, 243, 255)', color: 'rgb(231, 243, 255)'
}, }
}, },
axisLabel: { axisLabel: {
color: 'rgb(132, 136, 142)', color: 'rgb(132, 136, 142)',
fontFamily: 'Microsoft YaHei', fontFamily: 'Source Han Sans CN',
fontWeight: 400, fontWeight: 400,
fontSize:12, fontSize: labelFontSize,
interval: 0,
hideOverlap: false,
rotate: labelRotate,
}, margin: 6
}
} }
], ],
yAxis: [ yAxis: [
{ {
type: 'value', type: 'value',
splitLine:{ splitNumber: 4,
show:true, axisLabel: {
lineStyle:{ color: 'rgb(132, 136, 142)',
color:"rgb(231, 243, 255)", fontFamily: 'Source Han Sans CN',
type:'dashed' fontWeight: 400,
fontSize: 11,
margin: 6
},
splitLine: {
show: true,
lineStyle: {
color: 'rgb(231, 243, 255)',
type: 'dashed'
} }
} }
} }
], ],
series: [ series: echartsSeries
{
name: '人工智能',
type: 'line',
emphasis: {
focus: 'series'
},
data: dataY1
},
{
name: '集成电路',
type: 'line',
emphasis: {
focus: 'series'
},
data: dataY2
},
{
name: '量子科技',
type: 'line',
emphasis: {
focus: 'series'
},
data: dataY3
}
]
} }
} }
......
...@@ -180,7 +180,7 @@ ...@@ -180,7 +180,7 @@
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="box"> <div class="box box-core-researchers">
<!-- <div class="box-header"> <!-- <div class="box-header">
<div class="header-left"></div> <div class="header-left"></div>
<div class="title">核心研究人员</div> <div class="title">核心研究人员</div>
...@@ -213,16 +213,17 @@ ...@@ -213,16 +213,17 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<AnalysisBox title="核心研究人员"> <AnalysisBox title="核心研究人员" width="1104px" height="900px">
<div class="box3-main"> <div class="box3-main">
<div class="box3-main-left"> <div class="box3-main-top">
<div id="box3Chart"></div> <div id="box3Chart"></div>
</div>
<div class="source"> <div class="source">
<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> <div class="box3-main-bottom">
<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="" />
...@@ -244,7 +245,7 @@ ...@@ -244,7 +245,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted, nextTick } 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";
...@@ -602,7 +603,7 @@ const handleBox3Chart = () => { ...@@ -602,7 +603,7 @@ const handleBox3Chart = () => {
const box3Chart = getTreeMapChart(treemapData); const box3Chart = getTreeMapChart(treemapData);
delete box3Chart.series[0].itemStyle; delete box3Chart.series[0].itemStyle;
box3Chart.series[0].sort = false; box3Chart.series[0].sort = false;
// 图表充满 box3-main-left,左右各留 12px // 图表充满 box3-main-top,左右各留 12px
box3Chart.series[0].left = 12; box3Chart.series[0].left = 12;
box3Chart.series[0].right = 12; box3Chart.series[0].right = 12;
box3Chart.series[0].top = 0; box3Chart.series[0].top = 0;
...@@ -610,11 +611,10 @@ const handleBox3Chart = () => { ...@@ -610,11 +611,10 @@ const handleBox3Chart = () => {
// 方块内文字与区域标题统一为黑色,方块内文字加 1px 白色描边(等效 CSS text-stroke: 1px #fff) // 方块内文字与区域标题统一为黑色,方块内文字加 1px 白色描边(等效 CSS text-stroke: 1px #fff)
box3Chart.series[0].label = { box3Chart.series[0].label = {
...box3Chart.series[0].label, ...box3Chart.series[0].label,
color: 'rgb(51,51,51)', color: 'white',
textBorderColor: '#fff',
textBorderWidth: 0.7, fontSize: 16,
// 可选:白色阴影兜底
textShadowColor: '#fff',
textShadowBlur: 0, textShadowBlur: 0,
textShadowOffsetX: 0, textShadowOffsetX: 0,
textShadowOffsetY: 0 textShadowOffsetY: 0
...@@ -739,7 +739,9 @@ onMounted(() => { ...@@ -739,7 +739,9 @@ onMounted(() => {
handleGetThinkTankResearchAreae() handleGetThinkTankResearchAreae()
handleGetThinkPerson() handleGetThinkPerson()
nextTick(() => {
handleBox3Chart(); handleBox3Chart();
});
}); });
</script> </script>
...@@ -876,7 +878,8 @@ onMounted(() => { ...@@ -876,7 +878,8 @@ onMounted(() => {
.right { .right {
width: 1104px; width: 1104px;
height: 1245px; /* 三栏:390 + 390 + 900,间距 16×2 */
height: 1712px;
margin-top: 16px; margin-top: 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -885,6 +888,7 @@ onMounted(() => { ...@@ -885,6 +888,7 @@ onMounted(() => {
.box { .box {
width: 1104px; width: 1104px;
height: 390px; height: 390px;
flex-shrink: 0;
// box-sizing: border-box; // box-sizing: border-box;
// border: 1px solid rgba(234, 236, 238, 1); // border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px; // border-radius: 10px;
...@@ -1230,17 +1234,22 @@ onMounted(() => { ...@@ -1230,17 +1234,22 @@ onMounted(() => {
.box3-main { .box3-main {
display: flex; display: flex;
flex-direction: column;
.box3-main-left { .box3-main-top {
width: 536px; width: 1100px;
height: 326px; height: 372px;
margin-left: 9px;
box-sizing: border-box;
box-sizing: border-box;
padding-left: 35px;
#box3Chart { #box3Chart {
width: 536px; width: 1035px;
height: 290px; height: 372px;
}
} }
.source { .source {
...@@ -1278,11 +1287,9 @@ onMounted(() => { ...@@ -1278,11 +1287,9 @@ onMounted(() => {
} }
} }
} .box3-main-bottom {
.box3-main-right {
margin-left: 28px; margin-left: 28px;
margin-top: 12px; margin-top: 24px;
width: 536px; width: 536px;
height: 326px; height: 326px;
...@@ -1356,9 +1363,15 @@ onMounted(() => { ...@@ -1356,9 +1363,15 @@ onMounted(() => {
} }
} }
} }
} }
.box.box-core-researchers {
width: 1104px;
height: 900px;
flex-shrink: 0;
}
} }
} }
</style> </style>
\ No newline at end of file
<template>
<div class="tip-wrapper">
<div class="icon">
<img src="./tip-icon.svg" alt="">
</div>
<div class="text text-tip-2 text-primary-50-clor">{{ `${text}` }}</div>
</div>
</template>
<script setup>
const props = defineProps({
dataSource: {
type: String,
default: '美国国会官网'
},
dataTime: {
type: String,
default: '2023.1至2025.12'
},
text: {
type: String,
default: ''
},
})
</script>
<style lang="scss" scoped>
.tip-wrapper {
width: 100%;
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
height: 22px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
}
</style>
\ No newline at end of file
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
<rect id="容器 704" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<circle id="椭圆 96" cx="8" cy="8" r="7" fill="rgb(230,231,232)" />
<circle id="椭圆 97" cx="8" cy="4" r="1" fill="rgb(132,136,142)" />
<path id="矩形 241" d="M6.49996 6L8.00028 6.0004C8.55256 6.0004 9.00028 6.44811 9.00028 7.00039L9.00028 10.4992C9.00028 10.7754 9.22408 10.9989 9.50033 10.9992L9.50033 10.9997C9.77657 10.9998 10.0005 11.2236 10.0005 11.4998L10.0003 11.5001C10.0002 11.7765 9.77622 12.0006 9.49978 12.0006L8.00028 12.0004L6.50033 12.0004C6.22423 12.0004 6.00064 11.7767 6.00049 11.5006L6.00021 11.5005C6.00021 11.2243 6.22418 11.0003 6.50037 11.0003L6.50037 11.0006C6.77649 11.0007 7.00042 10.7766 7.00042 10.5005L7.00017 7.50005C7.00017 7.22376 6.77644 7.00047 6.50015 7.00002L6.49946 6.99922C6.22357 6.999 6 6.77565 6 6.49976C6.00011 6.22373 6.22393 6 6.49996 6Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</svg>
<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" @keyup.enter="handleGetThinkTankList()">
<template #suffix>
<img src="../assets/images/Line_Search.png" class="search-icon" alt="搜索"
@click="handleGetThinkTankList()">
</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="toggleSort()"
:key="true" label="正序" :value="true" v-if="sort === true" />
<img src="../assets/images/sort-desc.png" class="select-prefix-img" alt="" @click.stop="toggleSort()"
:key="false" label="倒序" :value="false" v-if="sort === false" />
</template>
<el-option :key="true" label="正序" :value="true" />
<el-option :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 class="time-tab-pane">
<TimeTabPane @time-click="handleTimeClick" />
</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="red-info" v-if="item.increaseReportNumber != 0 && item.increaseReportNumber != null">{{ "+" }}{{
item.increaseReportNumber }}</div>
<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";
import TimeTabPane from '@/components/base/TimeTabPane/index.vue'
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);
// 只有选择“倒序(false)”才反转;初始为 null 时走“正序”规则
return sort.value === false ? an - bn : bn - an;
});
});
// el-pagination 是 1-based
const currentPage = ref(1)
const pageSize = ref(15)
const total = ref(0)
const timePeriod = ref("WEEK")
const handleTimeClick = item => {
const time = item?.time
if (time === "近一周") {
timePeriod.value = "WEEK"
} else if (time === "近一月") {
timePeriod.value = "MONTH"
} else if (time === "近一年") {
timePeriod.value = "YEAR"
}
// 切换时间范围后重新拉取(从第一页开始)
currentPage.value = 1
handleGetThinkTankList()
}
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,
keyword: searchPolicy.value,
timePeriod: timePeriod.value
});
console.log("智库列表", res);
cardList.value = [];
total.value = 0;
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);
cardList.value = [];
total.value = 0;
}
};
// 初始为 null:el-select 显示 placeholder;但排序仍按“正序”规则(见 sortedCardList)
const sort = ref(null);
const toggleSort = () => {
sort.value = sort.value === false ? true : false
};
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;
justify-content: space-between;
.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);
}
}
.time-tab-pane {
width: 248px;
height: 36px;
}
}
.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;
position: relative;
.red-info {
color: rgb(255, 255, 255);
display: inline-flex;
position: absolute;
left: 277px;
bottom: 208px;
background-color: rgba(255, 77, 79, 1);
align-items: center;
justify-content: center;
border-radius: 50px;
border: 1px solid rgb(255, 255, 255);
width: auto;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
padding: 2px 8px;
/* 左右留空隙,更美观 */
white-space: nowrap;
z-index: 9999;
}
.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,23 @@ ...@@ -42,9 +42,23 @@
<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 class="time-tab-pane">
<TimeTabPane @time-click="handleTimeClick" />
</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="red-info" v-if="item.increaseReportNumber != 0 && item.increaseReportNumber != null">{{ "+" }}{{
item.increaseReportNumber }}</div>
<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 +66,8 @@ ...@@ -52,9 +66,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 +87,12 @@ ...@@ -74,9 +87,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>
...@@ -160,21 +176,50 @@ ...@@ -160,21 +176,50 @@
<div class="box5-header-icon"> <div class="box5-header-icon">
<img src="./assets/images/box3-header-icon.png" alt="" /> <img src="./assets/images/box3-header-icon.png" alt="" />
</div> </div>
<div class="box5-header-title">{{ "政策建议趋势分布" }}</div> <div class="box5-header-title">{{ "数量变化趋势" }}</div>
</div> </div>
<div class="box5-select-box"> <div class="box5-select-box">
<el-select v-model="box5selectetedMonths" placeholder="选择时间" style="width: 120px"> <el-select v-model="box5selectetedArea" placeholder="选择领域" style="width: 120px"
<el-option v-for="item in box5MonthsList" :key="item.value" :label="item.label" :value="item.value" @change="handleBox5AreaChange">
@click="changeBox5Data(item.value)" /> <el-option label="全部领域" value="全部领域" />
<el-option v-for="item in box5RawData.data" :key="item.name" :label="item.name" :value="item.name" />
</el-select>
<el-select v-model="box5selectetedYear" placeholder="选择年份" style="width: 120px"
@change="changeBox5Data">
<el-option v-for="item in box5YearList" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</div>
</div> </div>
<div class="box5-main" :class="{ 'box5-main--empty': !hasBox5ChartData }">
<template v-if="!hasBox5ChartData">
<el-empty class="box5-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<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>
<button v-if="box5LegendShowNav" type="button" class="box5-legend-nav-btn"
:disabled="!box5LegendCanNext" aria-label="下一页图例" @click="handleBox5LegendNext">
</button>
</div> </div>
<div class="box5-main">
<div id="box5Chart"></div>
<div class="source"> <div class="source">
<div class="info"><img src="./assets/images/image-exclamation.png"></div> <TipTab :text="'智库报告数量变化趋势,数据来源:美国各智库官网'" />
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> </div>
<div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div> </div>
<div class="content-box" v-if="isShowAiContentBox5"
@mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" />
</div>
</div>
</template>
</div> </div>
</div> </div>
<div class="box6"> <div class="box6">
...@@ -182,20 +227,39 @@ ...@@ -182,20 +227,39 @@
<div class="header-icon"> <div class="header-icon">
<img src="./assets/images/box6-header-icon.png" alt="" /> <img src="./assets/images/box6-header-icon.png" alt="" />
</div> </div>
<div class="header-title">{{ "政策建议领域分布" }}</div> <div class="header-title">{{ "领域分布情况" }}</div>
<div class="box6-select-box"> <div class="box6-select-box">
<el-select v-model="box6selectetedTank" placeholder="选择智库" style="width: 120px">
<el-option label="全部智库" value="全部智库" />
<el-option v-for="item in box6TankList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="box6selectetedYear" placeholder="选择时间" style="width: 120px"> <el-select v-model="box6selectetedYear" placeholder="选择时间" style="width: 120px">
<el-option v-for="item in box6YearList" :key="item.value" :label="item.label" :value="item.value" <el-option v-for="item in box6YearList" :key="item.value" :label="item.label" :value="item.value"
@click="handleBox6()" /> @click="handleBox6()" />
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box6-main"> <div class="box6-main" :class="{ 'box6-main--empty': !hasBox6ChartData }">
<template v-if="!hasBox6ChartData">
<el-empty class="box6-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div id="box6Chart"></div> <div id="box6Chart"></div>
<div class="source"> <div class="source">
<div class="info"><img src="./assets/images/image-exclamation.png"></div> <TipTab :text="'智库报告领域分布情况,数据来源:美国各智库官网'" />
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> </div>
<div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentBox6" @mouseenter="handleSwitchAiContentShowBox6(true)">
<AiButton />
</div>
<div class="content-box" v-if="isShowAiContentBox6"
@mouseleave="handleSwitchAiContentShowBox6(false)">
<AiPane :aiContent="aiContentBox6" />
</div>
</div> </div>
</template>
</div> </div>
</div> </div>
</div> </div>
...@@ -209,7 +273,26 @@ ...@@ -209,7 +273,26 @@
<div class="box7-header-title">{{ "智库资金流向" }}</div> <div class="box7-header-title">{{ "智库资金流向" }}</div>
</div> </div>
</div> </div>
<div class="box7-main" id="box7Chart"></div> <div class="box7-main" :class="{ 'box7-main--empty': !hasBox7ChartData }">
<template v-if="!hasBox7ChartData">
<el-empty class="box7-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div id="box7Chart"></div>
<div class="source">
<TipTab :text="'美国科技智库与主要政府机构之间的资金往来,数据来源:美国各智库官网'" />
</div>
<div class="chart-box">
<div class="btn-box" v-if="!isShowAiContentBox7" @mouseenter="handleSwitchAiContentShowBox7(true)">
<AiButton />
</div>
<div class="content-box" v-if="isShowAiContentBox7"
@mouseleave="handleSwitchAiContentShowBox7(false)">
<AiPane :aiContent="aiContentBox7" />
</div>
</div>
</template>
</div>
</div> </div>
<div class="box8"> <div class="box8">
<div class="box8-header"> <div class="box8-header">
...@@ -219,14 +302,10 @@ ...@@ -219,14 +302,10 @@
</div> </div>
<div class="box8-header-title">{{ "智库研究热点" }}</div> <div class="box8-header-title">{{ "智库研究热点" }}</div>
</div> </div>
<div class="box8-select-box">
<el-select v-model="box8selectetedYear" placeholder="选择时间" style="width: 120px">
<el-option v-for="item in box8YearList" :key="item.value" :label="item.label" :value="item.value"
@click="changeBox8Data(item.value)" />
</el-select>
</div>
</div> </div>
<div class="box8-main"> <div class="box8-main">
<div class="box8-main-item">
<div class="box8-item" v-for="(item, index) in box8Data" :key="index"> <div class="box8-item" v-for="(item, index) in box8Data" :key="index">
<div class="item-left" <div class="item-left"
:class="{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }"> :class="{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }">
...@@ -244,6 +323,9 @@ ...@@ -244,6 +323,9 @@
<!-- <div class="item-right">{{ `${item.count}份报告 >` }}</div> --> <!-- <div class="item-right">{{ `${item.count}份报告 >` }}</div> -->
<div class="item-count">{{ item.count + "份报告 >" }}</div> <div class="item-count">{{ item.count + "份报告 >" }}</div>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -331,6 +413,7 @@ import { ...@@ -331,6 +413,7 @@ import {
getThinkTankPolicyIndustryChange, getThinkTankPolicyIndustryChange,
getThinkTankPolicyIndustry, getThinkTankPolicyIndustry,
getThinkTankDonation, getThinkTankDonation,
getAllThinkTankList,
getThinkTankHot, getThinkTankHot,
getNewReport, getNewReport,
getHylyList, getHylyList,
...@@ -340,9 +423,10 @@ import { ...@@ -340,9 +423,10 @@ 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 from "./utils/multiLineChart"; import getMultiLineChart, { MULTILINE_LEGEND_SHOW_COUNT } 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 defaultNewsIcon from "@/assets/icons/default-icon-news.png"; import defaultNewsIcon from "@/assets/icons/default-icon-news.png";
import defaultHeaderIcin from "@/assets/icons/default-icon1.png"; import defaultHeaderIcin from "@/assets/icons/default-icon1.png";
import News1 from "./assets/images/news1.png"; import News1 from "./assets/images/news1.png";
...@@ -350,6 +434,9 @@ import News2 from "./assets/images/news2.png"; ...@@ -350,6 +434,9 @@ import News2 from "./assets/images/news2.png";
import News3 from "./assets/images/news3.png"; import News3 from "./assets/images/news3.png";
import News4 from "./assets/images/news4.png"; import News4 from "./assets/images/news4.png";
import News5 from "./assets/images/news5.png"; import News5 from "./assets/images/news5.png";
import AiButton from '@/components/base/Ai/AiButton/index.vue'
import AiPane from '@/components/base/Ai/AiPane/index.vue'
import TipTab from "./TipTab/index.vue";
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";
...@@ -378,8 +465,79 @@ import Box1Img from "./assets/images/box1-img.png"; ...@@ -378,8 +465,79 @@ 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 isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
}
};
const isShowAiContentBox6 = ref(false);
const aiContentBox6 = ref("");
const isBox6InterpretLoading = ref(false);
const handleSwitchAiContentShowBox6 = (val) => {
isShowAiContentBox6.value = val;
if (val) {
fetchBox6ChartInterpretation();
}
};
const isShowAiContentBox7 = ref(false);
const aiContentBox7 = ref("");
const isBox7InterpretLoading = ref(false);
const handleSwitchAiContentShowBox7 = (val) => {
isShowAiContentBox7.value = val;
if (val) {
fetchBox7ChartInterpretation();
}
};
const handleGetAllThinkTankList = async () => {
try {
const res = await getAllThinkTankList({
// 后端通常是 0-based,这里做一次转换
currentPage: currentPage.value - 1,
pageSize: pageSize.value,
timePeriod: timePeriod.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 timePeriod = ref("WEEK")
const handleTimeClick = item => {
const time = item?.time
if (time === "近一周") {
timePeriod.value = "WEEK"
} else if (time === "近一月") {
timePeriod.value = "MONTH"
} else if (time === "近一年") {
timePeriod.value = "YEAR"
}
// 切换时间范围后重新拉取(从第一页开始)
currentPage.value = 1
handleGetAllThinkTankList()
// 同步刷新首页卡片数据(包含 increaseReportNumber)
handleGetThinkTankList()
}
const getStatCountInfo = async () => { const getStatCountInfo = async () => {
...@@ -446,14 +604,31 @@ const cardList = ref([ ...@@ -446,14 +604,31 @@ 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 {
const res = await getThinkTankList(); const res = await getAllThinkTankList({
// 后端通常是 0-based,这里做一次转换
currentPage: currentPage.value - 1,
pageSize: pageSize.value,
timePeriod: timePeriod.value
});
console.log("智库列表", res); console.log("智库列表", res);
cardList.value = [];
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
cardList.value = res.data.map(item => { // 兼容两种返回结构:
return { // 1) data.content(分页)
// 2) data 直接是数组(非分页)
const list = Array.isArray(res.data) ? res.data : (res.data?.content || []);
cardList.value = list.map(item => ({
id: item.id, id: item.id,
logo: item.imageUrl, logo: item.imageUrl,
name: item.name, name: item.name,
...@@ -461,9 +636,9 @@ const handleGetThinkTankList = async () => { ...@@ -461,9 +636,9 @@ const handleGetThinkTankList = async () => {
country: item.country, country: item.country,
desc: item.describe, desc: item.describe,
rank: item.id, rank: item.id,
reportNumber: item.reportNumber reportNumber: item.reportNumber,
}; increaseReportNumber: item.increaseReportNumber
}); }));
} }
} catch (error) { } catch (error) {
console.error("获取智库列表error", error); console.error("获取智库列表error", error);
...@@ -711,23 +886,15 @@ const handleGetThinkTankReportRemarks = async () => { ...@@ -711,23 +886,15 @@ const handleGetThinkTankReportRemarks = async () => {
} }
}; };
// 政策建议趋势分布 // 政策建议趋势分布(接口全量,按领域筛选后用于绘图)
const box5Data = ref({ const box5RawData = ref({
// title: ["2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025"], title: [],
// data: [ data: []
// { });
// name: "人工智能", const hasBox5ChartData = computed(() => {
// value: [73, 32, 42, 48, 38, 49, 63, 75, 70, 86, 95, 87] const v = box5RawData.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;
// name: "集成电路",
// value: [8, 3, 2, 8, 9, 10, 12, 18, 16, 18, 20, 22]
// },
// {
// name: "量子科技",
// value: [1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 3]
// }
// ]
}); });
//获取当前时间x年前的日期 //获取当前时间x年前的日期
...@@ -754,33 +921,34 @@ function getDateMonthsAgo(months) { ...@@ -754,33 +921,34 @@ function getDateMonthsAgo(months) {
const day = String(pastDate.getDate()).padStart(2, "0"); const day = String(pastDate.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
const box5selectetedMonths = ref(24);
const box5MonthsList = ref([ /** 自然年日期范围(传给 policyIndustryChange) */
{ const getBox5YearDateRange = year => {
label: "近3个月", const y = Number(year);
value: 3 return {
}, startDate: `${y}-01-01`,
{ endDate: `${y}-12-31`
label: "近6个月", };
value: 6 };
},
{ const box5selectetedYear = ref(2025);
label: "近12个月", const box5YearList = ref([
value: 12 { label: "2026", value: 2026 },
}, { label: "2025", value: 2025 },
{ { label: "2024", value: 2024 },
label: "近24个月", { label: "2023", value: 2023 },
value: 24 { label: "2022", value: 2022 }
}
]); ]);
function changeBox5Data(value) { const box5selectetedArea = ref("全部领域");
handleBox5(getDateMonthsAgo(value));
} const changeBox5Data = year => {
handleBox5(year);
};
// 政策建议趋势分布 // 政策建议趋势分布
const handleGetThinkTankPolicyIndustryChange = async date => { const handleGetThinkTankPolicyIndustryChange = async range => {
try { try {
const res = await getThinkTankPolicyIndustryChange(date); const res = await getThinkTankPolicyIndustryChange(range);
console.log("政策建议趋势分布", res); console.log("政策建议趋势分布", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const originalData = res.data; const originalData = res.data;
...@@ -812,21 +980,153 @@ const handleGetThinkTankPolicyIndustryChange = async date => { ...@@ -812,21 +980,153 @@ const handleGetThinkTankPolicyIndustryChange = async date => {
}); });
result.data.push(industryData); result.data.push(industryData);
}); });
box5Data.value = result; box5RawData.value = result;
console.log(box5RawData.value.data, "box5RawData");
box5RawData.value.data.forEach(item => {
console.log(item.name);
});
} else { } else {
box5Data.value = []; box5RawData.value = { title: [], data: [] };
} }
} catch (error) { } catch (error) {
console.error("获取政策建议趋势分布error", error); console.error("获取政策建议趋势分布error", error);
box5RawData.value = { title: [], data: [] };
}
};
/** 按所选领域得到折线图数据(全部领域 = 全量序列) */
const box5ChartDisplayData = computed(() => {
const raw = box5RawData.value;
if (!raw || !Array.isArray(raw.title) || !Array.isArray(raw.data)) {
return { title: [], data: [] };
}
if (box5selectetedArea.value === "全部领域") {
return { title: raw.title, data: [...raw.data] };
}
const series = raw.data.filter(d => d.name === box5selectetedArea.value);
return {
title: raw.title,
data: series.length ? series : [...raw.data]
};
});
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 handleBox5 = async date => {
await handleGetThinkTankPolicyIndustryChange(date);
let box5Chart = box5Data.value ? getMultiLineChart(box5Data.value) : ""; const renderBox5Chart = () => {
const chartInput = box5ChartDisplayData.value;
if (!chartInput.title.length || !chartInput.data.length) {
return;
}
const totalNames = chartInput.data.length;
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 = () => {
aiContentBox5.value = "";
box5LegendPageIndex.value = 0;
renderBox5Chart();
};
const handleBox5 = async year => {
const y = year != null ? Number(year) : box5selectetedYear.value;
box5selectetedYear.value = y;
box5selectetedArea.value = "全部领域";
box5LegendPageIndex.value = 0;
await handleGetThinkTankPolicyIndustryChange(getBox5YearDateRange(y));
renderBox5Chart();
aiContentBox5.value = "";
};
/** 请求 box5 折线图解读并更新 aiContentBox5(支持流式 SSE) */
const fetchBox5ChartInterpretation = async () => {
const v = box5ChartDisplayData.value;
if (!v || !Array.isArray(v.title) || v.title.length === 0 || !Array.isArray(v.data) || v.data.length === 0) {
aiContentBox5.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentBox5.value &&
aiContentBox5.value !== "解读生成中…" &&
aiContentBox5.value !== "解读加载失败" &&
aiContentBox5.value !== "暂无图表数据";
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
type: "折线图",
name: "数量变化趋势",
data: v.title.map((label, i) => {
const point = { period: label };
v.data.forEach((s) => {
point[s.name] = s.value[i] ?? 0;
});
return point;
})
};
try {
const text = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentBox5.value = chunk;
}
}
}
);
aiContentBox5.value = text || "未返回有效解读内容";
} catch (error) {
console.error("图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
} finally {
isBox5InterpretLoading.value = false;
}
};
// 政策建议领域分布 // 政策建议领域分布
const box6Data = ref([ const box6Data = ref([
{ {
...@@ -867,6 +1167,10 @@ const box6Data = ref([ ...@@ -867,6 +1167,10 @@ const box6Data = ref([
]); ]);
const box6selectetedYear = ref("2025"); const box6selectetedYear = ref("2025");
const box6YearList = ref([ const box6YearList = ref([
{
label: "2026",
value: "2026"
},
{ {
label: "2025", label: "2025",
value: "2025" value: "2025"
...@@ -884,6 +1188,94 @@ const box6YearList = ref([ ...@@ -884,6 +1188,94 @@ const box6YearList = ref([
value: "2022" value: "2022"
} }
]); ]);
const box6selectetedArea = ref("全部领域");
/** 领域分布:有接口返回的有效分项时才展示图表(与 box5 空态一致) */
const hasBox6ChartData = computed(() => {
const list = Array.isArray(box6Data.value) ? box6Data.value : [];
return list.length > 0;
});
/** 按所选领域得到饼图数据(全部领域 = 全部分块) */
const box6PieDisplayData = computed(() => {
const list = Array.isArray(box6Data.value) ? box6Data.value : [];
if (!list.length) {
return [];
}
if (box6selectetedArea.value === "全部领域") {
return list;
}
const filtered = list.filter(item => item.name === box6selectetedArea.value);
if (filtered.length) {
return filtered;
}
return [{ name: "暂无该领域数据", value: 1, color: "#E0E0E0" }];
});
/** 饼图解读用数据:排除占位项 */
const getBox6InterpretationSeries = () => {
const pie = box6PieDisplayData.value;
return pie.filter(
item => item.name !== "暂无数据" && item.name !== "暂无该领域数据"
);
};
/** 请求 box6 领域分布饼图解读(入参:{ text: JSON.stringify({ type, name, data }) },与柱状图示例同结构) */
const fetchBox6ChartInterpretation = async () => {
const series = getBox6InterpretationSeries();
if (!series.length) {
aiContentBox6.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentBox6.value &&
aiContentBox6.value !== "解读生成中…" &&
aiContentBox6.value !== "解读加载失败" &&
aiContentBox6.value !== "暂无图表数据";
if (hasValidContent || isBox6InterpretLoading.value) {
return;
}
isBox6InterpretLoading.value = true;
aiContentBox6.value = "解读生成中…";
const chartPayload = {
type: "饼图",
name: "领域分布情况",
year: box6selectetedYear.value,
data: series.map(item => ({
name: item.name,
value: item.value
}))
};
try {
const text = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentBox6.value = chunk;
}
}
}
);
aiContentBox6.value = text || "未返回有效解读内容";
} catch (error) {
console.error("领域分布图表解读请求失败", error);
aiContentBox6.value = "解读加载失败";
} finally {
isBox6InterpretLoading.value = false;
}
};
const box6AreaList = ref([
]);
const box6selectetedTank = ref("全部智库");
const box6TankList = ref([
{
label: "兰德公司",
value: "兰德公司"
}
]);
function transformToChartFormat(data) { function transformToChartFormat(data) {
// 预设颜色池(可按需修改或扩展) // 预设颜色池(可按需修改或扩展)
const colorPalette = ["#4096FF", "#FFA39E", "#ADC6FF", "#FFC069", "#B5F5EC", "#B37FEB", "#D6E4FF", "#FF8C8C", "#87E8DE"]; const colorPalette = ["#4096FF", "#FFA39E", "#ADC6FF", "#FFC069", "#B5F5EC", "#B37FEB", "#D6E4FF", "#FF8C8C", "#87E8DE"];
...@@ -912,52 +1304,49 @@ const handleGetThinkTankPolicyIndustry = async () => { ...@@ -912,52 +1304,49 @@ const handleGetThinkTankPolicyIndustry = async () => {
} }
} catch (error) { } catch (error) {
console.error("获取政策建议领域分布error", error); console.error("获取政策建议领域分布error", error);
box6Data.value = [];
} }
}; };
const renderBox6Chart = () => {
if (!hasBox6ChartData.value) {
return;
}
const pieData = box6PieDisplayData.value;
if (!pieData.length) {
return;
}
const box6Chart = getPieChart(pieData);
setChart(box6Chart, "box6Chart");
};
const handleBox6AreaChange = () => {
aiContentBox6.value = "";
renderBox6Chart();
};
const handleBox6 = async () => { const handleBox6 = async () => {
box6selectetedArea.value = "全部领域";
aiContentBox6.value = "";
await handleGetThinkTankPolicyIndustry(); await handleGetThinkTankPolicyIndustry();
const box6Chart = getPieChart(box6Data.value); renderBox6Chart();
setChart(box6Chart, "box6Chart");
}; };
// 智库资金流向 // 智库资金流向
const box7Data = ref({ const box7Data = ref({
nodes: [ nodes: [],
{ name: "美国", itemStyle: { color: "#1f5cb8" } }, links: []
{ name: "英国", itemStyle: { color: "#6f42c1" } }, });
{ name: "加拿大", itemStyle: { color: "#d9534f" } },
// 中间层 /** 资金流向:有节点与连线时才展示桑基图 */
{ name: "美国能源部", itemStyle: { color: "#a4c639" } }, const hasBox7ChartData = computed(() => {
{ name: "美国财政部", itemStyle: { color: "#f06292" } }, const raw = box7Data.value;
{ name: "美国国务院", itemStyle: { color: "#b08d55" } }, if (!raw || typeof raw !== "object") {
{ name: "美国国际开发署", itemStyle: { color: "#4285f4" } }, return false;
{ name: "美国内政部", itemStyle: { color: "#8cae9e" } }, }
{ name: "美国国家民主基金会", itemStyle: { color: "#a1887f" } }, const nodes = Array.isArray(raw.nodes) ? raw.nodes : [];
// 右侧层 (部分) const links = Array.isArray(raw.links) ? raw.links : [];
{ name: "麻省理工学院科技评论", itemStyle: { color: "#7cb342" } }, return nodes.length > 0 && links.length > 0;
{ name: "麦肯锡全球研究院", itemStyle: { color: "#cddc39" } },
{ name: "卡内基国际和平研究院", itemStyle: { color: "#afb42b" } },
{ name: "战略与国际研究中心", itemStyle: { color: "#827717" } },
{ name: "兰德公司", itemStyle: { color: "#689f38" } }
],
links: [
{ source: "美国", target: "美国能源部", value: 50 },
{ source: "美国", target: "美国财政部", value: 40 },
{ source: "美国", target: "美国国务院", value: 60 },
{ source: "美国", target: "美国国际开发署", value: 20 },
{ source: "美国", target: "美国内政部", value: 15 },
{ source: "英国", target: "美国国务院", value: 10 },
{ source: "加拿大", target: "美国能源部", value: 5 },
// Flow to right
{ source: "美国能源部", target: "麻省理工学院科技评论", value: 20 },
{ source: "美国能源部", target: "麦肯锡全球研究院", value: 25 },
{ source: "美国财政部", target: "卡内基国际和平研究院", value: 30 },
{ source: "美国国务院", target: "战略与国际研究中心", value: 40 },
{ source: "美国国务院", target: "兰德公司", value: 15 }
]
}); });
// 遍历每个智库 // 遍历每个智库
...@@ -1032,19 +1421,77 @@ const handleGetThinkTankDonation = async () => { ...@@ -1032,19 +1421,77 @@ const handleGetThinkTankDonation = async () => {
try { try {
const res = await getThinkTankDonation(); const res = await getThinkTankDonation();
console.log("智库资金流向", res.data, transformDataToSankey(res.data)); console.log("智库资金流向", res.data, transformDataToSankey(res.data));
if (res.code === 200 && res.data) { if (res.code === 200 && Array.isArray(res.data)) {
box7Data.value = transformDataToSankey(res.data); box7Data.value = transformDataToSankey(res.data);
} else {
box7Data.value = { nodes: [], links: [] };
} }
} catch (error) { } catch (error) {
console.error("获取智库资金流向error", error); console.error("获取智库资金流向error", error);
box7Data.value = { nodes: [], links: [] };
} }
}; };
const handleBox7 = async () => { const handleBox7 = async () => {
aiContentBox7.value = "";
await handleGetThinkTankDonation(); await handleGetThinkTankDonation();
const box7Chart = getSankeyChart(box7Data.value.nodes, box7Data.value.links); if (!hasBox7ChartData.value) {
return;
}
const nodes = box7Data.value?.nodes ?? [];
const links = box7Data.value?.links ?? [];
const box7Chart = getSankeyChart(nodes, links);
setChart(box7Chart, "box7Chart"); setChart(box7Chart, "box7Chart");
}; };
/** 请求 box7 智库资金流向桑基图解读(入参:{ text: JSON.stringify({ type, name, nodes, links }) }) */
const fetchBox7ChartInterpretation = async () => {
const raw = box7Data.value;
const nodes = Array.isArray(raw?.nodes) ? raw.nodes : [];
const links = Array.isArray(raw?.links) ? raw.links : [];
if (!nodes.length && !links.length) {
aiContentBox7.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentBox7.value &&
aiContentBox7.value !== "解读生成中…" &&
aiContentBox7.value !== "解读加载失败" &&
aiContentBox7.value !== "暂无图表数据";
if (hasValidContent || isBox7InterpretLoading.value) {
return;
}
isBox7InterpretLoading.value = true;
aiContentBox7.value = "解读生成中…";
const chartPayload = {
type: "桑基图",
name: "智库资金流向",
nodes: nodes.map(n => ({ name: n.name })),
links: links.map(l => ({
source: l.source,
target: l.target,
value: l.value
}))
};
try {
const text = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onInterpretationDelta: (chunk) => {
if (chunk) {
aiContentBox7.value = chunk;
}
}
}
);
aiContentBox7.value = text || "未返回有效解读内容";
} catch (error) {
console.error("智库资金流向图表解读请求失败", error);
aiContentBox7.value = "解读加载失败";
} finally {
isBox7InterpretLoading.value = false;
}
};
// 智库研究热点 // 智库研究热点
const box8Data = ref([ const box8Data = ref([
// { // {
...@@ -1460,7 +1907,7 @@ const handleSurveyCurrentChange = page => { ...@@ -1460,7 +1907,7 @@ const handleSurveyCurrentChange = page => {
const handleGetThinkTankSurvey = async () => { const handleGetThinkTankSurvey = async () => {
const params = { const params = {
currentPage: surveyCurrentPage.value - 1, pageNum: surveyCurrentPage.value,
pageSize: 12, pageSize: 12,
sortFun: surveySort.value, sortFun: surveySort.value,
researchTypeIds: arrayToString(surveySelectedAreaList.value), researchTypeIds: arrayToString(surveySelectedAreaList.value),
...@@ -1604,7 +2051,7 @@ function arrayToString(arr) { ...@@ -1604,7 +2051,7 @@ function arrayToString(arr) {
//获取智库报告 //获取智库报告
const handleGetetThinkTankReport = async () => { const handleGetetThinkTankReport = async () => {
const params = { const params = {
currentPage: currentPage.value - 1, pageNum: currentPage.value,
pageSize: 12, pageSize: 12,
sortFun: sort.value, sortFun: sort.value,
researchTypeIds: arrayToString(selectedAreaList.value), researchTypeIds: arrayToString(selectedAreaList.value),
...@@ -1739,6 +2186,7 @@ const handleSearch = () => { ...@@ -1739,6 +2186,7 @@ const handleSearch = () => {
onMounted(async () => { onMounted(async () => {
handleGetThinkTankList(); handleGetThinkTankList();
handleGetAllThinkTankList()
await getStatCountInfo(); await getStatCountInfo();
// 定义一个定时器,每隔2秒轮播一次 // 定义一个定时器,每隔2秒轮播一次
setInterval(() => { setInterval(() => {
...@@ -1752,7 +2200,7 @@ onMounted(async () => { ...@@ -1752,7 +2200,7 @@ onMounted(async () => {
handleGetNewReport(); handleGetNewReport();
handleGetThinkTankRiskSignal(); handleGetThinkTankRiskSignal();
handleBox5(getDateMonthsAgo(24)); handleBox5(box5selectetedYear.value);
handleBox6(); handleBox6();
handleBox7(); handleBox7();
handleGetHylyList(); handleGetHylyList();
...@@ -2009,12 +2457,56 @@ onMounted(async () => { ...@@ -2009,12 +2457,56 @@ onMounted(async () => {
} }
} }
.home-main-header-card-box { .title-info-bar {
width: 1600px;
height: 36px;
margin-top: 64px; margin-top: 64px;
display: flex;
justify-content: space-between;
.time-tab-pane {
width: 248px;
height: 36px;
}
.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;
gap: 16px; gap: 16px;
position: relative;
z-index: 9999999;
overflow: visible;
.card { .card {
width: 253px; width: 253px;
...@@ -2026,10 +2518,38 @@ onMounted(async () => { ...@@ -2026,10 +2518,38 @@ onMounted(async () => {
background: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.65);
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
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;
}
.red-info {
color: rgb(255, 255, 255);
display: inline-flex;
position: absolute;
left: 233px;
bottom: 208px;
background-color: rgba(255, 77, 79, 1);
align-items: center;
justify-content: center;
border-radius: 50px;
border: 1px solid rgb(255, 255, 255);
width: auto;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
padding: 2px 8px;
/* 左右留空隙,更美观 */
white-space: nowrap;
z-index: 10000000;
} }
.card-header { .card-header {
...@@ -2050,77 +2570,26 @@ onMounted(async () => { ...@@ -2050,77 +2570,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 +2721,7 @@ onMounted(async () => { ...@@ -2252,7 +2721,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 +2730,22 @@ onMounted(async () => { ...@@ -2261,6 +2730,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;
}
}
} }
} }
} }
...@@ -3080,11 +3565,13 @@ onMounted(async () => { ...@@ -3080,11 +3565,13 @@ onMounted(async () => {
} }
.box5-select-box { .box5-select-box {
width: 120px; width: 240px;
height: 28px; height: 28px;
position: absolute; position: absolute;
top: 11px; top: 11px;
right: 31px; right: 31px;
display: flex;
gap: 2px;
} }
} }
...@@ -3092,15 +3579,76 @@ onMounted(async () => { ...@@ -3092,15 +3579,76 @@ onMounted(async () => {
height: 412px; height: 412px;
width: 1063px; width: 1063px;
box-sizing: border-box; box-sizing: border-box;
position: relative;
padding: 24px 24px 64px 24px;
#box5Chart { &.box5-main--empty {
height: 340px; display: flex;
width: 1063px; align-items: center;
justify-content: center;
padding: 24px;
:deep(.el-empty__image) {
margin-bottom: 0;
}
}
.box5-el-empty {
padding: 0;
margin: 0;
}
.box5-chart-row {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
width: 100%;
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 {
flex: 1;
min-width: 0;
height: 324px;
} }
.source { .source {
margin: 0 auto;
margin-top: 23px; position: absolute;
bottom: 21px;
left: 50%;
/* 让元素左边对齐父级中线 */
transform: translateX(-50%);
/* 上下0,左右自动居中 */ /* 上下0,左右自动居中 */
width: 370px; width: 370px;
...@@ -3132,6 +3680,25 @@ onMounted(async () => { ...@@ -3132,6 +3680,25 @@ onMounted(async () => {
margin-left: 8px; margin-left: 8px;
} }
} }
.chart-box {
position: absolute;
right: 0;
bottom: 18px;
.btn-box {
width: 74px;
height: 28px;
}
.content-box {
width: 1063px;
position: absolute;
right: 0;
bottom: -18px;
}
}
} }
} }
...@@ -3177,11 +3744,13 @@ onMounted(async () => { ...@@ -3177,11 +3744,13 @@ onMounted(async () => {
} }
.box6-select-box { .box6-select-box {
width: 120px; width: 240px;
height: 28px; height: 28px;
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 25px; right: 25px;
display: flex;
gap: 2px
} }
} }
...@@ -3189,16 +3758,38 @@ onMounted(async () => { ...@@ -3189,16 +3758,38 @@ onMounted(async () => {
width: 520px; width: 520px;
height: 412px; height: 412px;
box-sizing: border-box; box-sizing: border-box;
padding: 24px 24px 0; padding: 24px 24px 64px 24px;
position: relative;
&.box6-main--empty {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
:deep(.el-empty__image) {
margin-bottom: 0;
}
}
.box6-el-empty {
padding: 0;
margin: 0;
}
#box6Chart { #box6Chart {
width: 446px; width: 472px;
height: 309px; height: 324px;
} }
.source { .source {
margin: 0 auto;
margin-top: 23px; position: absolute;
bottom: 21px;
left: 50%;
/* 让元素左边对齐父级中线 */
transform: translateX(-50%);
/* 让元素自身向左回退一半宽度 */
/* 上下0,左右自动居中 */ /* 上下0,左右自动居中 */
width: 370px; width: 370px;
...@@ -3230,6 +3821,25 @@ onMounted(async () => { ...@@ -3230,6 +3821,25 @@ onMounted(async () => {
margin-left: 8px; margin-left: 8px;
} }
} }
.chart-box {
position: absolute;
right: 0;
bottom: 18px;
.btn-box {
width: 74px;
height: 28px;
}
.content-box {
width: 520px;
position: absolute;
right: 0;
bottom: -18px;
}
}
} }
...@@ -3322,9 +3932,92 @@ onMounted(async () => { ...@@ -3322,9 +3932,92 @@ onMounted(async () => {
} }
.box7-main { .box7-main {
width: 1064px;
height: 412px; height: 412px;
box-sizing: border-box; box-sizing: border-box;
padding: 24px 24px 0; padding: 24px 24px 64px 24px;
position: relative;
&.box7-main--empty {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
:deep(.el-empty__image) {
margin-bottom: 0;
}
}
.box7-el-empty {
padding: 0;
margin: 0;
}
#box7Chart {
width: 1016px;
/* 填满宽度 */
height: 324px;
/* 填满高度 */
}
.source {
position: absolute;
bottom: 21px;
left: 50%;
/* 让元素左边对齐父级中线 */
transform: translateX(-50%);
/* 上下0,左右自动居中 */
height: 22px;
display: flex;
.info {
width: 16px;
height: 16px;
margin-top: 3px;
img {
width: 100%;
height: 100%;
}
}
.text {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
color: rgb(132, 136, 142);
margin-left: 8px;
}
}
.chart-box {
position: absolute;
right: 0;
bottom: 18px;
.btn-box {
width: 74px;
height: 28px;
}
.content-box {
width: 1063px;
position: absolute;
right: 0;
bottom: -18px;
}
}
} }
} }
...@@ -3383,6 +4076,10 @@ onMounted(async () => { ...@@ -3383,6 +4076,10 @@ onMounted(async () => {
} }
.box8-main { .box8-main {
height: 412px;
.box8-main-item {
margin: 0 auto; margin: 0 auto;
margin-top: 5px; margin-top: 5px;
width: 452px; width: 452px;
...@@ -3463,6 +4160,7 @@ onMounted(async () => { ...@@ -3463,6 +4160,7 @@ onMounted(async () => {
} }
} }
} }
}
.home-main-footer { .home-main-footer {
overflow: hidden; overflow: hidden;
......
...@@ -3,23 +3,20 @@ ...@@ -3,23 +3,20 @@
<div class="header"> <div class="header">
<div class="header-top"> <div class="header-top">
<div class="header-top-left"> <div class="header-top-left">
<img :src="thinkInfo.contentUrl" alt="" /> <img :src="thinkInfo.imageUrl" alt="" />
<div> <div>
<div class="title">{{ thinkInfo.name }}</div> <div class="title">{{ thinkInfo.name }}</div>
<div class="en-title"> <div class="en-title">
{{ thinkInfo.ename }} {{ thinkInfo.ename }}
</div> </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> </div>
<div class="header-top-right"> <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 class="name">{{ thinkInfo.thinkTankName }}</div>
</div>
<div class="time">{{ thinkInfo.times }}</div> <div class="time">{{ thinkInfo.times }}</div>
</div> </div>
</div> </div>
...@@ -30,19 +27,31 @@ ...@@ -30,19 +27,31 @@
<div style=" margin-top: 17px;"> <div style=" margin-top: 17px;">
智库报告原文 智库报告原文
</div> </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> </div>
</div>
<div class="report-box"> <div class="report-box">
<iframe :src="reportUrlWithPage" width="50%" height="100%"> <pdf v-if="valueSwitch && reportUrlEnWithPage" ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage"
class="pdf-pane" />
</iframe> <pdf v-if="reportUrlWithPage" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
<iframe :src="reportUrlEnWithPage" width="50%" height="100%"> :class="['pdf-pane', { 'pdf-pane-full': !valueSwitch }]" />
</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> -->
</div> </div>
</div> </div>
</div> </div>
...@@ -78,7 +87,83 @@ const buildPdfPageUrl = url => { ...@@ -78,7 +87,83 @@ const buildPdfPageUrl = url => {
const reportUrlWithPage = computed(() => buildPdfPageUrl(reportUrl.value)) const reportUrlWithPage = computed(() => buildPdfPageUrl(reportUrl.value))
const reportUrlEnWithPage = computed(() => buildPdfPageUrl(reportUrlEn.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 () => { const handleGetThinkTankReportSummary = async () => {
...@@ -121,36 +206,34 @@ onMounted(async () => { ...@@ -121,36 +206,34 @@ onMounted(async () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.wrap { .wrap {
width: 100%;
height: 100%;
overflow: hidden;
overflow-y: auto;
.header { .header {
width: 100%; width: 100%;
height: 88px;
box-sizing: border-box; box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1); 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); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 99; z-index: 99999;
overflow: hidden;
.header-top { .header-top {
margin: 0 auto;
margin-top: 20px; margin-top: 20px;
margin-left: 160px; width: 1600px;
margin-bottom: 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-right: 160px;
.header-top-left { .header-top-left {
display: flex; display: flex;
img { img {
width: 72px; width: 44px;
height: 88px; height: 54px;
} }
.title { .title {
...@@ -166,6 +249,7 @@ onMounted(async () => { ...@@ -166,6 +249,7 @@ onMounted(async () => {
} }
.en-title { .en-title {
margin-top: 4px;
margin-left: 20px; margin-left: 20px;
height: 24px; height: 24px;
color: rgba(95, 101, 108, 1); color: rgba(95, 101, 108, 1);
...@@ -182,26 +266,24 @@ onMounted(async () => { ...@@ -182,26 +266,24 @@ onMounted(async () => {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-left: 20px; 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 { .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 { .name {
height: 24px; height: 24px;
color: rgba(95, 101, 108, 1); color: rgba(95, 101, 108, 1);
...@@ -211,6 +293,23 @@ onMounted(async () => { ...@@ -211,6 +293,23 @@ onMounted(async () => {
line-height: 24px; line-height: 24px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: right; text-align: right;
}
.image {
width: 16px;
height: 16px;
margin-top: 5px;
img {
width: 100%;
height: 100%;
}
}
} }
.time { .time {
...@@ -281,25 +380,118 @@ onMounted(async () => { ...@@ -281,25 +380,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 { .btn-box {
display: flex; 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 { .btn {
width: 120px; width: 88px;
height: 36px; height: 32px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1); border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px; border-radius: 6px;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
display: flex; display: flex;
align-items: center;
justify-content: center;
gap: 8px; gap: 8px;
.icon { .icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
display: inline-flex;
margin-top: 8px;
margin-left: 16px;
img { img {
width: 100%; width: 100%;
...@@ -308,61 +500,49 @@ onMounted(async () => { ...@@ -308,61 +500,49 @@ onMounted(async () => {
} }
.text { .text {
width: 66px; margin-top: 4px;
height: 22px; width: 32px;
color: rgba(95, 101, 108, 1); height: 24px;
font-family: Microsoft YaHei; color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 22px; line-height: 24px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: center; text-align: left;
} }
} }
.btn1 { .search-btn {
border-radius: 6px; cursor: pointer;
background: var(--color-main-active);
.text {
color: rgba(255, 255, 255, 1);
}
} }
.search-input-wrap {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: 4px;
} }
} }
} }
.main { .report-box {
margin: 0 auto; margin-left: 70px;
background: #ffffff; width: 1456px;
width: 1600px; height: 881px;
height: 1025px; display: flex;
overflow: hidden; overflow-y: auto;
/* 右侧统一滚动条,控制两侧原文+译文一起滚动 */
overflow-x: hidden;
}
.main-header { .pdf-pane {
height: 64px; width: 50%;
/* box-sizing: border-box; */ height: 100%;
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;
} }
.report-box { .pdf-pane-full {
width: 100%; width: 100%;
height: 750px;
overflow: auto;
display: flex;
} }
} }
} }
......
<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();
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 = { const renderContext = {
canvasContext: context, canvasContext: context,
viewport: viewport viewport: viewport
}; };
pdfCanvas.value.width = viewport.width; canvas.width = viewport.width;
pdfCanvas.value.height = viewport.height; canvas.height = viewport.height;
await page.render(renderContext).promise; 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 {
......
import * as echarts from 'echarts'; import * as echarts from 'echarts';
const getMultiLineChart = (data) => { /** 图例分页:每页展示的图例项数量(box5 数量变化趋势) */
export const MULTILINE_LEGEND_SHOW_COUNT = 11;
/**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data
* @param {{ legendShowCount?: number, legendPageIndex?: number }} [options]
*/
const getMultiLineChart = (data, options = {}) => {
// 提取标题和系列数据 // 提取标题和系列数据
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 pageCount = Math.max(1, Math.ceil(allNames.length / legendShowCount))
const legendPageIndex = Math.min(Math.max(0, rawPageIndex), pageCount - 1)
const legendStart = legendPageIndex * legendShowCount
const legendData = allNames.slice(legendStart, legendStart + legendShowCount)
// 定义配色数组 // 定义配色数组
const colorList = [ const colorList = [
'rgba(5, 95, 194, 1)', // #055fc2 'rgba(5, 95, 194, 1)', // #055fc2
...@@ -48,6 +66,7 @@ const getMultiLineChart = (data) => { ...@@ -48,6 +66,7 @@ const getMultiLineChart = (data) => {
return ({ return ({
name: item.name, name: item.name,
type: 'line', type: 'line',
smooth: true,
// 新增/优化:面积填充渐变效果 // 新增/优化:面积填充渐变效果
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
...@@ -80,8 +99,9 @@ const getMultiLineChart = (data) => { ...@@ -80,8 +99,9 @@ const getMultiLineChart = (data) => {
} }
} }
}, },
/* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */
grid: { grid: {
top: '15%', top: '28%',
right: '5%', right: '5%',
bottom: '5%', bottom: '5%',
left: '5%', left: '5%',
...@@ -89,7 +109,9 @@ const getMultiLineChart = (data) => { ...@@ -89,7 +109,9 @@ const getMultiLineChart = (data) => {
}, },
legend: { legend: {
show: true, show: true,
top: 10, type: 'plain',
data: legendData,
top: 8,
left: 'center', left: 'center',
icon: 'circle', icon: 'circle',
textStyle: { textStyle: {
...@@ -112,6 +134,22 @@ const getMultiLineChart = (data) => { ...@@ -112,6 +134,22 @@ const getMultiLineChart = (data) => {
yAxis: [ yAxis: [
{ {
type: 'value', type: 'value',
name: '数量',
/* 纵轴:end = 坐标轴最大值一端,即竖轴上方 */
nameLocation: 'end',
/* 在默认基础上再向左约 20px:nameGap 为轴标题与轴线间距(左侧 Y 轴顶端时增大则标题更靠左) */
nameGap: 20,
nameRotate: 0,
nameTextStyle: {
color: 'rgba(170, 173, 177, 1)',
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 22,
letterSpacing: 0,
align: 'right',
verticalAlign: 'bottom'
},
splitLine: { splitLine: {
show: true, // 显示网格线 show: true, // 显示网格线
lineStyle: { lineStyle: {
......
...@@ -10,7 +10,8 @@ const getPieChart = (data) => { ...@@ -10,7 +10,8 @@ const getPieChart = (data) => {
// left: '15%', // left: '15%',
// containLabel: true // containLabel: true
// }, // },
radius: [70, 100], // 使用百分比半径,避免固定像素导致饼图“看起来没铺满容器”
radius: ['45%', '60%'],
height: '100%', height: '100%',
left: 'center', left: 'center',
width: '100%', width: '100%',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论