index.vue 8.89 KB
<!--
 * @Date: 2026-01-31
 * @Description: 产品知识库 - 已改造为 NutTabs 版本
-->
<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>

    <!-- 列表容器(独立于 nut-tab-pane) -->
    <view
      v-if="listVisible"
      :key="listRenderKey"
      class="flex-1 min-h-0 overflow-y-auto px-[40rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))]"
    >
      <!-- Section Title -->
      <view class="text-[#1F2937] text-[32rpx] font-bold mb-[24rpx]">
        {{ currentTabName }}
      </view>

      <!-- Product Grid -->
      <view class="flex flex-wrap justify-between">
        <!-- Card Item -->
        <view v-for="(item, index) in filteredProducts" :key="index"
          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.image" class="w-full h-full object-cover bg-gray-100" referrerpolicy="no-referrer" />
            <!-- Tag -->
            <view v-if="item.tag"
              class="absolute top-[12rpx] right-[12rpx] bg-red-500 text-white text-[20rpx] px-[12rpx] py-[4rpx] rounded-full">
              {{ item.tag }}
            </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.title }}
            </view>

            <!-- Desc -->
            <view class="mt-auto self-start text-[22rpx] px-[12rpx] py-[4rpx] rounded-full"
              :class="[getDescColor(item.id).bg, getDescColor(item.id).text]">
              {{ item.desc }}
            </view>
          </view>
        </view>
      </view>

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

<script setup>
import { ref, computed, nextTick } from 'vue'
import NavHeader from '@/components/NavHeader.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'

const activeTabId = ref('')
const listVisible = ref(true)
const listRenderKey = ref(0)

/**
 * Tab 数据源
 * @description 包含分类信息和对应的产品列表
 */
const tabsData = ref([
  { id: '', name: '全部产品', list: [] },
  { id: 'life', name: '人寿保险', list: [] },
  { id: 'medical', name: '医疗保险', list: [] },
  { id: 'accident', name: '意外保险', list: [] },
])

/**
 * Desc 颜色调色板
 *
 * @description 为不同描述提供柔和的背景色和对应的文字颜色
 */
const descColorPalette = [
  { bg: 'bg-blue-50', text: 'text-blue-600' },      // 蓝色
  { bg: 'bg-green-50', text: 'text-green-600' },    // 绿色
  { bg: 'bg-purple-50', text: 'text-purple-600' },  // 紫色
  { bg: 'bg-orange-50', text: 'text-orange-600' },  // 橙色
  { bg: 'bg-pink-50', text: 'text-pink-600' },     // 粉色
  { bg: 'bg-teal-50', text: 'text-teal-600' },     // 青色
  { bg: 'bg-indigo-50', text: 'text-indigo-600' },  // 靛蓝色
  { bg: 'bg-red-50', text: 'text-red-600' },       // 红色
]

/**
 * 获取 desc 的颜色样式
 *
 * @description 根据 ID 获取固定的背景色和文字颜色
 * @param {number} id - 产品 ID
 * @returns {Object} 包含 bg 和 text 属性的颜色对象
 */
const getDescColor = (id) => {
  const index = id % descColorPalette.length
  return descColorPalette[index]
}

/**
 * 生成产品数据
 *
 * @description 生成模拟产品列表数据,每个产品包含唯一 id
 * @returns {Array} 产品列表
 */
const generateProducts = () => {
  return [
    { id: 1, title: '终身寿险尊享版', tag: '热卖', desc: '5年超值', image: `https://picsum.photos/seed/1/200/200` },
    { id: 2, title: '百万医疗保险计划', desc: '收益率3.5%', image: `https://picsum.photos/seed/2/200/200` },
    { id: 3, title: '意外伤害保障计划', desc: '保证收益万能', image: `https://picsum.photos/seed/3/200/200` },
    { id: 4, title: '分红型年金保险', tag: '热卖', desc: '保证收益万能', image: `https://picsum.photos/seed/4/200/200` },
    { id: 5, title: '重大疾病保险', desc: '收益率4.2%', image: `https://picsum.photos/seed/5/200/200` },
    { id: 6, title: '少儿教育金保险', tag: '热卖', desc: '教育专属', image: `https://picsum.photos/seed/6/200/200` },
    { id: 7, title: '高端医疗服务', desc: '尊享服务', image: `https://picsum.photos/seed/7/200/200` },
    { id: 8, title: '家庭财产保险', desc: '全家无忧', image: `https://picsum.photos/seed/8/200/200` },
    { id: 9, title: '长期护理保险', desc: '贴心保障', image: `https://picsum.photos/seed/9/200/200` },
    { id: 10, title: '投资连结保险', tag: '新品', desc: '稳健增值', image: `https://picsum.photos/seed/10/200/200` },
  ]
}

const allProducts = ref(generateProducts())

/**
 * 初始化数据分布
 * @description 根据分类规则将 allProducts 中的数据分配到各个 tab 中
 */
const initTabsData = () => {
  tabsData.value.forEach((tab, index) => {
    if (tab.id === '') {
      tab.list = [...allProducts.value]
    } else {
      // 模拟分类逻辑:根据索引取余分配
      // 保持与原逻辑一致:result = result.filter((_, i) => (i + index) % (index + 2) === 0)
      tab.list = allProducts.value.filter((_, i) => (i + index) % (index + 2) === 0)
    }
  })
}

/**
 * 当前选中 Tab 的名称
 * @description 根据 activeTabId 获取对应 tab 的名称
 */
const currentTabName = computed(() => {
  const currentTab = tabsData.value.find(tab => tab.id === activeTabId.value)
  return currentTab ? currentTab.name : ''
})

/**
 * 当前选中 Tab 的产品列表
 * @description 根据 activeTabId 获取对应 tab 的产品列表
 */
const filteredProducts = computed(() => {
  // 找到当前选中的 tab
  const currentTab = tabsData.value.find(tab => tab.id === activeTabId.value)
  if (!currentTab) return []

  return currentTab.list
})

/**
 * Tab 点击处理
 */
const onTabClick = (id) => {
  activeTabId.value = id
  listVisible.value = false
  nextTick(() => {
    listRenderKey.value += 1
    listVisible.value = true
  })
  // 可以在这里触发加载逻辑
  // loadProductsByCategory(id)
}

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

// 初始化数据
initTabsData()
</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>