提交 a9c00d6f authored 作者: yanpeng's avatar yanpeng

Merge branch 'pre' of http://8.140.26.4:10003/caijian/risk-monitor into yp-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,
......
<template> <template>
<el-dialog <el-dialog v-model="visible" class="risk-signal-detail-dialog" modal-class="risk-signal-detail-modal" width="1280px"
v-model="visible" align-center :z-index="zIndex" :show-close="true" destroy-on-close @closed="handleClosed">
class="risk-signal-detail-dialog"
modal-class="risk-signal-detail-modal"
width="1280px"
align-center
:z-index="zIndex"
:show-close="true"
destroy-on-close
@closed="handleClosed"
>
<template #header> <template #header>
<img class="header-icon" src="@/views/viewRiskSignal/assets/images/risk-icon.png" alt="" /> <img class="header-icon" :src="headerIconSrc" alt="" />
<span <span v-if="listLevelText" class="risk-signal-detail-dialog__level" :class="listLevelModifierClass">{{
v-if="listLevelText" listLevelText }}</span>
class="risk-signal-detail-dialog__level"
:class="listLevelModifierClass"
>{{ listLevelText }}</span>
<div v-if="bodyFromApi" class="risk-signal-detail-dialog__read-indicator"> <div v-if="bodyFromApi" class="risk-signal-detail-dialog__read-indicator">
<el-icon v-if="riskDetailStatus === false" class="risk-signal-detail-dialog__header-badge-close"> <el-icon v-if="riskDetailStatus === false" class="risk-signal-detail-dialog__header-badge-close">
<Close /> <Close />
</el-icon> </el-icon>
<img <img v-else-if="riskDetailStatus === true" class="risk-signal-detail-dialog__header-badge-read"
v-else-if="riskDetailStatus === true" :src="greenRightImg" alt="" />
class="risk-signal-detail-dialog__header-badge-read"
:src="greenRightImg"
alt=""
/>
<span v-if="riskDetailStatus != null" class="read">{{ riskDetailStatus ? "已读" : "未读" }}</span> <span v-if="riskDetailStatus != null" class="read">{{ riskDetailStatus ? "已读" : "未读" }}</span>
</div> </div>
</template> </template>
...@@ -35,28 +19,19 @@ ...@@ -35,28 +19,19 @@
<div v-if="riskDetailItem.title" class="risk-signal-detail-dialog__body"> <div v-if="riskDetailItem.title" class="risk-signal-detail-dialog__body">
<span class="risk-signal-detail-dialog__title">{{ riskDetailItem.title }}</span> <span class="risk-signal-detail-dialog__title">{{ riskDetailItem.title }}</span>
<div v-if="riskDetailItem.directionLabels.length" class="risk-signal-detail-dialog__directions"> <div v-if="riskDetailItem.directionLabels.length" class="risk-signal-detail-dialog__directions">
<div <div v-for="(dirLabel, dirIndex) in riskDetailItem.directionLabels"
v-for="(dirLabel, dirIndex) in riskDetailItem.directionLabels"
:key="'overview-risk-detail-direction-' + dirIndex + '-' + dirLabel" :key="'overview-risk-detail-direction-' + dirIndex + '-' + dirLabel"
class="risk-signal-detail-dialog__origin" class="risk-signal-detail-dialog__origin">{{ dirLabel }}</div>
>{{ dirLabel }}</div>
</div> </div>
<div class="risk-signal-detail-dialog__meta"> <div class="risk-signal-detail-dialog__meta">
<span>{{ metaLine }}</span> <span>{{ metaLine }}</span>
<div v-if="riskDetailItem.tag.length" class="risk-signal-detail-dialog__tags"> <div v-if="riskDetailItem.tag.length" class="risk-signal-detail-dialog__tags">
<AreaTag <AreaTag v-for="(tag, index) in riskDetailItem.tag" :key="'overview-risk-detail-tag-' + index + '-' + tag"
v-for="(tag, index) in riskDetailItem.tag" :tag-name="tag">{{ tag }}</AreaTag>
:key="'overview-risk-detail-tag-' + index + '-' + tag"
:tag-name="tag"
>{{ tag }}</AreaTag>
</div> </div>
</div> </div>
</div> </div>
<div <div v-if="showRelationBar" class="risk-signal-detail-dialog_relation" @click="handleRelationClick">
v-if="showRelationBar"
class="risk-signal-detail-dialog_relation"
@click="handleRelationClick"
>
<div class="relation"> <div class="relation">
<div class="logo"> <div class="logo">
<img src="@/views/viewRiskSignal/assets/images/logo.png" alt="" /> <img src="@/views/viewRiskSignal/assets/images/logo.png" alt="" />
...@@ -72,12 +47,8 @@ ...@@ -72,12 +47,8 @@
</div> </div>
</div> </div>
<template #footer> <template #footer>
<el-button <el-button type="primary" class="risk-signal-detail-dialog__action-btn" :loading="confirmLoading"
type="primary" @click="handleConfirm">
class="risk-signal-detail-dialog__action-btn"
:loading="confirmLoading"
@click="handleConfirm"
>
确定 确定
</el-button> </el-button>
</template> </template>
...@@ -91,6 +62,11 @@ import AreaTag from "@/components/base/AreaTag/index.vue"; ...@@ -91,6 +62,11 @@ import AreaTag from "@/components/base/AreaTag/index.vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { Close } from "@element-plus/icons-vue"; import { Close } from "@element-plus/icons-vue";
import greenRightImg from "@/views/viewRiskSignal/assets/images/green-right.png"; import greenRightImg from "@/views/viewRiskSignal/assets/images/green-right.png";
import riskIconRed from "@/views/viewRiskSignal/assets/images/risk-icon-red.png";
import riskIconOrange from "@/views/viewRiskSignal/assets/images/risk-icon-orange.png";
import riskIconYellow from "@/views/viewRiskSignal/assets/images/risk-icon-yellow.png";
import riskIconGreen from "@/views/viewRiskSignal/assets/images/risk-icon-green.png";
import riskIconBlue from "@/views/viewRiskSignal/assets/images/risk-icon-blue.png";
import { getRiskSignalInfoById, updateRiskSignalStatus } from "@/api/riskSignal/index.js"; import { getRiskSignalInfoById, updateRiskSignalStatus } from "@/api/riskSignal/index.js";
import { import {
buildListRowFallbackFromRawRow, buildListRowFallbackFromRawRow,
...@@ -180,6 +156,15 @@ const listLevelModifierClass = computed( ...@@ -180,6 +156,15 @@ const listLevelModifierClass = computed(
() => `risk-signal-detail-dialog__level--${getRiskDetailLevelModifier(listLevelText.value)}` () => `risk-signal-detail-dialog__level--${getRiskDetailLevelModifier(listLevelText.value)}`
); );
const headerIconSrc = computed(() => {
const lv = getRiskDetailLevelModifier(listLevelText.value);
if (lv === "lv1") return riskIconRed;
if (lv === "lv2") return riskIconOrange;
if (lv === "lv3") return riskIconYellow;
if (lv === "lv4") return riskIconGreen;
return riskIconBlue;
});
const fieldMap = computed(() => ({ const fieldMap = computed(() => ({
nameField: props.nameField, nameField: props.nameField,
postDateField: props.postDateField, postDateField: props.postDateField,
......
<template>
<div class="time-filter">
<el-checkbox-group class="checkbox-group" v-model="selectedValues" @change="handleChange">
<el-checkbox class="filter-checkbox" v-for="option in options" :key="option.value" :label="option.value"
:disabled="option.disabled">
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
<!-- 自定义日期选择器,仅在选中“自定义”时显示 -->
<div v-if="showCustomPicker" class="custom-date-picker">
<el-date-picker v-model="customDateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" @change="handleCustomDateChange"
style="width: 100%;" />
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// Props 定义
const props = defineProps({
// 外部控制选中值(支持 v-model)
modelValue: {
type: Array,
default: () => []
},
// 外部控制自定义日期范围
customDate: {
type: Array,
default: () => []
}
})
// Emits 定义
const emit = defineEmits(['update:modelValue', 'update:customDate', 'change'])
// 响应式数据
const selectedValues = ref([]) // 当前选中的选项值数组
const customDateRange = ref([]) // 自定义日期范围 [start, end]
const options = ref([]) // 动态生成的选项列表
// 是否显示自定义日期选择器
const showCustomPicker = computed(() => {
return selectedValues.value.includes('custom')
})
// 生成选项列表:全部时间 + 最近5个年份 + 自定义
const initOptions = () => {
const currentYear = new Date().getFullYear()
// 生成最近5个年份(包含当前年份)
const recentYears = []
for (let i = 0; i < 5; i++) {
recentYears.push(currentYear - i)
}
// 年份选项(倒序展示,从大到小)
const yearOptions = recentYears.map(year => ({
label: `${year}年`,
value: String(year)
}))
options.value = [
{ label: '全部时间', value: 'all' },
...yearOptions,
{ label: '自定义', value: 'custom' }
]
}
// 向上层派发事件
const emitChange = () => {
const eventData = {
type: 'time',
value: selectedValues.value[0], // 单选,只有一个值
customRange: selectedValues.value.includes('custom') ? customDateRange.value : []
}
emit('update:modelValue', selectedValues.value)
emit('update:customDate', customDateRange.value)
emit('change', eventData)
}
// 选项变化处理(单选逻辑)
const handleChange = (selected) => {
// 实现单选逻辑
let lastSelected = null
if (selected.length > selectedValues.value.length) {
// 新增了选项:找到新增的那个
const added = selected.find(v => !selectedValues.value.includes(v))
lastSelected = added
} else if (selected.length < selectedValues.value.length) {
// 移除了选项:选中“全部时间”
lastSelected = 'all'
} else if (selected.length === 1 && selectedValues.value.length === 1 && selected[0] !== selectedValues.value[0]) {
// 直接替换场景
lastSelected = selected[0]
} else if (selected.length > 1) {
// 多选时,只保留最后一个选中的
lastSelected = selected[selected.length - 1]
}
if (lastSelected) {
selectedValues.value = [lastSelected]
} else if (selected.length === 0) {
// 全部取消,默认给“全部时间”
selectedValues.value = ['all']
} else {
// 确保只有一个
selectedValues.value = selected.slice(0, 1)
}
// 如果选中的不是“自定义”,清空自定义日期范围
if (!selectedValues.value.includes('custom')) {
customDateRange.value = []
}
emitChange()
}
// 自定义日期范围变化处理
const handleCustomDateChange = (val) => {
if (val && val.length === 2) {
customDateRange.value = [...val]
} else {
customDateRange.value = []
}
emitChange()
}
// 重置方法(供外部调用)
const reset = () => {
selectedValues.value = ['all']
customDateRange.value = []
emitChange()
}
// 获取当前筛选条件描述(供外部调用)
const getFilterDescription = () => {
const selectedVal = selectedValues.value[0]
if (selectedVal === 'all') {
return '全部时间'
} else if (selectedVal === 'custom') {
if (customDateRange.value && customDateRange.value.length === 2) {
return `${customDateRange.value[0]}${customDateRange.value[1]}`
} else {
return '自定义(未选择日期)'
}
} else {
return `${selectedVal}年`
}
}
// 暴露方法给父组件
defineExpose({
reset,
getFilterDescription
})
// 监听外部传入的 modelValue 变化
watch(() => props.modelValue, (newVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(selectedValues.value)) {
selectedValues.value = [...newVal]
}
}, { immediate: true })
// 监听外部传入的自定义日期变化
watch(() => props.customDate, (newVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(customDateRange.value)) {
customDateRange.value = [...newVal]
}
}, { immediate: true })
// 初始化
onMounted(() => {
initOptions()
// 初始化选中值:如果外部没有传入,默认选中“全部时间”
if (!props.modelValue || props.modelValue.length === 0) {
selectedValues.value = ['all']
} else {
selectedValues.value = [...props.modelValue]
}
})
</script>
<style lang="scss" scoped>
.time-filter {
// display: flex;
// flex-direction: column;
// gap: 12px;
width: 100%;
.checkbox-group {
display: flex;
flex-wrap: wrap;
width: 100%;
.filter-checkbox {
width: 50%;
margin-right: 0;
margin-bottom: 4px;
// background: orange;
:deep(.el-checkbox__label) {
color: var(--text-primary-65-color);
font-size: var(--font-size-base);
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
}
}
}
}
.custom-date-picker {
width: 100%;
margin-top: 8px;
}
</style>
\ No newline at end of file
...@@ -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
} }
}, },
......
<template>
<el-row class="wrapper layout-grid-line">
<el-col :span="span">
<pre>
{{
`
`
}}
</pre>
<div class="time-box">
<TimePickerBox />
</div>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
import '@/styles/common.scss'
import TimePickerBox from '@/components/base/TimePickerBox/index.vue'
const span = 12
</script>
<style lang="scss" scoped>
.time-box {
width: 300px;
height: 700px;
background: #F2F8FF;
border: 1px solid var(--bg-black-5);
display: flex;
gap: 8px;
}
</style>
\ No newline at end of file
...@@ -9,7 +9,8 @@ ...@@ -9,7 +9,8 @@
<TextStyle /> <TextStyle />
<div class="text-title-1-show">通用样式/组件</div> <div class="text-title-1-show">通用样式/组件</div>
<div style="position: relative; height: 800px;"> <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-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>
...@@ -76,6 +77,9 @@ ...@@ -76,6 +77,9 @@
<el-tab-pane label="自定义排序" lazy> <el-tab-pane label="自定义排序" lazy>
<TimeSortSelectBox /> <TimeSortSelectBox />
</el-tab-pane> </el-tab-pane>
<!-- <el-tab-pane label="时间筛选框" lazy>
<TimePickerBox />
</el-tab-pane> -->
</el-tabs> </el-tabs>
</div> </div>
</el-space> </el-space>
...@@ -111,6 +115,7 @@ import RelationCenterChart from './RelationCenterChart/index.vue' ...@@ -111,6 +115,7 @@ import RelationCenterChart from './RelationCenterChart/index.vue'
import RelationForceChart from './RelationForceChart/index.vue' import RelationForceChart from './RelationForceChart/index.vue'
import WorkingBox from './WorkingBox/index.vue' import WorkingBox from './WorkingBox/index.vue'
import TimeSortSelectBox from './TimeSortSelectBox/index.vue' import TimeSortSelectBox from './TimeSortSelectBox/index.vue'
import TimePickerBox from './TimePickerBox/index.vue'
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
差异被折叠。
...@@ -4,6 +4,7 @@ import getQuarterRange from './getQuarterRange'; ...@@ -4,6 +4,7 @@ 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'
import { goToDataCountryBill } from './goToPage';
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,28 +13,61 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -12,28 +13,61 @@ 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) {
// console.log('当前点击', selectParam, params.seriesName, params.name); // console.log('当前点击', selectParam, params.seriesName, params.name);
selectParam.selectedStatus = params.seriesName selectParam.selectedStatus = params.seriesName
selectParam.selectedDate = JSON.stringify(getMonthRange(params.name)) selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
const route = router.resolve({ // const route = router.resolve({
path: "/dataLibrary/countryBill", // path: "/dataLibrary/countryBill",
query: selectParam // query: selectParam
}); // });
window.open(route.href, "_blank"); // window.open(route.href, "_blank");
return // goToDataCountryBill(selectParam)
// return
} else if (selectParam.key === 2) { } else if (selectParam.key === 2) {
selectParam.domains = params.name selectParam.domains = params.name
const route = router.resolve({ // const route = router.resolve({
path: "/dataLibrary/countryBill", // path: "/dataLibrary/countryBill",
query: selectParam // query: selectParam
}); // });
window.open(route.href, "_blank"); // window.open(route.href, "_blank");
return // goToDataCountryBill(selectParam)
// return
} else if (selectParam.key === 3) { } else if (selectParam.key === 3) {
if (params.name === '众议院' || params.name === '参议院') { if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name selectParam.selectedCongress = params.name
...@@ -48,20 +82,26 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -48,20 +82,26 @@ const setChart = (option, chartId, allowClick, selectParam) => {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31']) selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
} }
} }
const route = router.resolve({ // const route = router.resolve({
path: "/dataLibrary/countryBill", // path: "/dataLibrary/countryBill",
query: selectParam // query: selectParam
}); // });
window.open(route.href, "_blank"); // window.open(route.href, "_blank");
return // goToDataCountryBill(selectParam)
// return
} else { } else {
selectParam.selectedStatus = params.name selectParam.selectedStatus = params.name
const route = router.resolve({ // const route = router.resolve({
path: "/dataLibrary/countryBill", // path: "/dataLibrary/countryBill",
query: selectParam // query: selectParam
}); // });
window.open(route.href, "_blank"); // window.open(route.href, "_blank");
// goToDataCountryBill(selectParam)
} }
goToDataCountryBill(selectParam)
break break
case '政令': case '政令':
...@@ -112,6 +152,7 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -112,6 +152,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;
}; };
......
...@@ -322,6 +322,7 @@ import iconILetter from "./assets/icons/icon-iLetter.png"; ...@@ -322,6 +322,7 @@ import iconILetter from "./assets/icons/icon-iLetter.png";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useGotoNewsDetail } from "@/router/modules/news"; import { useGotoNewsDetail } from "@/router/modules/news";
import { goToBill } from "@/utils/goToPage";
// 跳转人物主页(MessageBubble 的 person-click 传入整条列表项,需取 personId) // 跳转人物主页(MessageBubble 的 person-click 传入整条列表项,需取 personId)
const handleClickToCharacter = async item => { const handleClickToCharacter = async item => {
...@@ -518,11 +519,12 @@ const handleClickToDetail = () => { ...@@ -518,11 +519,12 @@ const handleClickToDetail = () => {
}; };
// 查看详情 传递参数 // 查看详情 传递参数
const handleClickToDetailO = item => { const handleClickToDetailO = item => {
window.sessionStorage.setItem("billId", item.billId); // window.sessionStorage.setItem("billId", item.billId);
window.sessionStorage.setItem("curTabName", item.name || item.signalTitle); // window.sessionStorage.setItem("curTabName", item.name || item.signalTitle);
const route = router.resolve("/billLayout?billId=" + item.billId); // const route = router.resolve("/billLayout?billId=" + item.billId);
window.open(route.href, "_blank"); // window.open(route.href, "_blank");
// router.push("/billLayout?billId=" + item.billId) // router.push("/billLayout?billId=" + item.billId)
goToBill(item.billId, item.signalTitle)
}; };
const isRiskOverviewDetailOpen = ref(false); const isRiskOverviewDetailOpen = ref(false);
......
...@@ -24,25 +24,29 @@ const props = defineProps({ ...@@ -24,25 +24,29 @@ const props = defineProps({
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.com-title { .com-title {
width: 1575px; width: 1600px;
height: 42px; height: 42px;
display: flex; display: flex;
align-items: center; align-items: center;
.cl1 { .cl1 {
width: 24px; width: 24px;
height: 30px; height: 30px;
background-color: rgba(174, 214, 255, 1); background-color: rgba(174, 214, 255, 1);
margin-right: 8px; margin-right: 8px;
} }
.cl2 { .cl2 {
width: 8px; width: 8px;
height: 30px; height: 30px;
background-color: rgba(174, 214, 255, 1); background-color: rgba(174, 214, 255, 1);
margin-right: 8px; margin-right: 8px;
} }
.title { .title {
width: 152px;
height: 42px; height: 42px;
text-align: center; text-align: center;
font-size: 32px; font-size: 32px;
...@@ -50,13 +54,16 @@ const props = defineProps({ ...@@ -50,13 +54,16 @@ const props = defineProps({
font-family: 'Microsoft YaHei'; font-family: 'Microsoft YaHei';
line-height: 42px; line-height: 42px;
margin-right: 8px; margin-right: 8px;
width: fit-content;
/* 核心:强制不换行 */
white-space: nowrap;
} }
.cl3 { .cl3 {
width: 1367px; width: 100%;
height: 1px; height: 1px;
background-color: rgba(174, 214, 255, 1); background-color: rgba(174, 214, 255, 1);
box-sizing: border-box; box-sizing: border-box;
} }
} }
</style> </style>
...@@ -33,12 +33,9 @@ ...@@ -33,12 +33,9 @@
</li> </li>
<li> <li>
<span class="ul-title">涉及领域:</span> <span class="ul-title">涉及领域:</span>
<div class="ul-tags" v-if="item.AREA"> <div class="ul-tags" v-if="getAreaTagList(item).length">
<span v-for="(field, fIndex) in typeof item.AREA === 'string' <AreaTag v-for="(field, fIndex) in getAreaTagList(item)" :key="`${field}-${fIndex}`"
? item.AREA.split(',') :tagName="field" />
: item.AREA" :key="fIndex" class="ul-pie" :class="'cl' + ((fIndex % 3) + 1)">
{{ field }}
</span>
</div> </div>
<span v-else class="ul-content">未知</span> <span v-else class="ul-content">未知</span>
</li> </li>
...@@ -113,13 +110,8 @@ ...@@ -113,13 +110,8 @@
<RiskSignal :list="riskSignals" @more-click="handleToMoreRiskSignal" postDate="time" name="content" <RiskSignal :list="riskSignals" @more-click="handleToMoreRiskSignal" postDate="time" name="content"
riskLevel="title" @item-click="handleRiskSignalItemToManage" /> riskLevel="title" @item-click="handleRiskSignalItemToManage" />
<RiskSignalOverviewDetailDialog <RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" :row="riskOverviewDetailRow" name-field="content"
v-model="isRiskOverviewDetailOpen" post-date-field="time" risk-level-field="title" />
:row="riskOverviewDetailRow"
name-field="content"
post-date-field="time"
risk-level-field="title"
/>
</div> </div>
</template> </template>
...@@ -132,6 +124,7 @@ import { navigateToViewRiskSignal } from "@/utils/riskSignalOverviewNavigate"; ...@@ -132,6 +124,7 @@ import { navigateToViewRiskSignal } from "@/utils/riskSignalOverviewNavigate";
import { getCoopRestrictionTrends, getCoopRestrictionSignals } from "@/api/coopRestriction/coopRestriction.js"; import { getCoopRestrictionTrends, getCoopRestrictionSignals } from "@/api/coopRestriction/coopRestriction.js";
import defaultImg from "./assets/usImg.png"; import defaultImg from "./assets/usImg.png";
import CommonPrompt from "../../commonPrompt/index.vue"; import CommonPrompt from "../../commonPrompt/index.vue";
import AreaTag from "@/components/base/AreaTag/index.vue";
// 合作限制-查询风险信号数据 // 合作限制-查询风险信号数据
const getCoopRestrictionSignalsData = async () => { const getCoopRestrictionSignalsData = async () => {
...@@ -168,6 +161,20 @@ const getCoopRestrictionTrendsData = async () => { ...@@ -168,6 +161,20 @@ const getCoopRestrictionTrendsData = async () => {
} }
}; };
const getAreaTagList = (item) => {
const raw = item?.AREA;
if (Array.isArray(raw)) {
return raw.map(v => String(v || "").trim()).filter(Boolean);
}
if (typeof raw === "string") {
return raw
.split(",")
.map(v => String(v || "").trim())
.filter(Boolean);
}
return [];
};
// 轮播图手动切换 // 轮播图手动切换
const handlePrev = () => { const handlePrev = () => {
if (carouselRef.value) { if (carouselRef.value) {
...@@ -350,7 +357,7 @@ onMounted(() => { ...@@ -350,7 +357,7 @@ onMounted(() => {
display: flex; display: flex;
.left-center-main { .left-center-main {
width: 439px; width: 1000dvb;
height: 175px; height: 175px;
position: relative; position: relative;
...@@ -364,7 +371,7 @@ onMounted(() => { ...@@ -364,7 +371,7 @@ onMounted(() => {
} }
.left-center-main-ul { .left-center-main-ul {
width: 439px; width: 1000px;
height: 132px; height: 132px;
ul { ul {
...@@ -410,6 +417,7 @@ onMounted(() => { ...@@ -410,6 +417,7 @@ onMounted(() => {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
width: 600px;
} }
.ul-pie { .ul-pie {
......
...@@ -74,8 +74,8 @@ import TipTab from "@/views/thinkTank/TipTab/index.vue"; ...@@ -74,8 +74,8 @@ import TipTab from "@/views/thinkTank/TipTab/index.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue"; import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue"; import AiPane from "@/components/base/Ai/AiPane/index.vue";
const COOP_LEFT_TIP_TEXT = "各类型合作限制政策对比,数据来源:美对华科技合作限制信息平台"; const COOP_LEFT_TIP_TEXT = "数据来源:美对华科技合作限制信息平台";
const COOP_RIGHT_TIP_TEXT = "各领域规则分布情况,数据来源:美对华科技合作限制信息平台"; const COOP_RIGHT_TIP_TEXT = "数据来源:美对华科技合作限制信息平台";
// 临时展示 mock(不改样式):右侧“各领域规则分布情况” // 临时展示 mock(不改样式):右侧“各领域规则分布情况”
// 用完把这个开关改回 false 即可恢复走接口 // 用完把这个开关改回 false 即可恢复走接口
...@@ -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;
} }
}; };
......
...@@ -2,21 +2,14 @@ ...@@ -2,21 +2,14 @@
<div class="reslib-page" ref="reslibContainer"> <div class="reslib-page" ref="reslibContainer">
<div class="nav"> <div class="nav">
<div v-for="item in navList" :key="item.id" class="nav-item" :class="{ active: item.id === activeItem }" <div v-for="item in navList" :key="item.id" class="nav-item" :class="{ active: item.id === activeItem }"
@click="activeItem = item.id"> @click="handleNavItemClick(item.id)">
{{ item.name }} {{ item.name }}
</div> </div>
</div> </div>
<el-select v-model="sortModel" placeholder="发布时间" class="select" popper-class="coop-select-dropdown" <div class="select">
:teleported="true" placement="bottom-start" :popper-options="sortPopperOptions" @change="handleSortChange"> <TimeSortSelectBox :key="`coop-reslib-sort-${activeItem}`" :sort-demension="1"
<template #prefix> @handle-px-change="handleCoopReslibPxChange" />
<img v-if="sortModel !== true" src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png" </div>
class="select-prefix-img" alt="" @click.stop="toggleSortPrefix" />
<img v-else src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image up.png" class="select-prefix-img"
alt="" @click.stop="toggleSortPrefix" />
</template>
<el-option :key="true" label="正序" :value="true" />
<el-option :key="false" label="倒序" :value="false" />
</el-select>
<div class="main"> <div class="main">
<div class="left"> <div class="left">
<div class="left-ti1"></div> <div class="left-ti1"></div>
...@@ -61,17 +54,17 @@ ...@@ -61,17 +54,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>
...@@ -82,6 +75,7 @@ ...@@ -82,6 +75,7 @@
import { ref, onMounted, watch, computed } from "vue"; import { ref, onMounted, watch, computed } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getCoopRestrictionList } from "@/api/coopRestriction/coopRestriction"; import { getCoopRestrictionList } from "@/api/coopRestriction/coopRestriction";
import TimeSortSelectBox from "@/components/base/TimeSortSelectBox/index.vue";
import defaultImg from "../../assets/images/default-icon2.png"; import defaultImg from "../../assets/images/default-icon2.png";
...@@ -172,21 +166,15 @@ const activeItem = ref("0"); ...@@ -172,21 +166,15 @@ const activeItem = ref("0");
/** null:占位「发布时间」且默认倒序;true 正序;false 倒序(显式),与智库概览一致 */ /** null:占位「发布时间」且默认倒序;true 正序;false 倒序(显式),与智库概览一致 */
const sort = ref(null); const sort = ref(null);
const sortPopperOptions = { const handleNavItemClick = (id) => {
modifiers: [ if (activeItem.value === id) {
{ name: "preventOverflow", options: { mainAxis: false, altAxis: false } }, return;
{ name: "flip", enabled: false }
]
};
const sortModel = computed({
get() {
return sort.value;
},
set(v) {
sort.value = v;
} }
}); // 切换 tab:默认回到倒序,并从第一页开始
sort.value = null;
currentPage.value = 1;
activeItem.value = id;
};
const handleSortChange = () => { const handleSortChange = () => {
// 改变排序后从第一页开始 // 改变排序后从第一页开始
...@@ -197,14 +185,10 @@ const handleSortChange = () => { ...@@ -197,14 +185,10 @@ const handleSortChange = () => {
} }
}; };
const toggleSortPrefix = () => { /** 合作限制数据库排序公共组件回调:1=时间倒序,2=时间正序(映射到现有 sort(true/false/null)) */
sort.value = sort.value === true ? false : true; const handleCoopReslibPxChange = (val) => {
// 切换排序后从第一页开始 sort.value = Number(val) === 2 ? true : false;
if (currentPage.value === 1) { handleSortChange();
getMainDataList();
} else {
currentPage.value = 1;
}
}; };
const dataList = ref([ const dataList = ref([
{ {
...@@ -293,6 +277,11 @@ const total = ref(0); ...@@ -293,6 +277,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 +647,16 @@ watch(currentPage, () => { ...@@ -658,13 +647,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 {
......
...@@ -432,9 +432,6 @@ const dataList3 = ref([ ...@@ -432,9 +432,6 @@ const dataList3 = ref([
padding: 19px 0 20px; padding: 19px 0 20px;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
position: sticky;
top: 0;
z-index: 9;
.nav-main { .nav-main {
width: 1600px; width: 1600px;
...@@ -442,6 +439,7 @@ const dataList3 = ref([ ...@@ -442,6 +439,7 @@ const dataList3 = ref([
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative;
img { img {
width: 72px; width: 72px;
...@@ -488,8 +486,9 @@ const dataList3 = ref([ ...@@ -488,8 +486,9 @@ const dataList3 = ref([
display: flex; display: flex;
justify-content: right; justify-content: right;
position: absolute; position: absolute;
bottom: 0; right: 0;
margin-left: 1224px; bottom: -20px;
margin-left: 0;
.btn1 { .btn1 {
......
...@@ -34,14 +34,14 @@ ...@@ -34,14 +34,14 @@
</div> </div>
<!-- 数据总览 --> <!-- 数据总览 -->
<div class="datasub" id="position3"> <div class="datasub" id="position3">
<com-title title="数据总览" /> <com-title title="全景概览" />
<div class="datasub-main"> <div class="datasub-main">
<dataSub /> <dataSub />
</div> </div>
</div> </div>
<!-- 资源库 --> <!-- 资源库 -->
<div class="reslib" id="position4"> <div class="reslib" id="position4">
<com-title title="资源库" /> <com-title title="合作限制数据库" />
<div class="reslib-main"> <div class="reslib-main">
<resLib /> <resLib />
</div> </div>
......
...@@ -173,6 +173,7 @@ import { getPostOrgList, getPostMemberList } from '@/api/bill/billHome' ...@@ -173,6 +173,7 @@ import { getPostOrgList, getPostMemberList } from '@/api/bill/billHome'
import { search, getStatusList } from '@/api/comprehensiveSearch' import { search, getStatusList } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange' import getDateRange from '@/utils/getDateRange'
import { getDecodedParams } from '@/utils/goToPage'
const route = useRoute(); const route = useRoute();
...@@ -1151,33 +1152,36 @@ const handleDownloadCurChartData = () => { ...@@ -1151,33 +1152,36 @@ const handleDownloadCurChartData = () => {
// 跳转到当前页 初始化筛选条件 // 跳转到当前页 初始化筛选条件
const initParam = () => { const initParam = () => {
const hasQuery = Object.keys(route.query).length > 0; const routeQuery = getDecodedParams()
console.log('routeQuery',routeQuery);
const hasQuery = Object.keys(routeQuery).length > 0;
if (hasQuery) { if (hasQuery) {
if (route.query.selectedAreaList) { if (routeQuery.selectedAreaList) {
selectedArea.value = JSON.parse(route.query.selectedAreaList) selectedArea.value = JSON.parse(routeQuery.selectedAreaList)
} else { } else {
selectedArea.value = route.query.domains ? [route.query.domains] : ['全部领域'] selectedArea.value = routeQuery.domains ? [routeQuery.domains] : ['全部领域']
} }
if (route.query.selectedDate && Array.isArray(JSON.parse(route.query.selectedDate)) && JSON.parse(route.query.selectedDate).length) { if (routeQuery.selectedDate && Array.isArray(JSON.parse(routeQuery.selectedDate)) && JSON.parse(routeQuery.selectedDate).length) {
selectedDate.value = '自定义' selectedDate.value = '自定义'
customTime.value = JSON.parse(route.query.selectedDate) customTime.value = JSON.parse(routeQuery.selectedDate)
} }
isInvolveCn.value = route.query.isInvolveCn ? true : false isInvolveCn.value = routeQuery.isInvolveCn ? true : false
if (route.query.selectedStatus && route.query.selectedStatus !== '全部阶段') { if (routeQuery.selectedStatus && routeQuery.selectedStatus !== '全部阶段') {
selectedStatus.value = statusList.value.filter(item => { selectedStatus.value = statusList.value.filter(item => {
return item.name === route.query.selectedStatus return item.name === routeQuery.selectedStatus
})[0].id })[0].id
} else { } else {
selectedStatus.value = '全部阶段' selectedStatus.value = '全部阶段'
} }
selectedCongress.value = route.query.selectedCongress ? route.query.selectedCongress : '全部议院' selectedCongress.value = routeQuery.selectedCongress ? routeQuery.selectedCongress : '全部议院'
selectedOrg.value = route.query.selectedOrg ? [route.query.selectedOrg] : ['全部委员会'] selectedOrg.value = routeQuery.selectedOrg ? [routeQuery.selectedOrg] : ['全部委员会']
selectedmember.value = route.query.selectedmember ? JSON.parse(route.query.selectedmember) : ['全部议员'] selectedmember.value = routeQuery.selectedmember ? JSON.parse(routeQuery.selectedmember) : ['全部议员']
const query = route.query; const query = routeQuery;
if (Object.keys(query).length > 0) { if (Object.keys(query).length > 0) {
sessionStorage.setItem('countryBillRouteQuery', JSON.stringify(query)); sessionStorage.setItem('countryBillRouteQuery', JSON.stringify(query));
} }
......
...@@ -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>
...@@ -80,12 +63,8 @@ ...@@ -80,12 +63,8 @@
</div> </div>
<div class="report-box"> <div class="report-box">
<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 class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage"> <div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" /> <pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
...@@ -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) {
......
<template> <template>
<div class="wrap"> <div class="wrap">
<div class="scroll-inner"> <div class="scroll-inner" ref="pageScrollRef">
<div class="header"> <div class="header">
<div class="header-top"> <div class="header-top">
<div class="header-top-left"> <div class="header-top-left">
...@@ -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>
...@@ -231,7 +231,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png' ...@@ -231,7 +231,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue" import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.vue" import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SearchContainer from "@/components/SearchContainer.vue"; import SearchContainer from "@/components/SearchContainer.vue";
import { ref, onMounted, computed, defineProps } from "vue"; import { ref, onMounted, computed, defineProps, nextTick } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { import {
getThinkTankReportAbstract, getThinkTankReportAbstract,
...@@ -304,15 +304,22 @@ const handleGetThinkTankHearingInfo = async () => { ...@@ -304,15 +304,22 @@ 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 = [];
} }
...@@ -588,8 +592,30 @@ const switchTab = name => { ...@@ -588,8 +592,30 @@ const switchTab = name => {
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
const total = ref(0); const total = ref(0);
const pageScrollRef = ref(null);
const getScrollableParent = (el) => {
let cur = el;
while (cur && cur !== document.body && cur !== document.documentElement) {
const style = window.getComputedStyle(cur);
const overflowY = style?.overflowY;
const isScrollable = overflowY === "auto" || overflowY === "scroll";
if (isScrollable && cur.scrollHeight > cur.clientHeight + 1) {
return cur;
}
cur = cur.parentElement;
}
return null;
};
const scrollToTop = async () => {
await nextTick();
const anchor = pageScrollRef.value;
if (!anchor) return;
const scrollEl = getScrollableParent(anchor) || anchor;
scrollEl.scrollTop = 0;
};
const handleCurrentChange = page => { const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
scrollToTop();
handleGetThinkTankReportViewpoint(); handleGetThinkTankReportViewpoint();
}; };
...@@ -663,6 +689,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -663,6 +689,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 +707,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -677,6 +707,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 +716,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -685,10 +716,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;
} }
}; };
...@@ -737,9 +771,6 @@ onMounted(() => { ...@@ -737,9 +771,6 @@ onMounted(() => {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden; overflow: hidden;
.header-top { .header-top {
......
...@@ -232,9 +232,6 @@ const handleDownloadDocument = async () => { ...@@ -232,9 +232,6 @@ const handleDownloadDocument = async () => {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden; overflow: hidden;
.header-top { .header-top {
...@@ -248,7 +245,7 @@ const handleDownloadDocument = async () => { ...@@ -248,7 +245,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("");
......
<template> <template>
<div class="wrap"> <div class="wrap" ref="pageScrollRef">
<div class="top"> <div class="top">
<WarningPane :warnningLevel="riskSignal?.level" :warnningContent="riskSignal?.content" v-if="riskSignal?.level"> <WarningPane :warnningLevel="riskSignal?.level" :warnningContent="riskSignal?.content" v-if="riskSignal?.level">
</WarningPane> </WarningPane>
...@@ -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>
...@@ -217,7 +217,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png' ...@@ -217,7 +217,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue" import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.vue" import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SearchContainer from "@/components/SearchContainer.vue"; import SearchContainer from "@/components/SearchContainer.vue";
import { ref, onMounted, computed, defineProps } from "vue"; import { ref, onMounted, computed, defineProps, nextTick } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { import {
getThinkTankReportAbstract, getThinkTankReportAbstract,
...@@ -258,15 +258,22 @@ const props = defineProps({ ...@@ -258,15 +258,22 @@ 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 = [];
} }
...@@ -541,8 +545,30 @@ const switchTab = name => { ...@@ -541,8 +545,30 @@ const switchTab = name => {
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
const total = ref(0); const total = ref(0);
const pageScrollRef = ref(null);
const getScrollableParent = (el) => {
let cur = el;
while (cur && cur !== document.body && cur !== document.documentElement) {
const style = window.getComputedStyle(cur);
const overflowY = style?.overflowY;
const isScrollable = overflowY === "auto" || overflowY === "scroll";
if (isScrollable && cur.scrollHeight > cur.clientHeight + 1) {
return cur;
}
cur = cur.parentElement;
}
return null;
};
const scrollToTop = async () => {
await nextTick();
const anchor = pageScrollRef.value;
if (!anchor) return;
const scrollEl = getScrollableParent(anchor) || anchor;
scrollEl.scrollTop = 0;
};
const handleCurrentChange = page => { const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
scrollToTop();
handleGetThinkTankReportViewpoint(); handleGetThinkTankReportViewpoint();
}; };
...@@ -617,6 +643,10 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -617,6 +643,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 +661,7 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -631,6 +661,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 +670,13 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -639,10 +670,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 +997,7 @@ onMounted(() => { ...@@ -963,7 +997,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>
...@@ -184,15 +184,22 @@ const applySurveyProjectDocumentTitle = (title) => { ...@@ -184,15 +184,22 @@ const applySurveyProjectDocumentTitle = (title) => {
document.title = text; document.title = text;
}; };
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;
} }
}; };
...@@ -624,9 +636,6 @@ onMounted(() => { ...@@ -624,9 +636,6 @@ onMounted(() => {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden; overflow: hidden;
.header-top { .header-top {
......
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,
......
...@@ -64,6 +64,7 @@ import PolicyTracking from "./PolicyTracking/index.vue"; ...@@ -64,6 +64,7 @@ import PolicyTracking from "./PolicyTracking/index.vue";
import ThinkInfo from "./thinkInfo/index.vue"; import ThinkInfo from "./thinkInfo/index.vue";
import { getThinkTankSummary } from "@/api/thinkTank/overview"; import { getThinkTankSummary } from "@/api/thinkTank/overview";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getDecodedParams } from "@/utils/goToPage";
const router = useRouter(); const router = useRouter();
const tabActiveName = ref("智库动态"); const tabActiveName = ref("智库动态");
...@@ -74,9 +75,13 @@ const switchTab = name => { ...@@ -74,9 +75,13 @@ const switchTab = name => {
const thinkTank = ref({}); const thinkTank = ref({});
// 获取智库基本信息 // 获取智库基本信息
const handleGetThinkTankSummary = async () => { const handleGetThinkTankSummary = async () => {
const id = getDecodedParams()
try { try {
const parmas = { const parmas = {
id: router.currentRoute._value.params.id id: id
}; };
const res = await getThinkTankSummary(parmas); const res = await getThinkTankSummary(parmas);
console.log("智库信息", res); console.log("智库信息", res);
...@@ -120,9 +125,6 @@ onMounted(async () => { ...@@ -120,9 +125,6 @@ onMounted(async () => {
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1); box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: relative; position: relative;
position: sticky;
top: 0;
z-index: 99999;
overflow: visible; overflow: visible;
.header-top { .header-top {
......
...@@ -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;
......
<template> <template>
<div class="wrap"> <div class="wrap" ref="pageScrollRef">
<div class="main-header"> <div class="main-header">
<div class="search-box"> <div class="search-box">
...@@ -19,33 +19,8 @@ ...@@ -19,33 +19,8 @@
<div class="select-box"> <div class="select-box">
<div class="select-box-sort"> <div class="select-box-sort">
<el-select v-model="sort" placeholder="发布时间" style="width: 120px" :teleported="true" <TimeSortSelectBox :key="`dynamics-sort-${tabResetKey}`" :sort-demension="1"
:placement="'bottom-start'" :popper-options="{ @handle-px-change="handleDynamicsPxChange" />
modifiers: [
{
name: 'preventOverflow', // 禁用自动翻转逻辑
options: {
mainAxis: false, // 禁用垂直方向的自动调整
altAxis: false, // 禁用水平方向的自动调整
}
},
{
name: 'flip', // 完全禁用翻转功能
enabled: false
}
]
}">
<template #prefix>
<img v-if="sort !== true" src="./images/image down.png" class="select-prefix-img" alt=""
@click.stop="toggleSortAndFetch()" />
<img v-else src="./images/image up.png" class="select-prefix-img" alt=""
@click.stop="toggleSortAndFetch()" />
</template>
<el-option :key="'think-dynamics-sort-asc'" label="正序" :value="true"
@click="handleGetThinkDynamicsReport()" />
<el-option :key="'think-dynamics-sort-desc'" label="倒序" :value="false"
@click="handleGetThinkDynamicsReport()" />
</el-select>
</div> </div>
</div> </div>
</div> </div>
...@@ -53,18 +28,19 @@ ...@@ -53,18 +28,19 @@
<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"
@update:selected-filters="handleSelectedFiltersUpdate" :loading="isThinkTankReportLoading" @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" />
...@@ -74,6 +50,7 @@ ...@@ -74,6 +50,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted, nextTick } from "vue"; import { ref, reactive, onMounted, nextTick } from "vue";
import SurveyForm from "./SurveyForm/index.vue" import SurveyForm from "./SurveyForm/index.vue"
import TimeSortSelectBox from "@/components/base/TimeSortSelectBox/index.vue";
// import Img1 from "./images/img1.png"; // import Img1 from "./images/img1.png";
// import Img2 from "./images/img2.png"; // import Img2 from "./images/img2.png";
// import Img3 from "./images/img3.png"; // import Img3 from "./images/img3.png";
...@@ -108,6 +85,10 @@ const isThinkTankReport = ref(true); ...@@ -108,6 +85,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 +229,10 @@ const handleChooseType = async (type) => { ...@@ -248,6 +229,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年',
...@@ -320,14 +305,37 @@ const handleGetHylyList = async () => { ...@@ -320,14 +305,37 @@ const handleGetHylyList = async () => {
} }
}; };
const toggleSortAndFetch = async () => { /** 智库动态排序公共组件回调:1=时间倒序,2=时间正序(映射到现有 sort(true/false/null)) */
sort.value = sort.value === true ? false : true; const handleDynamicsPxChange = async (val) => {
sort.value = Number(val) === 2 ? true : false;
await handleGetThinkDynamicsReport(); await handleGetThinkDynamicsReport();
}; };
const currentPage = ref(1); const currentPage = ref(1);
const pageScrollRef = ref(null);
const getScrollableParent = (el) => {
let cur = el;
while (cur && cur !== document.body && cur !== document.documentElement) {
const style = window.getComputedStyle(cur);
const overflowY = style?.overflowY;
const isScrollable = overflowY === "auto" || overflowY === "scroll";
if (isScrollable && cur.scrollHeight > cur.clientHeight + 1) {
return cur;
}
cur = cur.parentElement;
}
return null;
};
const scrollToTop = async () => {
await nextTick();
const anchor = pageScrollRef.value;
if (!anchor) return;
const scrollEl = getScrollableParent(anchor) || anchor;
scrollEl.scrollTop = 0;
};
// 处理页码改变事件 // 处理页码改变事件
const handleCurrentChange = page => { const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
scrollToTop();
handleGetThinkDynamicsReport() handleGetThinkDynamicsReport()
}; };
...@@ -395,6 +403,11 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -395,6 +403,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 +428,9 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -415,6 +428,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 +497,10 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -481,6 +497,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;
} }
}; };
...@@ -593,14 +613,6 @@ onMounted(async () => { ...@@ -593,14 +613,6 @@ onMounted(async () => {
margin-top: 16px; margin-top: 16px;
display: flex; display: flex;
.select-box-time,
.select-box-sort {
background: rgb(255, 255, 255);
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
border: 1px solid rgb(230, 231, 232);
border-radius: 4px;
height: 32px;
}
.select-prefix-img { .select-prefix-img {
width: 8px; width: 8px;
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
</div> </div>
<div class="source"> <div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div> <div class="info"><img src="./images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> <div class="text"> 数据来源:美国国会官网</div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -222,7 +222,7 @@ ...@@ -222,7 +222,7 @@
</div> </div>
<div class="source"> <div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div> <div class="info"><img src="./images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div> <div class="text"> 数据来源:美国国会官网</div>
</div> </div>
<div class="middle"> <div class="middle">
<div class="middle-text">{{ "共" }}{{ personTotal }}{{ "名核心研究人员" }}</div> <div class="middle-text">{{ "共" }}{{ personTotal }}{{ "名核心研究人员" }}</div>
......
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: {
......
...@@ -32,7 +32,7 @@ const props = defineProps({ ...@@ -32,7 +32,7 @@ const props = defineProps({
width: 100%; width: 100%;
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: center; justify-content: flex-start;
align-items: center; align-items: center;
height: 22px; height: 22px;
......
...@@ -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,11 +313,14 @@ const handleYearGroupChange = (val) => { ...@@ -298,11 +313,14 @@ const handleYearGroupChange = (val) => {
.right { .right {
width: 1224px; width: 1224px;
display: flex;
flex-direction: column;
.card-box { .card-box {
width: 100%; width: 100%;
height: 1134px; padding-bottom: 32px;
;
display: flex; display: flex;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
box-sizing: border-box; box-sizing: border-box;
...@@ -311,9 +329,18 @@ const handleYearGroupChange = (val) => { ...@@ -311,9 +329,18 @@ 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;
margin-top: 33px; margin-top: 33px;
margin-left: 37px; margin-left: 37px;
...@@ -332,7 +359,7 @@ const handleYearGroupChange = (val) => { ...@@ -332,7 +359,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>
...@@ -307,8 +309,9 @@ onMounted(async () => { ...@@ -307,8 +309,9 @@ onMounted(async () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.wrap { .wrap {
overflow-y: auto;
height: 100vh; height: 100vh;
overflow-y: auto;
overflow-x: hidden;
.header { .header {
...@@ -318,9 +321,6 @@ onMounted(async () => { ...@@ -318,9 +321,6 @@ onMounted(async () => {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden; overflow: hidden;
.header-top { .header-top {
...@@ -701,7 +701,6 @@ onMounted(async () => { ...@@ -701,7 +701,6 @@ onMounted(async () => {
height: 881px; height: 881px;
display: flex; display: flex;
overflow-y: auto; overflow-y: auto;
/* 右侧统一滚动条,控制两侧原文+译文一起滚动 */
overflow-x: hidden; overflow-x: hidden;
} }
......
...@@ -326,34 +326,39 @@ export default { ...@@ -326,34 +326,39 @@ export default {
} }
} }
if (matchList.value.length > 0) jumpTo(0); if (matchList.value.length > 0) {
// 先把所有命中都标黄,再把“当前命中”改成蓝底
renderAllHighlights();
jumpTo(0);
}
}; };
// 跳转到第 N 个匹配项 const setActiveHighlight = (idx) => {
const jumpTo = (idx) => { const all = document.querySelectorAll('.highlight-rect[data-match-idx]');
if (idx < 0 || idx >= matchList.value.length) return; all.forEach((el) => {
matchIdx.value = idx; const isActive = Number(el.getAttribute('data-match-idx')) === Number(idx);
const m = matchList.value[idx]; if (isActive) {
if (m?.fallback) { el.classList.add('highlight-rect--active');
// 兜底命中:只定位页码,不做高亮 } else {
goToPage(m.pageNum); el.classList.remove('highlight-rect--active');
return;
} }
const firstSeg = m?.segments?.[0]; });
const el = firstSeg?.el; };
if (!el) return;
const renderAllHighlights = () => {
clearHighlights(); clearHighlights();
const list = Array.isArray(matchList.value) ? matchList.value : [];
list.forEach((m, idx) => {
if (!m || m.fallback) return;
const layer = overlayMap[m.pageNum]; const layer = overlayMap[m.pageNum];
if (!layer) return; if (!layer) return;
const pageWrap = layer.closest('.page-wrap'); const pageWrap = layer.closest('.page-wrap');
const container = (pageWrap || layer);
// 用 Range 精确计算“子串”在页面上的矩形位置,再画黄色块(支持跨 span) const containerRect = container.getBoundingClientRect();
const containerRect = (pageWrap || layer).getBoundingClientRect();
const segs = Array.isArray(m?.segments) ? m.segments : []; const segs = Array.isArray(m?.segments) ? m.segments : [];
for (const seg of segs) { for (const seg of segs) {
const segEl = seg?.el; const segEl = seg?.el;
if (!segEl) continue; if (!segEl) continue;
const textNode = segEl.firstChild; const textNode = segEl.firstChild;
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) continue; if (!textNode || textNode.nodeType !== Node.TEXT_NODE) continue;
try { try {
...@@ -365,25 +370,26 @@ export default { ...@@ -365,25 +370,26 @@ export default {
rectList.forEach(r => { rectList.forEach(r => {
const mark = document.createElement('div'); const mark = document.createElement('div');
mark.className = 'highlight-rect'; mark.className = 'highlight-rect';
mark.setAttribute('data-match-idx', String(idx));
mark.style.zIndex = '5'; mark.style.zIndex = '5';
mark.style.left = (r.left - containerRect.left) + 'px'; mark.style.left = (r.left - containerRect.left) + 'px';
mark.style.top = (r.top - containerRect.top) + 'px'; mark.style.top = (r.top - containerRect.top) + 'px';
mark.style.width = r.width + 'px'; mark.style.width = r.width + 'px';
mark.style.height = r.height + 'px'; mark.style.height = r.height + 'px';
(pageWrap || layer).appendChild(mark); container.appendChild(mark);
}); });
} else { } else {
// Range 兜底为空时:用 span 自身的矩形画块(精度低,但尽量可见)
const r = segEl.getBoundingClientRect(); const r = segEl.getBoundingClientRect();
if (r.width > 0 && r.height > 0) { if (r.width > 0 && r.height > 0) {
const mark = document.createElement('div'); const mark = document.createElement('div');
mark.className = 'highlight-rect'; mark.className = 'highlight-rect';
mark.setAttribute('data-match-idx', String(idx));
mark.style.zIndex = '5'; mark.style.zIndex = '5';
mark.style.left = (r.left - containerRect.left) + 'px'; mark.style.left = (r.left - containerRect.left) + 'px';
mark.style.top = (r.top - containerRect.top) + 'px'; mark.style.top = (r.top - containerRect.top) + 'px';
mark.style.width = r.width + 'px'; mark.style.width = r.width + 'px';
mark.style.height = r.height + 'px'; mark.style.height = r.height + 'px';
(pageWrap || layer).appendChild(mark); container.appendChild(mark);
} }
} }
range.detach?.(); range.detach?.();
...@@ -391,6 +397,24 @@ export default { ...@@ -391,6 +397,24 @@ export default {
// ignore // ignore
} }
} }
});
setActiveHighlight(matchIdx.value);
};
// 跳转到第 N 个匹配项
const jumpTo = (idx) => {
if (idx < 0 || idx >= matchList.value.length) return;
matchIdx.value = idx;
const m = matchList.value[idx];
if (m?.fallback) {
// 兜底命中:只定位页码,不做高亮
goToPage(m.pageNum);
return;
}
const firstSeg = m?.segments?.[0];
const el = firstSeg?.el;
if (!el) return;
setActiveHighlight(idx);
// 优先只滚动右侧 report-box,避免触发整页滚动导致 header 遮挡 // 优先只滚动右侧 report-box,避免触发整页滚动导致 header 遮挡
const container = el.closest('.report-box'); const container = el.closest('.report-box');
...@@ -510,6 +534,10 @@ canvas { ...@@ -510,6 +534,10 @@ canvas {
z-index: 5; z-index: 5;
} }
.textLayer :deep(.highlight-rect--active) {
background: rgb(184, 222, 254);
}
.page-wrap :deep(.highlight-rect) { .page-wrap :deep(.highlight-rect) {
position: absolute; position: absolute;
background: #ff0; background: #ff0;
...@@ -519,6 +547,10 @@ canvas { ...@@ -519,6 +547,10 @@ canvas {
z-index: 3; z-index: 3;
} }
.page-wrap :deep(.highlight-rect--active) {
background: rgb(184, 222, 254);
}
.loading { .loading {
position: absolute; position: absolute;
top: 50%; top: 50%;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论