index.vue 7.8 KB
<!--
 * @Date: 2026-01-31
 * @Description: 我的收藏 - 已改造为 NutTabs 版本
-->
<template>
  <view class="h-screen bg-gray-50 flex flex-col">
    <view class="bg-gray-50 z-10">
      <NavHeader title="我的收藏" />

      <!-- Tabs Container -->
      <view class="bg-white mt-[2rpx]">
        <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>

    <view
      v-if="listVisible"
      :key="listRenderKey"
      class="flex-1 min-h-0 overflow-y-auto px-[24rpx] py-[24rpx] pb-[200rpx]"
    >
      <view v-for="(item, index) in filteredList" :key="index"
        class="bg-white rounded-[24rpx] p-[24rpx] mb-[24rpx] shadow-sm favorite-item"
        :style="{ animationDelay: `${index * 50}ms` }">

        <!-- Header with Icon -->
        <view class="flex gap-[24rpx] mb-[12rpx]">
          <!-- Document Icon -->
          <view class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start">
            <image :src="getDocumentIcon(item.title)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
          </view>

          <!-- Title -->
          <view class="flex-1 min-w-0">
            <view class="text-[30rpx] font-bold text-gray-900 leading-normal mb-1">{{ item.title }}</view>
            <view class="bg-blue-50 text-blue-600 text-[22rpx] px-[12rpx] py-[4rpx] rounded-[8rpx] inline-block">{{ item.category }}</view>
          </view>
        </view>

        <!-- Date -->
        <view class="text-gray-500 text-[24rpx] mb-[20rpx] text-right">
          <text>{{ item.date }}</text>
        </view>

        <!-- Divider -->
        <view class="h-[1rpx] bg-gray-100 mb-[20rpx]"></view>

        <!-- Actions -->
        <ListItemActions
          :viewable="true"
          :deletable="true"
          @view="viewFile({...item, fileName: item.title})"
          @delete="onDelete(item)"
        />
      </view>

      <!-- Empty State -->
      <view v-if="filteredList.length === 0">
        <nut-empty description="暂无收藏内容" image="empty" />
      </view>
    </view>

    <!-- TabBar -->
    <!-- <TabBar current="me" /> -->
  </view>
</template>

<script setup>
import { ref, computed, nextTick } from 'vue'
import Taro from '@tarojs/taro'
import { useGo } from '@/hooks/useGo'
import { useFileOperation } from '@/composables/useFileOperation'
import { getDocumentIcon } from '@/utils/documentIcons'
import IconFont from '@/components/IconFont.vue'
import NavHeader from '@/components/NavHeader.vue'
import ListItemActions from '@/components/ListItemActions/index.vue'

const go = useGo()
const { viewFile } = useFileOperation()
const activeTabId = ref('all')
const listVisible = ref(true)
const listRenderKey = ref(0)

/**
 * Tab 数据源
 * @description 包含分类信息和对应的收藏列表
 */
const tabsData = ref([
  { id: 'all', name: '全部', list: [] },
  { id: 'onboarding', name: '入职培训', list: [] },
  { id: 'signing', name: '签单相关', list: [] },
  { id: 'product', name: '产品知识', list: [] }
])

/**
 * Mock 数据:收藏列表
 *
 * @description 包含不同类型的文档文件
 */
const allList = ref([
  {
    id: 1,
    title: '新员工入职培训手册.pdf',
    category: '入职培训',
    date: '2024-01-15',
    type: 'onboarding',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/test.pdf'
  },
  {
    id: 2,
    title: '保险产品销售话术.docx',
    category: '签单相关',
    date: '2024-01-14',
    type: 'signing',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE%E6%9C%80%E7%BB%88v3.1.docx'
  },
  {
    id: 3,
    title: '重疾险产品知识详解.pptx',
    category: '产品知识',
    date: '2024-01-13',
    type: 'product',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/%E8%82%A1%E5%88%A4%E5%90%88%E5%8F%8B%E7%94%A8%E7%9F%A5%E8%AF%86%E8%AF%B4%E6%98%8E20240112110417414.pptx'
  },
  {
    id: 4,
    title: '2024年最新保险政策解读.txt',
    category: '政策解读',
    date: '2024-01-12',
    type: 'other',
    downloadUrl: ''
  },
  {
    id: 5,
    title: '重疾险产品知识详解.pptx',
    category: '产品知识',
    date: '2024-01-13',
    type: 'product',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/%E8%82%A1%E5%88%A4%E5%90%88%E5%8F%8B%E7%94%A8%E7%9F%A5%E8%AF%86%E8%AF%B4%E6%98%8E20240112110417414.pptx'
  },
  {
    id: 6,
    title: '2024年最新保险政策解读.txt',
    category: '政策解读',
    date: '2024-01-12',
    type: 'other',
    downloadUrl: ''
  }
])

/**
 * 初始化数据分布
 * @description 根据分类规则将 allList 中的数据分配到各个 tab 中
 */
const initTabsData = () => {
  tabsData.value.forEach((tab) => {
    if (tab.id === 'all') {
      tab.list = [...allList.value]
    } else if (tab.id === 'onboarding') {
      // 入职培训:type 为 onboarding 或 other
      tab.list = allList.value.filter(item => item.type === 'onboarding' || item.type === 'other')
    } else {
      tab.list = allList.value.filter(item => item.type === tab.id)
    }
  })
}

const filteredList = 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
  })
}

/**
 * 删除收藏
 */
const onDelete = (item) => {
  Taro.showModal({
    title: '提示',
    content: '确定要删除该收藏吗?',
    success: (res) => {
      if (res.confirm) {
        // 从 allList 中删除
        const index = allList.value.findIndex(i => i.id === item.id)
        if (index !== -1) {
          allList.value.splice(index, 1)
          // 重新初始化 tabsData
          initTabsData()
          Taro.showToast({ title: '已删除', icon: 'success' })
        }
      }
    }
  })
}

// 初始化数据
initTabsData()
</script>

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

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

.favorite-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 24rpx;
  gap: 24rpx;
  transition: all 0.3s ease;
  background-color: #fff;
  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>