index.vue 12.3 KB
<!--
 * @Date: 2026-01-30
 * @Description: 产品详情页
-->
<template>
  <div class="min-h-screen bg-[#F9FAFB] pb-[calc(160rpx+env(safe-area-inset-bottom))]">
    <NavHeader title="产品详情" />

    <!-- Banner Image -->
    <div class="w-full h-[420rpx] relative">
      <img
        class="w-full h-full object-cover"
        :src="bannerImage"
        mode="aspectFill"
      />
      <div class="absolute top-[32rpx] right-[32rpx] flex items-center gap-[16rpx]">
        <!-- Hot Tag -->
        <div class="bg-red-500 text-white text-[24rpx] px-[20rpx] py-[10rpx] rounded-full shadow-sm backdrop-blur-sm bg-opacity-90">
          热卖
        </div>
      </div>
    </div>

    <!-- Product Header -->
    <div class="relative mt-[-40rpx] bg-white rounded-t-[40rpx] px-[40rpx] pt-[48rpx] pb-[40rpx] z-10">
      <div class="flex items-start justify-between mb-[24rpx]">
        <h1 class="text-[#1F2937] text-[44rpx] font-bold flex-1 mr-[24rpx] leading-[1.2]">终身寿险尊享版</h1>
        <!-- Favorite Button -->
        <div
          class="w-[72rpx] h-[72rpx] flex-shrink-0 flex items-center justify-center rounded-full bg-gray-50 active:scale-95 transition-transform"
          @tap="toggleCollect"
        >
          <IconFont
            :name="isCollected ? 'heart-fill' : 'heart'"
            size="24"
            :color="isCollected ? '#EF4444' : '#9CA3AF'"
          />
        </div>
      </div>

      <div class="flex flex-wrap gap-[16rpx]">
        <div class="px-[16rpx] py-[6rpx] bg-red-50 rounded-[8rpx]">
          <span class="text-red-600 text-[24rpx]">收益率3.5%</span>
        </div>
        <div class="px-[16rpx] py-[6rpx] bg-orange-50 rounded-[8rpx]">
          <span class="text-orange-600 text-[24rpx]">5年超值</span>
        </div>
        <div class="px-[16rpx] py-[6rpx] bg-green-50 rounded-[8rpx]">
          <span class="text-green-600 text-[24rpx]">保证收益万能</span>
        </div>
      </div>
    </div>

    <!-- Stats Grid -->
    <div class="px-[32rpx] mt-[24rpx]">
      <div class="grid grid-cols-2 gap-[24rpx]">
        <div
          v-for="(item, index) in stats"
          :key="index"
          class="bg-white rounded-[24rpx] p-[32rpx] border border-gray-100"
        >
          <div class="text-[#6B7280] text-[24rpx] mb-[12rpx]">{{ item.label }}</div>
          <div class="text-[#1F2937] text-[30rpx] font-medium">{{ item.value }}</div>
        </div>
      </div>
    </div>

    <!-- Product Features -->
    <div class="px-[32rpx] mt-[32rpx]">
      <div class="bg-white rounded-[32rpx] p-[40rpx]">
        <h2 class="text-[#1F2937] text-[32rpx] font-bold mb-[32rpx]">产品特色</h2>
        <div class="flex flex-col gap-[32rpx]">
          <div v-for="(feature, index) in features" :key="index" class="flex items-start">
            <div class="w-[48rpx] h-[48rpx] rounded-full bg-blue-50 flex items-center justify-center mr-[24rpx] flex-shrink-0">
              <IconFont name="Check" size="14" color="#2563EB" />
            </div>
            <div class="flex-1">
              <div class="text-[#1F2937] text-[28rpx] font-medium leading-[1.4]">{{ feature.title }}</div>
              <div class="text-[#6B7280] text-[24rpx] mt-[8rpx] leading-[1.4]">{{ feature.desc }}</div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- Attachments -->
    <div class="px-[32rpx] mt-[32rpx]">
      <div class="bg-white rounded-[32rpx] p-[40rpx]">
        <h2 class="text-[#1F2937] text-[32rpx] font-bold mb-[32rpx]">相关附件</h2>
        <div class="flex flex-col gap-[24rpx]">
          <div
            v-for="(file, index) in files"
            :key="index"
            class="flex flex-col p-[24rpx] bg-gray-50 rounded-[16rpx]"
          >
            <div class="flex items-center justify-between mb-[8rpx]">
              <div class="flex items-center flex-1 mr-[24rpx]">
                <IconFont :name="file.iconName" size="24" :color="file.iconColor" class="mr-[24rpx]" />
                <div class="flex flex-col">
                  <span class="text-[#1F2937] text-[28rpx] font-medium mb-[4rpx] line-clamp-1">{{ file.name }}</span>
                  <span class="text-[#9CA3AF] text-[24rpx]">{{ file.size }}</span>
                </div>
              </div>
              <IconFont name="download" size="20" color="#2563EB" @tap="onDownload(file)" />
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- TabBar -->
    <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, { useLoad } from '@tarojs/taro'

// 接收页面参数
const productId = ref(null)

useLoad((options) => {
  console.log('产品详情页参数:', options)

  if (options.id) {
    productId.value = options.id
    console.log('产品ID:', productId.value)

    // TODO: 根据 productId 获取产品详情数据
    // 这里可以调用 API 获取对应产品的数据
    fetchProductDetail(options.id)
  } else {
    console.warn('未接收到产品ID')
    Taro.showToast({
      title: '产品ID不存在',
      icon: 'none',
      duration: 2000
    })
  }
})

// 根据 ID 获取产品详情(模拟)
const fetchProductDetail = async (id) => {
  console.log('正在获取产品ID', id, '的详情...')

  // TODO: 实际调用 API
  // const res = await getProductDetailAPI({ i: id })
  // if (res.code === 1) {
  //   // 更新产品数据
  // }

  // 模拟根据不同ID显示不同产品
  const productNames = {
    '1': '家庭财富传承保障计划(分红)',
    '2': '儿童教育金储备方案(分红)'
  }

  if (productNames[id]) {
    console.log('产品名称:', productNames[id])
  }
}

// Random banner image
const bannerImage = `https://picsum.photos/seed/${Math.floor(Math.random() * 1000)}/750/420`

const stats = ref([
  { label: '投保年龄', value: '30天-70周岁' },
  { label: '保障期限', value: '终身' },
  { label: '缴费方式', value: '3/5/10年交' },
  { label: '起投金额', value: '10000元起' }
])

const features = ref([
  { title: '身故保险金', desc: '赔付100%基本保额,给家人留爱不留债' },
  { title: '全残保险金', desc: '赔付100%基本保额,生活有保障' },
  { title: '保费豁免', desc: '确诊重疾后免交剩余保费,保障继续有效' },
  { title: '保单贷款', desc: '最高可贷现金价值80%,资金周转灵活' }
])

const files = ref([
  {
    name: '产品条款.pdf',
    size: '2.3MB',
    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',
    iconName: 'order',
    iconColor: '#EF4444',
    fileType: 'pdf',
    showTip: false,
    tipText: ''
  },
  {
    name: '投保须知.docx',
    size: '1.8MB',
    fileName: '投保须知.docx',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/%E8%80%81%E6%9D%A5%E8%B5%9B%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96.docx',
    iconName: 'order',
    iconColor: '#2563EB',
    fileType: 'docx',
    showTip: true,
    tipText: 'Word 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看'
  },
  {
    name: '健康告知.pptx',
    size: '3.2MB',
    fileName: '健康告知.pptx',
    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',
    iconName: 'order',
    iconColor: '#F59E0B',
    fileType: 'pptx',
    showTip: true,
    tipText: 'PPT 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看'
  },
  {
    name: '保险责任说明.xlsx',
    size: '1.5MB',
    fileName: '保险责任说明.xlsx',
    downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/%E8%80%81%E6%9D%A5%E8%B5%9B%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96.docx',
    iconName: 'order',
    iconColor: '#10B981',
    fileType: 'xlsx',
    showTip: true,
    tipText: 'Excel 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看'
  }
])

// 收藏状态
const isCollected = ref(false)

// 记录是否已经提示过 Office 文档预览限制
const hasShownOfficeTip = ref(false)

// 切换收藏状态
const toggleCollect = () => {
  isCollected.value = !isCollected.value
  if (isCollected.value) {
    Taro.showToast({
      title: '已收藏',
      icon: 'success'
    })
  } else {
    Taro.showToast({
      title: '已取消收藏',
      icon: 'none'
    })
  }
}

// 打开文件的通用函数
const openFile = async (filePath, file) => {
  try {
    await Taro.openDocument({
      filePath: filePath,
      showMenu: true,
      success: () => {
        console.log('文件打开成功')
      },
      fail: (err) => {
        console.error('打开文件失败:', err)

        const fileExt = file.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 (file) => {
  try {
    const downloadResult = await Taro.downloadFile({
      url: file.downloadUrl
    })

    if (downloadResult.statusCode !== 200) {
      throw new Error(`打开失败: HTTP ${downloadResult.statusCode}`)
    }

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

    Taro.hideLoading()

    const fileExt = file.fileName.split('.').pop()?.toLowerCase() || ''
    const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']

    if (unsupportedFormats.includes(fileExt)) {
      // 对于 Office 文档,如果还没提示过,就提示一次
      if (!hasShownOfficeTip.value) {
        Taro.showModal({
          title: '预览提示',
          content: `小程序对 ${fileExt.toUpperCase()} 文档的预览支持有限,如果显示为空白,请点击右上角"..."菜单,选择"发送给朋友"后在电脑或其他应用中打开。\n\n是否继续尝试预览?`,
          confirmText: '继续',
          cancelText: '取消',
          success: (modalRes) => {
            if (modalRes.confirm) {
              hasShownOfficeTip.value = true // 标记已提示过
              openFile(downloadResult.tempFilePath, file)
            }
          }
        })
      } else {
        // 已经提示过,直接打开
        await openFile(downloadResult.tempFilePath, file)
      }
    } else {
      // PDF 等其他格式直接打开
      await openFile(downloadResult.tempFilePath, file)
    }
  } 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 onDownload = (file) => {
  if (!file.downloadUrl) {
    Taro.showToast({
      title: '该文件暂无下载地址',
      icon: 'none',
      duration: 2000
    })
    return
  }

  Taro.showLoading({
    title: '打开中...',
    mask: true
  })

  downloadAndOpenFile(file)
}
</script>