提交 4d67858d authored 作者: 张烨's avatar 张烨

Merge remote-tracking branch 'origin/pre' into zy-dev

...@@ -75,7 +75,7 @@ export function getThinkTankReportDomainStats(params) { ...@@ -75,7 +75,7 @@ export function getThinkTankReportDomainStats(params) {
export function getThinkTankPolicyIndustry(params) { export function getThinkTankPolicyIndustry(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankOverview/policyIndustry/${params.year}`, url: `/api/thinkTankOverview/policyIndustry`,
params params
}) })
} }
...@@ -180,7 +180,8 @@ export function getThinkTankTestimoniesByThinkTankId(params) { ...@@ -180,7 +180,8 @@ export function getThinkTankTestimoniesByThinkTankId(params) {
params: { params: {
pageNum: params.pageNum, pageNum: params.pageNum,
pageSize: params.pageSize, pageSize: params.pageSize,
sortField: params.sortField,
sortOrder: params.sortOrder,
domainIds: params.domainIds, domainIds: params.domainIds,
startDate: params.startDate, startDate: params.startDate,
endDate: params.endDate, endDate: params.endDate,
......
<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">
<rect id="降序 12" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="路径" d="M12.6473 7.70235L14.0825 6.30313C14.1606 6.22657 14.2121 6.12657 14.2278 6.01719C14.27 5.74376 14.0793 5.49063 13.8059 5.45001L9.83871 4.87344L8.06527 1.27813C8.01683 1.17969 7.93715 1.10001 7.83871 1.05157C7.59183 0.929693 7.29183 1.03126 7.1684 1.27813L5.39496 4.87344L1.42777 5.45001C1.3184 5.46563 1.2184 5.51719 1.14183 5.59532C0.949647 5.79376 0.952772 6.10938 1.15121 6.30313L4.02152 9.10157L3.3434 13.0531C3.32465 13.1609 3.34183 13.2734 3.3934 13.3703C3.52152 13.6141 3.82465 13.7094 4.0684 13.5797L7.61684 11.7141L8.50394 12.1805" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<template>
<div class="grade-sort-select-box">
<el-select v-model="isSort" placeholder="评分排序" style="width: 130px" @change="handlePxChange">
<template #prefix>
<div style="display: flex; align-items: center; height: 100%">
<img v-if="isSort" src="./down.svg" style="width: 16px; height: 16px" />
<img v-else src="./up.svg" style="width: 16px; height: 16px" />
</div>
</template>
<el-option v-for="item in gradeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isSort = ref(true)
const gradeList = ref([
{ label: "评分倒序", value: true },
{ label: "评分正序", value: false }
])
const emits = defineEmits(['handlePxChange'])
const handlePxChange = () => {
emits('handlePxChange', isSort.value)
}
</script>
<style scoped>
.grade-sort-select-box {
height: 42px;
box-sizing: border-box;
padding: 5px 0;
}
</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">
<rect id="降序 11" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,10)" />
<path id="矢量 1984" d="M10 10.9969L12.0003 9L14 10.9969" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="路径" d="M12.6473 7.70235L14.0825 6.30313C14.1606 6.22657 14.2121 6.12657 14.2278 6.01719C14.27 5.74376 14.0793 5.49063 13.8059 5.45001L9.83871 4.87344L8.06527 1.27813C8.01683 1.17969 7.93715 1.10001 7.83871 1.05157C7.59183 0.929693 7.29183 1.03126 7.1684 1.27813L5.39496 4.87344L1.42777 5.45001C1.3184 5.46563 1.2184 5.51719 1.14183 5.59532C0.949647 5.79376 0.952772 6.10938 1.15121 6.30313L4.02152 9.10157L3.3434 13.0531C3.32465 13.1609 3.34183 13.2734 3.3934 13.3703C3.52152 13.6141 3.82465 13.7094 4.0684 13.5797L7.61684 11.7141L9.39105 12.6469" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<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">
<rect id="降序 8" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<rect id="矩形 6349" width="12.000000" height="1.000000" x="2.000000" y="3.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6350" width="12.000000" height="1.000000" x="2.000000" y="6.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6351" width="6.000000" height="1.000000" x="2.000000" y="9.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6352" width="6.000000" height="1.000000" x="2.000000" y="12.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<template>
<div class="heat-sort-select-box">
<el-select v-model="isSort" placeholder="热度排序" style="width: 130px" @change="handlePxChange">
<template #prefix>
<div style="display: flex; align-items: center; height: 100%">
<img v-if="isSort" src="./down.svg" style="width: 16px; height: 16px" />
<img v-else src="./up.svg" style="width: 16px; height: 16px" />
</div>
</template>
<el-option v-for="item in heatList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isSort = ref(true)
const heatList = ref([
{ label: "热度倒序", value: true },
{ label: "热度正序", value: false }
])
const emits = defineEmits(['handlePxChange'])
const handlePxChange = () => {
emits('handlePxChange', isSort.value)
}
</script>
<style scoped>
.heat-sort-select-box {
height: 42px;
box-sizing: border-box;
padding: 5px 0;
}
</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">
<rect id="降序 10" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,10)" />
<path id="矢量 1984" d="M10 10.9969L12.0003 9L14 10.9969" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<rect id="矩形 6349" width="12.000000" height="1.000000" x="2.000000" y="3.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6350" width="12.000000" height="1.000000" x="2.000000" y="6.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6351" width="6.000000" height="1.000000" x="2.000000" y="9.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6352" width="6.000000" height="1.000000" x="2.000000" y="12.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
</svg>
<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">
<rect id="降序 8" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<rect id="矩形 6349" width="12.000000" height="1.000000" x="2.000000" y="3.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6350" width="12.000000" height="1.000000" x="2.000000" y="6.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6351" width="6.000000" height="1.000000" x="2.000000" y="9.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6352" width="6.000000" height="1.000000" x="2.000000" y="12.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<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">
<rect id="降序 12" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="路径" d="M12.6473 7.70235L14.0825 6.30313C14.1606 6.22657 14.2121 6.12657 14.2278 6.01719C14.27 5.74376 14.0793 5.49063 13.8059 5.45001L9.83871 4.87344L8.06527 1.27813C8.01683 1.17969 7.93715 1.10001 7.83871 1.05157C7.59183 0.929693 7.29183 1.03126 7.1684 1.27813L5.39496 4.87344L1.42777 5.45001C1.3184 5.46563 1.2184 5.51719 1.14183 5.59532C0.949647 5.79376 0.952772 6.10938 1.15121 6.30313L4.02152 9.10157L3.3434 13.0531C3.32465 13.1609 3.34183 13.2734 3.3934 13.3703C3.52152 13.6141 3.82465 13.7094 4.0684 13.5797L7.61684 11.7141L8.50394 12.1805" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<template> <template>
<div class="time-sort-select-box"> <div class="time-sort-select-box">
<el-select v-model="isSort" placeholder="发布时间" style="width: 130px" @change="handlePxChange"> <el-select v-model="sortValue" placeholder="排序方式" style="width: 130px" @change="handlePxChange">
<template #prefix> <template #prefix>
<div style="display: flex; align-items: center; height: 100%"> <div style="display: flex; align-items: center; height: 100%">
<img v-if="isSort" src="./down.svg" style="width: 16px; height: 16px" /> <img v-if="sortValue === 1" src="./down.svg" style="width: 16px; height: 16px" />
<img v-else src="./up.svg" style="width: 16px; height: 16px" /> <img v-else-if="sortValue === 2" src="./up.svg" style="width: 16px; height: 16px" />
<img v-else-if="sortValue === 3" src="./down1.svg" style="width: 16px; height: 16px" />
<img v-else-if="sortValue === 4" src="./down2.svg" style="width: 16px; height: 16px" />
</div> </div>
</template> </template>
<el-option v-for="item in timeList" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in sortList" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { computed, ref } from 'vue'
const isSort = ref(true) const props = defineProps({
sortDemension: {
type: Number,
default: 1
}
})
const timeList = ref([ const sortValue = ref(1)
{ label: "时间倒序", value: true },
{ label: "时间正序", value: false } const sortList = computed(() => {
]) if (props.sortDemension === 1) {
return [
{ label: "时间倒序", value: 1 },
{ label: "时间正序", value: 2 },
]
} else if (props.sortDemension === 2) {
return [
{ label: "时间倒序", value: 1 },
{ label: "时间正序", value: 2 },
{ label: "评分倒序", value: 3 },
]
} else {
return [
{ label: "时间倒序", value: 1 },
{ label: "时间正序", value: 2 },
{ label: "评分倒序", value: 3 },
{ label: "热度倒序", value: 4 },
]
}
})
const emits = defineEmits(['handlePxChange']) const emits = defineEmits(['handlePxChange'])
const handlePxChange = () => { const handlePxChange = () => {
emits('handlePxChange', isSort.value) emits('handlePxChange', sortValue.value)
} }
</script> </script>
......
...@@ -15,7 +15,7 @@ const thinktankRoutes = [ ...@@ -15,7 +15,7 @@ const thinktankRoutes = [
name: "thinkTank", name: "thinkTank",
component: thinkTank, component: thinkTank,
meta: { meta: {
title: "科技智库概览", title: "美国科技智库概览",
isShowHeader: true isShowHeader: true
} }
}, },
......
...@@ -4,15 +4,19 @@ ...@@ -4,15 +4,19 @@
<pre> <pre>
{{ {{
` `
import TimeSortSelectBox from '@/components/base/TimeSortSelectBox/index.vue' import TimeSortSelectBox from '@/components/base/TimeSortSelectBox/index.vue'
<div class="time-box"> <div class="time-box">
<TimeSortSelectBox @handle-px-change="handlePx" /> <TimeSortSelectBox @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="2" @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="3" @handle-px-change="handleTimePx" />
</div> </div>
` `
}} }}
</pre> </pre>
<div class="time-box"> <div class="time-box">
<TimeSortSelectBox @handle-px-change="handlePx" /> <TimeSortSelectBox @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="2" @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="3" @handle-px-change="handleTimePx" />
</div> </div>
</el-col> </el-col>
...@@ -24,12 +28,24 @@ ...@@ -24,12 +28,24 @@
import { ref } from 'vue' import { ref } from 'vue'
import '@/styles/common.scss' import '@/styles/common.scss'
import TimeSortSelectBox from '@/components/base/TimeSortSelectBox/index.vue' import TimeSortSelectBox from '@/components/base/TimeSortSelectBox/index.vue'
import HeatSortSelectBox from '@/components/base/HeatSortSelectBox/index.vue'
import GradeSortSelectBox from '@/components/base/GradeSortSelectBox/index.vue'
const span = 12 const span = 12
const handlePx = (val) => { const handleTimePx = (val) => {
alert(val)
}
const handleHeatPx = (val) => {
alert(val) alert(val)
} }
const handleGradePx = (val) => {
alert(val)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -39,5 +55,7 @@ const handlePx = (val) => { ...@@ -39,5 +55,7 @@ const handlePx = (val) => {
padding: 100px; padding: 100px;
background: #F2F8FF; background: #F2F8FF;
border: 1px solid var(--bg-black-5); border: 1px solid var(--bg-black-5);
display: flex;
gap: 8px;
} }
</style> </style>
\ No newline at end of file
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
<div class="text-title-1-show">文字样式</div> <div class="text-title-1-show">文字样式</div>
<TextStyle /> <TextStyle />
<div class="text-title-1-show">通用样式/组件</div> <div class="text-title-1-show">通用样式/组件</div>
<div style="position: relative; min-height: 700px;"> <div style="position: relative; height: 800px;">
<el-tabs tabPosition="left" class="tabs-nav-no-wrap left-float-nav-tabs dev-style-tabs"> <el-tabs tabPosition="left" style="position: relative; height: 700px;" class="tabs-nav-no-wrap left-float-nav-tabs dev-style-tabs">
<el-tab-pane label="通用" lazy> <el-tab-pane label="通用" lazy>
<common-page /> <common-page />
</el-tab-pane> </el-tab-pane>
...@@ -73,10 +73,9 @@ ...@@ -73,10 +73,9 @@
<el-tab-pane label="激活工作框" lazy> <el-tab-pane label="激活工作框" lazy>
<WorkingBox /> <WorkingBox />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="时间排序" lazy> <el-tab-pane label="自定义排序" lazy>
<TimeSortSelectBox /> <TimeSortSelectBox />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</el-space> </el-space>
......
...@@ -4,6 +4,69 @@ import getQuarterRange from './getQuarterRange'; ...@@ -4,6 +4,69 @@ import getQuarterRange from './getQuarterRange';
import * as echarts from 'echarts' import * as echarts from 'echarts'
import 'echarts-wordcloud'; import 'echarts-wordcloud';
import router from '@/router/index' import router from '@/router/index'
const LEGEND_ARROW_NAMES = new Set(['__legend_prev__', '__legend_next__'])
const shouldShowLegendPagingArrows = (chart, option) => {
const w = typeof chart?.getWidth === 'function' ? chart.getWidth() : 0
const legendOpt = option?.legend
const legend = Array.isArray(legendOpt) ? legendOpt[0] : legendOpt
if (!legend || legend.type !== 'scroll') return false
const names = Array.isArray(legend.data) ? legend.data : []
if (!names.length) return false
const resolveSize = (val, base) => {
if (val == null) return 0
if (typeof val === 'number' && Number.isFinite(val)) return val
const s = String(val).trim()
if (!s) return 0
if (s.endsWith('%')) {
const n = parseFloat(s.slice(0, -1))
return Number.isFinite(n) ? (base * n) / 100 : 0
}
const n = Number(s)
return Number.isFinite(n) ? n : 0
}
const available = resolveSize(legend.width, w) || Math.floor(w * 0.82)
if (!available || available <= 0) return true
const fontSize = Number(legend?.textStyle?.fontSize || 14)
const fontFamily = legend?.textStyle?.fontFamily || 'Source Han Sans CN'
const fontWeight = legend?.textStyle?.fontWeight || 400
const iconW = Number(legend?.itemWidth || 12)
const gap = 10
const paddingPerItem = iconW + gap + 10
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) return true
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`
const total = names.reduce((sum, n) => {
const text = String(n ?? '')
const textW = ctx.measureText(text).width || 0
return sum + textW + paddingPerItem
}, 0)
return total > available
}
const applyLegendPagingArrowVisibility = (chart, option) => {
const shouldShow = shouldShowLegendPagingArrows(chart, option)
const graphic = option?.graphic
if (!Array.isArray(graphic) || graphic.length === 0) return
const nextGraphic = graphic.map((g) => {
if (!g || !LEGEND_ARROW_NAMES.has(g.name)) return g
return {
...g,
invisible: !shouldShow,
silent: !shouldShow
}
})
chart.setOption({ graphic: nextGraphic }, false, true)
}
const setChart = (option, chartId, allowClick, selectParam) => { const setChart = (option, chartId, allowClick, selectParam) => {
let chartDom = document.getElementById(chartId); let chartDom = document.getElementById(chartId);
if (!chartDom) { if (!chartDom) {
...@@ -12,8 +75,39 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -12,8 +75,39 @@ const setChart = (option, chartId, allowClick, selectParam) => {
chartDom.removeAttribute("_echarts_instance_"); chartDom.removeAttribute("_echarts_instance_");
let chart = echarts.init(chartDom); let chart = echarts.init(chartDom);
chart.setOption(option); chart.setOption(option);
// 处理自定义图例分页箭头(左右分布,隐藏页码)
// 约定:graphic 元素 name 为 __legend_prev__ / __legend_next__
chart.on('click', function (params) {
if (params?.componentType !== 'graphic') return;
if (!LEGEND_ARROW_NAMES.has(params?.name)) return;
const opt = chart.getOption?.() || {};
const legend = Array.isArray(opt.legend) ? opt.legend[0] : null;
if (!legend || legend.type !== 'scroll') return;
const dataLen = Array.isArray(legend.data) ? legend.data.length : 0;
if (dataLen <= 0) return;
const cur = Number(legend.scrollDataIndex || 0);
const nextIndex = params.name === '__legend_prev__' ? Math.max(0, cur - 1) : Math.min(dataLen - 1, cur + 1);
if (nextIndex === cur) return;
chart.dispatchAction({
type: 'legendScroll',
scrollDataIndex: nextIndex
});
});
// 初次渲染后判断是否需要显示左右箭头(可一行展示则隐藏)
applyLegendPagingArrowVisibility(chart, option)
if (allowClick) { if (allowClick) {
chart.on('click', function (params) { chart.on('click', function (params) {
// 图例分页箭头只负责翻页,不走任何跳转
if (params?.componentType === 'graphic' && LEGEND_ARROW_NAMES.has(params?.name)) {
return
}
switch (selectParam.moduleType) { switch (selectParam.moduleType) {
case '国会法案': case '国会法案':
if (selectParam.key === 1) { if (selectParam.key === 1) {
...@@ -112,6 +206,7 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -112,6 +206,7 @@ const setChart = (option, chartId, allowClick, selectParam) => {
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致 // 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout(() => { setTimeout(() => {
chart.resize(); chart.resize();
applyLegendPagingArrowVisibility(chart, option)
}, 0); }, 0);
return chart; return chart;
}; };
......
...@@ -186,18 +186,26 @@ let leftChart; ...@@ -186,18 +186,26 @@ let leftChart;
const rightChartRef = ref(null); const rightChartRef = ref(null);
let rightChart; let rightChart;
const isShowAiLeft = ref(true); const isShowAiLeft = ref(false);
const aiContentLeft = ref(""); const aiContentLeft = ref("");
const isLeftInterpretLoading = ref(false); const isLeftInterpretLoading = ref(false);
const leftAiAbortController = ref(null);
const isShowAiRight = ref(true); const isShowAiRight = ref(false);
const aiContentRight = ref(""); const aiContentRight = ref("");
const isRightInterpretLoading = ref(false); const isRightInterpretLoading = ref(false);
const rightAiAbortController = ref(null);
const handleSwitchAiLeft = (val) => { const handleSwitchAiLeft = (val) => {
isShowAiLeft.value = val; isShowAiLeft.value = val;
if (val) { if (val) {
fetchLeftInterpretation(); fetchLeftInterpretation();
} else {
if (leftAiAbortController.value) {
leftAiAbortController.value.abort();
leftAiAbortController.value = null;
}
isLeftInterpretLoading.value = false;
} }
}; };
...@@ -205,6 +213,12 @@ const handleSwitchAiRight = (val) => { ...@@ -205,6 +213,12 @@ const handleSwitchAiRight = (val) => {
isShowAiRight.value = val; isShowAiRight.value = val;
if (val) { if (val) {
fetchRightInterpretation(); fetchRightInterpretation();
} else {
if (rightAiAbortController.value) {
rightAiAbortController.value.abort();
rightAiAbortController.value = null;
}
isRightInterpretLoading.value = false;
} }
}; };
...@@ -276,12 +290,17 @@ const fetchLeftInterpretation = async () => { ...@@ -276,12 +290,17 @@ const fetchLeftInterpretation = async () => {
if (hasValidContent || isLeftInterpretLoading.value) { if (hasValidContent || isLeftInterpretLoading.value) {
return; return;
} }
if (leftAiAbortController.value) {
leftAiAbortController.value.abort();
}
leftAiAbortController.value = new AbortController();
isLeftInterpretLoading.value = true; isLeftInterpretLoading.value = true;
aiContentLeft.value = "解读生成中…"; aiContentLeft.value = "解读生成中…";
try { try {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(payload) }, { text: JSON.stringify(payload) },
{ {
signal: leftAiAbortController.value.signal,
onChunk: (chunk) => { onChunk: (chunk) => {
// 与智库概览「数量变化趋势」一致:按 chunk 增量拼接展示 // 与智库概览「数量变化趋势」一致:按 chunk 增量拼接展示
appendAiInterpretationChunk(aiContentLeft, chunk); appendAiInterpretationChunk(aiContentLeft, chunk);
...@@ -292,10 +311,13 @@ const fetchLeftInterpretation = async () => { ...@@ -292,10 +311,13 @@ const fetchLeftInterpretation = async () => {
// 与智库概览一致:优先用最终「解读」收口;否则保留已拼接内容 // 与智库概览一致:优先用最终「解读」收口;否则保留已拼接内容
aiContentLeft.value = text || aiContentLeft.value || "未返回有效解读内容"; aiContentLeft.value = text || aiContentLeft.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("合作限制政策对比图表解读请求失败", error); console.error("合作限制政策对比图表解读请求失败", error);
aiContentLeft.value = "解读加载失败"; aiContentLeft.value = "解读加载失败";
}
} finally { } finally {
isLeftInterpretLoading.value = false; isLeftInterpretLoading.value = false;
leftAiAbortController.value = null;
} }
}; };
...@@ -348,12 +370,17 @@ const fetchRightInterpretation = async () => { ...@@ -348,12 +370,17 @@ const fetchRightInterpretation = async () => {
if (hasValidContent || isRightInterpretLoading.value) { if (hasValidContent || isRightInterpretLoading.value) {
return; return;
} }
if (rightAiAbortController.value) {
rightAiAbortController.value.abort();
}
rightAiAbortController.value = new AbortController();
isRightInterpretLoading.value = true; isRightInterpretLoading.value = true;
aiContentRight.value = "解读生成中…"; aiContentRight.value = "解读生成中…";
try { try {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(payload) }, { text: JSON.stringify(payload) },
{ {
signal: rightAiAbortController.value.signal,
onChunk: (chunk) => { onChunk: (chunk) => {
appendAiInterpretationChunk(aiContentRight, chunk); appendAiInterpretationChunk(aiContentRight, chunk);
} }
...@@ -362,10 +389,13 @@ const fetchRightInterpretation = async () => { ...@@ -362,10 +389,13 @@ const fetchRightInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentRight.value = text || aiContentRight.value || "未返回有效解读内容"; aiContentRight.value = text || aiContentRight.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("合作限制领域分布图表解读请求失败", error); console.error("合作限制领域分布图表解读请求失败", error);
aiContentRight.value = "解读加载失败"; aiContentRight.value = "解读加载失败";
}
} finally { } finally {
isRightInterpretLoading.value = false; isRightInterpretLoading.value = false;
rightAiAbortController.value = null;
} }
}; };
......
...@@ -61,17 +61,17 @@ ...@@ -61,17 +61,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="page">
<div class="count">{{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
</template> </template>
<template v-else> <template v-else>
<div class="right-main-empty"> <div class="right-main-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" /> <el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div> </div>
</template> </template>
<div class="page">
<div class="count">共 {{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :page-count="pageCount"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -293,6 +293,11 @@ const total = ref(0); ...@@ -293,6 +293,11 @@ const total = ref(0);
const pageSize = ref(10); const pageSize = ref(10);
const currentPage = ref(1); const currentPage = ref(1);
const reslibContainer = ref(null); const reslibContainer = ref(null);
const pageCount = computed(() => {
const size = Number(pageSize.value || 10) || 10;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const getTypeClass = (type) => { const getTypeClass = (type) => {
const map = { const map = {
...@@ -658,13 +663,16 @@ watch(currentPage, () => { ...@@ -658,13 +663,16 @@ watch(currentPage, () => {
.title { .title {
font-size: 20px; font-size: 20px;
width: 950px; max-width: 950px;
font-weight: 700; font-weight: 700;
font-family: "Microsoft YaHei"; font-family: "Microsoft YaHei";
line-height: 26px; line-height: 26px;
color: rgb(59, 65, 75); color: rgb(59, 65, 75);
margin-bottom: 9px; margin-bottom: 9px;
cursor: pointer; cursor: pointer;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
} }
.content { .content {
......
...@@ -26,27 +26,15 @@ ...@@ -26,27 +26,15 @@
<div class="btn-box"> <div class="btn-box">
<div class="translate"> <div class="translate">
<div class="search-input-wrap" v-if="showSearchInput"> <div class="search-input-wrap" v-if="showSearchInput">
<input <input v-model="searchKeywordText" class="search-input" placeholder="回车查询"
v-model="searchKeywordText" @keyup.enter="handleSearchInPdf" />
class="search-input"
placeholder="回车查询"
@keyup.enter="handleSearchInPdf"
/>
<div class="search-match-count">{{ matchInfo.current }}/{{ matchInfo.total }}</div> <div class="search-match-count">{{ matchInfo.current }}/{{ matchInfo.total }}</div>
<button <button class="search-nav-btn" type="button" @click="handlePrevMatch"
class="search-nav-btn" :disabled="matchInfo.total === 0 || matchInfo.current <= 1">
type="button"
@click="handlePrevMatch"
:disabled="matchInfo.total === 0 || matchInfo.current <= 1"
>
上一个 上一个
</button> </button>
<button <button class="search-nav-btn" type="button" @click="handleNextMatch"
class="search-nav-btn" :disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total">
type="button"
@click="handleNextMatch"
:disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total"
>
下一个 下一个
</button> </button>
</div> </div>
...@@ -54,19 +42,14 @@ ...@@ -54,19 +42,14 @@
<el-switch v-model="valueSwitch" /> <el-switch v-model="valueSwitch" />
</div> </div>
<div class="translate-image"> <div class="translate-image">
<img <img class="translate-icon" src="../assets/icon-translation.png" alt="" style="
class="translate-icon"
src="../assets/icon-translation.png"
alt=""
style="
width: 16px; width: 16px;
height: 16px; height: 16px;
max-width: 16px; max-width: 16px;
max-height: 16px; max-height: 16px;
display: block; display: block;
object-fit: contain; object-fit: contain;
" " />
/>
</div> </div>
<div class="translate-text">{{ "显示原文" }}</div> <div class="translate-text">{{ "显示原文" }}</div>
</div> </div>
...@@ -83,12 +66,8 @@ ...@@ -83,12 +66,8 @@
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" /> <pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div> </div>
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage"> <div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf <pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
:key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" class="pdf-pane-inner" />
ref="rightPdfRef"
:pdfUrl="reportUrlWithPage"
class="pdf-pane-inner"
/>
</div> </div>
</div> </div>
</div> </div>
...@@ -203,7 +182,7 @@ const handleSearchInPdf = async () => { ...@@ -203,7 +182,7 @@ const handleSearchInPdf = async () => {
try { try {
const { ElMessage } = await import("element-plus"); const { ElMessage } = await import("element-plus");
ElMessage.warning("未找到包含该关键词的页面"); ElMessage.warning("未找到包含该关键词的页面");
} catch (_) {} } catch (_) { }
} }
}; };
...@@ -246,7 +225,7 @@ const handleDownload = async () => { ...@@ -246,7 +225,7 @@ const handleDownload = async () => {
try { try {
const { ElMessage } = await import("element-plus"); const { ElMessage } = await import("element-plus");
ElMessage.warning("暂无下载链接"); ElMessage.warning("暂无下载链接");
} catch (_) {} } catch (_) { }
return; return;
} }
const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-"); const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-");
...@@ -659,6 +638,7 @@ onMounted(async () => { ...@@ -659,6 +638,7 @@ onMounted(async () => {
display: flex; display: flex;
gap: 8px; gap: 8px;
cursor: pointer; cursor: pointer;
.icon { .icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
......
...@@ -171,6 +171,10 @@ const options = [ ...@@ -171,6 +171,10 @@ const options = [
]; ];
/** value 须与 v-model 类型一致(数字),否则 el-select 无法匹配 label,会显示成「2025」而非「2025年」 */ /** value 须与 v-model 类型一致(数字),否则 el-select 无法匹配 label,会显示成「2025」而非「2025年」 */
const options1 = [ const options1 = [
{
value: 2026,
label: "2026年"
},
{ {
value: 2025, value: 2025,
label: "2025年" label: "2025年"
...@@ -719,11 +723,11 @@ let rightChart1; ...@@ -719,11 +723,11 @@ let rightChart1;
let leftSankey; let leftSankey;
let boxplotChart; let boxplotChart;
// ------- AI 解读(刷新后默认展开,行为对齐智库概览) ------- // ------- AI 解读(默认仅展示 AiButton,悬停后再请求 AI) -------
const isShowAiContentLeft1 = ref(true); const isShowAiContentLeft1 = ref(false);
const isShowAiContentLeft2 = ref(true); const isShowAiContentLeft2 = ref(false);
const isShowAiContentRight1 = ref(true); const isShowAiContentRight1 = ref(false);
const isShowAiContentRight2 = ref(true); const isShowAiContentRight2 = ref(false);
const aiContentLeft1 = ref(""); const aiContentLeft1 = ref("");
const aiContentLeft2 = ref(""); const aiContentLeft2 = ref("");
...@@ -735,6 +739,11 @@ const isAiLoadingLeft2 = ref(false); ...@@ -735,6 +739,11 @@ const isAiLoadingLeft2 = ref(false);
const isAiLoadingRight1 = ref(false); const isAiLoadingRight1 = ref(false);
const isAiLoadingRight2 = ref(false); const isAiLoadingRight2 = ref(false);
const left1AiAbortController = ref(null);
const left2AiAbortController = ref(null);
const right1AiAbortController = ref(null);
const right2AiAbortController = ref(null);
const AI_LOADING_TEXT = "解读生成中…"; const AI_LOADING_TEXT = "解读生成中…";
// 用于保证“切换筛选后只写入最新一次解读结果” // 用于保证“切换筛选后只写入最新一次解读结果”
const left1AiSeq = ref(0); const left1AiSeq = ref(0);
...@@ -767,7 +776,13 @@ const getInterpretationTextFromChartResponse = (res) => { ...@@ -767,7 +776,13 @@ const getInterpretationTextFromChartResponse = (res) => {
); );
}; };
const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSeqRef) => { const fetchChartInterpretationOnce = async (
payload,
targetRef,
loadingRef,
aiSeqRef,
abortControllerRef
) => {
if (loadingRef.value) return; if (loadingRef.value) return;
const hasValidContent = const hasValidContent =
...@@ -778,12 +793,19 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe ...@@ -778,12 +793,19 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe
if (hasValidContent) return; if (hasValidContent) return;
const localSeq = aiSeqRef.value; const localSeq = aiSeqRef.value;
if (abortControllerRef?.value) {
abortControllerRef.value.abort();
}
if (abortControllerRef) {
abortControllerRef.value = new AbortController();
}
loadingRef.value = true; loadingRef.value = true;
targetRef.value = AI_LOADING_TEXT; targetRef.value = AI_LOADING_TEXT;
try { try {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(payload) }, { text: JSON.stringify(payload) },
{ {
...(abortControllerRef?.value?.signal ? { signal: abortControllerRef.value.signal } : {}),
onChunk: (chunk) => { onChunk: (chunk) => {
if (aiSeqRef.value !== localSeq) return; if (aiSeqRef.value !== localSeq) return;
appendAiInterpretationChunk(targetRef, chunk, AI_LOADING_TEXT); appendAiInterpretationChunk(targetRef, chunk, AI_LOADING_TEXT);
...@@ -796,10 +818,13 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe ...@@ -796,10 +818,13 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe
targetRef.value = text || targetRef.value || "未返回有效解读内容"; targetRef.value = text || targetRef.value || "未返回有效解读内容";
} catch (e) { } catch (e) {
if (aiSeqRef.value !== localSeq) return; if (aiSeqRef.value !== localSeq) return;
if (e?.name !== "AbortError") {
console.error("图表解读请求失败", e); console.error("图表解读请求失败", e);
targetRef.value = "解读加载失败"; targetRef.value = "解读加载失败";
}
} finally { } finally {
if (aiSeqRef.value === localSeq) loadingRef.value = false; if (aiSeqRef.value === localSeq) loadingRef.value = false;
if (abortControllerRef) abortControllerRef.value = null;
} }
}; };
...@@ -842,43 +867,71 @@ const buildPayloadRight2 = () => { ...@@ -842,43 +867,71 @@ const buildPayloadRight2 = () => {
const handleSwitchAiLeft1 = async (val) => { const handleSwitchAiLeft1 = async (val) => {
isShowAiContentLeft1.value = val; isShowAiContentLeft1.value = val;
if (!val) return; if (!val) {
if (left1AiAbortController.value) {
left1AiAbortController.value.abort();
left1AiAbortController.value = null;
}
isAiLoadingLeft1.value = false;
return;
}
const payload = buildPayloadLeft1(); const payload = buildPayloadLeft1();
if (!payload) { if (!payload) {
aiContentLeft1.value = "暂无图表数据"; aiContentLeft1.value = "暂无图表数据";
return; return;
} }
await fetchChartInterpretationOnce(payload, aiContentLeft1, isAiLoadingLeft1, left1AiSeq); await fetchChartInterpretationOnce(payload, aiContentLeft1, isAiLoadingLeft1, left1AiSeq, left1AiAbortController);
}; };
const handleSwitchAiLeft2 = async (val) => { const handleSwitchAiLeft2 = async (val) => {
isShowAiContentLeft2.value = val; isShowAiContentLeft2.value = val;
if (!val) return; if (!val) {
if (left2AiAbortController.value) {
left2AiAbortController.value.abort();
left2AiAbortController.value = null;
}
isAiLoadingLeft2.value = false;
return;
}
const payload = buildPayloadLeft2(); const payload = buildPayloadLeft2();
if (!payload) { if (!payload) {
aiContentLeft2.value = "暂无图表数据"; aiContentLeft2.value = "暂无图表数据";
return; return;
} }
await fetchChartInterpretationOnce(payload, aiContentLeft2, isAiLoadingLeft2, left2AiSeq); await fetchChartInterpretationOnce(payload, aiContentLeft2, isAiLoadingLeft2, left2AiSeq, left2AiAbortController);
}; };
const handleSwitchAiRight1 = async (val) => { const handleSwitchAiRight1 = async (val) => {
isShowAiContentRight1.value = val; isShowAiContentRight1.value = val;
if (!val) return; if (!val) {
if (right1AiAbortController.value) {
right1AiAbortController.value.abort();
right1AiAbortController.value = null;
}
isAiLoadingRight1.value = false;
return;
}
const payload = buildPayloadRight1(); const payload = buildPayloadRight1();
if (!payload) { if (!payload) {
aiContentRight1.value = "暂无图表数据"; aiContentRight1.value = "暂无图表数据";
return; return;
} }
await fetchChartInterpretationOnce(payload, aiContentRight1, isAiLoadingRight1, right1AiSeq); await fetchChartInterpretationOnce(payload, aiContentRight1, isAiLoadingRight1, right1AiSeq, right1AiAbortController);
}; };
const handleSwitchAiRight2 = async (val) => { const handleSwitchAiRight2 = async (val) => {
isShowAiContentRight2.value = val; isShowAiContentRight2.value = val;
if (!val) return; if (!val) {
if (right2AiAbortController.value) {
right2AiAbortController.value.abort();
right2AiAbortController.value = null;
}
isAiLoadingRight2.value = false;
return;
}
const payload = buildPayloadRight2(); const payload = buildPayloadRight2();
if (!payload) { if (!payload) {
aiContentRight2.value = "暂无图表数据"; aiContentRight2.value = "暂无图表数据";
return; return;
} }
await fetchChartInterpretationOnce(payload, aiContentRight2, isAiLoadingRight2, right2AiSeq); await fetchChartInterpretationOnce(payload, aiContentRight2, isAiLoadingRight2, right2AiSeq, right2AiAbortController);
}; };
...@@ -1136,21 +1189,15 @@ const initLeftSankey = (data) => { ...@@ -1136,21 +1189,15 @@ const initLeftSankey = (data) => {
// }; // };
onMounted(() => { onMounted(() => {
// 刷新后 AiPane 默认展开:先给出“解读生成中…”占位,再在数据到位后触发解读请求 // 仅拉取图表数据;AI 解读仅在用户悬停打开面板时触发
aiContentLeft1.value = "解读生成中…"; void handleGetFundField();
aiContentLeft2.value = "解读生成中…";
aiContentRight1.value = "解读生成中…";
aiContentRight2.value = "解读生成中…";
// 先拉数据;每块数据到位后立即触发一次 AI 解读(不必等其它块完成)
void handleGetFundField().then(() => handleSwitchAiLeft1(true));
void handleFindCountryProjectAreaList(); void handleFindCountryProjectAreaList();
void handlegetCountryFundingChange().then(() => handleSwitchAiRight1(true)); void handlegetCountryFundingChange();
void handlegetCountryFundProjectChange(); void handlegetCountryFundProjectChange();
void handleGetOrgFundsArea().then(() => handleSwitchAiLeft2(true)); void handleGetOrgFundsArea();
void handlegetOrgFundStrength().then(() => handleSwitchAiRight2(true)); void handlegetOrgFundStrength();
}); });
// onBeforeUnmount(() => { // onBeforeUnmount(() => {
// window.removeEventListener("resize", handleResize); // window.removeEventListener("resize", handleResize);
......
...@@ -166,6 +166,10 @@ const handleTimeGroupChange = (val) => { ...@@ -166,6 +166,10 @@ const handleTimeGroupChange = (val) => {
}; };
const pubTimeList = ref([ const pubTimeList = ref([
{
id: 2026,
name: "2026年"
},
{ {
id: 2025, id: 2025,
name: "2025年" name: "2025年"
...@@ -192,13 +196,13 @@ const pubTimeList = ref([ ...@@ -192,13 +196,13 @@ const pubTimeList = ref([
} }
]); ]);
/** 选择「全部时间」时,yearlist 传 2000~2025 逐年 */ /** 选择「全部时间」时,yearlist 传 2000~2026 逐年 */
const YEAR_ALL_RANGE_START = 2000; const YEAR_ALL_RANGE_START = 2000;
const YEAR_ALL_RANGE_END = 2025; const YEAR_ALL_RANGE_END = 2026;
const buildYearlistForRequest = (selectedTimeModel) => { const buildYearlistForRequest = (selectedTimeModel) => {
const strippedTime = stripAllTimeForRequest(selectedTimeModel); const strippedTime = stripAllTimeForRequest(selectedTimeModel);
// 仅勾选「全部时间」、未选具体年份时,传 2000~2025 逐年 // 仅勾选「全部时间」、未选具体年份时,传 2000~2026 逐年
if (strippedTime.length === 0) { if (strippedTime.length === 0) {
const out = []; const out = [];
for (let y = YEAR_ALL_RANGE_START; y <= YEAR_ALL_RANGE_END; y += 1) { for (let y = YEAR_ALL_RANGE_START; y <= YEAR_ALL_RANGE_END; y += 1) {
......
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
<div class="box5-footer"> <div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div> </div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div> </div>
...@@ -305,14 +305,21 @@ const handleGetThinkTankHearingInfo = async () => { ...@@ -305,14 +305,21 @@ const handleGetThinkTankHearingInfo = async () => {
}; };
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"国会听证会关键词云,数据来源:美国兰德公司官网"; "国会听证会关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结 // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(true); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val; isShowAiContentBox5.value = val;
if (val) { if (val) {
fetchBox5ChartInterpretation(); fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
} }
}; };
...@@ -510,10 +517,7 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -510,10 +517,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) { if (data.length) {
box5WordCloudKey.value += 1; box5WordCloudKey.value += 1;
} }
// 刷新后默认展开 AI:数据就绪即触发解读 // 仅在用户打开 AI 面板时才请求解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -663,6 +667,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -663,6 +667,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) { if (hasValidContent || isBox5InterpretLoading.value) {
return; return;
} }
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true; isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…"; aiContentBox5.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -677,6 +685,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -677,6 +685,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: box5AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk); appendAiInterpretationChunk(aiContentBox5, chunk);
} }
...@@ -685,10 +694,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -685,10 +694,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容"; aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error); console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败"; aiContentBox5.value = "解读加载失败";
}
} finally { } finally {
isBox5InterpretLoading.value = false; isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
} }
}; };
......
...@@ -248,7 +248,7 @@ const handleDownloadDocument = async () => { ...@@ -248,7 +248,7 @@ const handleDownloadDocument = async () => {
display: flex; display: flex;
img { img {
width: 72px; width: 178px;
height: 88px; height: 88px;
} }
......
...@@ -68,11 +68,11 @@ ...@@ -68,11 +68,11 @@
<div class="info-content"> <div class="info-content">
<div class="info-item"> <div class="info-item">
<div class="info-text">{{ "相关领域:" }}</div> <div class="info-text">{{ "相关领域:" }}</div>
<div class="info-right" v-if="box1DataItem && box1DataItem.domains"> <div class="info-right" v-if="hasDomainTags">
<div class="tag-box"> <div class="tag-box">
<div class="tag" v-for="(item, index) in box1DataItem.domains" :key="index" v-show="item">{{ <div class="tag" v-for="(name, index) in domainTags" :key="name + '-' + index">
item.industryName {{ name }}
}}</div> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -155,6 +155,12 @@ const activeItemIndex = ref(0); ...@@ -155,6 +155,12 @@ const activeItemIndex = ref(0);
const pageSize = ref(10); const pageSize = ref(10);
// 当前选中 item 的数据 // 当前选中 item 的数据
const box1DataItem = ref(null); const box1DataItem = ref(null);
const domainTags = computed(() => {
const domains = box1DataItem.value?.domains;
const list = Array.isArray(domains) ? domains : [];
return list.map((d) => d?.industryName).filter(Boolean);
});
const hasDomainTags = computed(() => domainTags.value.length > 0);
// 整个页面容器,用于分页后滚回 wrap 顶部 // 整个页面容器,用于分页后滚回 wrap 顶部
const wrapRef = ref(null); const wrapRef = ref(null);
const reportUrl = ref(""); const reportUrl = ref("");
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
<div class="box5-footer"> <div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div> </div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div> </div>
...@@ -259,14 +259,21 @@ const props = defineProps({ ...@@ -259,14 +259,21 @@ const props = defineProps({
}); });
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网"; "智库报告关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结 // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(true); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val; isShowAiContentBox5.value = val;
if (val) { if (val) {
fetchBox5ChartInterpretation(); fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
} }
}; };
...@@ -463,10 +470,7 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -463,10 +470,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) { if (data.length) {
box5WordCloudKey.value += 1; box5WordCloudKey.value += 1;
} }
// 刷新后默认展开 AI:数据就绪即触发解读 // 仅在用户打开 AI 面板时才请求解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -617,6 +621,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -617,6 +621,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) { if (hasValidContent || isBox5InterpretLoading.value) {
return; return;
} }
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true; isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…"; aiContentBox5.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -631,6 +639,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -631,6 +639,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: box5AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk); appendAiInterpretationChunk(aiContentBox5, chunk);
} }
...@@ -639,10 +648,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -639,10 +648,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容"; aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error); console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败"; aiContentBox5.value = "解读加载失败";
}
} finally { } finally {
isBox5InterpretLoading.value = false; isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
} }
}; };
...@@ -963,7 +975,7 @@ onMounted(() => { ...@@ -963,7 +975,7 @@ onMounted(() => {
display: flex; display: flex;
.left { .left {
width: 56px; width: 150px;
height: 74px; height: 74px;
margin-top: 8px; margin-top: 8px;
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
<div class="box1-footer"> <div class="box1-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div> </div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div> </div>
...@@ -185,14 +185,21 @@ const applySurveyProjectDocumentTitle = (title) => { ...@@ -185,14 +185,21 @@ const applySurveyProjectDocumentTitle = (title) => {
}; };
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"调查项目关键词云,数据来源:美国兰德公司官网"; "调查项目关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结 // 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(true); const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val; isShowAiContentBox5.value = val;
if (val) { if (val) {
fetchBox5ChartInterpretation(); fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
} }
}; };
const searchOpinions = ref(''); const searchOpinions = ref('');
...@@ -403,10 +410,7 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -403,10 +410,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) { if (data.length) {
box5WordCloudKey.value += 1; box5WordCloudKey.value += 1;
} }
// 刷新后默认展开 AI:数据就绪即触发解读 // 仅在用户打开 AI 面板时才请求解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -548,6 +552,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -548,6 +552,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) { if (hasValidContent || isBox5InterpretLoading.value) {
return; return;
} }
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true; isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…"; aiContentBox5.value = "解读生成中…";
const chartPayload = { const chartPayload = {
...@@ -562,6 +570,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -562,6 +570,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis( const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) }, { text: JSON.stringify(chartPayload) },
{ {
signal: box5AiAbortController.value.signal,
onChunk: chunk => { onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk); appendAiInterpretationChunk(aiContentBox5, chunk);
} }
...@@ -570,10 +579,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -570,10 +579,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容"; aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error); console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败"; aiContentBox5.value = "解读加载失败";
}
} finally { } finally {
isBox5InterpretLoading.value = false; isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
} }
}; };
......
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { MUTICHARTCOLORS } from "@/common/constant.js";
// 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色) const parseHexToRgb = (hex) => {
const AREA_TAG_COLOR_BY_NAME = { const h = String(hex || '').replace('#', '').trim()
'人工智能': 'rgba(245, 34, 45, 1)', // tag1 if (h.length !== 6) return { r: 0, g: 0, b: 0 }
'生物科技': 'rgba(19, 168, 168, 1)', // tag2
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
// 兼容常见写法
'通信网络': 'rgba(5, 95, 194, 1)',
'量子科技': 'rgba(114, 46, 209, 1)', // tag4
'新能源': 'rgba(82, 196, 26, 1)', // tag5
'集成电路': 'rgba(22, 119, 255, 1)', // tag6
'海洋': 'rgba(15, 120, 199, 1)', // tag7
'先进制造': 'rgba(250, 173, 20, 1)', // tag8
'新材料': 'rgba(250, 140, 22, 1)', // tag9
'航空航天': 'rgba(47, 84, 235, 1)', // tag10
'太空': 'rgba(47, 84, 235, 1)', // tag11
'深海': 'rgba(73, 104, 161, 1)', // tag12
'极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
const fallbackColorList = [
'rgba(5, 95, 194, 1)',
'rgba(245, 34, 45, 1)',
'rgba(19, 168, 168, 1)',
'rgba(250, 140, 22, 1)',
'rgba(114, 46, 209, 1)',
'rgba(82, 196, 26, 1)',
'rgba(22, 119, 255, 1)',
'rgba(250, 84, 28, 1)',
'rgba(47, 84, 235, 1)',
'rgba(133, 165, 255, 1)'
]
const parseRgba = (colorStr) => {
const match = colorStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/)
if (match) {
return { return {
r: parseInt(match[1], 10), r: parseInt(h.slice(0, 2), 16),
g: parseInt(match[2], 10), g: parseInt(h.slice(2, 4), 16),
b: parseInt(match[3], 10), b: parseInt(h.slice(4, 6), 16),
a: parseFloat(match[4])
}
} }
return { r: 0, g: 0, b: 0, a: 1 }
} }
/** /**
...@@ -55,22 +19,14 @@ const getMultiLineChart = (chartInput) => { ...@@ -55,22 +19,14 @@ const getMultiLineChart = (chartInput) => {
const series = chartInput.data || [] const series = chartInput.data || []
const allNames = series.map((item) => item.name) const allNames = series.map((item) => item.name)
const lineSize = Math.ceil(allNames.length / 3)
const legendLine1 = allNames.slice(0, lineSize)
const legendLine2 = allNames.slice(lineSize, lineSize * 2)
const legendLine3 = allNames.slice(lineSize * 2)
const xCount = Array.isArray(title) ? title.length : 0 const xCount = Array.isArray(title) ? title.length : 0
const labelFontSize = xCount > 8 ? 10 : xCount > 5 ? 11 : 12 const labelFontSize = xCount > 8 ? 10 : xCount > 5 ? 11 : 12
const labelRotate = xCount > 6 ? 28 : 0 const labelRotate = xCount > 6 ? 28 : 0
const echartsSeries = series.map((item, index) => { const echartsSeries = series.map((item, index) => {
const baseColor = const baseColor = MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] || '#055FC2'
item.color || const { r, g, b } = parseHexToRgb(baseColor)
AREA_TAG_COLOR_BY_NAME[item.name] ||
fallbackColorList[index % fallbackColorList.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 { return {
name: item.name, name: item.name,
...@@ -102,39 +58,31 @@ const getMultiLineChart = (chartInput) => { ...@@ -102,39 +58,31 @@ const getMultiLineChart = (chartInput) => {
}, },
/* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */ /* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */
grid: { grid: {
top: 92, top: 60,
right: 10, right: 10,
bottom: 0, bottom: 0,
left: 20, left: 20,
containLabel: true containLabel: true
}, },
legend: [ legend: {
{
show: true, show: true,
type: 'plain', type: 'scroll',
data: legendLine1, orient: 'horizontal',
top: 4, left: 8,
left: 'center', top: 6,
width: '95%',
height: 24,
padding: [0, 24, 0, 24],
icon: 'circle', icon: 'circle',
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
},
itemWidth: 12, itemWidth: 12,
itemHeight: 12 itemHeight: 12,
}, data: allNames,
{ // 隐藏内置分页按钮与页码(使用 graphic 自定义左右箭头)
show: legendLine2.length > 0, pageButtonPosition: 'end',
type: 'plain', pageIconSize: 0,
data: legendLine2, pageButtonGap: 0,
top: 30, pageFormatter: () => '',
left: 'center', pageTextStyle: { fontSize: 0, color: 'transparent' },
icon: 'circle',
textStyle: { textStyle: {
fontFamily: 'Source Han Sans CN', fontFamily: 'Source Han Sans CN',
fontWeight: 400, fontWeight: 400,
...@@ -143,28 +91,32 @@ const getMultiLineChart = (chartInput) => { ...@@ -143,28 +91,32 @@ const getMultiLineChart = (chartInput) => {
letterSpacing: 0, letterSpacing: 0,
align: 'left', align: 'left',
color: 'rgb(95, 101, 108)' color: 'rgb(95, 101, 108)'
}
}, },
itemWidth: 12, graphic: [
itemHeight: 12
},
{ {
show: legendLine3.length > 0, type: 'polygon',
type: 'plain', name: '__legend_prev__',
data: legendLine3, left: 10,
top: 56, top: 14,
left: 'center', shape: { points: [[8, 0], [0, 6], [8, 12]] },
icon: 'circle', style: { fill: 'rgb(95, 101, 108)' },
textStyle: { cursor: 'pointer',
fontFamily: 'Source Han Sans CN', tooltip: { show: false },
fontWeight: 400, silent: false,
fontSize: 14, z: 100
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
}, },
itemWidth: 12, {
itemHeight: 12 type: 'polygon',
name: '__legend_next__',
right: 10,
top: 14,
shape: { points: [[0, 0], [8, 6], [0, 12]] },
style: { fill: 'rgb(95, 101, 108)' },
cursor: 'pointer',
tooltip: { show: false },
silent: false,
z: 100
} }
], ],
// 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致) // 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => { const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => { const seriesData = (Array.isArray(data) ? data : []).map((d, index) => {
const color = d?.color const color = MUTICHARTCOLORS[index % MUTICHARTCOLORS.length]
if (!color) return d
return { return {
...d, ...d,
itemStyle: { ...(d.itemStyle || {}), color }, itemStyle: { ...(d?.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色 // “飞线”(labelLine)跟随领域色
labelLine: { labelLine: {
...(d.labelLine || {}), ...(d?.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color } lineStyle: { ...(d?.labelLine?.lineStyle || {}), color }
} }
} }
}) })
...@@ -29,10 +30,14 @@ const getPieChart = (data) => { ...@@ -29,10 +30,14 @@ const getPieChart = (data) => {
alignTo: 'edge', alignTo: 'edge',
formatter: params => { formatter: params => {
const name = params.name || ""; const name = params.name || "";
const value = params.value ?? ""; const value = Number(params.value ?? 0) || 0;
const percent = params.percent != null ? Math.round(params.percent) : 0; const rawPercent =
params?.data?.percent != null
? params.data.percent
: params.percent;
const percent = rawPercent != null ? Number(rawPercent).toFixed(2) : "0.00";
return `{name|${name}}\n{time| ${percent}%}`; return `{name|${name}}\n{time|${value} ${percent}%}`;
}, },
minMargin: 10, minMargin: 10,
edgeDistance: 20, edgeDistance: 20,
......
...@@ -72,8 +72,9 @@ ...@@ -72,8 +72,9 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="card-content"> <div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id"> <div v-for="(item, index) in hearingData" :key="item.id">
<div class="card-item"> <div class="card-item">
...@@ -87,7 +88,8 @@ ...@@ -87,7 +88,8 @@
<span v-html="highlightText(item.time + ' · ' + item.content)"></span> <span v-html="highlightText(item.time + ' · ' + item.content)"></span>
<img src="../images/image open.png" alt="open icon" class="card-open-image" /> <img src="../images/image open.png" alt="open icon" class="card-open-image" />
</div> </div>
<div class="card-item-category"> <div class="card-item-category"
v-if="Array.isArray(item.category) && item.category.some(v => String(v || '').trim())">
<AreaTag v-for="(val, idx) in item.category" :key="idx" :tagName="val" /> <AreaTag v-for="(val, idx) in item.category" :key="idx" :tagName="val" />
</div> </div>
</div> </div>
...@@ -97,13 +99,19 @@ ...@@ -97,13 +99,19 @@
</div> </div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info"> <div class="info">
共 {{ total }} 篇国会听证会 共 {{ total }} 篇国会听证会
</div> </div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="10" background layout="prev, pager, next" :total="total" <el-pagination :page-size="10" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -169,6 +177,10 @@ const props = defineProps({ ...@@ -169,6 +177,10 @@ const props = defineProps({
searchKeyword: { searchKeyword: {
type: String, type: String,
default: "" default: ""
},
loading: {
type: Boolean,
default: false
} }
}); });
...@@ -181,6 +193,12 @@ const emit = defineEmits([ ...@@ -181,6 +193,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变 // 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage, researchHearingList, hearingData, selectedYear } = toRefs(props); const { researchTypeList, researchTimeList, curFooterList, total, currentPage, researchHearingList, hearingData, selectedYear } = toRefs(props);
const hasData = computed(() => Array.isArray(hearingData.value) && hearingData.value.length > 0);
const pageCount = computed(() => {
const size = 10;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const pageSize = 10; const pageSize = 10;
function getDateYearsAgo(years) { function getDateYearsAgo(years) {
...@@ -488,8 +506,11 @@ const handleToReportDetail = item => { ...@@ -488,8 +506,11 @@ const handleToReportDetail = item => {
.right { .right {
width: 1224px; width: 1224px;
display: flex;
flex-direction: column;
.card-box { .card-box {
flex: 1;
...@@ -498,6 +519,15 @@ const handleToReportDetail = item => { ...@@ -498,6 +519,15 @@ const handleToReportDetail = item => {
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
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;
......
...@@ -52,8 +52,9 @@ ...@@ -52,8 +52,9 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)"> @click="handleToReportDetail(item)">
<div class="footer-card-top"> <div class="footer-card-top">
...@@ -67,13 +68,19 @@ ...@@ -67,13 +68,19 @@
<div class="from">{{ item.thinktankName }}</div> <div class="from">{{ item.thinktankName }}</div>
</div> </div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info"> <div class="info">
共 {{ total }} 篇调查项目 共 {{ total }} 篇调查项目
</div> </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="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -81,7 +88,7 @@ ...@@ -81,7 +88,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, toRefs, watch } from "vue"; import { ref, toRefs, watch, computed } from "vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME, RESOURCE_FILTER_ALL_TIME,
...@@ -126,6 +133,10 @@ const props = defineProps({ ...@@ -126,6 +133,10 @@ const props = defineProps({
searchKeyword: { searchKeyword: {
type: String, type: String,
default: "" default: ""
},
loading: {
type: Boolean,
default: false
} }
}); });
...@@ -138,6 +149,12 @@ const emit = defineEmits([ ...@@ -138,6 +149,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变 // 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props); const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props);
const hasData = computed(() => Array.isArray(curFooterList.value) && curFooterList.value.length > 0);
const pageCount = computed(() => {
const size = 12;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]); const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]); const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
...@@ -360,15 +377,27 @@ const handleToReportDetail = item => { ...@@ -360,15 +377,27 @@ const handleToReportDetail = item => {
.right { .right {
width: 1284px; width: 1284px;
display: flex;
flex-direction: column;
.card-box { .card-box {
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 13px; gap: 13px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card { .footer-card {
width: 398px; width: 398px;
height: 300px; height: 300px;
......
...@@ -52,8 +52,9 @@ ...@@ -52,8 +52,9 @@
</div> --> </div> -->
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)"> @click="handleToReportDetail(item)">
<div class="footer-card-top"> <div class="footer-card-top">
...@@ -67,13 +68,19 @@ ...@@ -67,13 +68,19 @@
<div class="from">{{ item.thinkTankName }}</div> <div class="from">{{ item.thinkTankName }}</div>
</div> </div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info"> <div class="info">
共 {{ total }} 篇智库报告 共 {{ total }} 篇智库报告
</div> </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="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" /> @current-change="handleCurrentChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -81,7 +88,7 @@ ...@@ -81,7 +88,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, toRefs, watch } from "vue"; import { ref, toRefs, watch, computed } from "vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME, RESOURCE_FILTER_ALL_TIME,
...@@ -126,6 +133,10 @@ const props = defineProps({ ...@@ -126,6 +133,10 @@ const props = defineProps({
searchKeyword: { searchKeyword: {
type: String, type: String,
default: "" default: ""
},
loading: {
type: Boolean,
default: false
} }
}); });
...@@ -138,6 +149,12 @@ const emit = defineEmits([ ...@@ -138,6 +149,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变 // 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props); const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props);
const hasData = computed(() => Array.isArray(curFooterList.value) && curFooterList.value.length > 0);
const pageCount = computed(() => {
const size = 12;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]); const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]); const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
...@@ -360,15 +377,28 @@ const handleToReportDetail = item => { ...@@ -360,15 +377,28 @@ const handleToReportDetail = item => {
} }
.right { .right {
width: 1284px;
display: flex;
flex-direction: column;
.card-box { .card-box {
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 13px; gap: 13px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card { .footer-card {
width: 398px; width: 398px;
height: 300px; height: 300px;
......
...@@ -53,18 +53,20 @@ ...@@ -53,18 +53,20 @@
<ThinkTankReport v-if="isThinkTankReport" :research-type-list="researchTypeList" <ThinkTankReport v-if="isThinkTankReport" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters" :research-time-list="researchTimeList" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters"
:cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :search-keyword="searchReport" :cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :search-keyword="searchReport"
:loading="isThinkTankReportLoading"
@update:selected-filters="handleSelectedFiltersUpdate" @update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange" @filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToReportDetail" /> @report-click="handleToReportDetail" />
<CongressHearing v-else-if="isCongressHearing" :research-type-list="researchTypeList" <CongressHearing v-else-if="isCongressHearing" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`国会听证会-${tabResetKey}`" :research-hearing-list="researchHearingList" :research-time-list="researchTimeList" :key="`国会听证会-${tabResetKey}`" :research-hearing-list="researchHearingList"
:selected-filters="selectedFilters" :selected-year="selectedYear" :total="total" :current-page="currentPage" :selected-filters="selectedFilters" :selected-year="selectedYear" :total="total" :current-page="currentPage"
:search-keyword="searchReport" :hearing-data="hearingData" @update:selected-filters="handleSelectedFiltersUpdate" :search-keyword="searchReport" :hearing-data="hearingData" :loading="isCongressHearingLoading"
@update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange" @filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToHearingDetail" /> @report-click="handleToHearingDetail" />
<SurveyForm v-else-if="isSurveyForm" :research-type-list="researchTypeList" :research-time-list="researchTimeList" <SurveyForm v-else-if="isSurveyForm" :research-type-list="researchTypeList" :research-time-list="researchTimeList"
:key="`调查项目-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterProjectList" :key="`调查项目-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterProjectList"
:total="total" :current-page="currentPage" :search-keyword="searchReport" :total="total" :current-page="currentPage" :search-keyword="searchReport" :loading="isSurveyFormLoading"
@update:selected-filters="handleSelectedFiltersUpdate" @update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange" @filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToProjectDetail" /> @report-click="handleToProjectDetail" />
...@@ -108,6 +110,10 @@ const isThinkTankReport = ref(true); ...@@ -108,6 +110,10 @@ const isThinkTankReport = ref(true);
const isSurveyForm = ref(false); const isSurveyForm = ref(false);
const isCongressHearing = ref(false); const isCongressHearing = ref(false);
const searchReport = ref('') const searchReport = ref('')
// 智库详情-智库动态:右侧列表 loading(居中显示)
const isThinkTankReportLoading = ref(false);
const isSurveyFormLoading = ref(false);
const isCongressHearingLoading = ref(false);
const handleToReportDetail = (item) => { const handleToReportDetail = (item) => {
window.sessionStorage.setItem('curTabName', item.name) window.sessionStorage.setItem('curTabName', item.name)
...@@ -248,6 +254,10 @@ const handleChooseType = async (type) => { ...@@ -248,6 +254,10 @@ const handleChooseType = async (type) => {
await handleGetThinkDynamicsReport() await handleGetThinkDynamicsReport()
} }
const researchTimeList = ref([ const researchTimeList = ref([
{
id: '2026年',
name: '2026年',
},
{ {
id: '2025年', id: '2025年',
name: '2025年', name: '2025年',
...@@ -395,6 +405,11 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -395,6 +405,11 @@ const handleGetThinkDynamicsReport = async (payload) => {
return; return;
} }
try { try {
// 仅当前 tab 显示 loading,其它置 false,避免切换后残留
isThinkTankReportLoading.value = isThinkTankReport.value;
isSurveyFormLoading.value = isSurveyForm.value;
isCongressHearingLoading.value = isCongressHearing.value;
const strippedTime = stripAllTimeForRequest(nextFilters.researchTimeIds || []); const strippedTime = stripAllTimeForRequest(nextFilters.researchTimeIds || []);
const allTimeIds = (researchTimeList.value || []).map((x) => x.id); const allTimeIds = (researchTimeList.value || []).map((x) => x.id);
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection( const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
...@@ -415,6 +430,9 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -415,6 +430,9 @@ const handleGetThinkDynamicsReport = async (payload) => {
thinkTankId, thinkTankId,
pageNum: Math.max(0, (currentPage.value || 1)), pageNum: Math.max(0, (currentPage.value || 1)),
pageSize: 10, pageSize: 10,
// 国会听证会:排序语义与其它资源库相反(正序→desc,倒序→asc)
sortField: "createTime",
sortOrder: sort.value === true ? "desc" : "asc",
domainIds, domainIds,
startDate, startDate,
...@@ -481,6 +499,10 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -481,6 +499,10 @@ const handleGetThinkDynamicsReport = async (payload) => {
} }
} catch (error) { } catch (error) {
console.error("获取智库动态报告error", error); console.error("获取智库动态报告error", error);
} finally {
isThinkTankReportLoading.value = false;
isSurveyFormLoading.value = false;
isCongressHearingLoading.value = false;
} }
}; };
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => { const getPieChart = (data) => {
let option = { let option = {
series: [ series: [
...@@ -53,7 +55,10 @@ const getPieChart = (data) => { ...@@ -53,7 +55,10 @@ const getPieChart = (data) => {
labelLinePoints: points labelLinePoints: points
}; };
}, },
data: data data: (Array.isArray(data) ? data : []).map((d, index) => ({
...d,
itemStyle: { ...(d?.itemStyle || {}), color: d?.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] }
}))
}] }]
} }
return option return option
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getTreeMapChart = (treemapData) => { const getTreeMapChart = (treemapData) => {
const list = Array.isArray(treemapData) ? treemapData : []
const dataWithColors = list.map((node, idx) => ({
...node,
itemStyle: {
...(node?.itemStyle || {}),
color: node?.itemStyle?.color || MUTICHARTCOLORS[idx % MUTICHARTCOLORS.length]
}
}))
const option = { const option = {
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
...@@ -14,7 +25,7 @@ const getTreeMapChart = (treemapData) => { ...@@ -14,7 +25,7 @@ const getTreeMapChart = (treemapData) => {
series: [ series: [
{ {
type: 'treemap', type: 'treemap',
data: treemapData, data: dataWithColors,
roam: false, roam: false,
nodeClick: false, nodeClick: false,
breadcrumb: { breadcrumb: {
......
...@@ -474,7 +474,7 @@ onMounted(async () => { ...@@ -474,7 +474,7 @@ onMounted(async () => {
display: inline-flex; display: inline-flex;
position: absolute; position: absolute;
left: 277px; right: -8px;
bottom: 208px; bottom: 208px;
background-color: rgba(255, 77, 79, 1); background-color: rgba(255, 77, 79, 1);
align-items: center; align-items: center;
......
...@@ -36,8 +36,9 @@ ...@@ -36,8 +36,9 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)"> @click="emit('report-click', item)">
<div class="footer-card-top"> <div class="footer-card-top">
...@@ -51,11 +52,17 @@ ...@@ -51,11 +52,17 @@
<div class="from">{{ item.thinkTankName }}</div> <div class="from">{{ item.thinkTankName }}</div>
</div> </div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<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="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="emit('page-change', $event)" :current-page="currentPage" /> @current-change="emit('page-change', $event)" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -64,20 +71,22 @@ ...@@ -64,20 +71,22 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME, RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters"; } from "../utils/resourceLibraryFilters";
defineProps({ const props = defineProps({
areaList: { type: Array, default: () => [] }, areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] }, selectedAreaList: { type: Array, default: () => [] },
pubTimeList: { type: Array, default: () => [] }, pubTimeList: { type: Array, default: () => [] },
selectedPubTimeList: { type: Array, default: () => [] }, selectedPubTimeList: { type: Array, default: () => [] },
curFooterList: { type: Array, default: () => [] }, curFooterList: { type: Array, default: () => [] },
total: { type: Number, default: 0 }, total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 } currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
}); });
const emit = defineEmits([ const emit = defineEmits([
...@@ -102,6 +111,13 @@ const formatDate = (str) => { ...@@ -102,6 +111,13 @@ const formatDate = (str) => {
const [y, m, d] = str.split('T')[0].split('-') const [y, m, d] = str.split('T')[0].split('-')
return `${y}${+m}${+d}日` return `${y}${+m}${+d}日`
}; };
const hasData = computed(() => Array.isArray(props.curFooterList) && props.curFooterList.length > 0);
const pageCount = computed(() => {
const size = 12;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -195,14 +211,25 @@ const formatDate = (str) => { ...@@ -195,14 +211,25 @@ const formatDate = (str) => {
.right { .right {
width: 1284px; width: 1284px;
max-height: 1377px; max-height: 1377px;
display: flex;
flex-direction: column;
.card-box { .card-box {
width: 1226px; width: 1226px;
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px 16px; gap: 16px 16px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card { .footer-card {
width: 398px; width: 398px;
height: 300px; height: 300px;
......
...@@ -36,8 +36,9 @@ ...@@ -36,8 +36,9 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index" <div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)"> @click="emit('report-click', item)">
<div class="footer-card-top"> <div class="footer-card-top">
...@@ -51,11 +52,17 @@ ...@@ -51,11 +52,17 @@
<div class="from">{{ item.thinktankName }}</div> <div class="from">{{ item.thinktankName }}</div>
</div> </div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<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="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="emit('page-change', $event)" :current-page="currentPage" /> @current-change="emit('page-change', $event)" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -64,20 +71,22 @@ ...@@ -64,20 +71,22 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME, RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters"; } from "../utils/resourceLibraryFilters";
defineProps({ const props = defineProps({
areaList: { type: Array, default: () => [] }, areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] }, selectedAreaList: { type: Array, default: () => [] },
pubTimeList: { type: Array, default: () => [] }, pubTimeList: { type: Array, default: () => [] },
selectedPubTimeList: { type: Array, default: () => [] }, selectedPubTimeList: { type: Array, default: () => [] },
curFooterList: { type: Array, default: () => [] }, curFooterList: { type: Array, default: () => [] },
total: { type: Number, default: 0 }, total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 } currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
}); });
const formatDate = (str) => { const formatDate = (str) => {
if (!str) return '' if (!str) return ''
...@@ -101,6 +110,13 @@ const handleTimeGroupChange = (val) => { ...@@ -101,6 +110,13 @@ const handleTimeGroupChange = (val) => {
emit("update:selectedPubTimeList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME)); emit("update:selectedPubTimeList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME));
emit("filter-change"); emit("filter-change");
}; };
const hasData = computed(() => Array.isArray(props.curFooterList) && props.curFooterList.length > 0);
const pageCount = computed(() => {
const size = 12;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -170,14 +186,25 @@ const handleTimeGroupChange = (val) => { ...@@ -170,14 +186,25 @@ const handleTimeGroupChange = (val) => {
.right { .right {
width: 1284px; width: 1284px;
max-height: 1377px; max-height: 1377px;
display: flex;
flex-direction: column;
.card-box { .card-box {
width: 1226px; width: 1226px;
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px 16px; gap: 16px 16px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card { .footer-card {
width: 398px; width: 398px;
......
...@@ -55,10 +55,10 @@ ...@@ -55,10 +55,10 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="card-content"> <div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id ?? index"> <div v-for="(item, index) in hearingData" :key="item.id ?? index">
<div class="card-item"> <div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" /> <img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
...@@ -72,7 +72,8 @@ ...@@ -72,7 +72,8 @@
class="card-open-image" /> class="card-open-image" />
</div> </div>
<div class="card-item-category" v-if="item.domains"> <div class="card-item-category"
v-if="Array.isArray(item.domains) && item.domains.some(v => String(v || '').trim())">
<div v-for="(value, index) in item.domains" :key="`domain-${index}`"> <div v-for="(value, index) in item.domains" :key="`domain-${index}`">
<AreaTag :key="`cat-${item.id}`" :tagName="value" /> <AreaTag :key="`cat-${item.id}`" :tagName="value" />
</div> </div>
...@@ -81,16 +82,20 @@ ...@@ -81,16 +82,20 @@
</div> </div>
<div class="divider" v-if="index !== hearingData.length - 1"></div> <div class="divider" v-if="index !== hearingData.length - 1"></div>
</div> </div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="info"> <div class="info">
{{ hearingData.length }} 篇国会听证会 共 {{ total }} 篇国会听证会
</div> </div>
<div class="page-box"> <div class="page-box">
<el-pagination :page-size="pageSize" background layout="prev, pager, next" :total="total" <el-pagination :page-size="pageSize" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handlePageChange" :current-page="currentPage" /> @current-change="handlePageChange" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -121,7 +126,8 @@ const props = defineProps({ ...@@ -121,7 +126,8 @@ const props = defineProps({
selectedPubTimeList: { type: Array, default: () => [] }, selectedPubTimeList: { type: Array, default: () => [] },
hearingData: { type: Array, default: () => [] }, hearingData: { type: Array, default: () => [] },
total: { type: Number, default: 0 }, total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 } currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
}); });
const emit = defineEmits([ const emit = defineEmits([
...@@ -133,6 +139,12 @@ const emit = defineEmits([ ...@@ -133,6 +139,12 @@ const emit = defineEmits([
]); ]);
const pageSize = 10; const pageSize = 10;
const hasData = computed(() => Array.isArray(props.hearingData) && props.hearingData.length > 0);
const pageCount = computed(() => {
const size = Number(pageSize || 10) || 10;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
const selectedResearchIds = computed(() => ( const selectedResearchIds = computed(() => (
Array.isArray(props.selectedAreaList) && props.selectedAreaList.length Array.isArray(props.selectedAreaList) && props.selectedAreaList.length
...@@ -274,11 +286,13 @@ const handlePageChange = page => { ...@@ -274,11 +286,13 @@ const handlePageChange = page => {
.right { .right {
width: 1224px; width: 1224px;
display: flex;
flex-direction: column;
.card-box { .card-box {
width: 100%; width: 100%;
flex: 1;
display: flex; display: flex;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
box-sizing: border-box; box-sizing: border-box;
...@@ -287,6 +301,15 @@ const handlePageChange = page => { ...@@ -287,6 +301,15 @@ const handlePageChange = page => {
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1); box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
padding-right: 36px; padding-right: 36px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card-content { .card-content {
width: 1211px; width: 1211px;
......
...@@ -36,8 +36,9 @@ ...@@ -36,8 +36,9 @@
</div> </div>
</div> </div>
<div class="right"> <div class="right" v-loading="loading">
<div class="card-box"> <div class="card-box">
<template v-if="hasData">
<div class="card-content"> <div class="card-content">
<div v-for="(item, index) in list" :key="item.id ?? index"> <div v-for="(item, index) in list" :key="item.id ?? index">
<div class="card-item" @click="emit('item-click', item)"> <div class="card-item" @click="emit('item-click', item)">
...@@ -79,11 +80,17 @@ ...@@ -79,11 +80,17 @@
<div class="divider" v-if="index !== list.length - 1"></div> <div class="divider" v-if="index !== list.length - 1"></div>
</div> </div>
</div> </div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div> </div>
<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="pageSize" background layout="prev, pager, next" :total="total" <el-pagination :page-size="pageSize" :page-count="pageCount" background layout="prev, pager, next"
@current-change="p => emit('page-change', p)" :current-page="currentPage" /> @current-change="p => emit('page-change', p)" :current-page="currentPage" />
</div> </div>
</div> </div>
...@@ -92,7 +99,7 @@ ...@@ -92,7 +99,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { ref, computed } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import AreaTag from "@/components/base/AreaTag/index.vue"; import AreaTag from "@/components/base/AreaTag/index.vue";
import { import {
...@@ -103,13 +110,14 @@ import { ...@@ -103,13 +110,14 @@ import {
stripAllTimeForRequest stripAllTimeForRequest
} from "../utils/resourceLibraryFilters"; } from "../utils/resourceLibraryFilters";
defineProps({ const props = defineProps({
researchTypeList: { type: Array, default: () => [] }, researchTypeList: { type: Array, default: () => [] },
researchTimeList: { type: Array, default: () => [] }, researchTimeList: { type: Array, default: () => [] },
list: { type: Array, default: () => [] }, list: { type: Array, default: () => [] },
total: { type: Number, default: 0 }, total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 }, currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 7 }, pageSize: { type: Number, default: 7 },
loading: { type: Boolean, default: false },
}); });
const emit = defineEmits(["filter-change", "page-change", "item-click"]); const emit = defineEmits(["filter-change", "page-change", "item-click"]);
...@@ -158,6 +166,13 @@ const handleAdministrativeMoreClick = (ad) => { ...@@ -158,6 +166,13 @@ const handleAdministrativeMoreClick = (ad) => {
const selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]); const selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]); const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]);
const hasData = computed(() => Array.isArray(props.list) && props.list.length > 0);
const pageCount = computed(() => {
const size = Number(props.pageSize || 7) || 7;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
const emitFilterToParent = () => { const emitFilterToParent = () => {
emit("filter-change", { emit("filter-change", {
researchTypeIds: stripAllAreaForRequest(selectedTypeIds.value), researchTypeIds: stripAllAreaForRequest(selectedTypeIds.value),
...@@ -298,6 +313,8 @@ const handleYearGroupChange = (val) => { ...@@ -298,6 +313,8 @@ const handleYearGroupChange = (val) => {
.right { .right {
width: 1224px; width: 1224px;
display: flex;
flex-direction: column;
.card-box { .card-box {
...@@ -311,6 +328,15 @@ const handleYearGroupChange = (val) => { ...@@ -311,6 +328,15 @@ const handleYearGroupChange = (val) => {
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1); box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
padding-right: 36px; padding-right: 36px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card-content { .card-content {
width: 1211px; width: 1211px;
height: 1067px; height: 1067px;
...@@ -332,7 +358,7 @@ const handleYearGroupChange = (val) => { ...@@ -332,7 +358,7 @@ const handleYearGroupChange = (val) => {
.card-item-img { .card-item-img {
width: 56px; width: 156px;
height: 77px; height: 77px;
margin-right: 22px; margin-right: 22px;
flex-shrink: 0; flex-shrink: 0;
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
<img class="translate-icon" src="../ReportDetail/images/image-translate.png" alt="" <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;" /> style="width: 16px; height: 16px; max-width: 16px; max-height: 16px; display: block; object-fit: contain;" />
</div> </div>
<div class="translate-text">{{ "显示文" }}</div> <div class="translate-text">{{ "显示文" }}</div>
</div> </div>
<div class="btn" @click="handleDownload"> <div class="btn" @click="handleDownload">
<div class="icon"> <div class="icon">
...@@ -61,13 +61,15 @@ ...@@ -61,13 +61,15 @@
</div> </div>
</div> </div>
<div class="report-box"> <div class="report-box">
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage"> <!-- 英文原文:始终展示;关闭「显示译文」时占满宽度 -->
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" /> <div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlEnWithPage">
</div> <pdf :key="`left-pdf-${valueSwitch ? 'split' : 'full'}`" ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage"
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
class="pdf-pane-inner" /> class="pdf-pane-inner" />
</div> </div>
<!-- 中文译文:仅在开关打开时展示 -->
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlWithPage">
<pdf ref="rightPdfRef" :pdfUrl="reportUrlWithPage" class="pdf-pane-inner" />
</div>
</div> </div>
</div> </div>
</div> </div>
......
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { MUTICHARTCOLORS } from "@/common/constant.js";
/** /**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data * @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data
...@@ -9,70 +10,21 @@ const getMultiLineChart = (data) => { ...@@ -9,70 +10,21 @@ const getMultiLineChart = (data) => {
const series = data.data const series = data.data
const allNames = series.map((item) => item.name) const allNames = series.map((item) => item.name)
const legendSplitAt = Math.ceil(allNames.length / 2)
const legendFirstLine = allNames.slice(0, legendSplitAt)
const legendSecondLine = allNames.slice(legendSplitAt)
// 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色) const parseHexToRgb = (hex) => {
const AREA_TAG_COLOR_BY_NAME = { const h = String(hex || '').replace('#', '').trim()
'人工智能': 'rgba(245, 34, 45, 1)', // tag1 if (h.length !== 6) return { r: 0, g: 0, b: 0 }
'生物科技': 'rgba(19, 168, 168, 1)', // tag2
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
// 兼容后端/页面常见写法
'通信网络': 'rgba(5, 95, 194, 1)',
'量子科技': 'rgba(114, 46, 209, 1)', // tag4
'新能源': 'rgba(82, 196, 26, 1)', // tag5
'集成电路': 'rgba(22, 119, 255, 1)', // tag6
'海洋': 'rgba(15, 120, 199, 1)', // tag7
'先进制造': 'rgba(250, 173, 20, 1)', // tag8
'新材料': 'rgba(250, 140, 22, 1)', // tag9
'航空航天': 'rgba(47, 84, 235, 1)', // tag10
'太空': 'rgba(47, 84, 235, 1)', // tag11
'深海': 'rgba(73, 104, 161, 1)', // tag12
'极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
// 兜底颜色池(未命中 AreaTag 映射时使用)
const fallbackColorList = [
'rgba(5, 95, 194, 1)',
'rgba(19, 168, 168, 1)',
'rgba(250, 140, 22, 1)',
'rgba(114, 46, 209, 1)',
'rgba(82, 196, 26, 1)',
'rgba(250, 84, 28, 1)',
'rgba(22, 119, 255, 1)',
'rgba(95, 101, 108, 1)',
'rgba(47, 84, 235, 1)',
'rgba(133, 165, 255, 1)',
]
// 解析 RGBA 颜色的辅助函数
const parseRgba = (colorStr) => {
// 匹配 rgba(r, g, b, a) 格式
const match = colorStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/);
if (match) {
return { return {
r: parseInt(match[1]), r: parseInt(h.slice(0, 2), 16),
g: parseInt(match[2]), g: parseInt(h.slice(2, 4), 16),
b: parseInt(match[3]), b: parseInt(h.slice(4, 6), 16),
a: parseFloat(match[4]) }
};
} }
// 默认返回黑色
return { r: 0, g: 0, b: 0, a: 1 };
};
// 动态生成 series 配置 // 动态生成 series 配置
const echartsSeries = series.map((item, index) => { const echartsSeries = series.map((item, index) => {
// 获取当前系列的颜色(优先使用item.color,否则用预设颜色,再否则随机) const baseColor = item.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] || '#055FC2'
const baseColor = const { r, g, b } = parseHexToRgb(baseColor)
item.color ||
AREA_TAG_COLOR_BY_NAME[item.name] ||
fallbackColorList[index % fallbackColorList.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 ({ return ({
name: item.name, name: item.name,
...@@ -89,11 +41,11 @@ const getMultiLineChart = (data) => { ...@@ -89,11 +41,11 @@ const getMultiLineChart = (data) => {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, // 顶部 offset: 0, // 顶部
color: `rgba(${r}, ${g}, ${b}, 0.1)` // 按需求:0.1 -> 0 color: `rgba(${r}, ${g}, ${b}, 0.1)`
}, },
{ {
offset: 1, // 底部 offset: 1, // 底部
color: `rgba(${r}, ${g}, ${b}, 0)` // 0 透明度 color: `rgba(${r}, ${g}, ${b}, 0)`
} }
]) ])
}, },
...@@ -118,20 +70,24 @@ const getMultiLineChart = (data) => { ...@@ -118,20 +70,24 @@ const getMultiLineChart = (data) => {
}, },
/* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */ /* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */
grid: { grid: {
top: '34%', top: 68,
right: '3%', right: '3%',
bottom: '5%', bottom: '5%',
left: '2%', left: '2%',
containLabel: true containLabel: true
}, },
legend: [ legend: {
{
show: true, show: true,
type: 'plain', type: 'plain',
data: legendFirstLine, orient: 'horizontal',
top: 8,
left: 'center', left: 'center',
top: 8,
width: '90%',
height: 24,
icon: 'circle', icon: 'circle',
itemWidth: 12,
itemHeight: 12,
data: allNames,
textStyle: { textStyle: {
fontFamily: 'Source Han Sans CN', fontFamily: 'Source Han Sans CN',
fontWeight: 400, fontWeight: 400,
...@@ -141,23 +97,6 @@ const getMultiLineChart = (data) => { ...@@ -141,23 +97,6 @@ const getMultiLineChart = (data) => {
align: 'left' align: 'left'
} }
}, },
{
show: legendSecondLine.length > 0,
type: 'plain',
data: legendSecondLine,
top: 32,
left: 'center',
icon: 'circle',
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left'
}
}
],
// 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致) // 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
xAxis: [ xAxis: [
{ {
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => { const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => { const seriesData = (Array.isArray(data) ? data : []).map((d, index) => {
const color = d?.color const color = d?.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length]
if (!color) return d
return { return {
...d, ...d,
itemStyle: { ...(d.itemStyle || {}), color }, itemStyle: { ...(d?.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色 // “飞线”(labelLine)跟随领域色
labelLine: { labelLine: {
...(d.labelLine || {}), ...(d?.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color } lineStyle: { ...(d?.labelLine?.lineStyle || {}), color }
} }
} }
}) })
...@@ -37,10 +38,15 @@ const getPieChart = (data) => { ...@@ -37,10 +38,15 @@ const getPieChart = (data) => {
alignTo: 'edge', alignTo: 'edge',
formatter: params => { formatter: params => {
const name = params.name || ""; const name = params.name || "";
const value = params.value ?? ""; const value = Number(params.value ?? 0) || 0;
const percent = params.percent != null ? Math.round(params.percent) : 0; const rawPercent =
params?.data?.percent != null
? params.data.percent
: params.percent;
const percent = rawPercent != null ? Number(rawPercent).toFixed(2) : "0.00";
return `{name|${name}}\n{time| ${percent}%}`; // 第二行:数值 + 百分比(同一行、同一文字样式),保持与旧版一致的两行结构
return `{name|${name}}\n{time|${value}${percent}%}`;
}, },
minMargin: 5, minMargin: 5,
edgeDistance: 10, edgeDistance: 10,
......
...@@ -124,7 +124,20 @@ export function matchesEarlierChineseDate(timeStr) { ...@@ -124,7 +124,20 @@ export function matchesEarlierChineseDate(timeStr) {
/** 与政策追踪「仅全部时间」一致:固定起止(结束日按产品要求) */ /** 与政策追踪「仅全部时间」一致:固定起止(结束日按产品要求) */
export const RESOURCE_REPORT_ALL_TIME_START = "2000-01-01"; export const RESOURCE_REPORT_ALL_TIME_START = "2000-01-01";
export const RESOURCE_REPORT_ALL_TIME_END = "2025-12-31";
function getYesterdayYmd() {
const d = new Date();
// JS Date 自动处理跨月/跨年
d.setDate(d.getDate() - 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}`;
}
export function getResourceReportAllTimeEndYmd() {
return getYesterdayYmd();
}
function getResourceReportDateYearsAgo(years) { function getResourceReportDateYearsAgo(years) {
const currentDate = new Date(); const currentDate = new Date();
...@@ -145,7 +158,7 @@ function getResourceReportTodayYmd() { ...@@ -145,7 +158,7 @@ function getResourceReportTodayYmd() {
/** /**
* 资源库 /thinkTankOverview/report:由发布时间多选(数字年 +「更早」)推导 startDate/endDate,语义对齐政策追踪 getPolicyListDateRangeFromYearList。 * 资源库 /thinkTankOverview/report:由发布时间多选(数字年 +「更早」)推导 startDate/endDate,语义对齐政策追踪 getPolicyListDateRangeFromYearList。
* - 仅「全部时间」或选满全部具体年份 → 2000-01-01 ~ 2025-12-31 * - 仅「全部时间」或选满全部具体年份 → 2000-01-01 ~ 昨天
* - 单选/多选自然年 +「更早」→ 取最小年 01-01 与最大年 12-31 包络(「更早」为 2000~2020) * - 单选/多选自然年 +「更早」→ 取最小年 01-01 与最大年 12-31 包络(「更早」为 2000~2020)
* - 无法解析时的兜底:近 relativeYearsAgo 年至今天 * - 无法解析时的兜底:近 relativeYearsAgo 年至今天
* *
...@@ -169,7 +182,7 @@ export function getResourceLibraryReportDateRangeFromTimeSelection( ...@@ -169,7 +182,7 @@ export function getResourceLibraryReportDateRangeFromTimeSelection(
if (isAllTime) { if (isAllTime) {
return { return {
startDate: RESOURCE_REPORT_ALL_TIME_START, startDate: RESOURCE_REPORT_ALL_TIME_START,
endDate: RESOURCE_REPORT_ALL_TIME_END, endDate: getResourceReportAllTimeEndYmd(),
}; };
} }
...@@ -206,6 +219,6 @@ export function getResourceLibraryReportDateRangeFromTimeSelection( ...@@ -206,6 +219,6 @@ export function getResourceLibraryReportDateRangeFromTimeSelection(
} }
return { return {
startDate: `${minY}-01-01`, startDate: `${minY}-01-01`,
endDate: `${maxY}-12-31`, endDate: maxY === new Date().getFullYear() ? getYesterdayYmd() : `${maxY}-12-31`,
}; };
} }
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getSankeyChart = (nodes, links) => { const getSankeyChart = (nodes, links) => {
const formatAmountWan = (v) => {
const n = Number(v)
if (!Number.isFinite(n)) return '0.00万'
return `${n.toFixed(2)}万`
}
const option = { const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: 'rgba(234, 236, 238, 1)',
borderWidth: 1,
textStyle: {
color: 'rgb(59, 65, 75)',
fontFamily: 'Microsoft YaHei',
fontSize: 14,
lineHeight: 22
},
formatter: function (params) {
// 仅对连线展示资金金额;节点保持默认名称提示
if (params?.dataType === 'edge') {
const amount = formatAmountWan(params?.data?.value)
return `资金金额:${amount}`
}
return params?.name ?? ''
}
},
series: { series: {
type: 'sankey', type: 'sankey',
layout: 'none', layout: 'none',
...@@ -9,6 +37,11 @@ const getSankeyChart = (nodes, links) => { ...@@ -9,6 +37,11 @@ const getSankeyChart = (nodes, links) => {
right: '15%', right: '15%',
top: '5%', top: '5%',
bottom: '5%', bottom: '5%',
color: MUTICHARTCOLORS,
lineStyle: {
color: 'rgb(230, 231, 232)',
opacity: 1
},
emphasis: { emphasis: {
focus: 'adjacency' focus: 'adjacency'
}, },
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论