index.vue 7.24 KB
<template>
  <view class="index-page">
    <view class="page-content">
      <view v-if="hasTopBanner" class="home-top-banner">
        <view v-if="isMockHomeData" class="mock-state-badge">
          mock
        </view>

        <nut-swiper
          v-if="shouldUseTopBannerSwiper"
          :auto-play="3000"
          :loop="true"
          :pagination-visible="true"
          pagination-color="#ffffff"
          pagination-unselected-color="rgba(255, 255, 255, 0.45)"
          height="210"
        >
          <nut-swiper-item
            v-for="item in topBanners"
            :key="item.id"
          >
            <view
              class="home-banner"
              :class="{ clickable: hasLink(item) }"
              @tap="handleLinkedItemTap(item)"
            >
              <image class="home-banner__image" :src="item.image_url" mode="aspectFill" />
            </view>
          </nut-swiper-item>
        </nut-swiper>

        <view
          v-else
          class="home-banner"
          :class="{ clickable: hasLink(topBanners[0]) }"
          @tap="handleLinkedItemTap(topBanners[0])"
        >
          <image class="home-banner__image" :src="topBanners[0].image_url" mode="aspectFill" />
        </view>
      </view>

      <view class="nav-panel">
        <view
          v-for="item in homeIcons"
          :key="item.id"
          class="nav-entry"
          :class="{ clickable: hasLink(item) }"
          @tap="handleLinkedItemTap(item)"
        >
          <view class="nav-entry__icon-wrap">
            <i
              class="nav-entry__icon"
              :class="getIconClass(item)"
            ></i>
          </view>
          <text class="nav-entry__title">{{ item.title }}</text>
        </view>
      </view>

      <view class="image-link-list">
        <view
          v-for="item in bottomBanners"
          :key="item.id"
          class="image-link-card"
          :class="{ clickable: hasLink(item) }"
          @tap="handleLinkedItemTap(item)"
        >
          <image class="image-link-card__image" :src="item.image_url" mode="aspectFill" />
        </view>
      </view>
    </view>

    <AppTabbar current="home" />
  </view>
</template>

<script setup>
import { computed, ref } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
import AppTabbar from '@/components/AppTabbar.vue'
import { getHomeContentAPI } from '@/api'
import { buildWebviewPreviewUrl } from '@/utils/webview'
import { isMockEnabled } from '@/utils/config'

const defaultIcon = 'icon-jingxiuying'
const homeContent = ref({
  volunteer_top_banner: [],
  volunteer_bottom_banner: [],
  volunteer_home_icon: [],
  title: '',
  color: '',
})

const topBanners = computed(() => homeContent.value.volunteer_top_banner || [])
const bottomBanners = computed(() => homeContent.value.volunteer_bottom_banner || [])
const homeIcons = computed(() => homeContent.value.volunteer_home_icon || [])
const hasTopBanner = computed(() => topBanners.value.some((item) => item?.image_url))
const shouldUseTopBannerSwiper = computed(() => topBanners.value.filter((item) => item?.image_url).length > 1)
const isMockHomeData = computed(() => isMockEnabled())

const getItemTargetUrl = (item) => String(item?.target_url || '').trim()

const hasLink = (item) => !!getItemTargetUrl(item)

const isInternalMiniProgramPath = (url) => String(url || '').startsWith('/pages/')

const normalizeBannerItem = (item = {}) => {
  return {
    ...item,
    image_url: String(item?.value || '').trim(),
    target_url: String(item?.link || '').trim(),
  }
}

const getIconClass = (item) => {
  const icon = item?.icon || defaultIcon
  const fontClass = icon.startsWith('fa-') ? 'fa' : 'iconfont'
  return [fontClass, icon]
}

const handleLinkedItemTap = (item) => {
  if (!item || !hasLink(item)) return

  const targetUrl = getItemTargetUrl(item)

  if (isInternalMiniProgramPath(targetUrl)) {
    Taro.navigateTo({
      url: targetUrl,
    })
    return
  }

  Taro.navigateTo({
    url: buildWebviewPreviewUrl(targetUrl, item?.title || ''),
  })
}

const normalizeHomeContent = (data = {}) => ({
  volunteer_top_banner: Array.isArray(data.volunteer_top_banner)
    ? data.volunteer_top_banner.map((item) => normalizeBannerItem(item))
    : [],
  volunteer_bottom_banner: Array.isArray(data.volunteer_bottom_banner)
    ? data.volunteer_bottom_banner.map((item) => normalizeBannerItem(item))
    : [],
  volunteer_home_icon: Array.isArray(data.volunteer_home_icon)
    ? data.volunteer_home_icon.map((item) => ({
      ...item,
      target_url: item?.link || '',
    }))
    : [],
  title: data.title || '',
  color: data.color || '',
})

const fetchHomeContent = async () => {
  const response = await getHomeContentAPI()

  if (response?.code !== 1) {
    Taro.showToast({
      title: response?.msg || '获取首页内容失败',
      icon: 'none',
    })
    return
  }

  homeContent.value = normalizeHomeContent(response?.data)
}

useLoad(() => {
  fetchHomeContent()
})
</script>

<style lang="less">
.index-page {
  min-height: 100vh;
  background: #f1f1f1;

  .page-content {
    padding: 32rpx 32rpx 0;
    box-sizing: border-box;
  }

  .home-top-banner {
    position: relative;
    margin: -32rpx -32rpx 0;
  }

  .mock-state-badge {
    position: absolute;
    top: 20rpx;
    right: 20rpx;
    z-index: 3;
    padding: 8rpx 16rpx;
    border-radius: 999rpx;
    background: rgba(0, 0, 0, 0.42);
    font-size: 20rpx;
    line-height: 1;
    color: #ffffff;
    letter-spacing: 1rpx;
    text-transform: uppercase;
    pointer-events: none;
  }

  .home-banner {
    height: 420rpx;
    overflow: hidden;
    background: #e5e7eb;
  }

  .home-banner__image,
  .image-link-card__image {
    display: block;
    width: 100%;
    height: 100%;
  }

  .nav-panel {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    margin: 32rpx 0 0;
    overflow: hidden;
    border-radius: 16rpx;
    background: #fff;
    box-shadow: 0 16rpx 40rpx rgba(31, 41, 55, 0.06);
  }

  .nav-entry {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-width: 0;
    min-height: 184rpx;
    padding: 28rpx 8rpx 24rpx;
    box-sizing: border-box;
    color: #98271d;
  }

  .nav-entry::after {
    position: absolute;
    top: 0;
    right: 0;
    width: 1rpx;
    height: 100%;
    background: #ececec;
    content: '';
  }

  .nav-entry:nth-child(3n)::after {
    display: none;
  }

  .nav-entry:nth-child(n + 4)::before {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 1rpx;
    background: #ececec;
    content: '';
  }

  .nav-entry__icon-wrap {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 76rpx;
    height: 76rpx;
    line-height: 1;
  }

  .nav-entry__icon {
    font-size: 64rpx;
    color: #98271d;
  }

  .nav-entry__title {
    display: block;
    width: 100%;
    margin-top: 18rpx;
    font-size: 28rpx;
    line-height: 1.25;
    text-align: center;
    color: #98271d;
    word-break: break-all;
  }

  .image-link-list {
    display: flex;
    flex-direction: column;
    gap: 32rpx;
    margin-top: 40rpx;
    padding: 0 4rpx 40rpx;
  }

  .image-link-card {
    position: relative;
    height: 252rpx;
    overflow: hidden;
    border-radius: 12rpx;
    background: #d1d5db;
  }

  .clickable {
    cursor: pointer;
  }
}
</style>