提交 29e42ed8 authored 作者: 朱政's avatar 朱政

feat:多智库分析界面样式以及功能开发

上级 8055f9f1
......@@ -120,10 +120,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
})
}
......
......@@ -4,6 +4,7 @@ const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vu
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 = [
// 智库系统的主要路由
......@@ -37,17 +38,19 @@ const thinktankRoutes = [
path: "/thinkTank/reportOriginal/:id",
name: "ReportOriginal",
component: ReportOriginal,
// meta: {
// title: "报告原文"
// }
},
{
path: "/thinkTank/allThinkTank",
name: "allThinkTank",
component: allThinkTank,
// meta: {
// title: "报告原文"
// }
},
{
path: "/thinkTank/MultiThinkTankViewAnalysis/:id",
name: "MultiThinkTankViewAnalysis",
component: MultiThinkTankViewAnalysis,
},
]
......
<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
......@@ -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>
......@@ -185,7 +197,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,9 +209,20 @@ import {
} from "@/api/thinkTank/overview";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import { useRouter } from "vue-router";
const router = useRouter();
import "echarts-wordcloud";
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: {
type: Object,
......@@ -321,6 +344,8 @@ const box2Data = ref([
// value: 89
// }
]);
// 报告关键词云
const box5Data = ref([]);
//获取科技领域词云
const handleGetThinkTankReportIndustryCloud = async () => {
......@@ -332,19 +357,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 +603,7 @@ onMounted(() => {
}
.author {
height: 24px;
display: flex;
gap: 4px;
......@@ -607,9 +635,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 +683,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 +698,9 @@ onMounted(() => {
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
......@@ -669,6 +712,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;
......@@ -1131,7 +1198,8 @@ onMounted(() => {
height: 54px !important;
display: flex;
align-items: center;
.header-title > div {
.header-title>div {
line-height: 54px;
}
}
......
......@@ -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,
},
],
......
......@@ -24,9 +24,10 @@
</div>
<div class="select-box">
<div class="search-box">
<el-input placeholder="搜索政策建议" v-model="searchPolicy">
<el-input placeholder="搜索智库" v-model="searchPolicy">
<template #suffix>
<img src="../assets/images/Line_Search.png" class="search-icon" alt="搜索">
<img src="../assets/images/Line_Search.png" class="search-icon" alt="搜索"
@click="handleGetThinkTankList()">
</template>
</el-input>
......@@ -49,13 +50,13 @@
]
}">
<template #prefix>
<img src="../assets/images/sort-asc.png" class="select-prefix-img" alt=""
@click.stop="toggleSortAndFetch()" :key="true" label="正序" :value="true" v-if="sort" />
<img src="../assets/images/sort-desc.png" class="select-prefix-img" alt=""
@click.stop="toggleSortAndFetch()" :key="true" label="倒序" :value="true" v-if="!sort" />
<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 @click="handleGetThinkDynamicsReport()" :key="true" label="正序" :value="true" />
<el-option @click="handleGetThinkDynamicsReport()" :key="false" label="倒序" :value="false" />
<el-option :key="true" label="正序" :value="true" />
<el-option :key="false" label="倒序" :value="false" />
</el-select>
</div>
</div>
......@@ -73,6 +74,8 @@
<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="" />
......@@ -114,7 +117,8 @@ const sortedCardList = computed(() => {
return [...cardList.value].sort((a, b) => {
const an = Number(a?.reportNumber ?? 0);
const bn = Number(b?.reportNumber ?? 0);
return bn - an;
// 只有选择“倒序(false)”才反转;初始为 null 时走“正序”规则
return sort.value === false ? an - bn : bn - an;
});
});
// el-pagination 是 1-based
......@@ -137,9 +141,12 @@ const handleGetThinkTankList = async () => {
const res = await getAllThinkTankList({
// 后端通常是 0-based,这里做一次转换
currentPage: currentPage.value - 1,
pageSize: pageSize.value
pageSize: pageSize.value,
keyword: searchPolicy.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;
......@@ -159,9 +166,15 @@ const handleGetThinkTankList = async () => {
}
} 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);
......@@ -427,6 +440,31 @@ onMounted(async () => {
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
}
.item-header {
width: 287px;
......
......@@ -54,6 +54,8 @@
</div>
<div class="home-main-header-card-box">
<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="icon">
<img :src="item.logo" alt="" />
......@@ -1511,7 +1513,7 @@ const handleSurveyCurrentChange = page => {
const handleGetThinkTankSurvey = async () => {
const params = {
currentPage: surveyCurrentPage.value - 1,
pageNum: surveyCurrentPage.value,
pageSize: 12,
sortFun: surveySort.value,
researchTypeIds: arrayToString(surveySelectedAreaList.value),
......@@ -1655,7 +1657,7 @@ function arrayToString(arr) {
//获取智库报告
const handleGetetThinkTankReport = async () => {
const params = {
currentPage: currentPage.value - 1,
pageNum: currentPage.value,
pageSize: 12,
sortFun: sort.value,
researchTypeIds: arrayToString(selectedAreaList.value),
......@@ -2113,10 +2115,37 @@ onMounted(async () => {
background: rgba(255, 255, 255, 0.65);
transition: all 0.3s;
cursor: pointer;
position: relative; // 让 red-info 按当前 card 自身定位
z-index: 1;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
z-index: 2;
}
.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
}
.card-header {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论