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

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

feat:智库样式功能开发 查看合并请求 !193
VITE_BASE_API= '/api'
# 图表解读等 /aiAnalysis 代理服务密钥(勿提交到公开仓库时可改为本地 .env.local)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
# 线上地址
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 => {
config.headers['token'] = token
// 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
}, error => {
console.log(error)
......@@ -82,8 +90,14 @@ service.interceptors.response.use(
// 重复请求触发的取消不提示错误
if (isCanceledError) return Promise.reject(error)
// 处理token过期或无效的情况
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
// 处理token过期或无效的情况(排除 AI 分析服务:其 401 多为 API Key 问题)
const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
!isAiAnalysisRequest
) {
ElMessage({
message: 'Token已过期,请重新登录',
type: 'error',
......
......@@ -10,6 +10,13 @@ export function getThinkTankList() {
})
}
export function getAllThinkTankList(params) {
return request({
method: 'GET',
url: '/api/thinkTankOverview/thinkTanks/page',
params: params
})
}
//智库概览:获取智库发布
export function getNewReport() {
return request({
......@@ -28,13 +35,19 @@ export function getThinkTankRiskSignal() {
})
}
// 政策建议趋势分布
/**
* 政策建议趋势分布(数量变化趋势)
* @param {{ startDate: string, endDate: string }} params - 如 2024-01-01 ~ 2024-12-31
*/
export function getThinkTankPolicyIndustryChange(params) {
return request({
method: 'GET',
url: `/api/thinkTankOverview/policyIndustryChange/${params}`,
})
method: "GET",
url: `/api/thinkTankOverview/policyIndustryChange`,
params: {
startDate: params.startDate,
endDate: params.endDate
}
});
}
// 政策建议领域分布
......@@ -113,10 +126,44 @@ export function getThinkDynamicsReportType() {
//智库动态:获取智库报告
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({
method: 'GET',
url: `/api/thinkTankInfo/report/${params.id}/${params.startDate}`,
params: params.parmas
url: `/api/thinkTankInfo/report/${id}/${startDate}`,
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) {
})
}
//获取智库政策
/**
* 获取智库政策(政策追踪列表)
* GET /api/thinkTankInfo/policy
* Query: thinkTankId, startDate, endDate, orgIds, domainIds(科技领域/智库领域,逗号分隔 id), pageNum, pageSize, sortField, sortOrder, sortFun, reportId 等
*/
export function getThinkPolicy(params) {
return request({
method: 'GET',
url: `/api/thinkTankInfo/policy/${params.id}/${params.startDate}`,
url: '/api/thinkTankInfo/policy',
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) {
return request({
......@@ -299,9 +362,26 @@ export function getThinkTankReportPolicy(params) {
//获取相关政策动态
export function getThinkTankReportPolicyAction(params) {
const {
reportId,
currentPage,
pageSize,
keyword = "",
orgIds = "",
// 新增:按科技领域 / 标签过滤
industryName = ""
} = params;
return request({
method: 'GET',
url: `/api/thinkTankReport/policyAction/${params}`,
url: `/api/thinkTankReport/policyDetail/${reportId}`,
params: {
currentPage,
pageSize,
keyword,
// 后端按标签过滤使用的字段
industryName,
orgIds,
}
})
}
......
......@@ -24,17 +24,19 @@ const props = defineProps({
</script>
<style lang="scss" scoped>
.tip-wrapper{
.tip-wrapper {
width: 100%;
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
height: 22px;
.icon{
.icon {
width: 16px;
height: 16px;
img{
img {
width: 100%;
height: 100%;
}
......
......@@ -3,6 +3,8 @@ const thinkTank = () => import('@/views/thinkTank/index.vue')
const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue')
const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue')
const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue')
const allThinkTank= () => import('@/views/thinkTank/allThinkTank/index.vue')
const MultiThinkTankViewAnalysis= () => import('@/views/thinkTank/MultiThinkTankViewAnalysis/index.vue')
const thinktankRoutes = [
// 智库系统的主要路由
......@@ -36,9 +38,19 @@ const thinktankRoutes = [
path: "/thinkTank/reportOriginal/:id",
name: "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) => {
chartDom.removeAttribute("_echarts_instance_");
let chart = echarts.init(chartDom);
chart.setOption(option);
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout(() => {
chart.resize();
}, 0);
return chart;
};
......
......@@ -5,9 +5,8 @@
<div class="home-content">
<div class="home-content-header">
<SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef"
placeholder="搜索科技法案" :containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true"
defaultBillSearchType="federal" />
<SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef" placeholder="搜索科技法案"
:containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true" defaultBillSearchType="federal" />
</div>
<div class="committee-cards-section">
......@@ -55,8 +54,8 @@
</div>
</div>
<div class="box1-main" style="display: block">
<el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000"
arrow="never" indicator-position="none" @change="handleCarouselChange">
<el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000" arrow="never"
indicator-position="none" @change="handleCarouselChange">
<el-carousel-item v-for="(bill, billIndex) in hotBillList" :key="billIndex">
<div class="carousel-content" style="display: flex; height: 100%">
<div class="box1-main-left">
......@@ -64,8 +63,7 @@
{{ bill.billName }}
</div>
<div class="box1-main-left-info">
<AreaTag v-for="(item, index) in bill.hylyList" :key="index"
:tagName="item.industryName">
<AreaTag v-for="(item, index) in bill.hylyList" :key="index" :tagName="item.industryName">
</AreaTag>
</div>
<div class="box1-main-left-info1">
......@@ -83,18 +81,15 @@
</div>
</div>
<div class="box1-main-left-info2">
<div class="info2-item" v-for="(item, index) in bill.dyqkList"
:key="index">
<div class="time-line"
v-if="index !== bill.dyqkList.length - 1"></div>
<div class="info2-item" v-for="(item, index) in bill.dyqkList" :key="index">
<div class="time-line" v-if="index !== bill.dyqkList.length - 1"></div>
<div class="item-icon">
<img src="./assets/images/info2-icon.png" alt="" />
</div>
<div class="item-time" :class="{ itemTimeActive: index === 0 }">
{{ item.actionDate }}
</div>
<div class="item-title"
:class="{ itemTitleActive: index === 0 }">
<div class="item-title" :class="{ itemTitleActive: index === 0 }">
{{ item.actionContentCn }}
</div>
</div>
......@@ -121,9 +116,8 @@
</el-carousel>
</div>
</overviewMainBox>
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal"
@item-click="handleClickToDetailO" riskLevel="signalLevel" postDate="signalTime"
name="signalTitle" />
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" @item-click="handleClickToDetailO"
riskLevel="signalLevel" postDate="signalTime" name="signalTitle" />
</div>
<DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader>
......
......@@ -701,6 +701,7 @@
</template>
<script setup>
//这是一个备注
import NewsList from "@/components/base/newsList/index.vue";
import RiskSignal from "@/components/base/RiskSignal/index.vue";
import { onMounted, ref, computed, reactive, shallowRef, watch, nextTick } from "vue";
......
<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>
......@@ -62,7 +62,7 @@
</div>
<div class="text" @click="toReport()">{{ "报告原文" }}</div>
</div>
<div class="btn">
<div class="btn" @click="handleDownloadDocument">
<div class="icon">
<img src="./images/btn-icon3.png" alt="" />
</div>
......@@ -89,7 +89,7 @@ import { ref, onMounted } from "vue";
import ReportAnalysis from "./reportAnalysis/index.vue";
import PolicyTracking from "./policyTracking/index.vue";
import { getThinkTankReportSummary, getThinkTankReportRelated } from "@/api/thinkTank/overview";
import { getThinkTankReportSummary, getThinkTankReportRelated, getThinkTankReportcontentUrl } from "@/api/thinkTank/overview";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute();
......@@ -163,6 +163,56 @@ const goToOfficialWebsite = () => {
window.open(url, "_blank");
};
// 文档下载:先拉取 PDF 地址,再中英文都下载(与报告原文页相同方式)
const downloadOnePdf = async (url, filename) => {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/pdf" },
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
};
const handleDownloadDocument = async () => {
try {
const { ElMessage } = await import("element-plus");
const res = await getThinkTankReportcontentUrl(router.currentRoute._value.params.id);
if (res.code !== 200 || !res.data) {
ElMessage.warning("暂无下载链接");
return;
}
const urlZh = res.data.content ? String(res.data.content).split("#")[0] : "";
const urlEn = res.data.contentEn ? String(res.data.contentEn).split("#")[0] : "";
if (!urlZh && !urlEn) {
ElMessage.warning("暂无下载链接");
return;
}
const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-");
if (urlZh) {
await downloadOnePdf(urlZh, `${baseName}_中文.pdf`);
}
if (urlEn) {
if (urlZh) await new Promise(r => setTimeout(r, 300));
await downloadOnePdf(urlEn, `${baseName}_英文.pdf`);
}
ElMessage.success(urlZh && urlEn ? "已下载中文、英文两份 PDF" : "下载成功");
} catch (error) {
console.error("文档下载失败:", error);
try {
const { ElMessage } = await import("element-plus");
ElMessage.error("PDF 下载失败,请稍后重试");
} catch (_) { }
}
};
</script>
<style lang="scss" scoped>
......
......@@ -37,18 +37,23 @@
<div class="author-title">报告作者:</div>
<div class="author-content">
<template v-if="Array.isArray(reportAuthors) && reportAuthors.length">
<span v-for="(author, idx) in reportAuthors" :key="idx">
{{ author.name }}
<span v-if="idx < reportAuthors.length - 1"></span>
<span v-if="reportAuthors.length === 1">
{{ reportAuthors[0].name }}
</span>
<!-- 多个作者:显示第一个 + 等 -->
<span v-else>
{{ reportAuthors[0].name }}
</span>
</template>
</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">
<div class="author-item">
<div class="image"><img :src="author.avatar" :alt="reportAuthors[0].name" /></div>
<div class="image"><img :src="author.avatar" alt="" /></div>
<div class="author-text">
<div class="author-name">{{ author.name }}</div>
<div class="author-position">{{ author.job }}</div>
......@@ -58,6 +63,13 @@
</div>
</AnalysisBox>
</div>
<div class="box5">
<AnalysisBox title="报告关键词云" :showAllBtn="true">
<div class="box5-main">
<div id="box5Chart"></div>
</div>
</AnalysisBox>
</div>
<div class="box2">
<!-- <div class="box-header">
<div class="header-left"></div>
......@@ -94,7 +106,7 @@
</div>
</div>
</div>
<div class="box2-btn">
<div class="box2-btn" @click="goToAllThinkTank">
<div class="btn-text">
多智库报告观点汇聚分析
</div>
......@@ -109,14 +121,12 @@
<div class="box3">
<AnalysisBox title="报告摘要" :showAllBtn="true">
<div class="box3-main">
<div class="box3-top">
<div class="top-title">
<div class="title-image">
<img src="../images/title-image.png" alt="" />
</div>
</div>
</div>
<div class="box3-text"> {{ box1Data }}</div>
<AiSummary>
<template #summary-content>
{{ box1Data }}
</template>
</AiSummary>
</div>
</AnalysisBox>
</div>
......@@ -185,7 +195,7 @@
<script setup>
import WarningPane from "@/components/base/WarningPane/index.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 getWordCloudChart from "./utils/worldCloudChart";
import {
......@@ -197,8 +207,20 @@ import {
} from "@/api/thinkTank/overview";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import { useRouter } from "vue-router";
const router = useRouter();
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({
thinkInfo: {
......@@ -321,6 +343,8 @@ const box2Data = ref([
// value: 89
// }
]);
// 报告关键词云
const box5Data = ref([]);
//获取科技领域词云
const handleGetThinkTankReportIndustryCloud = async () => {
......@@ -332,19 +356,22 @@ const handleGetThinkTankReportIndustryCloud = async () => {
const res = await getThinkTankReportIndustryCloud(params);
console.log("科技领域词云", res);
if (res.code === 200 && res.data) {
const data = [];
res.data.map(item => {
data.push({
const data = (res.data || []).map(item => ({
name: item.clause,
value: item.count
});
box2Data.value = data;
const box2Chart = getWordCloudChart(box2Data.value);
setChart(box2Chart, "box2Chart");
});
}));
// 该接口数据用于「报告关键词云」
box5Data.value = data;
await nextTick();
const box5Chart = getWordCloudChart(box5Data.value);
setChart(box5Chart, "box5Chart");
} else {
box5Data.value = [];
}
} catch (error) {
console.error("获取科技领域词云error", error);
box5Data.value = [];
}
};
//涉及科技领域
......@@ -575,7 +602,7 @@ onMounted(() => {
}
.author {
height: 24px;
display: flex;
gap: 4px;
......@@ -607,9 +634,18 @@ onMounted(() => {
.author-box {
width: 437px;
height: 220px;
height: auto;
/* 改为自适应高度,不要固定 220px */
max-height: 220px;
margin-top: 34px;
margin-left: 18px;
display: grid;
grid-template-columns: 1fr 1fr;
/* 两列等宽 */
column-gap: 4px;
/* 左右间距(同一行) */
row-gap: 8px;
/* 上下间距(同一列) */
.author-item {
width: 213px;
......@@ -646,6 +682,9 @@ onMounted(() => {
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.author-position {
......@@ -658,6 +697,9 @@ onMounted(() => {
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
......@@ -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 {
width: 480px;
......@@ -814,7 +880,7 @@ onMounted(() => {
.box3 {
width: 1103px;
height: 545px;
height: 890px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
......@@ -822,9 +888,10 @@ onMounted(() => {
// background: rgba(255, 255, 255, 1);
.box3-main {
width: 1058px;
height: 466px;
height: 830px;
margin-top: 3px;
margin-left: 22px;
padding-bottom: 22px;
......@@ -1129,5 +1196,11 @@ onMounted(() => {
:deep(.analysis-box-wrapper .wrapper-header) {
height: 54px !important;
display: flex;
align-items: center;
.header-title>div {
line-height: 54px;
}
}
</style>
......@@ -2,6 +2,8 @@
const getWordCloudChart = (data) => {
const option = {
width: 417,
height: 292,
grid: {
left: 0,
top: 0,
......@@ -11,7 +13,13 @@ const getWordCloudChart = (data) => {
series: [
{
type: "wordCloud",
shape: "circle", //
// 让词云渲染区域严格贴合容器
left: "center",
top: "center",
width: "100%",
height: "100%",
// 使用矩形词云更容易铺满容器且减少留白
shape: "rect", // ✅ 矩形 = 文字排版最整齐、最居中
// 其他形状你可以使用形状路径
// 或者自定义路径
// shape: 'circle' // 圆形(默认)
......@@ -22,22 +30,16 @@ const getWordCloudChart = (data) => {
// shape: 'pentagon' // 五边形
// shape: 'star' // 星形
// shape: 'cardioid' // 心形
gridSize: 30, // 网格大小,影响词间距。
sizeRange: [10, 25], // 定义词云中文字大小的范围
// 网格越大越稀疏,越容易产生留白;这里进一步调小以便更贴合容器
gridSize: 5,
// 适当放大最大字号,提升填充度(同时避免太大溢出)
sizeRange: [16, 24],
rotationRange: [0, 0],
rotationStep: 15,
drawOutOfBound: false, // 是否超出画布
drawOutOfBound: false,
layoutAnimation: false,
// 字体
textStyle: {
// normal: {
// color: function () {
// return 'rgb(' + [
// Math.round(Math.random() * 160),
// Math.round(Math.random() * 160),
// Math.round(Math.random() * 160)
// ].join(',') + ')';
// }
// },
color: function () {
let colors = [
"rgba(189, 33, 33, 1)",
......@@ -49,12 +51,12 @@ const getWordCloudChart = (data) => {
];
return colors[parseInt(Math.random() * colors.length)];
},
textAlign: "center", // ✅ 文字自身水平居中
emphasis: {
shadowBlur: 5,
shadowColor: "#333",
},
},
// 设置词云数据
data: data,
},
],
......
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论