Selaa lähdekoodia

产品和案例页面增加动画效果

gcz 1 viikko sitten
vanhempi
sitoutus
cd90689bf6
2 muutettua tiedostoa jossa 443 lisäystä ja 47 poistoa
  1. 253 28
      src/pages/cases/Index.vue
  2. 190 19
      src/pages/products/Index.vue

+ 253 - 28
src/pages/cases/Index.vue

@@ -2,23 +2,26 @@
   <DefaultLayout>
     <!-- banner -->
     <section class="banner-section">
-      <img src="@assets/cases-banner.png" alt="" srcset="">
+      <div class="banner-image animate-fade-in">
+        <img src="@assets/cases-banner.png" alt="" srcset="">
+      </div>
     </section>
 
     <!-- 智慧解决方案 -->
-    <section class="smart-solutions">
+    <section class="smart-solutions" ref="solutionsSection">
       <div class="container">
         <!-- 标题区域 -->
-        <div class="title-section">
+        <div class="title-section animate-fade-up" :class="{ 'animate-visible': isSolutionsVisible }">
           <h2 class="main-title">智慧解决方案</h2>
           <p class="sub-title">SMART SOLUTION</p>
           <p class="description">智慧管理系统采用先进的开发技术,为智慧产业提供一站式解决方案</p>
         </div>
 
         <!-- 解决方案列表 -->
-        <div class="solutions-grid">
-          <div v-for="solution in visibleSolutions" :key="solution.id" @click="goToCaseDetail(solution)"
-            class="solution-card">
+        <div class="solutions-grid animate-fade-up" :class="{ 'animate-visible': isSolutionsVisible }" style="animation-delay: 0.3s">
+          <div v-for="(solution, index) in visibleSolutions" :key="solution.id" @click="goToCaseDetail(solution)"
+            class="solution-card animate-slide-up" :class="{ 'animate-visible': isSolutionsVisible }"
+            :style="{ 'animation-delay': `${0.1 * (index % 6)}s` }">
             <div class="card-image">
               <img :src="solution.image" :alt="solution.title" />
             </div>
@@ -30,13 +33,13 @@
         </div>
 
         <!-- 切换按钮 -->
-        <div class="navigation-buttons">
-          <button class="nav-btn prev-btn" :class="{ disabled: currentPage === 0 }" @click="prevPage"
-            :disabled="currentPage === 0">
+        <div class="navigation-buttons animate-fade-up" :class="{ 'animate-visible': isSolutionsVisible }" style="animation-delay: 0.5s">
+          <button class="nav-btn prev-btn animate-bounce-in" :class="{ disabled: currentPage === 0, 'animate-visible': isSolutionsVisible }" @click="prevPage"
+            :disabled="currentPage === 0" style="animation-delay: 0.6s">
             <img src="@assets/slide-arrow-left.png" alt="上一页" />
           </button>
-          <button class="nav-btn next-btn" :class="{ disabled: currentPage >= maxPage }" @click="nextPage"
-            :disabled="currentPage >= maxPage">
+          <button class="nav-btn next-btn animate-bounce-in" :class="{ disabled: currentPage >= maxPage, 'animate-visible': isSolutionsVisible }" @click="nextPage"
+            :disabled="currentPage >= maxPage" style="animation-delay: 0.7s">
             <img src="@assets/slide-arrow-right.png" alt="下一页" />
           </button>
         </div>
@@ -44,18 +47,20 @@
     </section>
 
     <!-- 更多案例 -->
-    <section class="more-cases">
+    <section class="more-cases" ref="moreCasesSection">
       <div class="container">
         <!-- 标题区域 -->
-        <div class="title-section">
+        <div class="title-section animate-fade-up" :class="{ 'animate-visible': isMoreCasesVisible }">
           <h2 class="main-title">更多案例</h2>
           <p class="sub-title">MORE CASES</p>
         </div>
 
         <!-- 案例列表 -->
-        <div class="cases-list">
-          <div v-for="(caseItem, caseIndex) in casesList" :key="caseItem.id" class="case-item"
-            :class="{ 'fifth-row': Math.floor(caseIndex / 2) === 4 }" @click="goToCaseDetail(caseItem)">
+        <div class="cases-list animate-fade-up" :class="{ 'animate-visible': isMoreCasesVisible }" style="animation-delay: 0.3s">
+          <div v-for="(caseItem, caseIndex) in casesList" :key="caseItem.id" class="case-item animate-slide-right"
+            :class="{ 'fifth-row': Math.floor(caseIndex / 2) === 4, 'animate-visible': isMoreCasesVisible }" 
+            :style="{ 'animation-delay': `${0.05 * (caseIndex % 10)}s` }"
+            @click="goToCaseDetail(caseItem)">
             <div class="case-content">
               <h3 class="case-title">{{ caseItem.title }}</h3>
             </div>
@@ -67,7 +72,7 @@
 </template>
 
 <script>
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, nextTick } from 'vue'
 import { getProjectClassifyList, getProjectList } from '@/api/modules/home'
 import { useRouter } from 'vue-router'
 import DefaultLayout from '@/layouts/DefaultLayout.vue'
@@ -84,6 +89,14 @@ export default {
     const currentPage = ref(0)
     const itemsPerPage = 6 // 每页显示6个(2行 × 3列)
 
+    // 动画状态管理
+    const isSolutionsVisible = ref(false)
+    const isMoreCasesVisible = ref(false)
+
+    // 元素引用
+    const solutionsSection = ref(null)
+    const moreCasesSection = ref(null)
+
     // 解决方案数据
     let solutions = ref([
       {
@@ -238,8 +251,45 @@ export default {
       }
     }
 
+    // 设置滚动动画观察器
+    const setupScrollAnimations = () => {
+      const observerOptions = {
+        threshold: 0.2,
+        rootMargin: '0px 0px -50px 0px'
+      }
+
+      const observer = new IntersectionObserver((entries) => {
+        entries.forEach(entry => {
+          if (entry.isIntersecting) {
+            const target = entry.target
+            if (target.classList.contains('smart-solutions')) {
+              isSolutionsVisible.value = true
+            } else if (target.classList.contains('more-cases')) {
+              isMoreCasesVisible.value = true
+            }
+          }
+        })
+      }, observerOptions)
+
+      // 观察各个区域
+      nextTick(() => {
+        if (solutionsSection.value) observer.observe(solutionsSection.value)
+        if (moreCasesSection.value) observer.observe(moreCasesSection.value)
+      })
+    }
+
     onMounted(() => {
       loadProductsData()
+      
+      // 设置滚动动画
+      nextTick(() => {
+        setupScrollAnimations()
+      })
+
+      // 延迟显示第一个区域
+      setTimeout(() => {
+        isSolutionsVisible.value = true
+      }, 500)
     })
 
     return {
@@ -250,19 +300,132 @@ export default {
       prevPage,
       nextPage,
       casesList,
-      goToCaseDetail
+      goToCaseDetail,
+      // 动画状态
+      isSolutionsVisible,
+      isMoreCasesVisible,
+      // 元素引用
+      solutionsSection,
+      moreCasesSection
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
+// 动画定义
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes fadeUp {
+  from {
+    opacity: 0;
+    transform: translateY(40px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(30px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes slideRight {
+  from {
+    opacity: 0;
+    transform: translateX(-30px);
+  }
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+@keyframes bounceIn {
+  0% {
+    opacity: 0;
+    transform: scale(0.3);
+  }
+  50% {
+    transform: scale(1.05);
+  }
+  70% {
+    transform: scale(0.9);
+  }
+  100% {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+// 动画类
+.animate-fade-in {
+  animation: fadeIn 1s ease-out;
+}
+
+.animate-fade-up {
+  opacity: 0;
+  transform: translateY(40px);
+  transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
+  
+  &.animate-visible {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.animate-slide-up {
+  opacity: 1;
+  transform: translateY(20px);
+  transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
+  
+  &.animate-visible {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.animate-slide-right {
+  opacity: 0;
+  transform: translateX(-30px);
+  transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
+  
+  &.animate-visible {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+.animate-bounce-in {
+  opacity: 0;
+  transform: scale(0.3);
+  transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+  
+  &.animate-visible {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
 // banner区域
 .banner-section {
   position: relative;
 
-  img {
-    width: 100%;
+  .banner-image {
+    img {
+      width: 100%;
+    }
   }
 }
 
@@ -322,31 +485,52 @@ export default {
     border-radius: 8px;
     overflow: hidden;
     box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
-    transition: transform 0.3s ease, box-shadow 0.3s ease;
+    transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+    cursor: pointer;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background: linear-gradient(45deg, rgba(24, 144, 255, 0.05), transparent);
+      opacity: 0;
+      transition: opacity 0.3s ease;
+    }
 
     &:hover {
-      transform: translateY(-5px);
-      box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
+      transform: translateY(-8px) scale(1.02);
+      box-shadow: 0 12px 35px rgba(24, 144, 255, 0.15);
+      
+      &::before {
+        opacity: 1;
+      }
     }
 
     .card-image {
       height: 282px;
       overflow: hidden;
+      position: relative;
 
       img {
         width: 100%;
         height: 100%;
         object-fit: contain;
-        transition: transform 0.3s ease;
+        transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
       }
     }
 
     &:hover .card-image img {
-      transform: scale(1.05);
+      transform: scale(1.08);
     }
 
     .card-content {
       padding: 20px;
+      position: relative;
+      z-index: 2;
 
       .card-title {
         font-size: 24px;
@@ -354,6 +538,7 @@ export default {
         color: #333;
         margin-bottom: 10px;
         text-align: center;
+        transition: color 0.3s ease;
       }
 
       .card-description {
@@ -361,6 +546,17 @@ export default {
         color: #666;
         line-height: 1.6;
         text-align: center;
+        transition: color 0.3s ease;
+      }
+    }
+
+    &:hover .card-content {
+      .card-title {
+        color: #1890ff;
+      }
+      
+      .card-description {
+        color: #333;
       }
     }
   }
@@ -381,11 +577,33 @@ export default {
       align-items: center;
       justify-content: center;
       box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
-      transition: all 0.3s ease;
+      transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+      position: relative;
+      overflow: hidden;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: linear-gradient(45deg, rgba(24, 144, 255, 0.1), transparent);
+        opacity: 0;
+        transition: opacity 0.3s ease;
+      }
 
       &:hover:not(.disabled) {
-        transform: translateY(-2px);
-        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+        transform: translateY(-3px) scale(1.05);
+        box-shadow: 0 6px 20px rgba(24, 144, 255, 0.2);
+        
+        &::before {
+          opacity: 1;
+        }
+      }
+
+      &:active:not(.disabled) {
+        transform: translateY(-1px) scale(1.02);
       }
 
       &.disabled {
@@ -396,6 +614,13 @@ export default {
       img {
         width: 20px;
         height: 20px;
+        transition: transform 0.3s ease;
+        position: relative;
+        z-index: 2;
+      }
+
+      &:hover:not(.disabled) img {
+        transform: scale(1.1);
       }
     }
   }

+ 190 - 19
src/pages/products/Index.vue

@@ -2,17 +2,22 @@
   <DefaultLayout>
     <!-- banner -->
     <section class="banner-section">
-      <img src="@assets/products-banner.png" alt="" srcset="">
+      <div class="banner-image animate-fade-in">
+        <img src="@assets/products-banner.png" alt="" srcset="">
+      </div>
     </section>
     <!-- skills section -->
-    <section class="skills-section">
+    <section class="skills-section" ref="skillsSection">
       <!-- 白色背景的标题和菜单部分 -->
       <div class="skills-header">
         <div class="container">
-          <h2 class="section-title">为企业打造了一个全面的数字化基础设施</h2>
-          <div class="platform-tabs">
-            <div v-for="(platform, index) in platforms" :key="index" class="tab-item"
-              :class="{ active: activePlatform === index }" @click="switchPlatform(index)">
+          <h2 class="section-title animate-fade-up" :class="{ 'animate-visible': isSkillsVisible }">为企业打造了一个全面的数字化基础设施
+          </h2>
+          <div class="platform-tabs animate-fade-up" :class="{ 'animate-visible': isSkillsVisible }"
+            style="animation-delay: 0.2s">
+            <div v-for="(platform, index) in platforms" :key="index" class="tab-item animate-scale-in"
+              :class="{ active: activePlatform === index, 'animate-visible': isSkillsVisible }"
+              :style="{ 'animation-delay': `${0.1 * index}s` }" @click="switchPlatform(index)">
               {{ platform.name }}
               <img v-if="activePlatform === index" src="@assets/products-select.png" alt="" class="select-indicator">
             </div>
@@ -23,13 +28,15 @@
       <!-- 内容部分 -->
       <div class="skills-content">
         <div class="container">
-          <div class="content-wrapper" v-if="platforms[activePlatform]">
+          <div class="content-wrapper animate-fade-up" :class="{ 'animate-visible': isSkillsVisible }"
+            style="animation-delay: 0.4s" v-if="platforms[activePlatform]">
             <h3 class="content-title">{{ platforms[activePlatform].title }}</h3>
             <img src="@assets/products-select.png" alt="" class="title-indicator">
             <p class="content-subtitle">{{ platforms[activePlatform].subtitle }}</p>
 
             <!-- 图片轮播 -->
-            <div class="carousel-container" v-if="platforms[activePlatform] && platforms[activePlatform].images">
+            <div class="carousel-container animate-scale-in" :class="{ 'animate-visible': isSkillsVisible }"
+              style="animation-delay: 0.6s" v-if="platforms[activePlatform] && platforms[activePlatform].images">
               <div class="carousel-wrapper" @mouseenter="pauseAutoPlay" @mouseleave="resumeAutoPlay">
                 <div class="carousel-track" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">
                   <div v-for="(image, index) in platforms[activePlatform].images" :key="index" class="carousel-slide">
@@ -50,13 +57,16 @@
     </section>
 
     <!-- 产品优势 section -->
-    <section class="advantages-section">
+    <section class="advantages-section" ref="advantagesSection">
       <div class="container">
-        <h2 class="section-title">产品优势</h2>
-        <img src="@assets/products-select.png" alt="" class="title-indicator">
-
-        <div class="advantages-grid">
-          <div v-for="(advantage, index) in advantages" :key="index" class="advantage-item">
+        <h2 class="section-title animate-fade-up" :class="{ 'animate-visible': isAdvantagesVisible }">产品优势</h2>
+        <img src="@assets/products-select.png" alt="" class="title-indicator animate-scale-in"
+          :class="{ 'animate-visible': isAdvantagesVisible }" style="animation-delay: 0.2s">
+
+        <div class="advantages-grid animate-fade-up" :class="{ 'animate-visible': isAdvantagesVisible }"
+          style="animation-delay: 0.3s">
+          <div v-for="(advantage, index) in advantages" :key="index" class="advantage-item animate-slide-up"
+            :class="{ 'animate-visible': isAdvantagesVisible }" :style="{ 'animation-delay': `${0.1 * (index % 3)}s` }">
             <div class="advantage-content">
               <h3 class="advantage-title">{{ advantage.title }}</h3>
               <p class="advantage-description">{{ advantage.description }}</p>
@@ -72,7 +82,7 @@
 </template>
 
 <script>
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
 import DefaultLayout from '@/layouts/DefaultLayout.vue'
 import { getProjectClassifyList, getProjectList } from '@/api/modules/home'
 import productsBg01 from '@/assets/products-bg01.png'
@@ -98,6 +108,14 @@ export default {
     const currentSlide = ref(0)
     let autoPlayTimer = null
 
+    // 动画状态管理
+    const isSkillsVisible = ref(false)
+    const isAdvantagesVisible = ref(false)
+
+    // 元素引用
+    const skillsSection = ref(null)
+    const advantagesSection = ref(null)
+
     // 平台数据 - 从API获取
     const platforms = ref([])
     // 产品列表数据
@@ -252,9 +270,46 @@ export default {
       }
     }
 
+    // 设置滚动动画观察器
+    const setupScrollAnimations = () => {
+      const observerOptions = {
+        threshold: 0.2,
+        rootMargin: '0px 0px -50px 0px'
+      }
+
+      const observer = new IntersectionObserver((entries) => {
+        entries.forEach(entry => {
+          if (entry.isIntersecting) {
+            const target = entry.target
+            if (target.classList.contains('skills-section')) {
+              isSkillsVisible.value = true
+            } else if (target.classList.contains('advantages-section')) {
+              isAdvantagesVisible.value = true
+            }
+          }
+        })
+      }, observerOptions)
+
+      // 观察各个区域
+      nextTick(() => {
+        if (skillsSection.value) observer.observe(skillsSection.value)
+        if (advantagesSection.value) observer.observe(advantagesSection.value)
+      })
+    }
+
     onMounted(() => {
       handleGetProjectClassifyList1() // 获取产品分类列表
       startAutoPlay()
+
+      // 设置滚动动画
+      nextTick(() => {
+        setupScrollAnimations()
+      })
+
+      // 延迟显示第一个区域
+      setTimeout(() => {
+        isSkillsVisible.value = true
+      }, 500)
     })
 
     onUnmounted(() => {
@@ -273,19 +328,112 @@ export default {
       goToSlide,
       pauseAutoPlay,
       resumeAutoPlay,
-      imgHost
+      imgHost,
+      // 动画状态
+      isSkillsVisible,
+      isAdvantagesVisible,
+      // 元素引用
+      skillsSection,
+      advantagesSection
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
+// 动画定义
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes fadeUp {
+  from {
+    opacity: 0;
+    transform: translateY(40px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(30px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes scaleIn {
+  from {
+    opacity: 0;
+    transform: scale(0.8);
+  }
+
+  to {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+// 动画类
+.animate-fade-in {
+  animation: fadeIn 1s ease-out;
+}
+
+.animate-fade-up {
+  opacity: 0;
+  transform: translateY(40px);
+  transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
+
+  &.animate-visible {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.animate-slide-up {
+  opacity: 1;
+  transform: translateY(20px);
+  transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
+
+  &.animate-visible {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.animate-scale-in {
+  opacity: 1;
+  transform: scale(0.95);
+  transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
+
+  &.animate-visible {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
 // banner区域
 .banner-section {
   position: relative;
 
-  img {
-    width: 100%;
+  .banner-image {
+    img {
+      width: 100%;
+    }
   }
 }
 
@@ -317,15 +465,38 @@ export default {
         color: #666;
         cursor: pointer;
         padding: 10px 0;
-        transition: all 0.3s ease;
+        transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+        overflow: hidden;
+
+        &::before {
+          content: '';
+          position: absolute;
+          bottom: 0;
+          left: 50%;
+          width: 0;
+          height: 2px;
+          background: linear-gradient(90deg, #1890ff, #40a9ff);
+          transform: translateX(-50%);
+          transition: width 0.4s ease;
+        }
 
         &:hover {
           color: #1890ff;
+          transform: translateY(-2px);
+
+          &::before {
+            width: 100%;
+          }
         }
 
         &.active {
           color: #1890ff;
           font-weight: 600;
+          transform: translateY(-2px);
+
+          &::before {
+            width: 100%;
+          }
         }
 
         .select-indicator {