index.vue 8.37 KB
<!--
 * @Date: 2026-01-31
 * @Description: 产品知识库 - API 接口集成版本
-->
<template>
  <view class="h-screen bg-[#F9FAFB] flex flex-col">
    <view class="bg-[#F9FAFB] z-10">
      <NavHeader title="产品知识库" />

      <!-- Tabs Container -->
      <view class="flex-1 min-h-0 flex flex-col">
        <nut-tabs v-model="activeTabId">
          <!-- 自定义标签栏 -->
          <template #titles>
            <view class="filter-tabs-wrapper">
              <view
                v-for="item in tabsData"
                :key="item.id"
                :class="[
                  'filter-tab-item',
                  activeTabId === item.id ? 'filter-tab-active' : 'filter-tab-inactive'
                ]"
                @tap="onTabClick(item.id)"
              >
                <text class="filter-tab-text">{{ item.name }}</text>
              </view>
            </view>
          </template>
        </nut-tabs>
      </view>
    </view>

    <!-- 列表容器 -->
    <scroll-view
      class="flex-1 min-h-0 overflow-y-auto pb-[calc(160rpx+env(safe-area-inset-bottom))]"
      scroll-y
      @scrolltolower="onScrollToLower"
    >
      <!-- 加载状态 -->
      <view v-if="loading && products.length === 0" class="flex justify-center items-center py-[100rpx]">
        <text class="text-gray-400 text-[28rpx]">加载中...</text>
      </view>

      <!-- Product Grid -->
      <view v-else class="flex flex-wrap justify-between px-[40rpx]">
        <!-- Card Item -->
        <view v-for="(item, index) in products" :key="item.id"
          class="w-[48%] bg-white rounded-[24rpx] overflow-hidden mb-[24rpx] shadow-sm flex flex-col active:scale-[0.98] transition-transform duration-200"
          :style="{ animationDelay: `${index * 50}ms` }"
          @tap="handleProductClick(item)">
          <!-- Image Container -->
          <view class="relative w-full h-[200rpx] product-card-item">
            <image :src="item.cover_image" class="w-full h-full object-cover bg-gray-100" mode="aspectFill" />
            <!-- Tag -->
            <view v-if="item.recommend === 'hot'"
              class="absolute top-[12rpx] right-[12rpx] bg-red-500 text-white text-[20rpx] px-[12rpx] py-[4rpx] rounded-full">
              热卖
            </view>
          </view>

          <!-- Content -->
          <view class="p-[20rpx] flex flex-col flex-1">
            <!-- Title -->
            <view class="text-[#1F2937] text-[28rpx] font-medium leading-[1.4] line-clamp-2 mb-[16rpx]">
              {{ item.product_name }}
            </view>

            <!-- 动态标签 -->
            <view v-if="item.tags && item.tags.length > 0" class="flex flex-wrap gap-[8rpx] mt-auto">
              <view
                v-for="tag in item.tags.slice(0, 2)"
                :key="tag.id"
                class="text-[20rpx] px-[12rpx] py-[4rpx] rounded-full"
                :style="{
                  backgroundColor: tag.bg_color,
                  color: tag.text_color
                }"
              >
                {{ tag.name }}
              </view>
            </view>
          </view>
        </view>
      </view>

      <!-- 加载更多状态 -->
      <view v-if="loading && products.length > 0" class="flex justify-center items-center py-[40rpx]">
        <text class="text-gray-400 text-[28rpx]">加载中...</text>
      </view>

      <!-- 没有更多数据 -->
      <view v-if="!hasMore && products.length > 0" class="flex justify-center items-center py-[40rpx]">
        <text class="text-gray-400 text-[28rpx]">没有更多了</text>
      </view>

      <!-- 空状态 -->
      <view v-if="!loading && products.length === 0" class="flex flex-col items-center justify-center py-[100rpx]">
        <text class="text-gray-400 text-[28rpx] w-full text-center">暂无相关产品</text>
      </view>
    </scroll-view>
  </view>
</template>

<script setup>
import { ref, computed } from 'vue'
import Taro, { useLoad, useReachBottom } from '@tarojs/taro'
import NavHeader from '@/components/NavHeader.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
import { listAPI } from '@/api/get_product'

const activeTabId = ref('')

// 分页状态
const page = ref(0)
const limit = ref(10)
const loading = ref(false)
const hasMore = ref(true)

// 数据状态
const categories = ref([]) // 从接口获取的分类列表
const products = ref([]) // 当前产品列表
const total = ref(0) // 产品总数

/**
 * 标签栏数据(根据接口返回的 categories 生成)
 * @description 包含"全部"选项和接口返回的分类
 */
const tabsData = computed(() => {
  const allTab = { id: '', name: '全部' }
  const categoryTabs = categories.value.map(cat => ({
    id: String(cat.id),
    name: cat.name
  }))
  return [allTab, ...categoryTabs]
})

/**
 * 获取产品列表
 * @description 根据 activeTabId 获取对应分类的产品列表
 */
const fetchProducts = async (isLoadMore = false) => {
  if (loading.value) return

  loading.value = true

  try {
    const params = {
      page: String(page.value),
      limit: String(limit.value)
    }

    // 如果不是"全部"标签,添加分类 ID 参数
    if (activeTabId.value !== '') {
      params.cid = activeTabId.value
    }

    const res = await listAPI(params)

    if (res.code === 1 && res.data) {
      // 更新分类列表(首次加载时)
      if (!isLoadMore && res.data.categories) {
        categories.value = res.data.categories
      }

      // 处理产品列表
      if (isLoadMore) {
        // 加载更多:追加数据
        products.value = [...products.value, ...res.data.list]
      } else {
        // 首次加载或切换分类:替换数据
        products.value = res.data.list || []
      }

      // 更新总数和分页状态
      total.value = res.data.total || 0
      hasMore.value = products.value.length < total.value
    } else {
      Taro.showToast({
        title: res.msg || '获取产品列表失败',
        icon: 'none'
      })
    }
  } catch (err) {
    console.error('获取产品列表失败:', err)
    Taro.showToast({
      title: '网络错误,请重试',
      icon: 'none'
    })
  } finally {
    loading.value = false
  }
}

/**
 * Tab 点击处理
 * @description 切换分类,重置分页并重新加载数据
 */
const onTabClick = (id) => {
  if (activeTabId.value === id) return

  activeTabId.value = id

  // 重置分页状态
  page.value = 0
  products.value = []
  hasMore.value = true

  // 重新加载数据
  fetchProducts(false)
}

/**
 * 滚动到底部加载更多
 * @description 触发上拉加载更多
 */
const onScrollToLower = () => {
  if (!hasMore.value || loading.value) return

  page.value += 1
  fetchProducts(true)
}

/**
 * 使用产品列表点击处理器
 * @description 配置为产品类型列表,点击时跳转到产品详情页
 */
const { handleClick: handleProductClick } = useListItemClick({
  listType: ListType.PRODUCT,
  onAfterClick: (item) => {
    console.log('用户查看了产品:', item.product_name)
  }
})

/**
 * 页面加载时获取数据
 */
useLoad(() => {
  fetchProducts(false)
})

/**
 * 触底加载更多(备用方案)
 */
useReachBottom(() => {
  onScrollToLower()
})
</script>

<style lang="less">
@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(20rpx);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.product-card-item {
  animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}

// FilterTabs 风格的标签栏
.filter-tabs-wrapper {
  display: flex;
  overflow-x: auto;
  padding: 24rpx 40rpx;
  gap: 24rpx;
  transition: all 0.3s ease;
  background-color: #F9FAFB;
  width: 100%;

  // 隐藏滚动条
  &::-webkit-scrollbar {
    display: none;
    width: 0;
    height: 0;
  }

  -ms-overflow-style: none;
  scrollbar-width: none;
}

.filter-tab-item {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 32rpx;
  border-radius: 9999rpx;
  white-space: nowrap;
  transition: all 0.3s ease;
  flex-shrink: 0;
}

.filter-tab-active {
  background-color: #2563EB; // 蓝色背景
  color: #fff;
}

.filter-tab-inactive {
  background-color: #F3F4F6; // 灰色背景
  color: #6B7280;
}

.filter-tab-text {
  font-size: 28rpx;
  font-weight: 500;
}

// 覆盖 NutUI Tabs 默认样式,隐藏原有的头部和内容(因为我们使用自定义头部和外部列表)
:deep(.nut-tabs__titles) {
  display: none;
}

:deep(.nut-tabs__content) {
  display: none;
}
</style>