index.vue 12.7 KB
<!--
 * @Date: 2026-01-30
 * @Description: 资料列表页
-->
<template>
  <div class="min-h-screen bg-[#F9FAFB] pb-[calc(160rpx+env(safe-area-inset-bottom))]">
    <!-- Navigation Header -->
    <NavHeader title="资料列表" />

    <!-- Search Bar -->
    <div class="px-[32rpx] mt-[32rpx]">
      <div class="bg-white rounded-[20rpx] flex items-center px-[32rpx] py-[24rpx] shadow-sm border border-gray-50">
        <IconFont name="search" size="20" color="#9CA3AF" class="mr-[16rpx]" />
        <input v-model="searchValue" type="text" placeholder="搜索资料..."
          class="flex-1 text-[28rpx] text-[#1F2937] placeholder-gray-400 bg-transparent outline-none"
          @confirm="onSearch" />
      </div>
    </div>

    <!-- Material List -->
    <div class="px-[32rpx] mt-[32rpx]">
      <div class="flex flex-col gap-[24rpx]">
        <div v-for="(item, index) in list" :key="index"
          class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm flex items-start active:scale-[0.98] transition-all duration-200 border border-gray-50"
          :style="{ animationDelay: `${index * 50}ms` }" @click="onView(item)">

          <!-- 左侧图标 -->
          <div
            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">
            <IconFont :name="item.iconName || 'order'" size="32" :color="item.iconColor || '#2563EB'" />
          </div>

          <!-- 内容区域 -->
          <div class="flex-1 min-w-0">
            <div class="flex justify-between items-start gap-[16rpx]">
              <div class="flex-1 pr-[8rpx]">
                <h3 class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] line-clamp-2 mb-[8rpx]">
                  {{ item.title }}
                </h3>
                <p class="text-[#6B7280] text-[24rpx] leading-[1.4] line-clamp-1 mb-[16rpx]">
                  {{ item.desc }}
                </p>
              </div>

              <!-- 收藏图标 -->
              <div
                class="heart-btn w-[64rpx] h-[64rpx] -mt-[12rpx] -mr-[12rpx] flex items-center justify-center rounded-full active:bg-gray-50 transition-colors"
                @click.stop="toggleCollect(item)">
                <div :class="{ 'is-collected': item.collected }" class="transform transition-transform duration-300">
                  <IconFont :name="item.collected ? 'heart-fill' : 'heart'" size="24"
                    :color="item.collected ? '#EF4444' : '#D1D5DB'" />
                </div>
              </div>
            </div>

            <!-- 底部信息:文件大小 -->
            <div class="flex items-center gap-[12rpx]">
              <span
                class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]">
                PDF
              </span>
              <span class="text-[#9CA3AF] text-[22rpx]">
                {{ item.size }}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- Tab Bar -->
    <!-- <TabBar /> -->
  </div>
</template>

<script setup>
import { ref } from 'vue'
import NavHeader from '@/components/NavHeader.vue'
import TabBar from '@/components/TabBar.vue'
import IconFont from '@/components/IconFont.vue'
import Taro from '@tarojs/taro'

const searchValue = ref('')

const list = ref([
  {
    title: '2024年保险代理人考试大纲.pdf',
    desc: '最新考试范围与重点解析',
    size: '2.1MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: true,
    // 添加文件相关数据
    fileName: '2024年保险代理人考试大纲.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '历年真题汇总及解析.pdf',
    desc: '2019-2023年真题完整版',
    size: '5.3MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: false,
    fileName: '历年真题汇总及解析.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '考试技巧与经验分享.pdf',
    desc: '高分学员备考心得',
    size: '1.8MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: false,
    fileName: '考试技巧与经验分享.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '保险基础知识速记手册.pdf',
    desc: '核心知识点快速记忆',
    size: '3.2MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: false,
    fileName: '保险基础知识速记手册.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '模拟试卷10套及答案.pdf',
    desc: '考前冲刺模拟练习',
    size: '4.5MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: true,
    fileName: '模拟试卷10套及答案.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '法律法规重点条款解读.pdf',
    desc: '保险相关法规详解',
    size: '2.8MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: false,
    fileName: '法律法规重点条款解读.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '考试常见易错题分析.pdf',
    desc: '高频错题归纳总结',
    size: '1.5MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: false,
    fileName: '考试常见易错题分析.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '案例分析题库及解答.pdf',
    desc: '实务案例精选练习',
    size: '3.9MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: false,
    fileName: '案例分析题库及解答.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '考前冲刺复习资料.pdf',
    desc: '最后一周复习要点',
    size: '2.3MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: false,
    fileName: '考前冲刺复习资料.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  },
  {
    title: '考场注意事项及答题技巧.pdf',
    desc: '应试策略与时间分配',
    size: '1.2MB',
    iconName: 'order',
    iconColor: '#EF4444',
    collected: false,
    fileName: '考场注意事项及答题技巧.pdf',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
  }
])

/**
 * Search handler
 */
const onSearch = () => {
  console.log('Searching for:', searchValue.value)
}

/**
 * Toggle collect status
 * @param {Object} item
 */
const toggleCollect = (item) => {
  item.collected = !item.collected
}

// 打开文件的通用函数
const openFile = async (filePath, item) => {
  try {
    await Taro.openDocument({
      filePath: filePath,
      showMenu: true, // 显示右上角菜单,用户可以转发、保存等
      success: () => {
        console.log('文件打开成功')
        // 文件打开后,延迟提示用户如果看不到内容该如何操作
        const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''
        const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']

        if (unsupportedFormats.includes(fileExt)) {
          setTimeout(() => {
            Taro.showToast({
              title: '如无法预览,请使用右上角菜单分享',
              icon: 'none',
              duration: 3000
            })
          }, 1500)
        }
      },
      fail: (err) => {
        console.error('打开文件失败:', err)

        // 获取文件扩展名
        const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''

        // 根据文件类型给出提示
        let message = '文件打开失败'
        let suggestion = ''

        if (['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'].includes(fileExt)) {
          message = '暂不支持预览 Office 文档'
          suggestion = '\n\n建议:\n1. 点击右上角"..."菜单\n2. 选择"发送给朋友"\n3. 在电脑或支持的应用中打开'
        } else if (['pdf'].includes(fileExt)) {
          message = 'PDF 文件打开失败'
          suggestion = '\n\n文件可能已损坏,请联系管理员'
        } else {
          message = `暂不支持预览 ${fileExt.toUpperCase()} 格式文件`
          suggestion = '\n\n请在电脑或其他应用中打开'
        }

        Taro.showModal({
          title: '提示',
          content: message + suggestion,
          showCancel: false,
          confirmText: '我知道了'
        })
      }
    })
  } catch (error) {
    console.error('打开文件异常:', error)
    Taro.showToast({
      title: '打开文件失败',
      icon: 'none',
      duration: 2000
    })
  }
}

// 下载并打开文件的内部函数
const downloadAndOpenFile = async (item) => {
  try {
    // 下载文件
    const downloadResult = await Taro.downloadFile({
      url: item.downloadUrl
    })

    // 检查下载结果
    if (downloadResult.statusCode !== 200) {
      throw new Error(`打开失败: HTTP ${downloadResult.statusCode}`)
    }

    if (!downloadResult.tempFilePath) {
      throw new Error('打开失败: 未获取到文件')
    }

    // 隐藏加载提示
    Taro.hideLoading()

    // 获取文件扩展名
    const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''

    // 微信小程序对 Office 文档支持有限,提前提示用户
    const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']

    if (unsupportedFormats.includes(fileExt)) {
      // 对于 Office 文档,先提示用户,但仍尝试打开
      Taro.showModal({
        title: '预览提示',
        content: `小程序对 ${fileExt.toUpperCase()} 文档的预览支持有限,如果显示为空白,请点击右上角"..."菜单,选择"发送给朋友"后在电脑或其他应用中打开。\n\n是否继续尝试预览?`,
        confirmText: '继续',
        cancelText: '取消',
        success: (modalRes) => {
          if (modalRes.confirm) {
            openFile(downloadResult.tempFilePath, item)
          }
        }
      })
    } else {
      // 其他格式直接打开
      await openFile(downloadResult.tempFilePath, item)
    }
  } catch (error) {
    // 确保隐藏加载提示
    Taro.hideLoading()

    console.error('打开文件出错:', error)

    // 根据错误类型显示不同的提示
    let errorMessage = '打开失败,请重试'
    if (error.errMsg && error.errMsg.includes('network')) {
      errorMessage = '网络连接失败,请检查网络'
    } else if (error.errMsg && error.errMsg.includes('TLS')) {
      errorMessage = '安全连接失败,请检查网络'
    }

    Taro.showToast({
      title: errorMessage,
      icon: 'none',
      duration: 2000
    })
  }
}

const onView = (item) => {
  // 检查是否有下载地址
  if (!item.downloadUrl) {
    Taro.showToast({
      title: '该文件暂无查看地址',
      icon: 'none',
      duration: 2000
    })
    return
  }

  // 显示加载提示
  Taro.showLoading({
    title: '打开中...',
    mask: true
  })

  // 下载并打开文件
  downloadAndOpenFile(item)
}
</script>

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

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

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

/* 收藏成功的动画 */
.is-collected {
  animation: heartBeat 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

@keyframes heartBeat {
  0% {
    transform: scale(1);
  }

  14% {
    transform: scale(1.3);
  }

  28% {
    transform: scale(1);
  }

  42% {
    transform: scale(1.3);
  }

  70% {
    transform: scale(1);
  }
}
</style>