完成学术交流web页面

This commit is contained in:
2025-10-29 23:48:17 +08:00
parent 25df322e32
commit bc351b5904

View File

@@ -14,7 +14,77 @@
<el-button type="primary" size="small" @click="fetchMeetings">重新加载</el-button>
</div>
<template v-if="!loading && !error && currentMeeting">
<!-- 会议列表页面 -->
<template v-if="isAllMeetingsPage && !loading && !error">
<div class="page-header">
<div class="back-button">
<el-button icon="ArrowLeft" @click="navigateBack">返回会议首页</el-button>
</div>
<h1 class="page-title">所有会议列表</h1>
</div>
<!-- 会议列表加载状态 -->
<div v-if="allMeetingsLoading" class="all-meetings-loading">
<el-loading text="加载所有会议中..." />
</div>
<!-- 会议列表错误提示 -->
<div v-else-if="allMeetingsError" class="all-meetings-error">
{{ allMeetingsError }}
<el-button type="primary" size="small" @click="fetchAllMeetings">重新加载</el-button>
</div>
<!-- 无会议数据 -->
<div v-else-if="allMeetingsList.length === 0" class="no-all-meetings">
暂无会议数据
</div>
<!-- 会议列表内容 -->
<div v-else class="all-meetings-container">
<div class="meeting-item" v-for="(meeting, index) in allMeetingsList" :key="meeting.id">
<div class="meeting-header">
<h2 class="meeting-theme">{{ meeting.theme }}</h2>
<div class="meeting-date">{{ formatMeetingTime(meeting.start_time, meeting.end_time) }}</div>
</div>
<div class="meeting-content">
<div class="meeting-image">
<img :src="meeting.cover_url || 'https://picsum.photos/id/1059/600/400'" :alt="meeting.theme" class="meeting-img">
</div>
<div class="meeting-details">
<h3 class="meeting-subtitle">{{ meeting.subtitle }}</h3>
<div class="meeting-intro" v-html="meeting.intro"></div>
<div class="meeting-meta">
<div class="meta-item">
<span class="meta-label">创建时间</span>
<span class="meta-value">{{ formatDate(meeting.create_time) }}</span>
</div>
</div>
<el-button type="primary" size="small" @click="viewMeetingDetails(meeting)">查看详情</el-button>
</div>
</div>
</div>
<!-- 分页组件 -->
<div class="all-meetings-pagination">
<el-pagination
@size-change="handlePageSizeChange"
@current-change="handleCurrentPageChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15, 20]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="totalMeetings"
/>
</div>
</div>
</template>
<!-- 主会议页面 -->
<template v-if="!isAllMeetingsPage && !loading && !error && currentMeeting">
<!-- 锚点导航 -->
<el-anchor
:offset="100"
@@ -29,7 +99,7 @@
<el-anchor-link href="#review" title="往届回顾" />
</el-anchor>
<!-- 头部横幅当前会议信息 -->
<!-- 头部横幅当前会议信息- 宽度设置为屏幕宽度 -->
<div
class="header-banner"
id="conference"
@@ -46,7 +116,6 @@
<span class="meta-label">时间 / TIME</span>
<span class="meta-value">{{ formatMeetingTime(currentMeeting.start_time, currentMeeting.end_time) }}</span>
</div>
</div>
</div>
</div>
@@ -83,14 +152,16 @@
</div>
<div class="schedule-image-container">
<!-- 点击图片触发放大查看 -->
<img
:src="currentMeeting.schedule_image_url"
alt="会议议程安排"
class="schedule-image"
loading="lazy"
@click="showEnlargedImage(currentMeeting.schedule_image_url)"
>
<p class="schedule-caption">
点击图片可查看高清议程 | Click to view high-resolution schedule
点击图片可放大
</p>
</div>
</section>
@@ -116,7 +187,7 @@
<!-- 无嘉宾数据 -->
<div v-else-if="speakersList.length === 0" class="no-speakers">
无演讲嘉宾信息
无演讲嘉宾数据
</div>
<!-- 嘉宾列表 -->
@@ -136,27 +207,33 @@
</div>
</section>
<!-- 往届回顾第2-4个会议 -->
<!-- 往届回顾当前会议之外的最近3个会议 -->
<section class="review-section" id="review" ref="reviewRef">
<div class="section-header">
<h2>往届回顾</h2>
<p>Previous Review</p>
<a href="javascript:void(0)" class="more-link">More</a>
<a href="javascript:void(0)" class="more-link" @click="goToAllMeetingsPage">More</a>
</div>
<div class="review-content">
<!-- 往届会议卡片 - 添加点击事件 -->
<div
class="review-card"
v-for="(meeting, index) in pastMeetings"
:key="meeting.id"
@click="viewMeetingDetails(meeting)"
>
<img :src="meeting.cover_url" :alt="meeting.theme" class="review-image">
<div class="review-info">
<h4>{{ meeting.theme }}</h4>
<p>{{meeting.subtitle}}}</p>
<div class="review-desc" >{{ formatMeetingTime(meeting.start_time, meeting.end_time) }}</div>
<p>{{ meeting.subtitle }}</p>
<div class="review-desc">{{ formatMeetingTime(meeting.start_time, meeting.end_time) }}</div>
</div>
</div>
<!-- 无往届会议时显示提示 -->
<div v-if="pastMeetings.length === 0" class="no-past-meetings">
暂无更多往届会议数据
</div>
</div>
</section>
@@ -181,30 +258,67 @@
</div>
</div>
</el-dialog>
<!-- 图片放大查看弹窗 -->
<el-dialog
v-model="imageDialogVisible"
title="会议日程详情"
width="90%"
:before-close="handleImageDialogClose"
:modal="true"
:close-on-click-modal="true"
>
<div class="enlarged-image-container">
<img
:src="enlargedImageUrl"
alt="会议日程详情"
class="enlarged-image"
:style="{ maxHeight: `calc(100vh - 150px)` }"
>
</div>
</el-dialog>
</template>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue'
import { ElAnchor, ElAnchorLink, ElDialog, ElLoading, ElButton } from 'element-plus'
import { ref, onMounted, computed, watch } from 'vue'
import { ElAnchor, ElAnchorLink, ElDialog, ElLoading, ElButton, ElPagination, ElInput } from 'element-plus'
import axios from 'axios'
// 页面状态控制
const isAllMeetingsPage = ref(false) // 控制显示会议列表页还是主页面
// 基础配置 - 固定baseurl
const baseUrl = 'http://localhost:8080/api'
// 顶部横幅封面图URL
const pageImageUrl = ref('')
// 会议数据状态
const meetings = ref([])
const currentMeeting = ref(null) // 当前(最新)会议
const pastMeetings = ref([]) // 往届会议(第2-4个)
const meetings = ref([]) // 所有会议数据(用于筛选往届会议)
const currentMeeting = ref(null) // 当前(正在展示的)会议
const pastMeetings = ref([]) // 往届会议(当前会议之外的最近3个)
const loading = ref(true) // 全局加载状态
const error = ref(null) // 全局错误信息
// 所有会议列表(分页)状态
const allMeetingsList = ref([]) // 所有会议列表数据
const allMeetingsLoading = ref(false) // 加载状态
const allMeetingsError = ref(null) // 错误信息
const currentPage = ref(1) // 当前页码
const pageSize = ref(10) // 每页条数
const totalMeetings = ref(0) // 总会议数
// 演讲嘉宾状态
const speakersList = ref([]) // 嘉宾列表
const speakersLoading = ref(false) // 嘉宾加载状态
const speakersError = ref(null) // 嘉宾加载错误
// 图片放大查看相关状态
const imageDialogVisible = ref(false) // 图片弹窗显示控制
const enlargedImageUrl = ref('') // 放大查看的图片URL
// 元素引用
const conferenceRef = ref<HTMLElement>()
const reportRef = ref<HTMLElement>()
@@ -225,9 +339,24 @@ const currentSpeaker = ref({
is_delete: 0
})
// 头部背景样式(使用当前会议封面图
// 从后端获取封面图
const fetchCoverImage = async () => {
try {
const response = await axios.post('http://localhost:8080/api/page-image/get', { page: 'AcademicExchange' });
if (response.data.message === '查询成功' && response.data.images && response.data.images.length > 0) {
pageImageUrl.value = response.data.images[0].image_url;
}
console.log("获取封面成功:", response.data);
} catch (error) {
console.log('封面图加载失败,使用默认图', error);
}
};
// 头部背景样式(优先使用从接口获取的封面图,其次使用当前会议封面图,最后使用默认图)
const headerBackgroundStyle = computed(() => ({
backgroundImage: currentMeeting.value
backgroundImage: pageImageUrl.value
? `url(${pageImageUrl.value})`
: currentMeeting.value && currentMeeting.value.cover_url
? `url(${currentMeeting.value.cover_url})`
: 'url(https://picsum.photos/id/1059/1920/600)'
}))
@@ -260,15 +389,18 @@ const formatMeetingTime = (startTime: string, endTime: string) => {
return `${start.toLocaleString('zh-CN')} - ${end.toLocaleString('zh-CN')}`
}
// 获取会议列表
// 获取所有会议数据(用于首页和筛选往届会议)
const fetchMeetings = async () => {
try {
loading.value = true
error.value = null
// 先获取封面图
await fetchCoverImage()
const response = await axios.post(`${baseUrl}/meetings/list`, {
page: 1,
page_size: 10
page_size: 100 // 加载足够多的会议数据确保能筛选出最近3个往届会议
})
if (response.data.message === '查询成功' && response.data.meetings && response.data.meetings.length > 0) {
@@ -277,9 +409,23 @@ const fetchMeetings = async () => {
(a, b) => new Date(b.create_time).getTime() - new Date(a.create_time).getTime()
)
meetings.value = sortedMeetings
currentMeeting.value = sortedMeetings[0] // 最新会议
pastMeetings.value = sortedMeetings.slice(1, 4) // 第2-4个会议
meetings.value = sortedMeetings // 保存所有会议数据
// 如果当前没有选中的会议,默认选中最新的会议
if (!currentMeeting.value) {
currentMeeting.value = sortedMeetings[0]
} else {
// 如果已有选中的会议,确保它是最新的或保持选中状态
const existingMeeting = sortedMeetings.find(item => item.id === currentMeeting.value.id)
if (existingMeeting) {
currentMeeting.value = existingMeeting
} else {
currentMeeting.value = sortedMeetings[0]
}
}
// 筛选往届会议排除当前会议取剩下的最近3个
filterPastMeetings()
// 自动获取当前会议的嘉宾
if (currentMeeting.value.id) {
@@ -296,6 +442,55 @@ const fetchMeetings = async () => {
}
}
// 筛选往届会议排除当前会议取剩下的最近3个
const filterPastMeetings = () => {
if (!meetings.value.length || !currentMeeting.value) {
pastMeetings.value = []
return
}
// 过滤掉当前会议然后取前3个按创建时间排序后的
const filtered = meetings.value.filter(meeting => meeting.id !== currentMeeting.value.id)
pastMeetings.value = filtered.slice(0, 3) // 取最近的3个
}
// 监听currentMeeting变化重新筛选往届会议
watch(currentMeeting, () => {
filterPastMeetings()
})
// 获取所有会议(分页)
const fetchAllMeetings = async () => {
try {
allMeetingsLoading.value = true
allMeetingsError.value = null
const params = {
page: currentPage.value,
page_size: pageSize.value
}
const response = await axios.post(`${baseUrl}/meetings/list`, params)
if (response.data.message === '查询成功') {
// 按创建时间排序(最新在前)
const sortedMeetings = [...response.data.meetings].sort(
(a, b) => new Date(b.create_time).getTime() - new Date(a.create_time).getTime()
)
allMeetingsList.value = sortedMeetings
totalMeetings.value = response.data.total || sortedMeetings.length
} else {
allMeetingsError.value = '未获取到会议数据'
}
} catch (err) {
console.error('获取所有会议失败:', err)
allMeetingsError.value = '获取会议列表失败,请重试'
} finally {
allMeetingsLoading.value = false
}
}
// 根据会议ID获取演讲嘉宾
const fetchSpeakers = async (meetingId: number) => {
try {
@@ -314,7 +509,8 @@ const fetchSpeakers = async (meetingId: number) => {
// 按sort字段排序升序
speakersList.value = [...response.data.speakers].sort((a, b) => a.sort - b.sort)
} else {
speakersError.value = '未获取到演讲嘉宾数据'
// 接口调用成功但无数据时,不设置错误信息,由空列表状态处理
speakersList.value = []
}
} catch (err) {
console.error('获取演讲嘉宾失败:', err)
@@ -357,11 +553,71 @@ const showSpeakerInfo = (speaker: any) => {
dialogVisible.value = true
}
// 关闭弹窗
// 关闭嘉宾详情弹窗
const handleClose = () => {
dialogVisible.value = false
}
// 显示放大的图片
const showEnlargedImage = (imageUrl: string) => {
if (imageUrl) {
enlargedImageUrl.value = imageUrl
imageDialogVisible.value = true
}
}
// 关闭图片放大弹窗
const handleImageDialogClose = () => {
imageDialogVisible.value = false
enlargedImageUrl.value = ''
}
// 跳转到所有会议列表页面
const goToAllMeetingsPage = async () => {
isAllMeetingsPage.value = true
// 初始化页码
currentPage.value = 1
// 加载第一页数据
await fetchAllMeetings()
// 滚动到页面顶部
window.scrollTo(0, 0)
}
// 返回会议首页
const navigateBack = () => {
isAllMeetingsPage.value = false
// 滚动到页面顶部
window.scrollTo(0, 0)
}
// 查看会议详情 - 复用此方法处理所有会议卡片的点击
const viewMeetingDetails = (meeting: any) => {
// 切换到主页面并显示选中的会议
currentMeeting.value = meeting
isAllMeetingsPage.value = false
// 加载该会议的嘉宾
if (meeting.id) {
fetchSpeakers(meeting.id)
}
// 重新获取封面图(确保显示正确的封面)
fetchCoverImage()
// 滚动到页面顶部
window.scrollTo(0, 0)
}
// 分页大小改变
const handlePageSizeChange = async (size: number) => {
pageSize.value = size
currentPage.value = 1 // 重置为第一页
await fetchAllMeetings()
}
// 当前页码改变
const handleCurrentPageChange = async (page: number) => {
currentPage.value = page
await fetchAllMeetings()
}
// 初始化加载会议数据
onMounted(() => {
fetchMeetings()
@@ -375,6 +631,151 @@ onMounted(() => {
color: #333;
line-height: 1.6;
position: relative;
max-width: 1400px;
margin: 0 auto;
/* 移除默认padding避免影响全屏横幅 */
}
/* 页面切换相关样式 */
.page-header {
padding: 30px 20px;
border-bottom: 1px solid #eee;
margin-bottom: 30px;
}
.back-button {
margin-bottom: 15px;
}
.page-title {
margin: 0;
color: #2c3e50;
font-size: 2rem;
}
/* 会议列表样式 */
.all-meetings-container {
padding: 10px 20px;
}
.meeting-item {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-bottom: 30px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
}
.meeting-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.meeting-header {
background: #f8f9fa;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
}
.meeting-theme {
margin: 0;
color: #2c3e50;
font-size: 1.5rem;
}
.meeting-date {
color: #409eff;
font-weight: 500;
}
.meeting-content {
display: grid;
grid-template-columns: 300px 1fr;
gap: 30px;
padding: 30px;
}
.meeting-image {
border-radius: 8px;
overflow: hidden;
}
.meeting-img {
width: 100%;
height: 200px;
object-fit: cover;
}
.meeting-details {
display: flex;
flex-direction: column;
gap: 15px;
}
.meeting-subtitle {
margin: 0;
color: #34495e;
font-size: 1.2rem;
font-weight: 500;
}
.meeting-intro {
line-height: 1.8;
color: #555;
}
.meeting-intro p {
margin: 0 0 10px;
}
.meeting-meta {
margin-top: 10px;
padding-top: 15px;
border-top: 1px dashed #eee;
}
.meeting-meta .meta-item {
display: inline-flex;
align-items: center;
gap: 8px;
}
/* 修改创建时间样式 - 移除阴影和灰白效果 */
.meeting-meta .meta-label {
color: #333; /* 改为深色,移除灰白效果 */
font-size: 0.9rem;
text-shadow: none; /* 移除阴影 */
font-weight: 500; /* 稍微加粗,提高可见度 */
}
.meeting-meta .meta-value {
color: #333; /* 改为深色,移除灰白效果 */
font-size: 0.95rem;
text-shadow: none; /* 移除阴影 */
}
.all-meetings-pagination {
margin: 40px 20px 60px;
text-align: right;
}
/* 会议列表状态样式 */
.all-meetings-loading, .all-meetings-error, .no-all-meetings {
text-align: center;
padding: 80px 20px;
}
.all-meetings-error {
color: #f56c6c;
}
.no-all-meetings {
color: #909399;
font-size: 1.2rem;
}
/* 错误提示样式 */
@@ -438,7 +839,7 @@ onMounted(() => {
font-weight: 600;
}
/* 头部横幅样式 */
/* 头部横幅样式 - 设置为屏幕宽度 */
.header-banner {
background-size: cover;
background-position: center;
@@ -449,6 +850,9 @@ onMounted(() => {
min-height: 400px;
display: flex;
align-items: center;
width: 100vw; /* 设置宽度为视口宽度 */
margin-left: calc(-50vw + 50%); /* 使元素居中并充满屏幕宽度 */
left: 0;
}
.banner-mask {
@@ -457,7 +861,7 @@ onMounted(() => {
left: 0;
width: 100%;
height: 100%;
background-color: transparent;
background-color: rgba(0, 0, 0, 0.4);
z-index: 1;
}
@@ -467,6 +871,7 @@ onMounted(() => {
padding: 0 20px;
position: relative;
z-index: 2;
width: 100%; /* 确保内容区域在横幅内正确显示 */
}
.hdic-logo {
@@ -491,18 +896,19 @@ onMounted(() => {
gap: 25px;
}
.meta-item {
/* 注意这里只修改会议列表中的创建时间样式不影响头部横幅的meta样式 */
.conference-meta .meta-item {
display: flex;
flex-direction: column;
}
.meta-label {
.conference-meta .meta-label {
font-size: 0.9rem;
margin-bottom: 5px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
}
.meta-value {
.conference-meta .meta-value {
font-size: 1rem;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
@@ -616,8 +1022,8 @@ onMounted(() => {
height: auto;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
transition: transform 0.3s ease;
cursor: pointer;
transition: transform 0.3s ease, cursor 0.3s;
cursor: zoom-in; /* 显示放大镜图标,提示可点击放大 */
}
.schedule-image:hover {
@@ -711,22 +1117,30 @@ onMounted(() => {
margin-top: 40px;
}
/* 往届会议卡片样式 - 添加鼠标悬停效果提示可点击 */
.review-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
transition: transform 0.3s;
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer; /* 显示手型指针提示可点击 */
}
.review-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.15); /* 增强阴影效果 */
}
.review-image {
width: 100%;
height: 200px;
object-fit: cover;
transition: transform 0.3s;
}
.review-card:hover .review-image {
transform: scale(1.05); /* 图片轻微放大效果 */
}
.review-info {
@@ -739,24 +1153,25 @@ onMounted(() => {
font-size: 1.2rem;
}
.review-date {
color: #409eff;
font-size: 0.9rem;
.review-info p {
color: #7f8c8d;
margin: 0 0 15px;
font-style: italic;
font-size: 1rem;
}
.review-desc {
color: #666;
margin: 0;
line-height: 1.6;
max-height: 100px;
overflow: hidden;
}
.review-desc p {
margin: 0;
font-size: 0.95rem;
/* 无往届会议提示 */
.no-past-meetings {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
color: #909399;
font-size: 1.2rem;
}
/* 嘉宾详情弹窗样式 */
@@ -809,11 +1224,29 @@ onMounted(() => {
text-align: justify;
}
/* 图片放大查看样式 */
.enlarged-image-container {
display: flex;
justify-content: center;
align-items: center;
padding: 10px 0;
}
.enlarged-image {
max-width: 100%;
height: auto;
object-fit: contain;
transition: all 0.3s ease;
}
/* 响应式适配 */
@media (max-width: 1200px) {
.speakers-grid {
grid-template-columns: repeat(3, 1fr);
}
.meeting-content {
grid-template-columns: 250px 1fr;
}
}
@media (max-width: 1024px) {
@@ -823,6 +1256,12 @@ onMounted(() => {
.speakers-grid {
grid-template-columns: repeat(2, 1fr);
}
.meeting-content {
grid-template-columns: 1fr;
}
.meeting-img {
height: 300px;
}
}
@media (max-width: 768px) {
@@ -855,5 +1294,16 @@ onMounted(() => {
flex-direction: column;
gap: 15px;
}
.review-content {
grid-template-columns: 1fr;
}
.meeting-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.meeting-img {
height: 200px;
}
}
</style>