|
@@ -2,7 +2,25 @@
|
|
|
<div class="project-cases">
|
|
<div class="project-cases">
|
|
|
<PageHeader />
|
|
<PageHeader />
|
|
|
|
|
|
|
|
- <main class="main-content">
|
|
|
|
|
|
|
+ <!-- 加载状态 -->
|
|
|
|
|
+ <div v-if="loading" class="loading-container">
|
|
|
|
|
+ <el-loading-spinner class="loading-spinner"></el-loading-spinner>
|
|
|
|
|
+ <p class="loading-text">正在加载项目数据...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 错误状态 -->
|
|
|
|
|
+ <div v-else-if="error" class="error-container">
|
|
|
|
|
+ <el-icon class="error-icon"><Warning /></el-icon>
|
|
|
|
|
+ <h3 class="error-title">加载失败</h3>
|
|
|
|
|
+ <p class="error-message">{{ error }}</p>
|
|
|
|
|
+ <el-button type="primary" @click="loadProjectData">
|
|
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
|
|
+ 重新加载
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 项目内容 -->
|
|
|
|
|
+ <main v-else-if="currentProject" class="main-content">
|
|
|
<div class="case-content">
|
|
<div class="case-content">
|
|
|
<!-- 项目封面区域 -->
|
|
<!-- 项目封面区域 -->
|
|
|
<section class="project-header">
|
|
<section class="project-header">
|
|
@@ -12,7 +30,14 @@
|
|
|
:alt="currentProject.title"
|
|
:alt="currentProject.title"
|
|
|
class="project-cover-image"
|
|
class="project-cover-image"
|
|
|
loading="lazy"
|
|
loading="lazy"
|
|
|
|
|
+ decoding="async"
|
|
|
|
|
+ @load="handleImageLoad"
|
|
|
|
|
+ @error="handleImageError"
|
|
|
>
|
|
>
|
|
|
|
|
+ <div v-if="!currentProject.coverImage" class="no-image-placeholder">
|
|
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
|
|
+ <p>暂无封面图片</p>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="project-info">
|
|
<div class="project-info">
|
|
|
<h1 class="project-title">{{ currentProject.title }}</h1>
|
|
<h1 class="project-title">{{ currentProject.title }}</h1>
|
|
@@ -155,7 +180,7 @@
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
<!-- 应用成效 -->
|
|
<!-- 应用成效 -->
|
|
|
- <section class="application-effects">
|
|
|
|
|
|
|
+ <section v-if="currentProject.applicationEffects && currentProject.applicationEffects.length > 0" class="application-effects">
|
|
|
<h2 class="section-title">应用成效</h2>
|
|
<h2 class="section-title">应用成效</h2>
|
|
|
<div class="news-list-container">
|
|
<div class="news-list-container">
|
|
|
<div
|
|
<div
|
|
@@ -461,12 +486,16 @@
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import PageHeader from './PageHeader.vue'
|
|
import PageHeader from './PageHeader.vue'
|
|
|
-import { ArrowLeft, ArrowRight, Calendar, Location, User, Star, VideoPlay, Picture, Document, EditPen, DataAnalysis, Reading, FolderOpened, Collection, Download, Check, ArrowDown } from '@element-plus/icons-vue'
|
|
|
|
|
-import { ref, onMounted, computed } from 'vue'
|
|
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
|
|
|
|
+import { ArrowLeft, ArrowRight, Calendar, Location, User, Star, VideoPlay, Picture, Document, EditPen, DataAnalysis, Reading, FolderOpened, Collection, Download, Check, ArrowDown, Warning, Refresh } from '@element-plus/icons-vue'
|
|
|
|
|
+import { ref, onMounted, computed, watch } from 'vue'
|
|
|
|
|
+import { ElMessage, ElLoading } from 'element-plus'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
import { allProjects, getProjectById } from '@/data/projects'
|
|
import { allProjects, getProjectById } from '@/data/projects'
|
|
|
|
|
|
|
|
|
|
+// 项目数据加载状态
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const error = ref(null)
|
|
|
|
|
+
|
|
|
// 视频播放状态
|
|
// 视频播放状态
|
|
|
const videoPlaying = ref(false)
|
|
const videoPlaying = ref(false)
|
|
|
const viewerVideoPlaying = ref(false)
|
|
const viewerVideoPlaying = ref(false)
|
|
@@ -607,25 +636,12 @@ const currentWeixinLink = ref('')
|
|
|
|
|
|
|
|
// 打开新闻详情弹窗
|
|
// 打开新闻详情弹窗
|
|
|
const openNewsDetail = (news) => {
|
|
const openNewsDetail = (news) => {
|
|
|
- // 判断是否是需要打开微信链接的新闻
|
|
|
|
|
- if (news.title.includes('抗咸保供水专项行动')) {
|
|
|
|
|
- // 微信公众号链接不允许嵌入iframe,直接在新窗口打开
|
|
|
|
|
- window.open('https://mp.weixin.qq.com/s/VQnFDQXvzJPY-YPhimapGg', '_blank')
|
|
|
|
|
- } else if (news.title.includes('水利部太湖流域管理局组织开展2024年太湖流域防洪调度演练')) {
|
|
|
|
|
- // 打开2024年防汛演练的微信链接
|
|
|
|
|
- window.open('https://mp.weixin.qq.com/s/cqVi5NhpI0UtamGdRWxysg', '_blank')
|
|
|
|
|
- } else if (news.title.includes('李国英调研数字孪生太湖建设工作')) {
|
|
|
|
|
- // 打开李国英调研数字孪生太湖建设工作的微信链接
|
|
|
|
|
- window.open('https://mp.weixin.qq.com/s/DoYywW9hos4zd3ZipDzwPA', '_blank')
|
|
|
|
|
- } else if (news.title.includes('水利部组织开展2023年太湖流域防洪调度演练')) {
|
|
|
|
|
- // 打开2023年防汛演练的新闻详情
|
|
|
|
|
- currentNews.value = news
|
|
|
|
|
- newsDetailVisible.value = true
|
|
|
|
|
- } else if (news.title.includes('水利部太湖流域管理局组织开展2025年太湖流域防洪调度演练')) {
|
|
|
|
|
- // 打开2025年防汛演练的新闻详情
|
|
|
|
|
- currentNews.value = news
|
|
|
|
|
- newsDetailVisible.value = true
|
|
|
|
|
|
|
+ // 判断是否有链接,有链接则跳转,否则显示详情
|
|
|
|
|
+ if (news.link) {
|
|
|
|
|
+ // 有链接则在新窗口打开
|
|
|
|
|
+ window.open(news.link, '_blank')
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ // 无链接则显示详情弹窗
|
|
|
currentNews.value = news
|
|
currentNews.value = news
|
|
|
newsDetailVisible.value = true
|
|
newsDetailVisible.value = true
|
|
|
}
|
|
}
|
|
@@ -676,18 +692,97 @@ const nextMedia = () => {
|
|
|
const route = useRoute()
|
|
const route = useRoute()
|
|
|
const router = useRouter()
|
|
const router = useRouter()
|
|
|
|
|
|
|
|
|
|
+// 项目数据验证和默认值设置
|
|
|
|
|
+const validateProjectData = (project) => {
|
|
|
|
|
+ if (!project) return null
|
|
|
|
|
+
|
|
|
|
|
+ // 确保所有必需字段存在
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...project,
|
|
|
|
|
+ title: project.title || '未命名项目',
|
|
|
|
|
+ subtitle: project.subtitle || '',
|
|
|
|
|
+ coverImage: project.coverImage || '',
|
|
|
|
|
+ date: project.date || '',
|
|
|
|
|
+ location: project.location || '',
|
|
|
|
|
+ client: project.client || '',
|
|
|
|
|
+ tags: project.tags || [],
|
|
|
|
|
+ overview: project.overview || '暂无项目概览',
|
|
|
|
|
+ materials: project.materials || [],
|
|
|
|
|
+ technicalHighlights: project.technicalHighlights || {},
|
|
|
|
|
+ achievements: project.achievements || [],
|
|
|
|
|
+ caseStudies: project.caseStudies || [],
|
|
|
|
|
+ applicationEffects: project.applicationEffects || []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 当前项目数据
|
|
// 当前项目数据
|
|
|
-const currentProject = ref(allProjects[0])
|
|
|
|
|
|
|
+const currentProject = ref(null)
|
|
|
|
|
|
|
|
// 页面加载时获取项目数据
|
|
// 页面加载时获取项目数据
|
|
|
-onMounted(() => {
|
|
|
|
|
- const projectId = route.params.projectId || 'tai-pu-river'
|
|
|
|
|
- const project = getProjectById(projectId)
|
|
|
|
|
- if (project) {
|
|
|
|
|
- currentProject.value = project
|
|
|
|
|
|
|
+const loadProjectData = async () => {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ error.value = null
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const projectId = route.params.projectId || 'tai-pu-river'
|
|
|
|
|
+ const project = getProjectById(projectId)
|
|
|
|
|
+
|
|
|
|
|
+ if (project) {
|
|
|
|
|
+ // 验证项目数据
|
|
|
|
|
+ currentProject.value = validateProjectData(project)
|
|
|
|
|
+ ElMessage.success(`成功加载项目: ${project.title}`)
|
|
|
|
|
+
|
|
|
|
|
+ // 默认选中第一个技术亮点
|
|
|
|
|
+ const highlights = Object.values(currentProject.value.technicalHighlights)
|
|
|
|
|
+ if (highlights.length > 0) {
|
|
|
|
|
+ activeHighlight.value = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error(`未找到项目ID为 ${projectId} 的数据`)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('项目数据加载失败:', err)
|
|
|
|
|
+ error.value = err.message
|
|
|
|
|
+ ElMessage.error('项目数据加载失败,请稍后重试')
|
|
|
|
|
+
|
|
|
|
|
+ // 加载默认项目数据
|
|
|
|
|
+ currentProject.value = validateProjectData(allProjects[0])
|
|
|
|
|
+
|
|
|
|
|
+ // 默认选中第一个技术亮点
|
|
|
|
|
+ const highlights = Object.values(currentProject.value.technicalHighlights)
|
|
|
|
|
+ if (highlights.length > 0) {
|
|
|
|
|
+ activeHighlight.value = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 监听路由参数变化
|
|
|
|
|
+watch(() => route.params.projectId, () => {
|
|
|
|
|
+ loadProjectData()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 页面加载时获取数据
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadProjectData()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 图片加载成功处理
|
|
|
|
|
+const handleImageLoad = (event) => {
|
|
|
|
|
+ event.target.style.opacity = '1'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 图片加载失败处理
|
|
|
|
|
+const handleImageError = (event) => {
|
|
|
|
|
+ console.error('图片加载失败:', event.target.src)
|
|
|
|
|
+ event.target.style.display = 'none'
|
|
|
|
|
+ const placeholder = event.target.nextElementSibling
|
|
|
|
|
+ if (placeholder) {
|
|
|
|
|
+ placeholder.style.display = 'flex'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 获取案例图标
|
|
// 获取案例图标
|
|
|
const getCaseIcon = (index) => {
|
|
const getCaseIcon = (index) => {
|
|
|
const icons = ['📱', '💻', '📊', '🔍', '🚀', '🌐']
|
|
const icons = ['📱', '💻', '📊', '🔍', '🚀', '🌐']
|
|
@@ -732,6 +827,73 @@ const goToHome = () => {
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
|
|
+/* 加载状态样式 */
|
|
|
|
|
+.loading-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-height: 60vh;
|
|
|
|
|
+ gap: 1.5rem;
|
|
|
|
|
+ padding: 2rem;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-spinner {
|
|
|
|
|
+ width: 50px;
|
|
|
|
|
+ height: 50px;
|
|
|
|
|
+ border: 3px solid #f3f3f3;
|
|
|
|
|
+ border-top: 3px solid #326ee2;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes spin {
|
|
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-text {
|
|
|
|
|
+ font-size: 1.2rem;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 错误状态样式 */
|
|
|
|
|
+.error-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-height: 60vh;
|
|
|
|
|
+ gap: 1.5rem;
|
|
|
|
|
+ padding: 2rem;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ margin: 2rem;
|
|
|
|
|
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.error-icon {
|
|
|
|
|
+ font-size: 4rem;
|
|
|
|
|
+ color: #e6a23c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.error-title {
|
|
|
|
|
+ font-size: 1.8rem;
|
|
|
|
|
+ color: #f56c6c;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.error-message {
|
|
|
|
|
+ font-size: 1.1rem;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ max-width: 600px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.project-cases {
|
|
.project-cases {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
@@ -779,6 +941,30 @@ const goToHome = () => {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
object-fit: cover;
|
|
object-fit: cover;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.no-image-placeholder {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ display: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.no-image-placeholder el-icon {
|
|
|
|
|
+ font-size: 3rem;
|
|
|
|
|
+ margin-bottom: 1rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.no-image-placeholder p {
|
|
|
|
|
+ font-size: 1.1rem;
|
|
|
|
|
+ margin: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.project-info {
|
|
.project-info {
|