提交 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) {
export function getThinkTankPolicyIndustry(params) {
return request({
method: 'GET',
url: `/api/thinkTankOverview/policyIndustry/${params.year}`,
url: `/api/thinkTankOverview/policyIndustry`,
params
})
}
......@@ -180,7 +180,8 @@ export function getThinkTankTestimoniesByThinkTankId(params) {
params: {
pageNum: params.pageNum,
pageSize: params.pageSize,
sortField: params.sortField,
sortOrder: params.sortOrder,
domainIds: params.domainIds,
startDate: params.startDate,
endDate: params.endDate,
......
<template>
<el-dialog
v-model="visible"
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"
>
<el-dialog v-model="visible" 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>
<img class="header-icon" src="@/views/viewRiskSignal/assets/images/risk-icon.png" alt="" />
<span
v-if="listLevelText"
class="risk-signal-detail-dialog__level"
:class="listLevelModifierClass"
>{{ listLevelText }}</span>
<img class="header-icon" :src="headerIconSrc" alt="" />
<span v-if="listLevelText" class="risk-signal-detail-dialog__level" :class="listLevelModifierClass">{{
listLevelText }}</span>
<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">
<Close />
</el-icon>
<img
v-else-if="riskDetailStatus === true"
class="risk-signal-detail-dialog__header-badge-read"
:src="greenRightImg"
alt=""
/>
<img v-else-if="riskDetailStatus === true" class="risk-signal-detail-dialog__header-badge-read"
:src="greenRightImg" alt="" />
<span v-if="riskDetailStatus != null" class="read">{{ riskDetailStatus ? "已读" : "未读" }}</span>
</div>
</template>
......@@ -35,28 +19,19 @@
<div v-if="riskDetailItem.title" class="risk-signal-detail-dialog__body">
<span class="risk-signal-detail-dialog__title">{{ riskDetailItem.title }}</span>
<div v-if="riskDetailItem.directionLabels.length" class="risk-signal-detail-dialog__directions">
<div
v-for="(dirLabel, dirIndex) in riskDetailItem.directionLabels"
<div v-for="(dirLabel, dirIndex) in riskDetailItem.directionLabels"
:key="'overview-risk-detail-direction-' + dirIndex + '-' + dirLabel"
class="risk-signal-detail-dialog__origin"
>{{ dirLabel }}</div>
class="risk-signal-detail-dialog__origin">{{ dirLabel }}</div>
</div>
<div class="risk-signal-detail-dialog__meta">
<span>{{ metaLine }}</span>
<div v-if="riskDetailItem.tag.length" class="risk-signal-detail-dialog__tags">
<AreaTag
v-for="(tag, index) in riskDetailItem.tag"
:key="'overview-risk-detail-tag-' + index + '-' + tag"
:tag-name="tag"
>{{ tag }}</AreaTag>
<AreaTag v-for="(tag, index) in riskDetailItem.tag" :key="'overview-risk-detail-tag-' + index + '-' + tag"
:tag-name="tag">{{ tag }}</AreaTag>
</div>
</div>
</div>
<div
v-if="showRelationBar"
class="risk-signal-detail-dialog_relation"
@click="handleRelationClick"
>
<div v-if="showRelationBar" class="risk-signal-detail-dialog_relation" @click="handleRelationClick">
<div class="relation">
<div class="logo">
<img src="@/views/viewRiskSignal/assets/images/logo.png" alt="" />
......@@ -72,12 +47,8 @@
</div>
</div>
<template #footer>
<el-button
type="primary"
class="risk-signal-detail-dialog__action-btn"
:loading="confirmLoading"
@click="handleConfirm"
>
<el-button type="primary" class="risk-signal-detail-dialog__action-btn" :loading="confirmLoading"
@click="handleConfirm">
确定
</el-button>
</template>
......@@ -91,6 +62,11 @@ import AreaTag from "@/components/base/AreaTag/index.vue";
import { ElMessage } from "element-plus";
import { Close } from "@element-plus/icons-vue";
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 {
buildListRowFallbackFromRawRow,
......@@ -180,6 +156,15 @@ const listLevelModifierClass = computed(
() => `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(() => ({
nameField: props.nameField,
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 = [
name: "thinkTank",
component: thinkTank,
meta: {
title: "科技智库概览",
title: "美国科技智库概览",
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 @@
<TextStyle />
<div class="text-title-1-show">通用样式/组件</div>
<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>
<common-page />
</el-tab-pane>
......@@ -76,6 +77,9 @@
<el-tab-pane label="自定义排序" lazy>
<TimeSortSelectBox />
</el-tab-pane>
<!-- <el-tab-pane label="时间筛选框" lazy>
<TimePickerBox />
</el-tab-pane> -->
</el-tabs>
</div>
</el-space>
......@@ -111,6 +115,7 @@ import RelationCenterChart from './RelationCenterChart/index.vue'
import RelationForceChart from './RelationForceChart/index.vue'
import WorkingBox from './WorkingBox/index.vue'
import TimeSortSelectBox from './TimeSortSelectBox/index.vue'
import TimePickerBox from './TimePickerBox/index.vue'
</script>
<style lang="scss" scoped>
......
差异被折叠。
......@@ -4,6 +4,7 @@ import getQuarterRange from './getQuarterRange';
import * as echarts from 'echarts'
import 'echarts-wordcloud';
import router from '@/router/index'
import { goToDataCountryBill } from './goToPage';
const setChart = (option, chartId, allowClick, selectParam) => {
let chartDom = document.getElementById(chartId);
if (!chartDom) {
......@@ -12,28 +13,61 @@ const setChart = (option, chartId, allowClick, selectParam) => {
chartDom.removeAttribute("_echarts_instance_");
let chart = echarts.init(chartDom);
chart.setOption(option);
// 处理自定义图例分页箭头(左右分布,隐藏页码)
// 约定:graphic 元素 name 为 __legend_prev__ / __legend_next__
chart.on('click', function (params) {
if (params?.componentType !== 'graphic') return;
if (!LEGEND_ARROW_NAMES.has(params?.name)) return;
const opt = chart.getOption?.() || {};
const legend = Array.isArray(opt.legend) ? opt.legend[0] : null;
if (!legend || legend.type !== 'scroll') return;
const dataLen = Array.isArray(legend.data) ? legend.data.length : 0;
if (dataLen <= 0) return;
const cur = Number(legend.scrollDataIndex || 0);
const nextIndex = params.name === '__legend_prev__' ? Math.max(0, cur - 1) : Math.min(dataLen - 1, cur + 1);
if (nextIndex === cur) return;
chart.dispatchAction({
type: 'legendScroll',
scrollDataIndex: nextIndex
});
});
// 初次渲染后判断是否需要显示左右箭头(可一行展示则隐藏)
applyLegendPagingArrowVisibility(chart, option)
if (allowClick) {
chart.on('click', function (params) {
// 图例分页箭头只负责翻页,不走任何跳转
if (params?.componentType === 'graphic' && LEGEND_ARROW_NAMES.has(params?.name)) {
return
}
switch (selectParam.moduleType) {
case '国会法案':
if (selectParam.key === 1) {
// console.log('当前点击', selectParam, params.seriesName, params.name);
selectParam.selectedStatus = params.seriesName
selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
return
// const route = router.resolve({
// path: "/dataLibrary/countryBill",
// query: selectParam
// });
// window.open(route.href, "_blank");
// goToDataCountryBill(selectParam)
// return
} else if (selectParam.key === 2) {
selectParam.domains = params.name
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
return
// const route = router.resolve({
// path: "/dataLibrary/countryBill",
// query: selectParam
// });
// window.open(route.href, "_blank");
// goToDataCountryBill(selectParam)
// return
} else if (selectParam.key === 3) {
if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name
......@@ -48,20 +82,26 @@ const setChart = (option, chartId, allowClick, selectParam) => {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
return
// const route = router.resolve({
// path: "/dataLibrary/countryBill",
// query: selectParam
// });
// window.open(route.href, "_blank");
// goToDataCountryBill(selectParam)
// return
} else {
selectParam.selectedStatus = params.name
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
// const route = router.resolve({
// path: "/dataLibrary/countryBill",
// query: selectParam
// });
// window.open(route.href, "_blank");
// goToDataCountryBill(selectParam)
}
goToDataCountryBill(selectParam)
break
case '政令':
......@@ -112,6 +152,7 @@ const setChart = (option, chartId, allowClick, selectParam) => {
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout(() => {
chart.resize();
applyLegendPagingArrowVisibility(chart, option)
}, 0);
return chart;
};
......
......@@ -322,6 +322,7 @@ import iconILetter from "./assets/icons/icon-iLetter.png";
import { ElMessage } from "element-plus";
import { useGotoNewsDetail } from "@/router/modules/news";
import { goToBill } from "@/utils/goToPage";
// 跳转人物主页(MessageBubble 的 person-click 传入整条列表项,需取 personId)
const handleClickToCharacter = async item => {
......@@ -518,11 +519,12 @@ const handleClickToDetail = () => {
};
// 查看详情 传递参数
const handleClickToDetailO = item => {
window.sessionStorage.setItem("billId", item.billId);
window.sessionStorage.setItem("curTabName", item.name || item.signalTitle);
const route = router.resolve("/billLayout?billId=" + item.billId);
window.open(route.href, "_blank");
// window.sessionStorage.setItem("billId", item.billId);
// window.sessionStorage.setItem("curTabName", item.name || item.signalTitle);
// const route = router.resolve("/billLayout?billId=" + item.billId);
// window.open(route.href, "_blank");
// router.push("/billLayout?billId=" + item.billId)
goToBill(item.billId, item.signalTitle)
};
const isRiskOverviewDetailOpen = ref(false);
......
<template>
<div class="com-title">
<div class="cl1"></div>
<div class="com-title">
<div class="cl1"></div>
<div class="cl2"></div>
<div class="title">{{ title }}</div>
<div class="cl3"></div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
// 传入的title数据
const props = defineProps({
title: {
type: String,
default: ""
}
title: {
type: String,
default: ""
}
});
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
.com-title {
width: 1575px;
height: 42px;
width: 1600px;
height: 42px;
display: flex;
align-items: center;
.cl1 {
width: 24px;
height: 30px;
background-color: rgba(174, 214, 255, 1);
margin-right: 8px;
}
.cl2 {
width: 8px;
height: 30px;
background-color: rgba(174, 214, 255, 1);
margin-right: 8px;
}
.title {
width: 152px;
height: 42px;
text-align: center;
font-size: 32px;
......@@ -50,13 +54,16 @@ const props = defineProps({
font-family: 'Microsoft YaHei';
line-height: 42px;
margin-right: 8px;
width: fit-content;
/* 核心:强制不换行 */
white-space: nowrap;
}
.cl3 {
width: 1367px;
width: 100%;
height: 1px;
background-color: rgba(174, 214, 255, 1);
box-sizing: border-box;
}
}
</style>
......@@ -33,12 +33,9 @@
</li>
<li>
<span class="ul-title">涉及领域:</span>
<div class="ul-tags" v-if="item.AREA">
<span v-for="(field, fIndex) in typeof item.AREA === 'string'
? item.AREA.split(',')
: item.AREA" :key="fIndex" class="ul-pie" :class="'cl' + ((fIndex % 3) + 1)">
{{ field }}
</span>
<div class="ul-tags" v-if="getAreaTagList(item).length">
<AreaTag v-for="(field, fIndex) in getAreaTagList(item)" :key="`${field}-${fIndex}`"
:tagName="field" />
</div>
<span v-else class="ul-content">未知</span>
</li>
......@@ -113,13 +110,8 @@
<RiskSignal :list="riskSignals" @more-click="handleToMoreRiskSignal" postDate="time" name="content"
riskLevel="title" @item-click="handleRiskSignalItemToManage" />
<RiskSignalOverviewDetailDialog
v-model="isRiskOverviewDetailOpen"
:row="riskOverviewDetailRow"
name-field="content"
post-date-field="time"
risk-level-field="title"
/>
<RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" :row="riskOverviewDetailRow" name-field="content"
post-date-field="time" risk-level-field="title" />
</div>
</template>
......@@ -132,6 +124,7 @@ import { navigateToViewRiskSignal } from "@/utils/riskSignalOverviewNavigate";
import { getCoopRestrictionTrends, getCoopRestrictionSignals } from "@/api/coopRestriction/coopRestriction.js";
import defaultImg from "./assets/usImg.png";
import CommonPrompt from "../../commonPrompt/index.vue";
import AreaTag from "@/components/base/AreaTag/index.vue";
// 合作限制-查询风险信号数据
const getCoopRestrictionSignalsData = 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 = () => {
if (carouselRef.value) {
......@@ -350,7 +357,7 @@ onMounted(() => {
display: flex;
.left-center-main {
width: 439px;
width: 1000dvb;
height: 175px;
position: relative;
......@@ -364,7 +371,7 @@ onMounted(() => {
}
.left-center-main-ul {
width: 439px;
width: 1000px;
height: 132px;
ul {
......@@ -410,6 +417,7 @@ onMounted(() => {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 600px;
}
.ul-pie {
......
......@@ -74,8 +74,8 @@ import TipTab from "@/views/thinkTank/TipTab/index.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
const COOP_LEFT_TIP_TEXT = "各类型合作限制政策对比,数据来源:美对华科技合作限制信息平台";
const COOP_RIGHT_TIP_TEXT = "各领域规则分布情况,数据来源:美对华科技合作限制信息平台";
const COOP_LEFT_TIP_TEXT = "数据来源:美对华科技合作限制信息平台";
const COOP_RIGHT_TIP_TEXT = "数据来源:美对华科技合作限制信息平台";
// 临时展示 mock(不改样式):右侧“各领域规则分布情况”
// 用完把这个开关改回 false 即可恢复走接口
......@@ -186,18 +186,26 @@ let leftChart;
const rightChartRef = ref(null);
let rightChart;
const isShowAiLeft = ref(true);
const isShowAiLeft = ref(false);
const aiContentLeft = ref("");
const isLeftInterpretLoading = ref(false);
const leftAiAbortController = ref(null);
const isShowAiRight = ref(true);
const isShowAiRight = ref(false);
const aiContentRight = ref("");
const isRightInterpretLoading = ref(false);
const rightAiAbortController = ref(null);
const handleSwitchAiLeft = (val) => {
isShowAiLeft.value = val;
if (val) {
fetchLeftInterpretation();
} else {
if (leftAiAbortController.value) {
leftAiAbortController.value.abort();
leftAiAbortController.value = null;
}
isLeftInterpretLoading.value = false;
}
};
......@@ -205,6 +213,12 @@ const handleSwitchAiRight = (val) => {
isShowAiRight.value = val;
if (val) {
fetchRightInterpretation();
} else {
if (rightAiAbortController.value) {
rightAiAbortController.value.abort();
rightAiAbortController.value = null;
}
isRightInterpretLoading.value = false;
}
};
......@@ -276,12 +290,17 @@ const fetchLeftInterpretation = async () => {
if (hasValidContent || isLeftInterpretLoading.value) {
return;
}
if (leftAiAbortController.value) {
leftAiAbortController.value.abort();
}
leftAiAbortController.value = new AbortController();
isLeftInterpretLoading.value = true;
aiContentLeft.value = "解读生成中…";
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
signal: leftAiAbortController.value.signal,
onChunk: (chunk) => {
// 与智库概览「数量变化趋势」一致:按 chunk 增量拼接展示
appendAiInterpretationChunk(aiContentLeft, chunk);
......@@ -292,10 +311,13 @@ const fetchLeftInterpretation = async () => {
// 与智库概览一致:优先用最终「解读」收口;否则保留已拼接内容
aiContentLeft.value = text || aiContentLeft.value || "未返回有效解读内容";
} catch (error) {
console.error("合作限制政策对比图表解读请求失败", error);
aiContentLeft.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("合作限制政策对比图表解读请求失败", error);
aiContentLeft.value = "解读加载失败";
}
} finally {
isLeftInterpretLoading.value = false;
leftAiAbortController.value = null;
}
};
......@@ -348,12 +370,17 @@ const fetchRightInterpretation = async () => {
if (hasValidContent || isRightInterpretLoading.value) {
return;
}
if (rightAiAbortController.value) {
rightAiAbortController.value.abort();
}
rightAiAbortController.value = new AbortController();
isRightInterpretLoading.value = true;
aiContentRight.value = "解读生成中…";
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
signal: rightAiAbortController.value.signal,
onChunk: (chunk) => {
appendAiInterpretationChunk(aiContentRight, chunk);
}
......@@ -362,10 +389,13 @@ const fetchRightInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res);
aiContentRight.value = text || aiContentRight.value || "未返回有效解读内容";
} catch (error) {
console.error("合作限制领域分布图表解读请求失败", error);
aiContentRight.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("合作限制领域分布图表解读请求失败", error);
aiContentRight.value = "解读加载失败";
}
} finally {
isRightInterpretLoading.value = false;
rightAiAbortController.value = null;
}
};
......
......@@ -2,21 +2,14 @@
<div class="reslib-page" ref="reslibContainer">
<div class="nav">
<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 }}
</div>
</div>
<el-select v-model="sortModel" placeholder="发布时间" class="select" popper-class="coop-select-dropdown"
:teleported="true" placement="bottom-start" :popper-options="sortPopperOptions" @change="handleSortChange">
<template #prefix>
<img v-if="sortModel !== true" src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png"
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="select">
<TimeSortSelectBox :key="`coop-reslib-sort-${activeItem}`" :sort-demension="1"
@handle-px-change="handleCoopReslibPxChange" />
</div>
<div class="main">
<div class="left">
<div class="left-ti1"></div>
......@@ -61,17 +54,17 @@
</div>
</div>
</div>
<div class="page">
<div class="count">{{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
</template>
<template v-else>
<div class="right-main-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
<div class="page">
<div class="count">共 {{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :page-count="pageCount"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
</div>
</div>
</div>
......@@ -82,6 +75,7 @@
import { ref, onMounted, watch, computed } from "vue";
import { useRouter } from "vue-router";
import { getCoopRestrictionList } from "@/api/coopRestriction/coopRestriction";
import TimeSortSelectBox from "@/components/base/TimeSortSelectBox/index.vue";
import defaultImg from "../../assets/images/default-icon2.png";
......@@ -172,21 +166,15 @@ const activeItem = ref("0");
/** null:占位「发布时间」且默认倒序;true 正序;false 倒序(显式),与智库概览一致 */
const sort = ref(null);
const sortPopperOptions = {
modifiers: [
{ name: "preventOverflow", options: { mainAxis: false, altAxis: false } },
{ name: "flip", enabled: false }
]
};
const sortModel = computed({
get() {
return sort.value;
},
set(v) {
sort.value = v;
const handleNavItemClick = (id) => {
if (activeItem.value === id) {
return;
}
});
// 切换 tab:默认回到倒序,并从第一页开始
sort.value = null;
currentPage.value = 1;
activeItem.value = id;
};
const handleSortChange = () => {
// 改变排序后从第一页开始
......@@ -197,14 +185,10 @@ const handleSortChange = () => {
}
};
const toggleSortPrefix = () => {
sort.value = sort.value === true ? false : true;
// 切换排序后从第一页开始
if (currentPage.value === 1) {
getMainDataList();
} else {
currentPage.value = 1;
}
/** 合作限制数据库排序公共组件回调:1=时间倒序,2=时间正序(映射到现有 sort(true/false/null)) */
const handleCoopReslibPxChange = (val) => {
sort.value = Number(val) === 2 ? true : false;
handleSortChange();
};
const dataList = ref([
{
......@@ -293,6 +277,11 @@ const total = ref(0);
const pageSize = ref(10);
const currentPage = ref(1);
const reslibContainer = ref(null);
const pageCount = computed(() => {
const size = Number(pageSize.value || 10) || 10;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const getTypeClass = (type) => {
const map = {
......@@ -658,13 +647,16 @@ watch(currentPage, () => {
.title {
font-size: 20px;
width: 950px;
max-width: 950px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(59, 65, 75);
margin-bottom: 9px;
cursor: pointer;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.content {
......
......@@ -432,9 +432,6 @@ const dataList3 = ref([
padding: 19px 0 20px;
background: rgba(255, 255, 255, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
position: sticky;
top: 0;
z-index: 9;
.nav-main {
width: 1600px;
......@@ -442,6 +439,7 @@ const dataList3 = ref([
margin: 0 auto;
display: flex;
align-items: center;
position: relative;
img {
width: 72px;
......@@ -488,8 +486,9 @@ const dataList3 = ref([
display: flex;
justify-content: right;
position: absolute;
bottom: 0;
margin-left: 1224px;
right: 0;
bottom: -20px;
margin-left: 0;
.btn1 {
......
......@@ -34,14 +34,14 @@
</div>
<!-- 数据总览 -->
<div class="datasub" id="position3">
<com-title title="数据总览" />
<com-title title="全景概览" />
<div class="datasub-main">
<dataSub />
</div>
</div>
<!-- 资源库 -->
<div class="reslib" id="position4">
<com-title title="资源库" />
<com-title title="合作限制数据库" />
<div class="reslib-main">
<resLib />
</div>
......
......@@ -173,6 +173,7 @@ import { getPostOrgList, getPostMemberList } from '@/api/bill/billHome'
import { search, getStatusList } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange'
import { getDecodedParams } from '@/utils/goToPage'
const route = useRoute();
......@@ -1151,33 +1152,36 @@ const handleDownloadCurChartData = () => {
// 跳转到当前页 初始化筛选条件
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 (route.query.selectedAreaList) {
selectedArea.value = JSON.parse(route.query.selectedAreaList)
if (routeQuery.selectedAreaList) {
selectedArea.value = JSON.parse(routeQuery.selectedAreaList)
} 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 = '自定义'
customTime.value = JSON.parse(route.query.selectedDate)
customTime.value = JSON.parse(routeQuery.selectedDate)
}
isInvolveCn.value = route.query.isInvolveCn ? true : false
if (route.query.selectedStatus && route.query.selectedStatus !== '全部阶段') {
isInvolveCn.value = routeQuery.isInvolveCn ? true : false
if (routeQuery.selectedStatus && routeQuery.selectedStatus !== '全部阶段') {
selectedStatus.value = statusList.value.filter(item => {
return item.name === route.query.selectedStatus
return item.name === routeQuery.selectedStatus
})[0].id
} else {
selectedStatus.value = '全部阶段'
}
selectedCongress.value = route.query.selectedCongress ? route.query.selectedCongress : '全部议院'
selectedOrg.value = route.query.selectedOrg ? [route.query.selectedOrg] : ['全部委员会']
selectedmember.value = route.query.selectedmember ? JSON.parse(route.query.selectedmember) : ['全部议员']
selectedCongress.value = routeQuery.selectedCongress ? routeQuery.selectedCongress : '全部议院'
selectedOrg.value = routeQuery.selectedOrg ? [routeQuery.selectedOrg] : ['全部委员会']
selectedmember.value = routeQuery.selectedmember ? JSON.parse(routeQuery.selectedmember) : ['全部议员']
const query = route.query;
const query = routeQuery;
if (Object.keys(query).length > 0) {
sessionStorage.setItem('countryBillRouteQuery', JSON.stringify(query));
}
......
......@@ -26,27 +26,15 @@
<div class="btn-box">
<div class="translate">
<div class="search-input-wrap" v-if="showSearchInput">
<input
v-model="searchKeywordText"
class="search-input"
placeholder="回车查询"
@keyup.enter="handleSearchInPdf"
/>
<input v-model="searchKeywordText" class="search-input" placeholder="回车查询"
@keyup.enter="handleSearchInPdf" />
<div class="search-match-count">{{ matchInfo.current }}/{{ matchInfo.total }}</div>
<button
class="search-nav-btn"
type="button"
@click="handlePrevMatch"
:disabled="matchInfo.total === 0 || matchInfo.current <= 1"
>
<button class="search-nav-btn" type="button" @click="handlePrevMatch"
:disabled="matchInfo.total === 0 || matchInfo.current <= 1">
上一个
</button>
<button
class="search-nav-btn"
type="button"
@click="handleNextMatch"
:disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total"
>
<button class="search-nav-btn" type="button" @click="handleNextMatch"
:disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total">
下一个
</button>
</div>
......@@ -54,19 +42,14 @@
<el-switch v-model="valueSwitch" />
</div>
<div class="translate-image">
<img
class="translate-icon"
src="../assets/icon-translation.png"
alt=""
style="
<img class="translate-icon" src="../assets/icon-translation.png" alt="" style="
width: 16px;
height: 16px;
max-width: 16px;
max-height: 16px;
display: block;
object-fit: contain;
"
/>
" />
</div>
<div class="translate-text">{{ "显示译文" }}</div>
</div>
......@@ -80,12 +63,8 @@
</div>
<div class="report-box">
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf
:key="`right-pdf-${valueSwitch ? 'split' : 'full'}`"
ref="rightPdfRef"
:pdfUrl="reportUrlWithPage"
class="pdf-pane-inner"
/>
<pdf :key="`right-pdf-${valueSwitch ? 'split' : 'full'}`" ref="rightPdfRef" :pdfUrl="reportUrlWithPage"
class="pdf-pane-inner" />
</div>
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
......@@ -203,7 +182,7 @@ const handleSearchInPdf = async () => {
try {
const { ElMessage } = await import("element-plus");
ElMessage.warning("未找到包含该关键词的页面");
} catch (_) {}
} catch (_) { }
}
};
......@@ -246,7 +225,7 @@ const handleDownload = async () => {
try {
const { ElMessage } = await import("element-plus");
ElMessage.warning("暂无下载链接");
} catch (_) {}
} catch (_) { }
return;
}
const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-");
......@@ -659,6 +638,7 @@ onMounted(async () => {
display: flex;
gap: 8px;
cursor: pointer;
.icon {
width: 16px;
height: 16px;
......
......@@ -171,6 +171,10 @@ const options = [
];
/** value 须与 v-model 类型一致(数字),否则 el-select 无法匹配 label,会显示成「2025」而非「2025年」 */
const options1 = [
{
value: 2026,
label: "2026年"
},
{
value: 2025,
label: "2025年"
......@@ -719,11 +723,11 @@ let rightChart1;
let leftSankey;
let boxplotChart;
// ------- AI 解读(刷新后默认展开,行为对齐智库概览) -------
const isShowAiContentLeft1 = ref(true);
const isShowAiContentLeft2 = ref(true);
const isShowAiContentRight1 = ref(true);
const isShowAiContentRight2 = ref(true);
// ------- AI 解读(默认仅展示 AiButton,悬停后再请求 AI) -------
const isShowAiContentLeft1 = ref(false);
const isShowAiContentLeft2 = ref(false);
const isShowAiContentRight1 = ref(false);
const isShowAiContentRight2 = ref(false);
const aiContentLeft1 = ref("");
const aiContentLeft2 = ref("");
......@@ -735,6 +739,11 @@ const isAiLoadingLeft2 = ref(false);
const isAiLoadingRight1 = ref(false);
const isAiLoadingRight2 = ref(false);
const left1AiAbortController = ref(null);
const left2AiAbortController = ref(null);
const right1AiAbortController = ref(null);
const right2AiAbortController = ref(null);
const AI_LOADING_TEXT = "解读生成中…";
// 用于保证“切换筛选后只写入最新一次解读结果”
const left1AiSeq = ref(0);
......@@ -767,7 +776,13 @@ const getInterpretationTextFromChartResponse = (res) => {
);
};
const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSeqRef) => {
const fetchChartInterpretationOnce = async (
payload,
targetRef,
loadingRef,
aiSeqRef,
abortControllerRef
) => {
if (loadingRef.value) return;
const hasValidContent =
......@@ -778,12 +793,19 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe
if (hasValidContent) return;
const localSeq = aiSeqRef.value;
if (abortControllerRef?.value) {
abortControllerRef.value.abort();
}
if (abortControllerRef) {
abortControllerRef.value = new AbortController();
}
loadingRef.value = true;
targetRef.value = AI_LOADING_TEXT;
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
...(abortControllerRef?.value?.signal ? { signal: abortControllerRef.value.signal } : {}),
onChunk: (chunk) => {
if (aiSeqRef.value !== localSeq) return;
appendAiInterpretationChunk(targetRef, chunk, AI_LOADING_TEXT);
......@@ -796,10 +818,13 @@ const fetchChartInterpretationOnce = async (payload, targetRef, loadingRef, aiSe
targetRef.value = text || targetRef.value || "未返回有效解读内容";
} catch (e) {
if (aiSeqRef.value !== localSeq) return;
console.error("图表解读请求失败", e);
targetRef.value = "解读加载失败";
if (e?.name !== "AbortError") {
console.error("图表解读请求失败", e);
targetRef.value = "解读加载失败";
}
} finally {
if (aiSeqRef.value === localSeq) loadingRef.value = false;
if (abortControllerRef) abortControllerRef.value = null;
}
};
......@@ -842,43 +867,71 @@ const buildPayloadRight2 = () => {
const handleSwitchAiLeft1 = async (val) => {
isShowAiContentLeft1.value = val;
if (!val) return;
if (!val) {
if (left1AiAbortController.value) {
left1AiAbortController.value.abort();
left1AiAbortController.value = null;
}
isAiLoadingLeft1.value = false;
return;
}
const payload = buildPayloadLeft1();
if (!payload) {
aiContentLeft1.value = "暂无图表数据";
return;
}
await fetchChartInterpretationOnce(payload, aiContentLeft1, isAiLoadingLeft1, left1AiSeq);
await fetchChartInterpretationOnce(payload, aiContentLeft1, isAiLoadingLeft1, left1AiSeq, left1AiAbortController);
};
const handleSwitchAiLeft2 = async (val) => {
isShowAiContentLeft2.value = val;
if (!val) return;
if (!val) {
if (left2AiAbortController.value) {
left2AiAbortController.value.abort();
left2AiAbortController.value = null;
}
isAiLoadingLeft2.value = false;
return;
}
const payload = buildPayloadLeft2();
if (!payload) {
aiContentLeft2.value = "暂无图表数据";
return;
}
await fetchChartInterpretationOnce(payload, aiContentLeft2, isAiLoadingLeft2, left2AiSeq);
await fetchChartInterpretationOnce(payload, aiContentLeft2, isAiLoadingLeft2, left2AiSeq, left2AiAbortController);
};
const handleSwitchAiRight1 = async (val) => {
isShowAiContentRight1.value = val;
if (!val) return;
if (!val) {
if (right1AiAbortController.value) {
right1AiAbortController.value.abort();
right1AiAbortController.value = null;
}
isAiLoadingRight1.value = false;
return;
}
const payload = buildPayloadRight1();
if (!payload) {
aiContentRight1.value = "暂无图表数据";
return;
}
await fetchChartInterpretationOnce(payload, aiContentRight1, isAiLoadingRight1, right1AiSeq);
await fetchChartInterpretationOnce(payload, aiContentRight1, isAiLoadingRight1, right1AiSeq, right1AiAbortController);
};
const handleSwitchAiRight2 = async (val) => {
isShowAiContentRight2.value = val;
if (!val) return;
if (!val) {
if (right2AiAbortController.value) {
right2AiAbortController.value.abort();
right2AiAbortController.value = null;
}
isAiLoadingRight2.value = false;
return;
}
const payload = buildPayloadRight2();
if (!payload) {
aiContentRight2.value = "暂无图表数据";
return;
}
await fetchChartInterpretationOnce(payload, aiContentRight2, isAiLoadingRight2, right2AiSeq);
await fetchChartInterpretationOnce(payload, aiContentRight2, isAiLoadingRight2, right2AiSeq, right2AiAbortController);
};
......@@ -1136,21 +1189,15 @@ const initLeftSankey = (data) => {
// };
onMounted(() => {
// 刷新后 AiPane 默认展开:先给出“解读生成中…”占位,再在数据到位后触发解读请求
aiContentLeft1.value = "解读生成中…";
aiContentLeft2.value = "解读生成中…";
aiContentRight1.value = "解读生成中…";
aiContentRight2.value = "解读生成中…";
// 先拉数据;每块数据到位后立即触发一次 AI 解读(不必等其它块完成)
void handleGetFundField().then(() => handleSwitchAiLeft1(true));
// 仅拉取图表数据;AI 解读仅在用户悬停打开面板时触发
void handleGetFundField();
void handleFindCountryProjectAreaList();
void handlegetCountryFundingChange().then(() => handleSwitchAiRight1(true));
void handlegetCountryFundingChange();
void handlegetCountryFundProjectChange();
void handleGetOrgFundsArea().then(() => handleSwitchAiLeft2(true));
void handlegetOrgFundStrength().then(() => handleSwitchAiRight2(true));
void handleGetOrgFundsArea();
void handlegetOrgFundStrength();
});
// onBeforeUnmount(() => {
// window.removeEventListener("resize", handleResize);
......
......@@ -166,6 +166,10 @@ const handleTimeGroupChange = (val) => {
};
const pubTimeList = ref([
{
id: 2026,
name: "2026年"
},
{
id: 2025,
name: "2025年"
......@@ -192,13 +196,13 @@ const pubTimeList = ref([
}
]);
/** 选择「全部时间」时,yearlist 传 2000~2025 逐年 */
/** 选择「全部时间」时,yearlist 传 2000~2026 逐年 */
const YEAR_ALL_RANGE_START = 2000;
const YEAR_ALL_RANGE_END = 2025;
const YEAR_ALL_RANGE_END = 2026;
const buildYearlistForRequest = (selectedTimeModel) => {
const strippedTime = stripAllTimeForRequest(selectedTimeModel);
// 仅勾选「全部时间」、未选具体年份时,传 2000~2025 逐年
// 仅勾选「全部时间」、未选具体年份时,传 2000~2026 逐年
if (strippedTime.length === 0) {
const out = [];
for (let y = YEAR_ALL_RANGE_START; y <= YEAR_ALL_RANGE_END; y += 1) {
......
<template>
<div class="wrap">
<div class="scroll-inner">
<div class="scroll-inner" ref="pageScrollRef">
<div class="header">
<div class="header-top">
<div class="header-top-left">
......@@ -136,7 +136,7 @@
<div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
......@@ -231,7 +231,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.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 {
getThinkTankReportAbstract,
......@@ -304,15 +304,22 @@ const handleGetThinkTankHearingInfo = async () => {
}
};
const REPORT_ANALYSIS_TIP_BOX5 =
"国会听证会关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
"数据来源:美国兰德公司官网";
// 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
}
};
......@@ -510,10 +517,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
// 仅在用户打开 AI 面板时才请求解读
} else {
box5Data.value = [];
}
......@@ -588,8 +592,30 @@ const switchTab = name => {
const currentPage = ref(1);
const pageSize = ref(10);
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 => {
currentPage.value = page;
scrollToTop();
handleGetThinkTankReportViewpoint();
};
......@@ -663,6 +689,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
......@@ -677,6 +707,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
signal: box5AiAbortController.value.signal,
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
......@@ -685,10 +716,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally {
isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
}
};
......@@ -737,9 +771,6 @@ onMounted(() => {
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden;
.header-top {
......
......@@ -232,9 +232,6 @@ const handleDownloadDocument = async () => {
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden;
.header-top {
......@@ -248,7 +245,7 @@ const handleDownloadDocument = async () => {
display: flex;
img {
width: 72px;
width: 178px;
height: 88px;
}
......
......@@ -68,11 +68,11 @@
<div class="info-content">
<div class="info-item">
<div class="info-text">{{ "相关领域:" }}</div>
<div class="info-right" v-if="box1DataItem && box1DataItem.domains">
<div class="info-right" v-if="hasDomainTags">
<div class="tag-box">
<div class="tag" v-for="(item, index) in box1DataItem.domains" :key="index" v-show="item">{{
item.industryName
}}</div>
<div class="tag" v-for="(name, index) in domainTags" :key="name + '-' + index">
{{ name }}
</div>
</div>
</div>
</div>
......@@ -155,6 +155,12 @@ const activeItemIndex = ref(0);
const pageSize = ref(10);
// 当前选中 item 的数据
const box1DataItem = ref(null);
const domainTags = computed(() => {
const domains = box1DataItem.value?.domains;
const list = Array.isArray(domains) ? domains : [];
return list.map((d) => d?.industryName).filter(Boolean);
});
const hasDomainTags = computed(() => domainTags.value.length > 0);
// 整个页面容器,用于分页后滚回 wrap 顶部
const wrapRef = ref(null);
const reportUrl = ref("");
......
<template>
<div class="wrap">
<div class="wrap" ref="pageScrollRef">
<div class="top">
<WarningPane :warnningLevel="riskSignal?.level" :warnningContent="riskSignal?.content" v-if="riskSignal?.level">
</WarningPane>
......@@ -79,7 +79,7 @@
<div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
......@@ -217,7 +217,7 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.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 {
getThinkTankReportAbstract,
......@@ -258,15 +258,22 @@ const props = defineProps({
}
});
const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
"数据来源:美国兰德公司官网";
// 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
}
};
......@@ -463,10 +470,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
// 仅在用户打开 AI 面板时才请求解读
} else {
box5Data.value = [];
}
......@@ -541,8 +545,30 @@ const switchTab = name => {
const currentPage = ref(1);
const pageSize = ref(10);
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 => {
currentPage.value = page;
scrollToTop();
handleGetThinkTankReportViewpoint();
};
......@@ -617,6 +643,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
......@@ -631,6 +661,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
signal: box5AiAbortController.value.signal,
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
......@@ -639,10 +670,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally {
isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
}
};
......@@ -963,7 +997,7 @@ onMounted(() => {
display: flex;
.left {
width: 56px;
width: 150px;
height: 74px;
margin-top: 8px;
......
......@@ -43,7 +43,7 @@
<div class="box1-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<div class="ai-wrap" v-if="!isShowAiContentBox5" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
......@@ -184,15 +184,22 @@ const applySurveyProjectDocumentTitle = (title) => {
document.title = text;
};
const REPORT_ANALYSIS_TIP_BOX5 =
"调查项目关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
"数据来源:美国兰德公司官网";
// 默认仅展示 AiButton,悬停后再请求 AI
const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const box5AiAbortController = ref(null);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
} else {
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
box5AiAbortController.value = null;
}
isBox5InterpretLoading.value = false;
}
};
const searchOpinions = ref('');
......@@ -403,10 +410,7 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
// 仅在用户打开 AI 面板时才请求解读
} else {
box5Data.value = [];
}
......@@ -548,6 +552,10 @@ const fetchBox5ChartInterpretation = async () => {
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
if (box5AiAbortController.value) {
box5AiAbortController.value.abort();
}
box5AiAbortController.value = new AbortController();
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
......@@ -562,6 +570,7 @@ const fetchBox5ChartInterpretation = async () => {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
signal: box5AiAbortController.value.signal,
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
......@@ -570,10 +579,13 @@ const fetchBox5ChartInterpretation = async () => {
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
if (error?.name !== "AbortError") {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
}
} finally {
isBox5InterpretLoading.value = false;
box5AiAbortController.value = null;
}
};
......@@ -624,9 +636,6 @@ onMounted(() => {
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden;
.header-top {
......
import * as echarts from 'echarts'
import { MUTICHARTCOLORS } from "@/common/constant.js";
// 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色)
const AREA_TAG_COLOR_BY_NAME = {
'人工智能': 'rgba(245, 34, 45, 1)', // tag1
'生物科技': 'rgba(19, 168, 168, 1)', // tag2
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
// 兼容常见写法
'通信网络': 'rgba(5, 95, 194, 1)',
'量子科技': 'rgba(114, 46, 209, 1)', // tag4
'新能源': 'rgba(82, 196, 26, 1)', // tag5
'集成电路': 'rgba(22, 119, 255, 1)', // tag6
'海洋': 'rgba(15, 120, 199, 1)', // tag7
'先进制造': 'rgba(250, 173, 20, 1)', // tag8
'新材料': 'rgba(250, 140, 22, 1)', // tag9
'航空航天': 'rgba(47, 84, 235, 1)', // tag10
'太空': 'rgba(47, 84, 235, 1)', // tag11
'深海': 'rgba(73, 104, 161, 1)', // tag12
'极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
const fallbackColorList = [
'rgba(5, 95, 194, 1)',
'rgba(245, 34, 45, 1)',
'rgba(19, 168, 168, 1)',
'rgba(250, 140, 22, 1)',
'rgba(114, 46, 209, 1)',
'rgba(82, 196, 26, 1)',
'rgba(22, 119, 255, 1)',
'rgba(250, 84, 28, 1)',
'rgba(47, 84, 235, 1)',
'rgba(133, 165, 255, 1)'
]
const parseRgba = (colorStr) => {
const match = colorStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/)
if (match) {
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
a: parseFloat(match[4])
}
const parseHexToRgb = (hex) => {
const h = String(hex || '').replace('#', '').trim()
if (h.length !== 6) return { r: 0, g: 0, b: 0 }
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
}
return { r: 0, g: 0, b: 0, a: 1 }
}
/**
......@@ -55,22 +19,14 @@ const getMultiLineChart = (chartInput) => {
const series = chartInput.data || []
const allNames = series.map((item) => item.name)
const lineSize = Math.ceil(allNames.length / 3)
const legendLine1 = allNames.slice(0, lineSize)
const legendLine2 = allNames.slice(lineSize, lineSize * 2)
const legendLine3 = allNames.slice(lineSize * 2)
const xCount = Array.isArray(title) ? title.length : 0
const labelFontSize = xCount > 8 ? 10 : xCount > 5 ? 11 : 12
const labelRotate = xCount > 6 ? 28 : 0
const echartsSeries = series.map((item, index) => {
const baseColor =
item.color ||
AREA_TAG_COLOR_BY_NAME[item.name] ||
fallbackColorList[index % fallbackColorList.length] ||
`rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)`
const { r, g, b } = parseRgba(baseColor)
const baseColor = MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] || '#055FC2'
const { r, g, b } = parseHexToRgb(baseColor)
return {
name: item.name,
......@@ -102,69 +58,65 @@ const getMultiLineChart = (chartInput) => {
},
/* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */
grid: {
top: 92,
top: 60,
right: 10,
bottom: 0,
left: 20,
containLabel: true
},
legend: [
{
show: true,
type: 'plain',
data: legendLine1,
top: 4,
left: 'center',
icon: 'circle',
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
},
itemWidth: 12,
itemHeight: 12
},
legend: {
show: true,
type: 'scroll',
orient: 'horizontal',
left: 8,
top: 6,
width: '95%',
height: 24,
padding: [0, 24, 0, 24],
icon: 'circle',
itemWidth: 12,
itemHeight: 12,
data: allNames,
// 隐藏内置分页按钮与页码(使用 graphic 自定义左右箭头)
pageButtonPosition: 'end',
pageIconSize: 0,
pageButtonGap: 0,
pageFormatter: () => '',
pageTextStyle: { fontSize: 0, color: 'transparent' },
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
}
},
graphic: [
{
show: legendLine2.length > 0,
type: 'plain',
data: legendLine2,
top: 30,
left: 'center',
icon: 'circle',
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
},
itemWidth: 12,
itemHeight: 12
type: 'polygon',
name: '__legend_prev__',
left: 10,
top: 14,
shape: { points: [[8, 0], [0, 6], [8, 12]] },
style: { fill: 'rgb(95, 101, 108)' },
cursor: 'pointer',
tooltip: { show: false },
silent: false,
z: 100
},
{
show: legendLine3.length > 0,
type: 'plain',
data: legendLine3,
top: 56,
left: 'center',
icon: 'circle',
textStyle: {
fontFamily: 'Source Han Sans CN',
fontWeight: 400,
fontSize: 14,
lineHeight: 24,
letterSpacing: 0,
align: 'left',
color: 'rgb(95, 101, 108)'
},
itemWidth: 12,
itemHeight: 12
type: 'polygon',
name: '__legend_next__',
right: 10,
top: 14,
shape: { points: [[0, 0], [8, 6], [0, 12]] },
style: { fill: 'rgb(95, 101, 108)' },
cursor: 'pointer',
tooltip: { show: false },
silent: false,
z: 100
}
],
// 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => {
const color = d?.color
if (!color) return d
const seriesData = (Array.isArray(data) ? data : []).map((d, index) => {
const color = MUTICHARTCOLORS[index % MUTICHARTCOLORS.length]
return {
...d,
itemStyle: { ...(d.itemStyle || {}), color },
itemStyle: { ...(d?.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色
labelLine: {
...(d.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color }
...(d?.labelLine || {}),
lineStyle: { ...(d?.labelLine?.lineStyle || {}), color }
}
}
})
......@@ -29,10 +30,14 @@ const getPieChart = (data) => {
alignTo: 'edge',
formatter: params => {
const name = params.name || "";
const value = params.value ?? "";
const percent = params.percent != null ? Math.round(params.percent) : 0;
const value = Number(params.value ?? 0) || 0;
const rawPercent =
params?.data?.percent != null
? params.data.percent
: params.percent;
const percent = rawPercent != null ? Number(rawPercent).toFixed(2) : "0.00";
return `{name|${name}}\n{time| ${percent}%}`;
return `{name|${name}}\n{time|${value} ${percent}%}`;
},
minMargin: 10,
edgeDistance: 20,
......
......@@ -64,6 +64,7 @@ import PolicyTracking from "./PolicyTracking/index.vue";
import ThinkInfo from "./thinkInfo/index.vue";
import { getThinkTankSummary } from "@/api/thinkTank/overview";
import { useRouter } from "vue-router";
import { getDecodedParams } from "@/utils/goToPage";
const router = useRouter();
const tabActiveName = ref("智库动态");
......@@ -74,9 +75,13 @@ const switchTab = name => {
const thinkTank = ref({});
// 获取智库基本信息
const handleGetThinkTankSummary = async () => {
const id = getDecodedParams()
try {
const parmas = {
id: router.currentRoute._value.params.id
id: id
};
const res = await getThinkTankSummary(parmas);
console.log("智库信息", res);
......@@ -120,9 +125,6 @@ onMounted(async () => {
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: relative;
position: sticky;
top: 0;
z-index: 99999;
overflow: visible;
.header-top {
......
......@@ -72,38 +72,46 @@
</div> -->
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)">
<span v-html="highlightText(item.title)"></span>
<template v-if="hasData">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)">
<span v-html="highlightText(item.title)"></span>
</div>
<div class="card-item-time">
<span v-html="highlightText(item.time + ' · ' + item.content)"></span>
<img src="../images/image open.png" alt="open icon" class="card-open-image" />
</div>
<div class="card-item-category"
v-if="Array.isArray(item.category) && item.category.some(v => String(v || '').trim())">
<AreaTag v-for="(val, idx) in item.category" :key="idx" :tagName="val" />
</div>
</div>
<div class="card-item-time">
<span v-html="highlightText(item.time + ' · ' + item.content)"></span>
<img src="../images/image open.png" alt="open icon" class="card-open-image" />
</div>
<div class="card-item-category">
<AreaTag v-for="(val, idx) in item.category" :key="idx" :tagName="val" />
</div>
</div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div>
</div>
</div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div>
<div class="right-footer">
<div class="info">
共 {{ total }} 篇国会听证会
</div>
<div class="page-box">
<el-pagination :page-size="10" background layout="prev, pager, next" :total="total"
<el-pagination :page-size="10" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" />
</div>
</div>
......@@ -169,6 +177,10 @@ const props = defineProps({
searchKeyword: {
type: String,
default: ""
},
loading: {
type: Boolean,
default: false
}
});
......@@ -181,6 +193,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage, researchHearingList, hearingData, selectedYear } = toRefs(props);
const hasData = computed(() => Array.isArray(hearingData.value) && hearingData.value.length > 0);
const pageCount = computed(() => {
const size = 10;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const pageSize = 10;
function getDateYearsAgo(years) {
......@@ -488,8 +506,11 @@ const handleToReportDetail = item => {
.right {
width: 1224px;
display: flex;
flex-direction: column;
.card-box {
flex: 1;
......@@ -498,6 +519,15 @@ const handleToReportDetail = item => {
background: rgba(255, 255, 255, 1);
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
......
......@@ -52,28 +52,35 @@
</div> -->
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)">
<div class="footer-card-top">
<img :src=item.projectCoverImgUrl alt="" />
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)">
<div class="footer-card-top">
<img :src=item.projectCoverImgUrl alt="" />
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.projectNameZh)"></span>
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.projectNameZh)"></span>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
</template>
</div>
<div class="right-footer">
<div class="info">
共 {{ total }} 篇调查项目
</div>
<div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total"
<el-pagination :page-size="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" />
</div>
</div>
......@@ -81,7 +88,7 @@
</div>
</template>
<script setup>
import { ref, toRefs, watch } from "vue";
import { ref, toRefs, watch, computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
......@@ -126,6 +133,10 @@ const props = defineProps({
searchKeyword: {
type: String,
default: ""
},
loading: {
type: Boolean,
default: false
}
});
......@@ -138,6 +149,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props);
const hasData = computed(() => Array.isArray(curFooterList.value) && curFooterList.value.length > 0);
const pageCount = computed(() => {
const size = 12;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
......@@ -360,15 +377,27 @@ const handleToReportDetail = item => {
.right {
width: 1284px;
display: flex;
flex-direction: column;
.card-box {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 13px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card {
width: 398px;
height: 300px;
......
......@@ -52,28 +52,35 @@
</div> -->
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)">
<div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="handleToReportDetail(item)">
<div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.name)"></span>
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
<div class="footer-card-title">
<span v-html="highlightText(item.name)"></span>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
</template>
</div>
<div class="right-footer">
<div class="info">
共 {{ total }} 篇智库报告
</div>
<div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total"
<el-pagination :page-size="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handleCurrentChange" :current-page="currentPage" />
</div>
</div>
......@@ -81,7 +88,7 @@
</div>
</template>
<script setup>
import { ref, toRefs, watch } from "vue";
import { ref, toRefs, watch, computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
......@@ -126,6 +133,10 @@ const props = defineProps({
searchKeyword: {
type: String,
default: ""
},
loading: {
type: Boolean,
default: false
}
});
......@@ -138,6 +149,12 @@ const emit = defineEmits([
// 解构 props,保持模板里变量名不变
const { researchTypeList, researchTimeList, curFooterList, total, currentPage } = toRefs(props);
const hasData = computed(() => Array.isArray(curFooterList.value) && curFooterList.value.length > 0);
const pageCount = computed(() => {
const size = 12;
const t = Number(total.value || 0) || 0;
return Math.max(1, Math.ceil(t / size));
});
const selectedResearchIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedResearchTimeIds = ref([RESOURCE_FILTER_ALL_TIME]);
......@@ -360,15 +377,28 @@ const handleToReportDetail = item => {
}
.right {
width: 1284px;
display: flex;
flex-direction: column;
.card-box {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 13px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card {
width: 398px;
height: 300px;
......
<template>
<div class="wrap">
<div class="wrap" ref="pageScrollRef">
<div class="main-header">
<div class="search-box">
......@@ -19,33 +19,8 @@
<div class="select-box">
<div class="select-box-sort">
<el-select v-model="sort" placeholder="发布时间" style="width: 120px" :teleported="true"
:placement="'bottom-start'" :popper-options="{
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>
<TimeSortSelectBox :key="`dynamics-sort-${tabResetKey}`" :sort-demension="1"
@handle-px-change="handleDynamicsPxChange" />
</div>
</div>
</div>
......@@ -53,18 +28,19 @@
<ThinkTankReport v-if="isThinkTankReport" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`智库报告-${tabResetKey}`" :selected-filters="selectedFilters"
:cur-footer-list="curFooterList" :total="total" :current-page="currentPage" :search-keyword="searchReport"
@update:selected-filters="handleSelectedFiltersUpdate"
:loading="isThinkTankReportLoading" @update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToReportDetail" />
<CongressHearing v-else-if="isCongressHearing" :research-type-list="researchTypeList"
:research-time-list="researchTimeList" :key="`国会听证会-${tabResetKey}`" :research-hearing-list="researchHearingList"
:selected-filters="selectedFilters" :selected-year="selectedYear" :total="total" :current-page="currentPage"
:search-keyword="searchReport" :hearing-data="hearingData" @update:selected-filters="handleSelectedFiltersUpdate"
:search-keyword="searchReport" :hearing-data="hearingData" :loading="isCongressHearingLoading"
@update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToHearingDetail" />
<SurveyForm v-else-if="isSurveyForm" :research-type-list="researchTypeList" :research-time-list="researchTimeList"
:key="`调查项目-${tabResetKey}`" :selected-filters="selectedFilters" :cur-footer-list="curFooterProjectList"
:total="total" :current-page="currentPage" :search-keyword="searchReport"
:total="total" :current-page="currentPage" :search-keyword="searchReport" :loading="isSurveyFormLoading"
@update:selected-filters="handleSelectedFiltersUpdate"
@filter-change="(payload) => handleGetThinkDynamicsReport(payload)" @page-change="handleCurrentChange"
@report-click="handleToProjectDetail" />
......@@ -74,6 +50,7 @@
<script setup>
import { ref, reactive, onMounted, nextTick } from "vue";
import SurveyForm from "./SurveyForm/index.vue"
import TimeSortSelectBox from "@/components/base/TimeSortSelectBox/index.vue";
// import Img1 from "./images/img1.png";
// import Img2 from "./images/img2.png";
// import Img3 from "./images/img3.png";
......@@ -108,6 +85,10 @@ const isThinkTankReport = ref(true);
const isSurveyForm = ref(false);
const isCongressHearing = ref(false);
const searchReport = ref('')
// 智库详情-智库动态:右侧列表 loading(居中显示)
const isThinkTankReportLoading = ref(false);
const isSurveyFormLoading = ref(false);
const isCongressHearingLoading = ref(false);
const handleToReportDetail = (item) => {
window.sessionStorage.setItem('curTabName', item.name)
......@@ -248,6 +229,10 @@ const handleChooseType = async (type) => {
await handleGetThinkDynamicsReport()
}
const researchTimeList = ref([
{
id: '2026年',
name: '2026年',
},
{
id: '2025年',
name: '2025年',
......@@ -320,14 +305,37 @@ const handleGetHylyList = async () => {
}
};
const toggleSortAndFetch = async () => {
sort.value = sort.value === true ? false : true;
/** 智库动态排序公共组件回调:1=时间倒序,2=时间正序(映射到现有 sort(true/false/null)) */
const handleDynamicsPxChange = async (val) => {
sort.value = Number(val) === 2 ? true : false;
await handleGetThinkDynamicsReport();
};
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 => {
currentPage.value = page;
scrollToTop();
handleGetThinkDynamicsReport()
};
......@@ -395,6 +403,11 @@ const handleGetThinkDynamicsReport = async (payload) => {
return;
}
try {
// 仅当前 tab 显示 loading,其它置 false,避免切换后残留
isThinkTankReportLoading.value = isThinkTankReport.value;
isSurveyFormLoading.value = isSurveyForm.value;
isCongressHearingLoading.value = isCongressHearing.value;
const strippedTime = stripAllTimeForRequest(nextFilters.researchTimeIds || []);
const allTimeIds = (researchTimeList.value || []).map((x) => x.id);
const { startDate, endDate } = getResourceLibraryReportDateRangeFromTimeSelection(
......@@ -415,6 +428,9 @@ const handleGetThinkDynamicsReport = async (payload) => {
thinkTankId,
pageNum: Math.max(0, (currentPage.value || 1)),
pageSize: 10,
// 国会听证会:排序语义与其它资源库相反(正序→desc,倒序→asc)
sortField: "createTime",
sortOrder: sort.value === true ? "desc" : "asc",
domainIds,
startDate,
......@@ -481,6 +497,10 @@ const handleGetThinkDynamicsReport = async (payload) => {
}
} catch (error) {
console.error("获取智库动态报告error", error);
} finally {
isThinkTankReportLoading.value = false;
isSurveyFormLoading.value = false;
isCongressHearingLoading.value = false;
}
};
......@@ -593,14 +613,6 @@ onMounted(async () => {
margin-top: 16px;
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 {
width: 8px;
......
......@@ -115,7 +115,7 @@
</div>
<div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div>
<div class="text"> 数据来源:美国国会官网</div>
</div>
</AnalysisBox>
</div>
......@@ -222,7 +222,7 @@
</div>
<div class="source">
<div class="info"><img src="./images/image-exclamation.png"></div>
<div class="text"> 数据来源:美国国会官网,数据时间:2015.1至2025.12</div>
<div class="text"> 数据来源:美国国会官网</div>
</div>
<div class="middle">
<div class="middle-text">{{ "共" }}{{ personTotal }}{{ "名核心研究人员" }}</div>
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getPieChart = (data) => {
let option = {
series: [
......@@ -53,7 +55,10 @@ const getPieChart = (data) => {
labelLinePoints: points
};
},
data: data
data: (Array.isArray(data) ? data : []).map((d, index) => ({
...d,
itemStyle: { ...(d?.itemStyle || {}), color: d?.color || MUTICHARTCOLORS[index % MUTICHARTCOLORS.length] }
}))
}]
}
return option
......
import { MUTICHARTCOLORS } from "@/common/constant.js";
const getTreeMapChart = (treemapData) => {
const list = Array.isArray(treemapData) ? treemapData : []
const dataWithColors = list.map((node, idx) => ({
...node,
itemStyle: {
...(node?.itemStyle || {}),
color: node?.itemStyle?.color || MUTICHARTCOLORS[idx % MUTICHARTCOLORS.length]
}
}))
const option = {
tooltip: {
trigger: 'item',
......@@ -14,7 +25,7 @@ const getTreeMapChart = (treemapData) => {
series: [
{
type: 'treemap',
data: treemapData,
data: dataWithColors,
roam: false,
nodeClick: false,
breadcrumb: {
......
......@@ -32,7 +32,7 @@ const props = defineProps({
width: 100%;
display: flex;
gap: 8px;
justify-content: center;
justify-content: flex-start;
align-items: center;
height: 22px;
......
......@@ -474,7 +474,7 @@ onMounted(async () => {
display: inline-flex;
position: absolute;
left: 277px;
right: -8px;
bottom: 208px;
background-color: rgba(255, 77, 79, 1);
align-items: center;
......
......@@ -36,26 +36,33 @@
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)">
<div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)">
<div class="footer-card-top">
<img :src="item.imageUrl" alt="" />
</div>
<div class="footer-card-title">
{{ item.name }}
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
<div class="footer-card-title">
{{ item.name }}
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.times) }}</div>
<div class="from">{{ item.thinkTankName }}</div>
</div>
</div>
</template>
</div>
<div class="right-footer">
<div class="info">共 {{ total }} 篇智库报告</div>
<div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total"
<el-pagination :page-size="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="emit('page-change', $event)" :current-page="currentPage" />
</div>
</div>
......@@ -64,20 +71,22 @@
</template>
<script setup>
import { computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters";
defineProps({
const props = defineProps({
areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] },
pubTimeList: { type: Array, default: () => [] },
selectedPubTimeList: { type: Array, default: () => [] },
curFooterList: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 }
currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
});
const emit = defineEmits([
......@@ -102,6 +111,13 @@ const formatDate = (str) => {
const [y, m, d] = str.split('T')[0].split('-')
return `${y}${+m}${+d}日`
};
const hasData = computed(() => Array.isArray(props.curFooterList) && props.curFooterList.length > 0);
const pageCount = computed(() => {
const size = 12;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
</script>
<style lang="scss" scoped>
......@@ -195,14 +211,25 @@ const formatDate = (str) => {
.right {
width: 1284px;
max-height: 1377px;
display: flex;
flex-direction: column;
.card-box {
width: 1226px;
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 16px 16px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card {
width: 398px;
height: 300px;
......
......@@ -36,26 +36,33 @@
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)">
<div class="footer-card-top">
<img :src="item.projectCoverImgUrl" alt="" />
<template v-if="hasData">
<div class="footer-card" v-for="(item, index) in curFooterList" :key="index"
@click="emit('report-click', item)">
<div class="footer-card-top">
<img :src="item.projectCoverImgUrl" alt="" />
</div>
<div class="footer-card-title">
{{ item.projectNameZh }}
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
<div class="footer-card-title">
{{ item.projectNameZh }}
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
<div class="footer-card-footer">
<div class="time">{{ formatDate(item.startDate) }}</div>
<div class="from">{{ item.thinktankName }}</div>
</div>
</div>
</template>
</div>
<div class="right-footer">
<div class="info">共 {{ total }} 篇调查项目</div>
<div class="page-box">
<el-pagination :page-size="12" background layout="prev, pager, next" :total="total"
<el-pagination :page-size="12" :page-count="pageCount" background layout="prev, pager, next"
@current-change="emit('page-change', $event)" :current-page="currentPage" />
</div>
</div>
......@@ -64,20 +71,22 @@
</template>
<script setup>
import { computed } from "vue";
import {
RESOURCE_FILTER_ALL_AREA,
RESOURCE_FILTER_ALL_TIME,
normalizeExclusiveAllOption
} from "../utils/resourceLibraryFilters";
defineProps({
const props = defineProps({
areaList: { type: Array, default: () => [] },
selectedAreaList: { type: Array, default: () => [] },
pubTimeList: { type: Array, default: () => [] },
selectedPubTimeList: { type: Array, default: () => [] },
curFooterList: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 }
currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
});
const formatDate = (str) => {
if (!str) return ''
......@@ -101,6 +110,13 @@ const handleTimeGroupChange = (val) => {
emit("update:selectedPubTimeList", normalizeExclusiveAllOption(val, RESOURCE_FILTER_ALL_TIME));
emit("filter-change");
};
const hasData = computed(() => Array.isArray(props.curFooterList) && props.curFooterList.length > 0);
const pageCount = computed(() => {
const size = 12;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
</script>
<style lang="scss" scoped>
......@@ -170,14 +186,25 @@ const handleTimeGroupChange = (val) => {
.right {
width: 1284px;
max-height: 1377px;
display: flex;
flex-direction: column;
.card-box {
width: 1226px;
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 16px 16px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.footer-card {
width: 398px;
......
......@@ -55,42 +55,47 @@
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id ?? index">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)">
{{ item.titleZh }}
</div>
<div class="card-item-time">
{{ item.testimonyDate + ' · ' + item.thinkTankName + ' · ' + item.committeeZh }}
<img src="../ThinkTankDetail/thinkDynamics/images/image open.png" alt="open icon"
class="card-open-image" />
</div>
<template v-if="hasData">
<div class="card-content">
<div v-for="(item, index) in hearingData" :key="item.id ?? index">
<div class="card-item">
<img class="card-item-img" :src="item.coverImgUrl" alt="report image" />
<div class="card-item-text">
<div class="card-item-title" @click="emit('report-click', item)">
{{ item.titleZh }}
</div>
<div class="card-item-time">
{{ item.testimonyDate + ' · ' + item.thinkTankName + ' · ' + item.committeeZh }}
<img src="../ThinkTankDetail/thinkDynamics/images/image open.png" alt="open icon"
class="card-open-image" />
</div>
<div class="card-item-category" v-if="item.domains">
<div v-for="(value, index) in item.domains" :key="`domain-${index}`">
<AreaTag :key="`cat-${item.id}`" :tagName="value" />
<div class="card-item-category"
v-if="Array.isArray(item.domains) && item.domains.some(v => String(v || '').trim())">
<div v-for="(value, index) in item.domains" :key="`domain-${index}`">
<AreaTag :key="`cat-${item.id}`" :tagName="value" />
</div>
</div>
</div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div>
<div class="divider" v-if="index !== hearingData.length - 1"></div>
</div>
</div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div>
<div class="right-footer">
<div class="info">
{{ hearingData.length }} 篇国会听证会
共 {{ total }} 篇国会听证会
</div>
<div class="page-box">
<el-pagination :page-size="pageSize" background layout="prev, pager, next" :total="total"
<el-pagination :page-size="pageSize" :page-count="pageCount" background layout="prev, pager, next"
@current-change="handlePageChange" :current-page="currentPage" />
</div>
</div>
......@@ -121,7 +126,8 @@ const props = defineProps({
selectedPubTimeList: { type: Array, default: () => [] },
hearingData: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 }
currentPage: { type: Number, default: 1 },
loading: { type: Boolean, default: false }
});
const emit = defineEmits([
......@@ -133,6 +139,12 @@ const emit = defineEmits([
]);
const pageSize = 10;
const hasData = computed(() => Array.isArray(props.hearingData) && props.hearingData.length > 0);
const pageCount = computed(() => {
const size = Number(pageSize || 10) || 10;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
const selectedResearchIds = computed(() => (
Array.isArray(props.selectedAreaList) && props.selectedAreaList.length
......@@ -274,11 +286,13 @@ const handlePageChange = page => {
.right {
width: 1224px;
display: flex;
flex-direction: column;
.card-box {
width: 100%;
flex: 1;
display: flex;
background: rgba(255, 255, 255, 1);
box-sizing: border-box;
......@@ -287,6 +301,15 @@ const handlePageChange = page => {
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
padding-right: 36px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card-content {
width: 1211px;
......
......@@ -36,54 +36,61 @@
</div>
</div>
<div class="right">
<div class="right" v-loading="loading">
<div class="card-box">
<div class="card-content">
<div v-for="(item, index) in list" :key="item.id ?? index">
<div class="card-item" @click="emit('item-click', item)">
<div class="card-item-img">
<img :src="item.imageUrl" alt="" />
</div>
<div class="card-item-text">
<div class="card-item-title">{{ item.name }}</div>
<div class="card-item-time">
<span class="info-text">{{ formatDate(item.times) }} · {{ item.thinkTankName }} · {{ item.reportName
}}</span>
<div class="card-open-image" @click.stop="handleOpenReportOriginal(item)">
<img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" />
</div>
</div>
<div class="card-item-category" v-if="item.tagList && item.tagList.length">
<AreaTag v-for="d in item.tagList" :key="d" :tagName="d" />
<template v-if="hasData">
<div class="card-content">
<div v-for="(item, index) in list" :key="item.id ?? index">
<div class="card-item" @click="emit('item-click', item)">
<div class="card-item-img">
<img :src="item.imageUrl" alt="" />
</div>
<div class="file-box">
<div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name">
<div class="type">法案</div>
<div class="title">{{ sv.name }}</div>
<div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
<div class="card-item-text">
<div class="card-item-title">{{ item.name }}</div>
<div class="card-item-time">
<span class="info-text">{{ formatDate(item.times) }} · {{ item.thinkTankName }} · {{ item.reportName
}}</span>
<div class="card-open-image" @click.stop="handleOpenReportOriginal(item)">
<img src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png" alt="" />
</div>
</div>
<div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div>
<div class="title">{{ sv.content }}</div>
<div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
<div class="card-item-category" v-if="item.tagList && item.tagList.length">
<AreaTag v-for="d in item.tagList" :key="d" :tagName="d" />
</div>
<div class="file-box">
<div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name">
<div class="type">法案</div>
<div class="title">{{ sv.name }}</div>
<div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
<div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div>
<div class="title">{{ sv.content }}</div>
<div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
</div>
</div>
</div>
<div class="divider" v-if="index !== list.length - 1"></div>
</div>
<div class="divider" v-if="index !== list.length - 1"></div>
</div>
</div>
</template>
<template v-else-if="!loading">
<div class="right-empty">
<el-empty class="right-el-empty" description="暂无数据" :image-size="100" />
</div>
</template>
</div>
<div class="right-footer">
<div class="info">共{{ total }}篇政策建议</div>
<div class="page-box">
<el-pagination :page-size="pageSize" background layout="prev, pager, next" :total="total"
<el-pagination :page-size="pageSize" :page-count="pageCount" background layout="prev, pager, next"
@current-change="p => emit('page-change', p)" :current-page="currentPage" />
</div>
</div>
......@@ -92,7 +99,7 @@
</template>
<script setup>
import { ref } from "vue";
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import AreaTag from "@/components/base/AreaTag/index.vue";
import {
......@@ -103,13 +110,14 @@ import {
stripAllTimeForRequest
} from "../utils/resourceLibraryFilters";
defineProps({
const props = defineProps({
researchTypeList: { type: Array, default: () => [] },
researchTimeList: { type: Array, default: () => [] },
list: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 7 },
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["filter-change", "page-change", "item-click"]);
......@@ -158,6 +166,13 @@ const handleAdministrativeMoreClick = (ad) => {
const selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]);
const hasData = computed(() => Array.isArray(props.list) && props.list.length > 0);
const pageCount = computed(() => {
const size = Number(props.pageSize || 7) || 7;
const total = Number(props.total || 0) || 0;
return Math.max(1, Math.ceil(total / size));
});
const emitFilterToParent = () => {
emit("filter-change", {
researchTypeIds: stripAllAreaForRequest(selectedTypeIds.value),
......@@ -298,11 +313,14 @@ const handleYearGroupChange = (val) => {
.right {
width: 1224px;
display: flex;
flex-direction: column;
.card-box {
width: 100%;
height: 1134px;
padding-bottom: 32px;
;
display: flex;
background: rgba(255, 255, 255, 1);
box-sizing: border-box;
......@@ -311,9 +329,18 @@ const handleYearGroupChange = (val) => {
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
padding-right: 36px;
.right-empty {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card-content {
width: 1211px;
height: 1067px;
margin-top: 33px;
margin-left: 37px;
......@@ -332,7 +359,7 @@ const handleYearGroupChange = (val) => {
.card-item-img {
width: 56px;
width: 156px;
height: 77px;
margin-right: 22px;
flex-shrink: 0;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论