index.vue 11.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-[12rpx] flex items-center px-[32rpx] py-[24rpx]">
        <IconFont name="Search" size="20" color="#9CA3AF" customClass="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-[40rpx]">
      <div class="bg-white rounded-[32rpx] p-[32rpx] shadow-sm">
        <div v-for="(item, index) in list" :key="index">
           <div class="flex justify-between items-start pt-[32rpx] first:pt-0">
              <div class="flex items-start flex-1 mr-[20rpx]" @tap="onView(item)">
                <div class="w-[80rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-blue-50 rounded-[12rpx]">
                  <IconFont :name="item.iconName || 'Order'" size="32" :color="item.iconColor || '#2563EB'" />
                </div>
                <div class="flex flex-col">
                  <span class="text-[#1F2937] text-[28rpx] font-normal leading-[1.2] mb-[14rpx] line-clamp-2">
                    {{ item.title }}
                  </span>
                  <span class="text-[#6B7280] text-[24rpx] leading-[1.3] line-clamp-1">
                    {{ item.desc }}
                  </span>
                </div>
              </div>
            </div>

            <div class="flex items-center mt-[16rpx] ml-[104rpx] pb-[32rpx]">
              <span class="text-[#9CA3AF] text-[24rpx] mr-[40rpx]">{{ item.size }}</span>
              <div class="flex items-center" @click="toggleCollect(item)">
                <IconFont
                  :name="item.collected ? 'StarFill' : 'Star'"
                  size="12"
                  :color="item.collected ? '#F59E0B' : '#9CA3AF'"
                  customClass="mr-[8rpx]"
                />
                <span :class="['text-[24rpx]', item.collected ? 'text-[#F59E0B]' : 'text-[#9CA3AF]']">
                  {{ item.collected ? '已收藏' : '收藏' }}
                </span>
              </div>
            </div>

            <!-- Divider -->
            <div v-if="index < list.length - 1" class="h-[1px] bg-[#E5E7EB] w-full mb-[32rpx]"></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>