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

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

......@@ -75,7 +75,7 @@ export function getThinkTankReportDomainStats(params) {
export function getThinkTankPolicyIndustry(params) {
return request({
method: 'GET',
url: `/api/thinkTankOverview/policyIndustry/${params.year}`,
url: `/api/thinkTankOverview/policyIndustry`,
params
})
}
......@@ -180,7 +180,8 @@ export function getThinkTankTestimoniesByThinkTankId(params) {
params: {
pageNum: params.pageNum,
pageSize: params.pageSize,
sortField: params.sortField,
sortOrder: params.sortOrder,
domainIds: params.domainIds,
startDate: params.startDate,
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>
<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>
<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" />
<img v-if="sortValue === 1" src="./down.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>
</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>
</div>
</template>
<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([
{ label: "时间倒序", value: true },
{ label: "时间正序", value: false }
])
const sortValue = ref(1)
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 handlePxChange = () => {
emits('handlePxChange', isSort.value)
emits('handlePxChange', sortValue.value)
}
</script>
......
......@@ -15,7 +15,7 @@ const thinktankRoutes = [
name: "thinkTank",
component: thinkTank,
meta: {
title: "科技智库概览",
title: "美国科技智库概览",
isShowHeader: true
}
},
......
......@@ -4,15 +4,19 @@
<pre>
{{
`
import TimeSortSelectBox from '@/components/base/TimeSortSelectBox/index.vue'
<div class="time-box">
<TimeSortSelectBox @handle-px-change="handlePx" />
</div>
import TimeSortSelectBox from '@/components/base/TimeSortSelectBox/index.vue'
<div class="time-box">
<TimeSortSelectBox @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="2" @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="3" @handle-px-change="handleTimePx" />
</div>
`
}}
</pre>
<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>
</el-col>
......@@ -24,12 +28,24 @@
import { ref } from 'vue'
import '@/styles/common.scss'
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 handlePx = (val) => {
const handleTimePx = (val) => {
alert(val)
}
const handleHeatPx = (val) => {
alert(val)
}
const handleGradePx = (val) => {
alert(val)
}
</script>
<style lang="scss" scoped>
......@@ -39,5 +55,7 @@ const handlePx = (val) => {
padding: 100px;
background: #F2F8FF;
border: 1px solid var(--bg-black-5);
display: flex;
gap: 8px;
}
</style>
\ No newline at end of file
......@@ -8,8 +8,8 @@
<div class="text-title-1-show">文字样式</div>
<TextStyle />
<div class="text-title-1-show">通用样式/组件</div>
<div style="position: relative; min-height: 700px;">
<el-tabs tabPosition="left" class="tabs-nav-no-wrap left-float-nav-tabs dev-style-tabs">
<div style="position: relative; height: 800px;">
<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>
<common-page />
</el-tab-pane>
......@@ -73,10 +73,9 @@
<el-tab-pane label="激活工作框" lazy>
<WorkingBox />
</el-tab-pane>
<el-tab-pane label="时间排序" lazy>
<el-tab-pane label="自定义排序" lazy>
<TimeSortSelectBox />
</el-tab-pane>
</el-tabs>
</div>
</el-space>
......
......@@ -4,6 +4,69 @@ import getQuarterRange from './getQuarterRange';
import * as echarts from 'echarts'
import 'echarts-wordcloud';
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) => {
let chartDom = document.getElementById(chartId);
if (!chartDom) {
......@@ -12,8 +75,39 @@ const setChart = (option, chartId, allowClick, selectParam) => {
chartDom.removeAttribute("_echarts_instance_");
let chart = echarts.init(chartDom);
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) {
chart.on('click', function (params) {
// 图例分页箭头只负责翻页,不走任何跳转
if (params?.componentType === 'graphic' && LEGEND_ARROW_NAMES.has(params?.name)) {
return
}
switch (selectParam.moduleType) {
case '国会法案':
if (selectParam.key === 1) {
......@@ -112,6 +206,7 @@ const setChart = (option, chartId, allowClick, selectParam) => {
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout(() => {
chart.resize();
applyLegendPagingArrowVisibility(chart, option)
}, 0);
return chart;
};
......
......@@ -186,18 +186,26 @@ let leftChart;
const rightChartRef = ref(null);
let rightChart;
const isShowAiLeft = ref(true);
const isShowAiLeft = ref(false);
const aiContentLeft = ref("");
const isLeftInterpretLoading = ref(false);
const leftAiAbortController = ref(null);
const isShowAiRight = ref(true);
const isShowAiRight = ref(false);
const aiContentRight = ref("");
const isRightInterpretLoading = ref(false);
const rightAiAbortController = ref(null);
const handleSwitchAiLeft = (val) => {
isShowAiLeft.value = val;
if (val) {
fetchLeftInterpretation();
} else {
if (leftAiAbortController.value) {
leftAiAbortController.value.abort();
leftAiAbortController.value = null;
}
isLeftInterpretLoading.value = false;
}
};
......@@ -205,6 +213,12 @@ const handleSwitchAiRight = (val) => {
isShowAiRight.value = val;
if (val) {
fetchRightInterpretation();
} else {
if (rightAiAbortController.value) {
rightAiAbortController.value.abort();
rightAiAbortController.value = null;
}
isRightInterpretLoading.value = false;
}
};
......@@ -276,12 +290,17 @@ const fetchLeftInterpretation = async () => {
if (hasValidContent || isLeftInterpretLoading.value) {
return;
}
if (leftAiAbortController.value) {
leftAiAbortController.value.abort();
}
leftAiAbortController.value = new AbortController();
isLeftInterpretLoading.value = true;
aiContentLeft.value = "解读生成中…";
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
signal: leftAiAbortController.value.signal,
onChunk: (chunk) => {
// 与智库概览「数量变化趋势」一致:按 chunk 增量拼接展示
appendAiInterpretationChunk(aiContentLeft, chunk);
......@@ -292,10 +311,13 @@ const fetchLeftInterpretation = async () => {
// 与智库概览一致:优先用最终「解读」收口;否则保留已拼接内容
aiContentLeft.value = text || aiContentLeft.value || "未返回有效解读内容";
} catch (error) {
console.error("合作限制政策对比图表解读请求失败", error);
aiContentLeft.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("合作限制政策对比图表解读请求失败", error);
aiContentLeft.value = "解读加载失败";
}
} finally {
isLeftInterpretLoading.value = false;
leftAiAbortController.value = null;
}
};
......@@ -348,12 +370,17 @@ const fetchRightInterpretation = async () => {
if (hasValidContent || isRightInterpretLoading.value) {
return;
}
if (rightAiAbortController.value) {
rightAiAbortController.value.abort();
}
rightAiAbortController.value = new AbortController();
isRightInterpretLoading.value = true;
aiContentRight.value = "解读生成中…";
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
signal: rightAiAbortController.value.signal,
onChunk: (chunk) => {
appendAiInterpretationChunk(aiContentRight, chunk);
}
......@@ -362,10 +389,13 @@ const fetchRightInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res);
aiContentRight.value = text || aiContentRight.value || "未返回有效解读内容";
} catch (error) {
console.error("合作限制领域分布图表解读请求失败", error);
aiContentRight.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("合作限制领域分布图表解读请求失败", error);
aiContentRight.value = "解读加载失败";
}
} finally {
isRightInterpretLoading.value = false;
rightAiAbortController.value = null;
}
};
......
......@@ -61,17 +61,17 @@
</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 v-else>
<div class="right-main-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</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>
......@@ -293,6 +293,11 @@ const total = ref(0);
const pageSize = ref(10);
const currentPage = ref(1);
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 map = {
......@@ -658,13 +663,16 @@ watch(currentPage, () => {
.title {
font-size: 20px;
width: 950px;
max-width: 950px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(59, 65, 75);
margin-bottom: 9px;
cursor: pointer;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.content {
......
......@@ -26,27 +26,15 @@
<div class="btn-box">
<div class="translate">
<div class="search-input-wrap" v-if="showSearchInput">
<input
v-model="searchKeywordText"
class="search-input"
placeholder="回车查询"
@keyup.enter="handleSearchInPdf"
/>
<input v-model="searchKeywordText" class="search-input" placeholder="回车查询"
@keyup.enter="handleSearchInPdf" />
<div class="search-match-count">{{ matchInfo.current }}/{{ matchInfo.total }}</div>
<button
class="search-nav-btn"
type="button"
@click="handlePrevMatch"
:disabled="matchInfo.total === 0 || matchInfo.current <= 1"
>
<button class="search-nav-btn" type="button" @click="handlePrevMatch"
:disabled="matchInfo.total === 0 || matchInfo.current <= 1">
上一个
</button>
<button
class="search-nav-btn"
type="button"
@click="handleNextMatch"
:disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total"
>
<button class="search-nav-btn" type="button" @click="handleNextMatch"
:disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total">
下一个
</button>
</div>
......@@ -54,19 +42,14 @@
<el-switch v-model="valueSwitch" />
</div>
<div class="translate-image">
<img
class="translate-icon"
src="../assets/icon-translation.png"
alt=""
style="
<img class="translate-icon" src="../assets/icon-translation.png" alt="" style="
width: 16px;
height: 16px;
max-width: 16px;
max-height: 16px;
display: block;
object-fit: contain;
"
/>
" />
</div>
<div class="translate-text">{{ "显示原文" }}</div>
</div>
......@@ -83,12 +66,8 @@
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div>
<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"
/>
<pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
class="pdf-pane-inner" />
</div>
</div>
</div>
......@@ -203,7 +182,7 @@ const handleSearchInPdf = async () => {
try {
const { ElMessage } = await import("element-plus");
ElMessage.warning("未找到包含该关键词的页面");
} catch (_) {}
} catch (_) { }
}
};
......@@ -246,7 +225,7 @@ const handleDownload = async () => {
try {
const { ElMessage } = await import("element-plus");
ElMessage.warning("暂无下载链接");
} catch (_) {}
} catch (_) { }
return;
}
const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-");
......@@ -659,6 +638,7 @@ onMounted(async () => {
display: flex;
gap: 8px;
cursor: pointer;
.icon {
width: 16px;
height: 16px;
......
......@@ -171,6 +171,10 @@ const options = [
];
/** value 须与 v-model 类型一致(数字),否则 el-select 无法匹配 label,会显示成「2025」而非「2025年」 */
const options1 = [
{
value: 2026,
label: "2026年"
},
{
value: 2025,
label: "2025年"
......@@ -719,11 +723,11 @@ let rightChart1;
let leftSankey;
let boxplotChart;
// ------- AI 解读(刷新后默认展开,行为对齐智库概览) -------
const isShowAiContentLeft1 = ref(true);
const isShowAiContentLeft2 = ref(true);
const isShowAiContentRight1 = ref(true);
const isShowAiContentRight2 = ref(true);
// ------- AI 解读(默认仅展示 AiButton,悬停后再请求 AI) -------
const isShowAiContentLeft1 = ref(false);
const isShowAiContentLeft2 = ref(false);
const isShowAiContentRight1 = ref(false);
const isShowAiContentRight2 = ref(false);
const aiContentLeft1 = ref("");
const aiContentLeft2 = ref("");
......@@ -735,6 +739,11 @@ const isAiLoadingLeft2 = ref(false);
const isAiLoadingRight1 = 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 left1AiSeq = ref(0);
......@@ -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;
const hasValidContent =
......@@ -778,12 +793,19 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe
if (hasValidContent) return;
const localSeq = aiSeqRef.value;
if (abortControllerRef?.value) {
abortControllerRef.value.abort();
}
if (abortControllerRef) {
abortControllerRef.value = new AbortController();
}
loadingRef.value = true;
targetRef.value = AI_LOADING_TEXT;
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
...(abortControllerRef?.value?.signal ? { signal: abortControllerRef.value.signal } : {}),
onChunk: (chunk) => {
if (aiSeqRef.value !== localSeq) return;
appendAiInterpretationChunk(targetRef, chunk, AI_LOADING_TEXT);
......@@ -796,10 +818,13 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe
targetRef.value = text || targetRef.value || "未返回有效解读内容";
} catch (e) {
if (aiSeqRef.value !== localSeq) return;
console.error("图表解读请求失败", e);
targetRef.value = "解读加载失败";
if (e?.name !== "AbortError") {
console.error("图表解读请求失败", e);
targetRef.value = "解读加载失败";
}
} finally {
if (aiSeqRef.value === localSeq) loadingRef.value = false;
if (abortControllerRef) abortControllerRef.value = null;
}
};
......@@ -842,43 +867,71 @@ const buildPayloadRight2 = () => {
const handleSwitchAiLeft1 = async (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();
if (!payload) {
aiContentLeft1.value = "暂无图表数据";
return;
}
await fetchChartInterpretationOnce(payload, aiContentLeft1, isAiLoadingLeft1, left1AiSeq);
await fetchChartInterpretationOnce(payload, aiContentLeft1, isAiLoadingLeft1, left1AiSeq, left1AiAbortController);
};
const handleSwitchAiLeft2 = async (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();
if (!payload) {
aiContentLeft2.value = "暂无图表数据";
return;
}
await fetchChartInterpretationOnce(payload, aiContentLeft2, isAiLoadingLeft2, left2AiSeq);
await fetchChartInterpretationOnce(payload, aiContentLeft2, isAiLoadingLeft2, left2AiSeq, left2AiAbortController);
};
const handleSwitchAiRight1 = async (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();
if (!payload) {
aiContentRight1.value = "暂无图表数据";
return;
}
await fetchChartInterpretationOnce(payload, aiContentRight1, isAiLoadingRight1, right1AiSeq);
await fetchChartInterpretationOnce(payload, aiContentRight1, isAiLoadingRight1, right1AiSeq, right1AiAbortController);
};
const handleSwitchAiRight2 = async (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();
if (!payload) {
aiContentRight2.value = "暂无图表数据";
return;
}
await fetchChartInterpretationOnce(payload, aiContentRight2, isAiLoadingRight2, right2AiSeq);
await fetchChartInterpretationOnce(payload, aiContentRight2, isAiLoadingRight2, right2AiSeq, right2AiAbortController);
};
......@@ -1136,21 +1189,15 @@ const initLeftSankey = (data) => {
// };
onMounted(() => {
// 刷新后 AiPane 默认展开:先给出“解读生成中…”占位,再在数据到位后触发解读请求
aiContentLeft1.value = "解读生成中…";
aiContentLeft2.value = "解读生成中…";
aiContentRight1.value = "解读生成中…";
aiContentRight2.value = "解读生成中…";
// 先拉数据;每块数据到位后立即触发一次 AI 解读(不必等其它块完成)
void handleGetFundField().then(() => handleSwitchAiLeft1(true));
// 仅拉取图表数据;AI 解读仅在用户悬停打开面板时触发
void handleGetFundField();
void handleFindCountryProjectAreaList();
void handlegetCountryFundingChange().then(() => handleSwitchAiRight1(true));
void handlegetCountryFundingChange();
void handlegetCountryFundProjectChange();
void handleGetOrgFundsArea().then(() => handleSwitchAiLeft2(true));
void handlegetOrgFundStrength().then(() => handleSwitchAiRight2(true));
void handleGetOrgFundsArea();
void handlegetOrgFundStrength();
});
// onBeforeUnmount(() => {
// window.removeEventListener("resize", handleResize);
......
......@@ -166,6 +166,10 @@ const handleTimeGroupChange = (val) => {
};
const pubTimeList = ref([
{
id: 2026,
name: "2026年"
},
{
id: 2025,
name: "2025年"
......@@ -192,13 +196,13 @@ const pubTimeList = ref([
}
]);
/** 选择「全部时间」时,yearlist 传 2000~2025 逐年 */
/** 选择「全部时间」时,yearlist 传 2000~2026 逐年 */
const YEAR_ALL_RANGE_START = 2000;
const YEAR_ALL_RANGE_END = 2025;
const YEAR_ALL_RANGE_END = 2026;
const buildYearlistForRequest = (selectedTimeModel) => {
const strippedTime = stripAllTimeForRequest(selectedTimeModel);
// 仅勾选「全部时间」、未选具体年份时,传 2000~2025 逐年
// 仅勾选「全部时间」、未选具体年份时,传 2000~2026 逐年
if (strippedTime.length === 0) {
const out = [];
for (let y = YEAR_ALL_RANGE_START; y <= YEAR_ALL_RANGE_END; y += 1) {
......
......@@ -136,7 +136,7 @@
<div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
......@@ -305,14 +305,21 @@ const handleGetThinkTankHearingInfo = async () => {
};
const REPORT_ANALYSIS_TIP_BOX5 =
"国会听证会关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
// 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
}
};
......@@ -510,10 +517,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
// 仅在用户打开 AI 面板时才请求解读
} else {
box5Data.value = [];
}
......@@ -663,6 +667,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
......@@ -677,6 +685,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
signal: box5AiAbortController.value.signal,
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
......@@ -685,10 +694,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally {
isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
}
};
......
......@@ -248,7 +248,7 @@ const handleDownloadDocument = async () => {
display: flex;
img {
width: 72px;
width: 178px;
height: 88px;
}
......
......@@ -68,11 +68,11 @@
<div class="info-content">
<div class="info-item">
<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" v-for="(item, index) in box1DataItem.domains" :key="index" v-show="item">{{
item.industryName
}}</div>
<div class="tag" v-for="(name, index) in domainTags" :key="name + '-' + index">
{{ name }}
</div>
</div>
</div>
</div>
......@@ -155,6 +155,12 @@ const activeItemIndex = ref(0);
const pageSize = ref(10);
// 当前选中 item 的数据
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 顶部
const wrapRef = ref(null);
const reportUrl = ref("");
......
......@@ -79,7 +79,7 @@
<div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
......@@ -259,14 +259,21 @@ const props = defineProps({
});
const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
// 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
}
};
......@@ -463,10 +470,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
// 仅在用户打开 AI 面板时才请求解读
} else {
box5Data.value = [];
}
......@@ -617,6 +621,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
......@@ -631,6 +639,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
signal: box5AiAbortController.value.signal,
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
......@@ -639,10 +648,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally {
isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
}
};
......@@ -963,7 +975,7 @@ onMounted(() => {
display: flex;
.left {
width: 56px;
width: 150px;
height: 74px;
margin-top: 8px;
......
......@@ -43,7 +43,7 @@
<div class="box1-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
......@@ -185,14 +185,21 @@ const applySurveyProjectDocumentTitle = (title) => {
};
const REPORT_ANALYSIS_TIP_BOX5 =
"调查项目关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
// 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
}
};
const searchOpinions = ref('');
......@@ -403,10 +410,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
// 仅在用户打开 AI 面板时才请求解读
} else {
box5Data.value = [];
}
......@@ -548,6 +552,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
......@@ -562,6 +570,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
signal: box5AiAbortController.value.signal,
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
......@@ -570,10 +579,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally {
isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
}
};
......
import * as echarts from 'echarts'
import { MUTICHARTCOLORS } from "@/common/constant.js";
// 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色)
const AREA_TAG_COLOR_BY_NAME = {
'人工智能': 'rgba(245, 34, 45, 1)', // tag1
'生物科技': '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 {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
a: parseFloat(match[4])
}
const parseHexToRgb = (hex) => {
const h = String(hex || '').replace('#', '').trim()
if (h.length !== 6) return { r: 0, g: 0, b: 0 }
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
}
return { r: 0, g: 0, b: 0, a: 1 }
}
/**
......@@ -55,22 +19,14 @@ const getMultiLineChart = (chartInput) => {
const series = chartInput.data || []
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 labelFontSize = xCount > 8 ? 10 : xCount > 5 ? 11 : 12
const labelRotate = xCount > 6 ? 28 : 0
const echartsSeries = series.map((item, index) => {
const 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)
const baseColor = MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] || '#055FC2'
const { r, g, b } = parseHexToRgb(baseColor)
return {
name: item.name,
......@@ -102,69 +58,65 @@ const getMultiLineChart = (chartInput) => {
},
/* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */
grid: {
top: 92,
top: 60,
right: 10,
bottom: 0,
left: 20,
containLabel: true
},
legend: [
{
show: true,
type: 'plain',
data: legendLine1,
top: 4,
left: 'center',
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,
itemHeight: 12
},
legend: {
show: true,
type: 'scroll',
orient: 'horizontal',
left: 8,
top: 6,
width: '95%',
height: 24,
padding: [0, 24, 0, 24],
icon: 'circle',
itemWidth: 12,
itemHeight: 12,
data: allNames,
// 隐藏内置分页按钮与页码(使用 graphic 自定义左右箭头)
pageButtonPosition: 'end',
pageIconSize: 0,
pageButtonGap: 0,
pageFormatter: () => '',
pageTextStyle: { fontSize: 0, color: 'transparent' },
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
}
},
graphic: [
{
show: legendLine2.length > 0,
type: 'plain',
data: legendLine2,
top: 30,
left: 'center',
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,
itemHeight: 12
type: 'polygon',
name: '__legend_prev__',
left: 10,
top: 14,
shape: { points: [[8, 0], [0, 6], [8, 12]] },
style: { fill: 'rgb(95, 101, 108)' },
cursor: 'pointer',
tooltip: { show: false },
silent: false,
z: 100
},
{
show: legendLine3.length > 0,
type: 'plain',
data: legendLine3,
top: 56,
left: 'center',
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,
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 一致)
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => {
const color = d?.color
if (!color) return d
const seriesData = (Array.isArray(data) ? data : []).map((d, index) => {
const color = MUTICHARTCOLORS[index % MUTICHARTCOLORS.length]
return {
...d,
itemStyle: { ...(d.itemStyle || {}), color },
itemStyle: { ...(d?.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色
labelLine: {
...(d.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color }
...(d?.labelLine || {}),
lineStyle: { ...(d?.labelLine?.lineStyle || {}), color }
}
}
})
......@@ -29,10 +30,14 @@ const getPieChart = (data) => {
alignTo: 'edge',
formatter: params => {
const name = params.name || "";
const value = params.value ?? "";
const percent = params.percent != null ? Math.round(params.percent) : 0;
const value = Number(params.value ?? 0) || 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,
edgeDistance: 20,
......
......@@ -72,38 +72,46 @@
</div> -->
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)">
<span v-html="highlightText(item.title)"></span>
<template v-if="hasData">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)">
<span v-html="highlightText(item.title)"></span>
</div>
<div class="card-item-time">
<span v-html="highlightText(item.time + ' · ' + item.content)"></span>
<img src="../images/image open.png" alt="open icon" class="card-open-image" />
</div>
<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" />
</div>
</div>
<div class="card-item-time">
<span v-html="highlightText(item.time + ' · ' + item.content)"></span>
<img src="../images/image open.png" alt="open icon" class="card-open-image" />
</div>
<div class="card-item-category">
<AreaTag v-for="(val, idx) in item.category" :key="idx" :tagName="val" />
</div>
</div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></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 class="right-footer">
<div class="info">
共 {{ total }} 篇国会听证会
</div>
<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" />
</div>
</div>
......@@ -169,6 +177,10 @@ const props = defineProps({
searchKeyword: {
type: String,
default: ""
},
loading: {
type: Boolean,
default: false
}
});
......@@ -181,6 +193,12 @@ const emit = defineEmits([
// 解构 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;
function getDateYearsAgo(years) {
......@@ -488,8 +506,11 @@ const handleToReportDetail = item => {
.right {
width: 1224px;
display: flex;
flex-direction: column;
.card-box {
flex: 1;
......@@ -498,6 +519,15 @@ const handleToReportDetail = item => {
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;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
......
......@@ -52,28 +52,35 @@
</div> -->
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)">
<div class="footer-card-top">
<img :src=item.projectCoverImgUrl alt="" />
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)">
<div class="footer-card-top">
<img :src=item.projectCoverImgUrl alt="" />
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.projectNameZh)"></span>
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.projectNameZh)"></span>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
</template>
</div>
<div class="right-footer">
<div class="info">
共 {{ total }} 篇调查项目
</div>
<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" />
</div>
</div>
......@@ -81,7 +88,7 @@
</div>
</template>
<script setup>
import { ref, toRefs, watch } from "vue";
import { ref, toRefs, watch, computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
......@@ -126,6 +133,10 @@ const props = defineProps({
searchKeyword: {
type: String,
default: ""
},
loading: {
type: Boolean,
default: false
}
});
......@@ -138,6 +149,12 @@ const emit = defineEmits([
// 解构 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 selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
......@@ -360,15 +377,27 @@ const handleToReportDetail = item => {
.right {
width: 1284px;
display: flex;
flex-direction: column;
.card-box {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 13px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card {
width: 398px;
height: 300px;
......
......@@ -52,28 +52,35 @@
</div> -->
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)">
<div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)">
<div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.name)"></span>
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.name)"></span>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
</template>
</div>
<div class="right-footer">
<div class="info">
共 {{ total }} 篇智库报告
</div>
<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" />
</div>
</div>
......@@ -81,7 +88,7 @@
</div>
</template>
<script setup>
import { ref, toRefs, watch } from "vue";
import { ref, toRefs, watch, computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
......@@ -126,6 +133,10 @@ const props = defineProps({
searchKeyword: {
type: String,
default: ""
},
loading: {
type: Boolean,
default: false
}
});
......@@ -138,6 +149,12 @@ const emit = defineEmits([
// 解构 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 selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
......@@ -360,15 +377,28 @@ const handleToReportDetail = item => {
}
.right {
width: 1284px;
display: flex;
flex-direction: column;
.card-box {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 13px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card {
width: 398px;
height: 300px;
......
......@@ -53,18 +53,20 @@
<ThinkTankReport v-if="isThinkTankReport" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters"
:cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :search-keyword="searchReport"
:loading="isThinkTankReportLoading"
@update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToReportDetail" />
<CongressHearing v-else-if="isCongressHearing" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`国会听证会-${tabResetKey}`" :research-hearing-list="researchHearingList"
: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"
@report-click="handleToHearingDetail" />
<SurveyForm v-else-if="isSurveyForm" :research-type-list="researchTypeList" :research-time-list="researchTimeList"
: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"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToProjectDetail" />
......@@ -108,6 +110,10 @@ const isThinkTankReport = ref(true);
const isSurveyForm = ref(false);
const isCongressHearing = ref(false);
const searchReport = ref('')
// 智库详情-智库动态:右侧列表 loading(居中显示)
const isThinkTankReportLoading = ref(false);
const isSurveyFormLoading = ref(false);
const isCongressHearingLoading = ref(false);
const handleToReportDetail = (item) => {
window.sessionStorage.setItem('curTabName', item.name)
......@@ -248,6 +254,10 @@ const handleChooseType = async (type) => {
await handleGetThinkDynamicsReport()
}
const researchTimeList = ref([
{
id: '2026年',
name: '2026年',
},
{
id: '2025年',
name: '2025年',
......@@ -395,6 +405,11 @@ const handleGetThinkDynamicsReport = async (payload) => {
return;
}
try {
// 仅当前 tab 显示 loading,其它置 false,避免切换后残留
isThinkTankReportLoading.value = isThinkTankReport.value;
isSurveyFormLoading.value = isSurveyForm.value;
isCongressHearingLoading.value = isCongressHearing.value;
const strippedTime = stripAllTimeForRequest(nextFilters.researchTimeIds || []);
const allTimeIds = (researchTimeList.value || []).map((x) => x.id);
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
......@@ -415,6 +430,9 @@ const handleGetThinkDynamicsReport = async (payload) => {
thinkTankId,
pageNum: Math.max(0, (currentPage.value || 1)),
pageSize: 10,
// 国会听证会:排序语义与其它资源库相反(正序→desc,倒序→asc)
sortField: "createTime",
sortOrder: sort.value === true ? "desc" : "asc",
domainIds,
startDate,
......@@ -481,6 +499,10 @@ const handleGetThinkDynamicsReport = async (payload) => {
}
} catch (error) {
console.error("获取智库动态报告error", error);
} finally {
isThinkTankReportLoading.value = false;
isSurveyFormLoading.value = false;
isCongressHearingLoading.value = false;
}
};
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => {
let option = {
series: [
......@@ -53,7 +55,10 @@ const getPieChart = (data) => {
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
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
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 = {
tooltip: {
trigger: 'item',
......@@ -14,7 +25,7 @@ const getTreeMapChart = (treemapData) => {
series: [
{
type: 'treemap',
data: treemapData,
data: dataWithColors,
roam: false,
nodeClick: false,
breadcrumb: {
......
......@@ -474,7 +474,7 @@ onMounted(async () => {
display: inline-flex;
position: absolute;
left: 277px;
right: -8px;
bottom: 208px;
background-color: rgba(255, 77, 79, 1);
align-items: center;
......
......@@ -36,26 +36,33 @@
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)">
<div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)">
<div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
</div>
<div class="footer-card-title">
{{ item.name }}
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
<div class="footer-card-title">
{{ item.name }}
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
</template>
</div>
<div class="right-footer">
<div class="info">共 {{ total }} 篇智库报告</div>
<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" />
</div>
</div>
......@@ -64,20 +71,22 @@
</template>
<script setup>
import { computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters";
defineProps({
const props = defineProps({
areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] },
pubTimeList: { type: Array, default: () => [] },
selectedPubTimeList: { type: Array, default: () => [] },
curFooterList: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 }
currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
});
const emit = defineEmits([
......@@ -102,6 +111,13 @@ const formatDate = (str) => {
const [y, m, d] = str.split('T')[0].split('-')
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>
<style lang="scss" scoped>
......@@ -195,14 +211,25 @@ const formatDate = (str) => {
.right {
width: 1284px;
max-height: 1377px;
display: flex;
flex-direction: column;
.card-box {
width: 1226px;
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 16px 16px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card {
width: 398px;
height: 300px;
......
......@@ -36,26 +36,33 @@
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)">
<div class="footer-card-top">
<img :src="item.projectCoverImgUrl" alt="" />
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)">
<div class="footer-card-top">
<img :src="item.projectCoverImgUrl" alt="" />
</div>
<div class="footer-card-title">
{{ item.projectNameZh }}
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
<div class="footer-card-title">
{{ item.projectNameZh }}
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
</template>
</div>
<div class="right-footer">
<div class="info">共 {{ total }} 篇调查项目</div>
<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" />
</div>
</div>
......@@ -64,20 +71,22 @@
</template>
<script setup>
import { computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters";
defineProps({
const props = defineProps({
areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] },
pubTimeList: { type: Array, default: () => [] },
selectedPubTimeList: { type: Array, default: () => [] },
curFooterList: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 }
currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
});
const formatDate = (str) => {
if (!str) return ''
......@@ -101,6 +110,13 @@ const handleTimeGroupChange = (val) => {
emit("update:selectedPubTimeList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME));
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>
<style lang="scss" scoped>
......@@ -170,14 +186,25 @@ const handleTimeGroupChange = (val) => {
.right {
width: 1284px;
max-height: 1377px;
display: flex;
flex-direction: column;
.card-box {
width: 1226px;
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 16px 16px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card {
width: 398px;
......
......@@ -55,42 +55,47 @@
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id ?? index">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)">
{{ item.titleZh }}
</div>
<div class="card-item-time">
{{ item.testimonyDate + ' · ' + item.thinkTankName + ' · ' + item.committeeZh }}
<img src="../ThinkTankDetail/thinkDynamics/images/image open.png" alt="open icon"
class="card-open-image" />
</div>
<template v-if="hasData">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id ?? index">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)">
{{ item.titleZh }}
</div>
<div class="card-item-time">
{{ item.testimonyDate + ' · ' + item.thinkTankName + ' · ' + item.committeeZh }}
<img src="../ThinkTankDetail/thinkDynamics/images/image open.png" alt="open icon"
class="card-open-image" />
</div>
<div class="card-item-category" v-if="item.domains">
<div v-for="(value, index) in item.domains" :key="`domain-${index}`">
<AreaTag :key="`cat-${item.id}`" :tagName="value" />
<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}`">
<AreaTag :key="`cat-${item.id}`" :tagName="value" />
</div>
</div>
</div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></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 class="right-footer">
<div class="info">
{{ hearingData.length }} 篇国会听证会
共 {{ total }} 篇国会听证会
</div>
<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" />
</div>
</div>
......@@ -121,7 +126,8 @@ const props = defineProps({
selectedPubTimeList: { type: Array, default: () => [] },
hearingData: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 }
currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
});
const emit = defineEmits([
......@@ -133,6 +139,12 @@ const emit = defineEmits([
]);
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(() => (
Array.isArray(props.selectedAreaList) && props.selectedAreaList.length
......@@ -274,11 +286,13 @@ const handlePageChange = page => {
.right {
width: 1224px;
display: flex;
flex-direction: column;
.card-box {
width: 100%;
flex: 1;
display: flex;
background: rgba(255, 255, 255, 1);
box-sizing: border-box;
......@@ -287,6 +301,15 @@ const handlePageChange = page => {
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
padding-right: 36px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card-content {
width: 1211px;
......
......@@ -36,54 +36,61 @@
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="card-content">
<div v-for="(item, index) in list" :key="item.id ?? index">
<div class="card-item" @click="emit('item-click', item)">
<div class="card-item-img">
<img :src="item.imageUrl" alt="" />
</div>
<div class="card-item-text">
<div class="card-item-title">{{ item.name }}</div>
<div class="card-item-time">
<span class="info-text">{{ formatDate(item.times) }} · {{ item.thinkTankName }} · {{ item.reportName
}}</span>
<div class="card-open-image" @click.stop="handleOpenReportOriginal(item)">
<img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" />
</div>
<template v-if="hasData">
<div class="card-content">
<div v-for="(item, index) in list" :key="item.id ?? index">
<div class="card-item" @click="emit('item-click', item)">
<div class="card-item-img">
<img :src="item.imageUrl" alt="" />
</div>
<div class="card-item-category" v-if="item.tagList && item.tagList.length">
<AreaTag v-for="d in item.tagList" :key="d" :tagName="d" />
</div>
<div class="file-box">
<div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name">
<div class="type">法案</div>
<div class="title">{{ sv.name }}</div>
<div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
<div class="card-item-text">
<div class="card-item-title">{{ item.name }}</div>
<div class="card-item-time">
<span class="info-text">{{ formatDate(item.times) }} · {{ item.thinkTankName }} · {{ item.reportName
}}</span>
<div class="card-open-image" @click.stop="handleOpenReportOriginal(item)">
<img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" />
</div>
</div>
<div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div>
<div class="title">{{ sv.content }}</div>
<div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
<div class="card-item-category" v-if="item.tagList && item.tagList.length">
<AreaTag v-for="d in item.tagList" :key="d" :tagName="d" />
</div>
<div class="file-box">
<div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name">
<div class="type">法案</div>
<div class="title">{{ sv.name }}</div>
<div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
<div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div>
<div class="title">{{ sv.content }}</div>
<div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
</div>
</div>
</div>
<div class="divider" v-if="index !== list.length - 1"></div>
</div>
<div class="divider" v-if="index !== list.length - 1"></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 class="right-footer">
<div class="info">共{{ total }}篇政策建议</div>
<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" />
</div>
</div>
......@@ -92,7 +99,7 @@
</template>
<script setup>
import { ref } from "vue";
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import AreaTag from "@/components/base/AreaTag/index.vue";
import {
......@@ -103,13 +110,14 @@ import {
stripAllTimeForRequest
} from "../utils/resourceLibraryFilters";
defineProps({
const props = defineProps({
researchTypeList: { type: Array, default: () => [] },
researchTimeList: { type: Array, default: () => [] },
list: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 7 },
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["filter-change", "page-change", "item-click"]);
......@@ -158,6 +166,13 @@ const handleAdministrativeMoreClick = (ad) => {
const selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]);
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 = () => {
emit("filter-change", {
researchTypeIds: stripAllAreaForRequest(selectedTypeIds.value),
......@@ -298,6 +313,8 @@ const handleYearGroupChange = (val) => {
.right {
width: 1224px;
display: flex;
flex-direction: column;
.card-box {
......@@ -311,6 +328,15 @@ const handleYearGroupChange = (val) => {
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
padding-right: 36px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card-content {
width: 1211px;
height: 1067px;
......@@ -332,7 +358,7 @@ const handleYearGroupChange = (val) => {
.card-item-img {
width: 56px;
width: 156px;
height: 77px;
margin-right: 22px;
flex-shrink: 0;
......
......@@ -49,7 +49,7 @@
<img class="translate-icon" src="../ReportDetail/images/image-translate.png" alt=""
style="width: 16px; height: 16px; max-width: 16px; max-height: 16px; display: block; object-fit: contain;" />
</div>
<div class="translate-text">{{ "显示文" }}</div>
<div class="translate-text">{{ "显示文" }}</div>
</div>
<div class="btn" @click="handleDownload">
<div class="icon">
......@@ -61,13 +61,15 @@
</div>
</div>
<div class="report-box">
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div>
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
<!-- 英文原文:始终展示;关闭「显示译文」时占满宽度 -->
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlEnWithPage">
<pdf :key="`left-pdf-${valueSwitch ? 'split' : 'full'}`" ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage"
class="pdf-pane-inner" />
</div>
<!-- 中文译文:仅在开关打开时展示 -->
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlWithPage">
<pdf ref="rightPdfRef" :pdfUrl="reportUrlWithPage" class="pdf-pane-inner" />
</div>
</div>
</div>
</div>
......
import * as echarts from 'echarts';
import { MUTICHARTCOLORS } from "@/common/constant.js";
/**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data
......@@ -9,70 +10,21 @@ const getMultiLineChart = (data) => {
const series = data.data
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 AREA_TAG_COLOR_BY_NAME = {
'人工智能': 'rgba(245, 34, 45, 1)', // tag1
'生物科技': '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 {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
a: parseFloat(match[4])
};
const parseHexToRgb = (hex) => {
const h = String(hex || '').replace('#', '').trim()
if (h.length !== 6) return { r: 0, g: 0, b: 0 }
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
}
// 默认返回黑色
return { r: 0, g: 0, b: 0, a: 1 };
};
}
// 动态生成 series 配置
const echartsSeries = series.map((item, index) => {
// 获取当前系列的颜色(优先使用item.color,否则用预设颜色,再否则随机)
const 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);
const baseColor = item.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] || '#055FC2'
const { r, g, b } = parseHexToRgb(baseColor)
return ({
name: item.name,
......@@ -89,11 +41,11 @@ const getMultiLineChart = (data) => {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0, // 顶部
color: `rgba(${r}, ${g}, ${b}, 0.1)` // 按需求:0.1 -> 0
color: `rgba(${r}, ${g}, ${b}, 0.1)`
},
{
offset: 1, // 底部
color: `rgba(${r}, ${g}, ${b}, 0)` // 0 透明度
color: `rgba(${r}, ${g}, ${b}, 0)`
}
])
},
......@@ -118,46 +70,33 @@ const getMultiLineChart = (data) => {
},
/* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */
grid: {
top: '34%',
top: 68,
right: '3%',
bottom: '5%',
left: '2%',
containLabel: true
},
legend: [
{
show: true,
type: 'plain',
data: legendFirstLine,
top: 8,
left: 'center',
icon: 'circle',
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
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'
}
legend: {
show: true,
type: 'plain',
orient: 'horizontal',
left: 'center',
top: 8,
width: '90%',
height: 24,
icon: 'circle',
itemWidth: 12,
itemHeight: 12,
data: allNames,
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left'
}
],
},
// 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
xAxis: [
{
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => {
const color = d?.color
if (!color) return d
const seriesData = (Array.isArray(data) ? data : []).map((d, index) => {
const color = d?.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length]
return {
...d,
itemStyle: { ...(d.itemStyle || {}), color },
itemStyle: { ...(d?.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色
labelLine: {
...(d.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color }
...(d?.labelLine || {}),
lineStyle: { ...(d?.labelLine?.lineStyle || {}), color }
}
}
})
......@@ -37,10 +38,15 @@ const getPieChart = (data) => {
alignTo: 'edge',
formatter: params => {
const name = params.name || "";
const value = params.value ?? "";
const percent = params.percent != null ? Math.round(params.percent) : 0;
const value = Number(params.value ?? 0) || 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,
edgeDistance: 10,
......
......@@ -124,7 +124,20 @@ export function matchesEarlierChineseDate(timeStr) {
/** 与政策追踪「仅全部时间」一致:固定起止(结束日按产品要求) */
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) {
const currentDate = new Date();
......@@ -145,7 +158,7 @@ function getResourceReportTodayYmd() {
/**
* 资源库 /thinkTankOverview/report:由发布时间多选(数字年 +「更早」)推导 startDate/endDate,语义对齐政策追踪 getPolicyListDateRangeFromYearList。
* - 仅「全部时间」或选满全部具体年份 → 2000-01-01 ~ 2025-12-31
* - 仅「全部时间」或选满全部具体年份 → 2000-01-01 ~ 昨天
* - 单选/多选自然年 +「更早」→ 取最小年 01-01 与最大年 12-31 包络(「更早」为 2000~2020)
* - 无法解析时的兜底:近 relativeYearsAgo 年至今天
*
......@@ -169,7 +182,7 @@ export function getResourceLibraryReportDateRangeFromTimeSelection(
if (isAllTime) {
return {
startDate: RESOURCE_REPORT_ALL_TIME_START,
endDate: RESOURCE_REPORT_ALL_TIME_END,
endDate: getResourceReportAllTimeEndYmd(),
};
}
......@@ -206,6 +219,6 @@ export function getResourceLibraryReportDateRangeFromTimeSelection(
}
return {
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 formatAmountWan = (v) => {
const n = Number(v)
if (!Number.isFinite(n)) return '0.00万'
return `${n.toFixed(2)}万`
}
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: {
type: 'sankey',
layout: 'none',
......@@ -9,6 +37,11 @@ const getSankeyChart = (nodes, links) => {
right: '15%',
top: '5%',
bottom: '5%',
color: MUTICHARTCOLORS,
lineStyle: {
color: 'rgb(230, 231, 232)',
opacity: 1
},
emphasis: {
focus: 'adjacency'
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论