hookehuyr

refactor(knowledge-base): 使用 NutTabs 重构产品知识库页面

- 移除 FilterTabs 组件,改用 NutUI Tabs 实现自定义标签栏
- 将数据模型重构为基于 ID 的分类,支持动态数据分配
- 优化列表渲染性能,添加切换动画和空状态提示
- 统一使用 view/image 等 uni-app 组件,提升跨端兼容性
<!--
* @Date: 2026-01-31
* @Description: 产品知识库 - 已改造为 NutTabs 版本
-->
<template>
<div class="h-screen bg-[#F9FAFB] flex flex-col">
<div class="bg-[#F9FAFB]">
<!-- Navigation Header -->
<view class="h-screen bg-[#F9FAFB] flex flex-col">
<view class="bg-[#F9FAFB] z-10">
<NavHeader title="产品知识库" />
<!-- Filter Tabs -->
<div class="px-[40rpx] mt-[40rpx]">
<FilterTabs v-model="activeTab" :tabs="tabs" wrapper-class="mb-[40rpx]" />
</div>
</div>
<!-- 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>
<div class="flex-1 overflow-y-auto px-[40rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))]">
<!-- 列表容器(独立于 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 -->
<div class="text-[#1F2937] text-[32rpx] font-bold mb-[24rpx]">
{{ tabs[activeTab] }}
</div>
<view class="text-[#1F2937] text-[32rpx] font-bold mb-[24rpx]">
{{ currentTabName }}
</view>
<!-- Product Grid -->
<div class="flex flex-wrap justify-between">
<view class="flex flex-wrap justify-between">
<!-- Card Item -->
<div v-for="(item, index) in filteredProducts" :key="index"
<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 -->
<div class="relative w-full h-[200rpx]">
<img :src="item.image" class="w-full h-full object-cover bg-gray-100" referrerpolicy="no-referrer" />
<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 -->
<div v-if="item.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 }}
</div>
</div>
</view>
</view>
<!-- Content -->
<div class="p-[20rpx] flex flex-col flex-1">
<view class="p-[20rpx] flex flex-col flex-1">
<!-- Title -->
<div class="text-[#1F2937] text-[28rpx] font-medium leading-[1.4] line-clamp-2 mb-[16rpx]">
<view class="text-[#1F2937] text-[28rpx] font-medium leading-[1.4] line-clamp-2 mb-[16rpx]">
{{ item.title }}
</div>
</view>
<!-- Desc -->
<div class="mt-auto self-start text-[22rpx] px-[12rpx] py-[4rpx] rounded-full"
<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 }}
</div>
</div>
</div>
</div>
</div>
<!-- Tab Bar -->
<!-- <TabBar current="home" /> -->
</div>
</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 } from 'vue'
import { ref, computed, nextTick } from 'vue'
import NavHeader from '@/components/NavHeader.vue'
import FilterTabs from '@/components/FilterTabs.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
const activeTab = ref(0)
const tabs = ['全部产品', '人寿保险', '医疗保险', '意外保险']
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 颜色调色板
......@@ -107,23 +146,65 @@ const generateProducts = () => {
{ 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 products = ref(generateProducts())
const allProducts = ref(generateProducts())
/**
* 根据当前标签筛选产品
*
* @description 根据选中的标签页筛选显示对应产品
* 初始化数据分布
* @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(() => {
if (activeTab.value === 0) return products.value
// Mock filtering
return products.value.filter((_, i) => (i + activeTab.value) % 2 === 0)
// 找到当前选中的 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 配置为产品类型列表,点击时跳转到产品详情页
......@@ -134,4 +215,80 @@ const { handleClick: handleProductClick } = useListItemClick({
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;
// 隐藏滚动条
&::-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>
......