HomePage.vue 38.6 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 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964
<!--
 * @Date: 2025-03-20 19:55:21
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-12-04 16:52:14
 * @FilePath: /mlaj/src/views/HomePage.vue
 * @Description: 美乐爱觉教育首页组件
 *
 * 主要功能模块:
 * 1. 用户欢迎区:显示用户信息和每日打卡
 * 2. 夏令营推广:展示特色夏令营活动
 * 3. 精选课程:轮播展示推荐课程
 * 4. 内容分类:推荐、直播、精选三个标签页
 * 5. 视频播放:支持在线视频播放和切换
 *
 * 状态管理:
 * - 用户认证状态:通过useAuth hook管理
 * - 打卡状态:包括选择的打卡类型和提交状态
 * - 轮播状态:当前轮播位置和滚动控制
 * - 视频播放状态:当前播放的视频索引
 *
 * 组件依赖:
 * - AppLayout:页面布局组件
 * - FrostedGlass:毛玻璃效果容器
 * - CourseCard:课程卡片组件
 * - LiveStreamCard:直播卡片组件
 * - ActivityCard:活动卡片组件
 * - SummerCampCard:夏令营卡片组件
 * - VideoPlayer:视频播放器组件
-->

<template>
  <AppLayout title="美乐爱觉教育" :rightContent="rightContent">
    <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="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-r border-gray-200 flex-1">
              <div class="text-lg font-bold">{{ currentUser?.total_days || 0 }}</div>
              <div class="text-xs text-gray-500">累计打卡</div>
            </div>
            <div class="border-gray-200 flex-1">
              <div class="text-lg font-bold">{{ currentUser?.consecutive_days || 0 }}</div>
              <div class="text-xs text-gray-500">连续打卡</div>
            </div>
            <div class="border-gray-200 flex-1">
              <div class="text-lg font-bold">{{ currentUser?.longest_consecutive_days || 0 }}</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">
            <div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center">
              <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
              </svg>
              <h4 class="text-green-700 font-medium mb-1">打卡成功!</h4>
              <!-- <p class="text-green-600 text-sm">+5 积分已添加到您的账户</p> -->
            </div>
            <template v-else>
              <div class="grid grid-cols-2 gap-2 py-2" style="max-height: 13rem; overflow: auto;">
                <button
                  v-for="checkInType in checkInTypes"
                  :key="checkInType.id"
                  class="flex flex-col items-center p-2 rounded-lg border transition-colors
                  bg-white/70 border-gray-100 hover:bg-white"
                  :class="{
                    'bg-green-100 border-green-200': selectedCheckIn?.id === checkInType.id
                  }"
                  @click="handleCheckInSelect(checkInType)"
                >
                <div class="w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors
                        bg-gray-100"
                    :class="{
                      'bg-green-500 text-white': selectedCheckIn?.id === checkInType.id
                    }"
                >
                    <van-icon v-if="checkInType.task_type === 'checkin'" name="edit" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" />
                    <van-icon v-if="checkInType.task_type === 'upload'" name="tosend" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" />
                  </div>
                  <span :class="['text-xs', checkInType.is_gray ? 'text-gray-500' : '']">{{ checkInType.name }}</span>
                </button>
              </div>

              <div v-if="selectedCheckIn" class="mt-3">
                <!-- <textarea
                  :placeholder="`请输入${selectedCheckIn.name}内容...`"
                  v-model="checkInContent"
                  class="w-full p-3 border border-gray-200 rounded-lg text-sm resize-none h-24"
                /> -->
                <button
                  class="mt-2 w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-2 rounded-lg flex items-center justify-center"
                  @click="handleCheckInSubmit"
                  :disabled="isCheckingIn"
                >
                  <template v-if="isCheckingIn">
                    <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
                    提交中...
                  </template>
                  <template v-else>提交打卡</template>
                </button>
              </div>
            </template>
          </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> -->

      <!-- Featured Courses Carousel -->
      <div v-if="goodCourses.length" class="mb-6">
        <div class="px-4 mb-2">
          <h3 class="font-medium">精选课程</h3>
        </div>
        <div class="relative">
          <div
            ref="carouselRef"
            class="flex overflow-x-scroll snap-x snap-mandatory"
            style="scrollbar-width: none; -ms-overflow-style: none;"
          >
            <div
              v-for="(course, index) in goodCourses"
              :key="course.id"
              class="flex-shrink-0 w-full snap-center px-4"
            >
              <div class="relative rounded-xl overflow-hidden shadow-lg h-48">
                <img
                  :src="course.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
                  :alt="course.title"
                  class="w-full h-full object-cover"
                />
                <!-- 已购标识 -->
                <div
                  v-if="course.is_buy"
                  class="absolute top-0 left-0 bg-orange-500 text-white text-xs px-2 py-1 rounded-br-lg font-medium"
                  style="background-color: rgba(249, 115, 22, 0.85)"
                >
                  已购
                </div>
                <div class="absolute inset-0 bg-gradient-to-b from-transparent via-black/20 to-black/60 flex flex-col justify-end p-4">
                  <div v-if="course.category" class="bg-amber-500/90 text-white px-2 py-1 rounded-full text-xs font-medium inline-block w-fit mb-1">
                    {{ course.category }}
                  </div>
                  <h2 class="text-2xl font-bold text-white drop-shadow-md">{{ course.title }}</h2>
                  <p class="text-white/90 text-sm drop-shadow-sm mb-1">{{ course.subtitle }}</p>
                  <div class="flex justify-between items-center">
                    <div class="flex items-center">
                      <div class="flex">
                        <svg
                          v-for="i in 5"
                          :key="i"
                          xmlns="http://www.w3.org/2000/svg"
                          :class="[`h-4 w-4`, i <= course.comment_score ? 'text-amber-400' : 'text-gray-300']"
                          viewBox="0 0 20 20"
                          fill="currentColor"
                        >
                          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
                        </svg>
                      </div>
                      <span class="text-white text-xs ml-1">{{ course.comment_count }}人评</span>
                    </div>
                    <router-link
                      :to="`/courses/${course.id}`"
                      class="bg-white/90 text-green-600 px-3 py-1 rounded-full text-xs font-medium"
                    >
                      {{ course.price === '0.00' ? '免费学习' : '立即学习' }}
                    </router-link>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- Carousel Indicators -->
          <div class="flex justify-center mt-4">
            <button
              v-for="(_, index) in goodCourses.slice(0, 4)"
              :key="index"
              @click="scrollToSlide(index)"
              :class="[
                'w-2 h-2 mx-1 rounded-full',
                currentSlide === index ? 'bg-green-600' : 'bg-gray-300'
              ]"
            />
          </div>
        </div>
      </div>

      <!-- 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 === '推荐'">
          <!-- Personalized Recommendations -->
          <section class="mb-7">
            <div class="flex justify-between items-center mb-3">
              <h3 class="font-medium">为您推荐</h3>
              <button
                class="text-xs text-gray-500 flex items-center"
                @click="displayedRecommendations = getRecommendations(true)"
              >
                换一批
                <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
                </svg>
              </button>
            </div>
            <div class="grid grid-cols-2 gap-4">
              <FrostedGlass
                v-for="(item, index) in displayedRecommendations"
                :key="index"
                class="p-3 rounded-xl"
              >
                <div class="flex flex-col h-full" @click="goToCourseDetail(item)">
                  <div class="h-28 mb-2 rounded-lg overflow-hidden relative">
                    <img
                      :src="item.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
                      :alt="item.title"
                      class="w-full h-full object-cover"
                    />
                    <!-- 已购标识 -->
                    <div
                      v-if="item?.is_buy"
                      class="absolute top-0 left-0 bg-orange-500 text-white text-xs px-2 py-1 rounded-br-lg font-medium"
                      style="background-color: rgba(249, 115, 22, 0.85)"
                    >
                      已购
                    </div>
                  </div>
                  <h4 class="font-medium text-sm mb-1 line-clamp-1">{{ item.title }}</h4>
                  <!--<p class="text-xs text-gray-500 flex items-center mt-auto">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
                    </svg>
                    <!~~ {{ item.duration }} ~~>
                  </p>-->
                </div>
              </FrostedGlass>
            </div>
          </section>

          <!-- Recent Activities -->
          <section class="mb-7">
            <div class="flex justify-between items-center mb-3">
              <h3 class="font-medium">最新活动</h3>
              <a href="https://wxm.behalo.cc/pages/activity/activity" target="_blank" class="text-xs text-gray-500 flex items-center">
                更多
                <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
                </svg>
              </a>
            </div>
            <div class="space-y-4">
              <div v-for="activity in activities.slice(0, 4)" :key="activity.id">
                <ActivityCard :activity="activity" />
              </div>
            </div>
          </section>

          <!-- Popular Courses -->
          <section>
            <div class="flex justify-between items-center mb-3">
              <h3 class="font-medium">热门课程</h3>
              <router-link to="/courses" class="text-xs text-gray-500 flex items-center">
                更多
                <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
                </svg>
              </router-link>
            </div>
            <div class="space-y-4">
              <CourseCard
                v-for="course in hotCourses"
                :key="course.id"
                :course="course"
              />
            </div>
          </section>
        </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="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="item.image"
                      :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"
                    ref="videoPlayerRefs"
                  />
                </div>
              </div>
            </div>
          </section>
        </div>
      </div>
    </div>
  </AppLayout>
</template>

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

// 导入布局和UI组件
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import CourseCard from '@/components/ui/CourseCard.vue'
import LiveStreamCard from '@/components/ui/LiveStreamCard.vue'
import ActivityCard from '@/components/ui/ActivityCard.vue'
import SummerCampCard from '@/components/ui/SummerCampCard.vue'
import VideoPlayer from '@/components/ui/VideoPlayer.vue'

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

// 导入接口
import { getCourseListAPI } from "@/api/course";
import { getTaskListAPI, checkinTaskAPI } from "@/api/checkin";

// 视频播放状态管理
const activeVideoIndex = ref(null); // 当前播放的视频索引
const videoPlayerRefs = ref([]); // 视频播放器组件引用数组

// 播放视频处理函数
const playVideo = (index, videoUrl) => {
  // 更新当前播放的视频索引
  activeVideoIndex.value = index;
};

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

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

// 响应式状态管理
const activeTab = ref('推荐') // 当前激活的内容标签页
const selectedCheckIn = ref(null) // 选中的打卡类型
const checkInContent = ref('') // 打卡内容
const currentSlide = ref(0) // 当前轮播图索引
const isCheckingIn = ref(false) // 打卡提交状态
const checkInSuccess = ref(false) // 打卡成功状态
const displayedRecommendations = ref([]) // 当前显示的推荐内容

//
const userRecommendations = ref([])
const hotCourses = ref([])
const goodCourses = ref([])
// 活动列表(从外部接口获取)
const activities = ref([])
// 获取推荐内容
const getRecommendations = (random = false) => {
  if (random) {
    const shuffled = [...userRecommendations.value].sort(() => 0.5 - Math.random())
    return shuffled.slice(0, 4)
  }
  return userRecommendations.value.slice(0, 4)
}

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

// 自动轮播
let carouselInterval
onMounted(async () => {
  // 获取课程列表
  const res = await getCourseListAPI({ sn: 'RMKC' })
  if (res.code) {
    userRecommendations.value = res.data
    // 初始化显示推荐内容
    displayedRecommendations.value = getRecommendations()
  }
  // 获取热门课程
  const res2 = await getCourseListAPI({
    sn: 'RMKC',
    limit: 8
  })
  if (res2.code) {
    hotCourses.value = res2.data
  }
  // 获取精选课程
  const res3 = await getCourseListAPI({
    sn: 'JXKC',
    limit: 4
  })
  if (res3.code) {
    goodCourses.value = res3.data
    carouselInterval = setInterval(() => {
      if (carouselRef.value) {
        const nextSlide = (currentSlide.value + 1) % goodCourses.value.slice(0, 4).length
        scrollToSlide(nextSlide)
      }
    }, 5000)
  }

  // 获取签到列表
  if(currentUser.value) {
    const task = await getTaskListAPI()
    if (task.code) {
      task.data.forEach(item => {
        checkInTypes.value.push({
          id: item.id,
          name: item.title,
          task_type: item.task_type,
          is_gray: item.is_gray
        })
      });
    }
  }

  // 获取最新活动(外部接口)
  await fetchExternalActivities()

  // 监听轮播容器的滚动,手动滑动时同步底部指示器
  if (carouselRef.value) {
      // 添加滚动监听(被动监听),在用户手动滑动时同步指示器
      carouselRef.value.addEventListener('scroll', handle_carousel_scroll, { passive: true })
  }
})

onUnmounted(() => {
  if (carouselInterval) {
    clearInterval(carouselInterval)
  }
  // 卸载时移除轮播滚动监听,避免内存泄漏
  if (carouselRef.value) {
      carouselRef.value.removeEventListener('scroll', handle_carousel_scroll)
  }
})

const carouselRef = ref(null) // 轮播图容器引用

/**
 * @function sync_current_slide
 * @description 根据滚动位置同步当前轮播索引
 * 注释:通过容器 scrollLeft 与容器宽度计算当前页,向最近页取整。
 * @returns {void}
 */
const sync_current_slide = () => {
    const container = carouselRef.value
    const total = goodCourses.value.slice(0, 4).length
    if (!container || total === 0) return
    const slide_width = container.offsetWidth
    if (slide_width === 0) return
    const raw_index = container.scrollLeft / slide_width
    const idx = Math.round(raw_index)
    const bounded = Math.min(Math.max(idx, 0), total - 1)
    currentSlide.value = bounded
}

/**
 * @function handle_carousel_scroll
 * @description 轮播滚动事件处理(被动监听),调用同步函数更新指示器位置。
 * @returns {void}
 */
const handle_carousel_scroll = () => {
    sync_current_slide()
}

// 右侧导航组件:搜索和消息通知

// 右侧内容组件
const RightContent = defineComponent({
  setup() {
    return () => (
      <div class="flex items-center">
        <button class="p-2 mr-1">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
          </svg>
        </button>
        <button class="p-2">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
          </svg>
        </button>
      </div>
    )
  }
})

const rightContent = h(RightContent)

// 工具函数:处理图片加载错误,设置默认头像
const handleImageError = (e) => {
  e.target.onerror = null // 防止循环触发错误
  e.target.src = '' // 设置默认头像
}

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

// 轮播图控制:滚动到指定位置
const scrollToSlide = (index) => {
  if (carouselRef.value) {
    const slideWidth = carouselRef.value.offsetWidth // 获取轮播图容器宽度
    carouselRef.value.scrollTo({
      left: index * slideWidth, // 计算目标滚动位置
      behavior: 'smooth' // 使用平滑滚动效果
    })
    currentSlide.value = index // 更新当前轮播图索引
  }
}

// 打卡功能:处理打卡类型选择
const handleCheckInSelect = (checkInType) => {
  if (checkInType.is_gray && checkInType.task_type === 'checkin') {
    showToast('您已经完成了今天的打卡')
    return
  }
  if (checkInType.task_type === 'upload') {
    $router.push({
      path: '/checkin/index',
      query: {
        id: checkInType.id,
      },
    })
  } else {
    selectedCheckIn.value = checkInType // 更新选中的打卡类型
    checkInContent.value = '' // 清空打卡内容
  }
}

// 打卡功能:处理打卡提交
const handleCheckInSubmit = async () => {
  // 表单验证
  if (!selectedCheckIn.value) {
    showToast('请选择打卡项目')
    return
  }
  // if (!checkInContent.value.trim()) {
  //   showToast('请输入打卡内容')
  //   return
  // }

  // API调用
  const { code, data } = await checkinTaskAPI({ task_id: selectedCheckIn.value.id });
  if (code) {
    isCheckingIn.value = true
    checkInSuccess.value = true
    checkInContent.value = ''
    setTimeout(() => {
      isCheckingIn.value = false
      selectedCheckIn.value = null
      checkInSuccess.value = false
    }, 1000);
  }
}

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

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

// 跳转到购买课程详情页
const goToCourseDetail = ({id}) => {
  $router.push(`/courses/${id}`)
}

/**
 * 获取并处理外部活动数据
 * @returns {Promise<void>} 无返回值,更新 activities 响应式数据
 * 注释:使用原生 fetch 请求外部接口,解析与映射为 ActivityCard 需要的结构,并筛选“报名中”状态。
 */
const fetchExternalActivities = async () => {
    // 外部接口地址(不复用项目 axios 配置)
    const url = 'https://bhapi.behalo.cc/api/get_act/?city_name=&pub_status=1&page_idx=1&page_size=300&search_option=4'
    try {
        // 发起请求
        const resp = await fetch(url, { method: 'GET' })
        // 解析结果
        const json = await resp.json()

        // 兼容不同返回结构,优先 data.list 数组
        const list = Array.isArray(json)
            ? json
            : Array.isArray(json?.data?.list)
                ? json.data.list
                : Array.isArray(json?.list)
                    ? json.list
                    : Array.isArray(json?.rows)
                        ? json.rows
                        : []

        // 当前时间
        const now = new Date()

        // 映射到 ActivityCard 结构
        const mapped = list.map((item) => {
            // 解析数值
            const xs_price = numberOrNull(item?.xs_price)
            const sc_price = numberOrNull(item?.sc_price)

            // 图片优先级:sl_img > fx_img > banner_img
            const imageUrl = item?.sl_img || item?.fx_img || item?.banner_img || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'

            // 时间区间字符串
            const period = formatPeriod(item?.act_start_at, item?.act_end_at)

            // 参与人数与上限(根据 stu_num_upper 与 bookgap_info 计算)
            const upper = numberOrNull(item?.stu_num_upper)
            const gap = numberOrNull(item?.bookgap_info?.stu_gap)
            const participantsCount = upper != null && gap != null ? Math.max(upper - gap, 0) : null

            // 报名状态判断:优先使用报名起止;缺失则按活动起止回退
            const enrollStatus = computeEnrollStatus(
                item?.stu_start_at,
                item?.stu_end_at,
                now,
                item?.act_start_at,
                item?.act_end_at
            )

            return {
                id: item?.act_id,
                title: item?.act_title || '',
                imageUrl,
                isHot: false,
                // isFree: xs_price === 0 || sc_price === 0,
                isFree: false,
                location: item?.act_address || item?.city_name || '',
                period,
                price: xs_price != null ? xs_price : '',
                originalPrice: sc_price != null && sc_price > 0 ? sc_price : '',
                participantsCount: participantsCount != null ? participantsCount : '',
                maxParticipants: upper != null ? upper : '',
                mock_link: 'https://wxm.behalo.cc/pages/activity/info?type=2&id=' +  item?.act_id,
                status: enrollStatus
            }
        })

        // 仅保留“报名中”的数据
        activities.value = mapped.filter((a) => a.status === '报名中')
    } catch (err) {
        console.error('获取外部活动数据失败:', err)
        // 失败时回退为空数组,避免页面报错
        activities.value = []
    }
}

/**
 * 计算报名状态
 * @param {string} stu_start_at 报名开始时间字符串
 * @param {string} stu_end_at 报名结束时间字符串
 * @param {Date} now 当前时间
 * @param {string} act_start_at 活动开始时间
 * @param {string} act_end_at 活动结束时间
 * @returns {string} 状态字符串:报名中 / 即将开始 / 进行中 / 已结束
 * 注释:优先以报名起止判断“报名中”;若缺失则按活动起止时间回退:未开始视为“报名中”,进行中为“进行中”。
 */
const computeEnrollStatus = (stu_start_at, stu_end_at, now, act_start_at, act_end_at) => {
    // 优先使用报名时间窗口
    if (stu_start_at && stu_end_at) {
        const start = new Date(stu_start_at)
        const end = new Date(stu_end_at)
        if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
            if (now >= start && now <= end) return '报名中'
            if (now < start) return '即将开始'
            return '已结束'
        }
    }

    // 回退:使用活动时间窗口
    if (act_start_at && act_end_at) {
        const aStart = new Date(act_start_at)
        const aEnd = new Date(act_end_at)
        if (!isNaN(aStart.getTime()) && !isNaN(aEnd.getTime())) {
            if (now < aStart) return '报名中' // 活动未开始,视为报名中
            if (now >= aStart && now <= aEnd) return '进行中'
            if (now > aEnd) return '已结束'
        }
    }

    // 默认回退
    return '即将开始'
}

/**
 * 格式化活动时间区间
 * @param {string} startStr 活动开始时间
 * @param {string} endStr 活动结束时间
 * @returns {string} 例如:2025-11-09 至 2025-12-20
 * 注释:展示活动期信息;若解析失败则返回空串。
 */
const formatPeriod = (startStr, endStr) => {
    if (!startStr || !endStr) return ''
    const start = new Date(startStr)
    const end = new Date(endStr)
    if (isNaN(start.getTime()) || isNaN(end.getTime())) return ''
    const s = formatDateOnly(start)
    const e = formatDateOnly(end)
    return `${s} 至 ${e}`
}

/**
 * 仅格式化为日期字符串(YYYY-MM-DD)
 * @param {Date} d 日期对象
 * @returns {string} 日期字符串
 */
const formatDateOnly = (d) => {
    const y = d.getFullYear()
    const m = String(d.getMonth() + 1).padStart(2, '0')
    const day = String(d.getDate()).padStart(2, '0')
    return `${y}-${m}-${day}`
}

/**
 * 将任意值安全转换为数字
 * @param {any} v 原始值
 * @returns {number|null} 数字或空
 */
const numberOrNull = (v) => {
    if (v === null || v === undefined || v === '') return null
    const n = Number(v)
    return isNaN(n) ? null : n
}
</script>