HomePage.vue 17.4 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
<!--
 * @Date: 2025-03-20 19:55:21
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2026-01-20 16:30:23
 * @FilePath: /mlaj/src/views/HomePage.vue
 * @Description: 美乐爱觉教育首页组件
 *
 * 主要功能模块:
 * 1. 用户欢迎区:显示用户信息和每日打卡
 * 2. 夏令营推广:展示特色夏令营活动
 * 3. 精选课程:轮播展示推荐课程
 * 4. 内容分类:推荐、直播、精选三个标签页
 * 5. 视频播放:支持在线视频播放和切换
 *
 * 状态管理:
 * - 用户认证状态:通过useAuth hook管理
 * - 打卡状态:包括选择的打卡类型和提交状态
 * - 轮播状态:当前轮播位置和滚动控制
 * - 视频播放状态:当前播放的视频索引
 *
 * 组件依赖:
 * - AppLayout:页面布局组件
 * - FrostedGlass:毛玻璃效果容器
 * - CourseCard:课程卡片组件
 * - LiveStreamCard:直播卡片组件
 * - ActivityCard:活动卡片组件
 * - SummerCampCard:夏令营卡片组件
 * - VideoPlayer:视频播放器组件
-->

<template>
  <AppLayout title="美乐爱觉教育">
    <div class="bg-gradient-to-b from-white via-green-50/10 to-blue-50/10">
      <!-- Header Section with Welcome & Weather -->
      <div v-if="currentUser" class="px-4 pt-3 pb-4">
        <FrostedGlass class="p-4 rounded-xl mb-4">
          <div class="flex justify-between items-center mb-3">
            <div class="flex items-center">
              <div class="w-10 h-10 rounded-full overflow-hidden mr-3">
                <img
                  :src="buildCdnImageUrl(currentUser?.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')"
                  class="w-full h-full object-cover"
                  @error="handleImageError" />
              </div>
              <div>
                <h2 class="text-xl font-bold">欢迎回来,{{ currentUser.name || '登录用户' }}!</h2>
                <p class="text-sm text-gray-500">{{ formatToday() }}</p>
              </div>
            </div>
            <!-- <div class="flex items-center text-sm">
              <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
              </svg>
              <span class="ml-1 font-medium">23°C</span>
              <span class="ml-1 text-gray-500">晴朗</span>
            </div> -->
          </div>

          <!-- User Stats -->
          <div class="flex justify-between text-center py-2">
            <div class=" border-gray-200 flex-1">
              <div class="text-lg font-bold flex items-baseline justify-center">
                <span>{{ currentUser?.total_days || 0 }}</span>
                <span class="text-xs ml-1 font-normal">天</span>
              </div>
              <div class="text-xs text-gray-500">累计打卡</div>
            </div>
            <div class="border-gray-200 flex-1">
              <div class="text-lg font-bold flex items-baseline justify-center">
                <span>{{ currentUser?.consecutive_days || 0 }}</span>
                <span class="text-xs ml-1 font-normal">天</span>
              </div>
              <div class="text-xs text-gray-500">连续打卡</div>
            </div>
            <div class="border-gray-200 flex-1">
              <div class="text-lg font-bold flex items-baseline justify-center">
                <span>{{ currentUser?.longest_consecutive_days || 0 }}</span>
                <span class="text-xs ml-1 font-normal">天</span>
              </div>
              <div class="text-xs text-gray-500">最长连续</div>
            </div>
          </div>
        </FrostedGlass>

        <!-- Daily Check-in -->
        <FrostedGlass class="p-4 rounded-xl">
          <div class="flex justify-between items-center mb-3">
            <h3 class="font-medium">今日打卡</h3>
            <router-link to="/profile" class="text-green-600 text-sm">打卡记录</router-link>
          </div>
          <template v-if="checkInTypes.length">
            <CheckInList :items="checkInTypes" dense scroll :plain="true" @submit-success="handleHomeCheckInSuccess" />
          </template>
          <template v-else>
            <div class="text-center">
              <p class="text-gray-500">暂无打卡任务</p>
            </div>
          </template>
        </FrostedGlass>
      </div>

      <!-- Summer Camp Promotion -->
      <!-- <div class="px-4 mb-6">
        <SummerCampCard :items="[
          {
            title: '大国少年-世界正东方',
            subtitle: '亲子夏令营',
            badge: '亲子夏令营',
            price: '¥1280',
            discount: '限时优惠',
            episodes: 16,
            subscribers: 1140,
            imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/summer-camp.jpg'
          },
          {
            title: '暑期特训营',
            subtitle: '提升学习能力',
            badge: '特训营',
            price: '¥1580',
            discount: '早鸟优惠',
            episodes: 20,
            subscribers: 980,
            imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/summer-camp-2.jpg'
          },
          {
            title: '艺术创想营',
            subtitle: '激发创造力',
            badge: '艺术营',
            price: '¥1380',
            discount: '新课优惠',
            episodes: 12,
            subscribers: 760,
            imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/summer-camp-3.jpg'
          }
        ]" />
      </div> -->

      <FeaturedCoursesSection />

      <!-- Custom Tab Navigation -->
      <!-- <div class="sticky top-0 bg-white/70 backdrop-blur-lg" style="z-index: 9;">
        <div class="px-4 border-b border-gray-200">
          <div class="flex space-x-6">
            <button
              v-for="tab in ['推荐', '直播']"
              :key="tab"
              @click="activeTab = tab"
              :class="[
                'pb-3 pt-3 px-1 font-medium',
                activeTab === tab
                  ? 'text-green-600 border-b-2 border-green-600'
                  : 'text-gray-500'
              ]"
            >
              {{ tab }}
              <span
                v-if="tab === '直播'"
                class="ml-1 px-1.5 py-0.5 bg-red-500 text-white text-xs rounded-full"
              >
                2
              </span>
            </button>
          </div>
        </div>
      </div> -->

      <!-- Content Based on Active Tab -->
      <div class="px-4 mt-5" ref="contentRef">
        <!-- Recommended Content -->
        <div v-if="activeTab === '推荐'">
          <RecommendationsSection />
          <LatestActivitiesSection />
          <HotCoursesSection />
        </div>

        <!-- Live Content -->
        <div v-if="activeTab === '直播'">
          <section>
            <div class="flex justify-between items-center mb-3">
              <h3 class="font-medium">正在直播</h3>
              <div class="text-xs text-red-500 flex items-center">
                <div class="w-2 h-2 bg-red-500 rounded-full mr-1 animate-pulse"></div>
                2个直播中
              </div>
            </div>
            <div class="grid grid-cols-2 gap-4 mb-7">
              <LiveStreamCard
                v-for="stream in liveStreams"
                :key="stream.id"
                :stream="stream"
              />
            </div>

            <div class="mb-5">
              <div class="flex justify-between items-center mb-3">
                <h3 class="font-medium">直播日历</h3>
                <router-link to="/live-calendar" class="text-xs text-blue-500">
                  查看日历
                </router-link>
              </div>
              <FrostedGlass class="p-3 rounded-xl">
                <div class="flex space-x-2 overflow-x-auto py-1">
                  <div
                    v-for="(day, i) in ['今天', '明天', '周三', '周四', '周五', '周六', '周日']"
                    :key="day"
                    :class="[
                      'flex-shrink-0 w-10 h-14 flex flex-col items-center justify-center rounded-lg',
                      i === 0 ? 'bg-green-500 text-white' : 'bg-white/50'
                    ]"
                  >
                    <div class="text-xs">{{ day }}</div>
                    <div class="font-bold mt-1">{{ new Date().getDate() + i }}</div>
                  </div>
                </div>
              </FrostedGlass>
            </div>

            <div>
              <h3 class="font-medium mb-3">直播预告</h3>
              <div class="space-y-3">
                <FrostedGlass
                  v-for="(item, index) in [
                    { title: '亲子阅读会第1期', time: '今天 19:30-20:30', image: 'https://cdn.ipadbiz.cn/mlaj/images/live-1.jpg' },
                    { title: '儿童心理健康讲座', time: '明天 20:00-21:00', image: 'https://cdn.ipadbiz.cn/mlaj/images/live-2.jpg' },
                    { title: '家庭教育经验分享', time: '周三 19:00-20:00', image: 'https://cdn.ipadbiz.cn/mlaj/images/live-3.jpg' }
                  ]"
                  :key="index"
                  class="p-3 rounded-xl"
                >
                  <div class="flex justify-between items-center">
                    <div class="flex items-center">
                      <div class="w-12 h-12 bg-green-100 rounded-lg overflow-hidden mr-3 flex-shrink-0">
                        <img
                          :src="buildCdnImageUrl(item.image)"
                          :alt="item.title"
                          class="w-full h-full object-cover"
                          @error="handleImageError"
                        />
                      </div>
                      <div>
                        <h4 class="font-medium text-sm">{{ item.title }}</h4>
                        <p class="text-xs text-gray-500 mt-1">{{ item.time }}</p>
                      </div>
                    </div>
                    <button class="bg-white text-green-600 border border-green-600 px-3 py-1 rounded-full text-xs flex-shrink-0">
                      预约
                    </button>
                  </div>
                </FrostedGlass>
              </div>
            </div>
          </section>
        </div>

        <!-- Featured Content -->
        <div v-if="activeTab === '精选'">
          <section>
            <div class="mb-5">
              <h3 class="font-medium mb-3">精选内容</h3>
              <FrostedGlass class="p-4 rounded-xl">
                <div class="flex flex-col">
                  <div class="inline-block px-2 py-1 bg-blue-100 text-blue-600 text-xs rounded-full mb-2 w-fit">
                    独家专栏
                  </div>
                  <h4 class="font-medium text-lg mb-2">《如何培养孩子的阅读习惯》</h4>
                  <p class="text-gray-600 text-sm mb-4 line-clamp-2">
                    阅读习惯的培养是一个长期过程,本文将分享如何从日常生活点滴培养孩子的阅读兴趣和习惯...
                  </p>
                  <router-link to="/articles/1" class="text-green-600 text-sm font-medium">
                    查看完整文章
                  </router-link>
                </div>
              </FrostedGlass>
            </div>

            <div>
              <h3 class="font-medium mb-3">推荐视频</h3>
              <div class="space-y-4">
                <div
                  v-for="(item, index) in [
                    { title: '亲子沟通的艺术', views: '1.2万', duration: '08:25', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-1.jpg', video_url: 'https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4' },
                    { title: '如何做好家庭教育', views: '8千', duration: '12:40', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-2.jpg', video_url: 'https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4' },
                    { title: '孩子营养餐制作指南', views: '5千', duration: '15:18', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-3.jpg', video_url: 'https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4' }
                  ]"
                  :key="index"
                  class="relative rounded-xl overflow-hidden shadow-md h-48"
                >
                  <template v-if="activeVideoIndex !== index">
                    <img
                      :src="buildCdnImageUrl(item.image, 400)"
                      :alt="item.title"
                      class="w-full h-full object-cover"
                      @error="handleImageError"
                    />
                    <div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/70 flex flex-col justify-end p-4">
                      <h4 class="text-white font-medium mb-1">{{ item.title }}</h4>
                      <div class="flex justify-between items-center">
                        <p class="text-white/80 text-xs">{{ item.views }}次播放 · {{ item.duration }}</p>
                        <button
                          class="bg-white/20 backdrop-blur-sm p-2 rounded-full"
                          @click="playVideo(index, item.video_url)"
                        >
                          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
                            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
                          </svg>
                        </button>
                      </div>
                    </div>
                  </template>
                  <VideoPlayer
                    v-else
                    :video-url="item.video_url"
                    :video-id="`home_recommend_video_${index}`"
                    :use-native-on-ios="false"
                  />
                </div>
              </div>
            </div>
          </section>
        </div>
      </div>
    </div>
  </AppLayout>
</template>

<script setup>
// 导入所需的Vue核心功能和组件
import { ref, onMounted, defineComponent, h, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'

// 导入布局和UI组件
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/effects/FrostedGlass.vue'
import LiveStreamCard from '@/components/courses/LiveStreamCard.vue'
import VideoPlayer from '@/components/media/VideoPlayer.vue'
import CheckInList from '@/components/checkin/CheckInList.vue'

import FeaturedCoursesSection from '@/components/homePage/FeaturedCoursesSection.vue'
import RecommendationsSection from '@/components/homePage/RecommendationsSection.vue'
import LatestActivitiesSection from '@/components/homePage/LatestActivitiesSection.vue'
import HotCoursesSection from '@/components/homePage/HotCoursesSection.vue'

// 导入模拟数据和工具函数
import { liveStreams } from '@/utils/mockData'
import { useTitle } from '@vueuse/core'
import { useAuth } from '@/contexts/auth'
import { showToast } from 'vant'
import { useHomeVideoPlayer } from '@/composables/useHomeVideoPlayer'
import { useImageLoader } from '@/composables/useImageLoader'

// 导入接口
import { getTaskListAPI } from "@/api/checkin";
import { normalizeCheckinTaskItems, buildCdnImageUrl } from '@/utils/tools'

// 图片加载错误处理
const { handleImageError } = useImageLoader()

// 视频播放状态管理
const { activeVideoIndex, playVideo } = useHomeVideoPlayer()

// 路由相关
const $route = useRoute()
useTitle($route.meta.title) // 设置页面标题

// 获取用户认证状态
const { currentUser } = useAuth()

// 响应式状态管理
const activeTab = ref('推荐') // 当前激活的内容标签页

// 签到列表
const checkInTypes = ref([]);

onMounted(() => {
  watch(() => currentUser.value, async (newVal) => {
    if (!newVal) return
    const task = await getTaskListAPI()
    if (task && task.code) {
      checkInTypes.value = normalizeCheckinTaskItems(task.data)
    }
  }, { immediate: true })
})

// 工具函数:格式化今天的日期为中文格式
const formatToday = () => {
  const today = new Date()
  const options = { month: 'long', day: 'numeric', weekday: 'long' } // 设置日期格式选项
  return today.toLocaleDateString('zh-CN', options) // 返回中文格式的日期
}

/**
 * @function handleHomeCheckInSuccess
 * @description 首页打卡成功后刷新签到任务列表,更新置灰状态,并给出轻提示。
 * @returns {Promise<void>}
 */
const handleHomeCheckInSuccess = async () => {
    // 轻提示
    showToast('打卡成功')
    // 统一刷新:重新获取签到任务列表并更新置灰状态
    const task = await getTaskListAPI()
    if (task?.code) {
        checkInTypes.value = normalizeCheckinTaskItems(task.data)
    }
}

const contentRef = ref(null) // 内容区域的ref引用

// 监听activeTab变化,重置内容区域位置
watch(activeTab, () => {
  nextTick(() => {
    if (contentRef.value) {
      const sticky_el = document.querySelector('.sticky')
      const navHeight = sticky_el ? sticky_el.offsetHeight : 0
      const marginTop = parseInt(window.getComputedStyle(contentRef.value).marginTop)
      window.scrollTo({
        top: contentRef.value.offsetTop - navHeight - marginTop,
        behavior:'smooth'
      })
    }
  })
})
</script>