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

    <!-- Loading State -->
    <div v-if="loading" class="flex items-center justify-center h-screen">
      <div class="flex flex-col items-center">
        <div class="w-[64rpx] h-[64rpx] border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin mb-[24rpx]"></div>
        <text class="text-gray-600 text-[28rpx]">加载中...</text>
      </div>
    </div>

    <!-- Content -->
    <div v-else>
      <!-- Banner Image -->
      <div class="w-full h-[420rpx] relative">
        <img
          class="w-full h-full object-cover"
          :src="productDetail.cover_image || bannerImage"
          mode="aspectFill"
        />
        <div class="absolute top-[32rpx] right-[32rpx] flex items-center gap-[16rpx]">
          <!-- Hot Tag -->
          <div
            v-if="productDetail.recommend === 'hot'"
            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">
        <h1 class="text-[#1F2937] text-[44rpx] font-bold leading-[1.2] mb-[24rpx]">
          {{ productDetail.product_name || '产品详情' }}
        </h1>

        <!-- 动态标签 -->
        <div v-if="productDetail.tags && productDetail.tags.length" class="flex flex-wrap gap-[16rpx]">
          <div
            v-for="tag in productDetail.tags"
            :key="tag.id"
            class="rounded-[8rpx] px-[16rpx] py-[6rpx]"
            :style="{
              backgroundColor: tag.bg_color,
              color: tag.text_color
            }"
          >
            <span class="text-[24rpx]">{{ tag.name }}</span>
          </div>
        </div>
      </div>

      <!-- Product Description (富文本) -->
      <div v-if="productDetail.product_description" class="px-[32rpx] mt-[32rpx]">
        <div class="bg-white rounded-[32rpx] p-[40rpx]">
          <h2 class="text-[#1F2937] text-[32rpx] font-bold mb-[32rpx]">产品描述</h2>
          <!-- 使用 rich-text 渲染富文本 -->
          <rich-text :nodes="productDetail.product_description" class="text-[#4B5563] text-[28rpx] leading-[1.6]"></rich-text>
        </div>
      </div>

      <!-- Attachments -->
      <div v-if="productDetail.documents && productDetail.documents.length" class="px-[32rpx] mt-[32rpx]">
        <div class="bg-white rounded-[32rpx] p-[40rpx]">
          <h2 class="text-[#1F2937] text-[32rpx] font-bold mb-[16rpx]">相关附件</h2>
          <!-- <text class="text-[#9CA3AF] text-[24rpx] mb-[24rpx] block">如需下载,可在预览页点击右上角「...」转发至其他设备</text> -->
          <div class="flex flex-col gap-[24rpx]">
            <div
              v-for="(doc, index) in productDetail.documents"
              :key="index"
              class="flex flex-col p-[24rpx] bg-gray-50 rounded-[16rpx]"
            >
              <div class="flex items-center justify-between">
                <div class="flex items-center flex-1 mr-[24rpx]">
                  <image
                    :src="getDocumentIcon({ extension: doc.extension, fileName: doc.file_name })"
                    class="w-[58rpx] h-[58rpx] mr-[24rpx]"
                    mode="aspectFit"
                  />
                  <div class="flex flex-col">
                    <span class="text-[#1F2937] text-[28rpx] font-medium mb-[4rpx] line-clamp-2">{{ doc.file_name }}</span>
                    <span class="text-[#9CA3AF] text-[24rpx]">{{ getDocumentLabel({ extension: doc.extension, fileName: doc.file_name }) }} · {{ doc.file_size_formatted }}</span>
                  </div>
                </div>
                <IconFont name="eye" size="14" color="#2563EB" @tap="viewDocument(doc)" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- TabBar -->
    <!-- <TabBar /> -->

    <!-- 计划书按钮 -->
    <div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-[32rpx] py-[24rpx] flex items-center justify-center">
      <nut-button
        color="#2563EB"
        class="!w-full !h-[88rpx] !rounded-[16rpx] !text-[28rpx] !font-bold"
        @tap="() => checkPlanPermission(() => openPlanPopup())"
      >
        制作计划书
      </nut-button>
    </div>

    <!-- 计划书表单容器 -->
    <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 -->
    <!-- 使用 v-if 条件渲染,避免 productDetail 为 null 时的 prop 类型检查错误 -->
    <view v-if="showPlanPopup && productDetail.id">
      <PlanFormContainer
        v-model:visible="showPlanPopup"
        :product="productDetail"
        @close="showPlanPopup = false"
        @submit="handlePlanSubmit"
      />
    </view>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import NavHeader from '@/components/navigation/NavHeader.vue'
import IconFont from '@/components/icons/IconFont.vue'
import PlanFormContainer from '@/components/plan/PlanFormContainer.vue'
import { useFileOperation } from '@/composables/useFileOperation'
import Taro, { useLoad } from '@tarojs/taro'
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
import { detailAPI } from '@/api/get_product'
import { usePlanSubmit } from '@/composables/usePlanSubmit'
import { usePlanPermission } from '@/composables/usePlanPermission'

const { viewFile } = useFileOperation()

// 计划书弹窗状态
const showPlanPopup = ref(false)

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

// 加载状态
const loading = ref(true)

// 产品详情数据
const productDetail = ref({
  id: null,
  product_name: '',
  recommend: '',
  product_description: '',
  cover_image: '',
  tags: [],
  documents: []
})

/**
 * 获取产品详情
 *
 * @description 调用 detailAPI 获取产品详情数据
 * @param {string} id - 产品ID
 */
const fetchProductDetail = async (id) => {
  try {
    loading.value = true

    const res = await detailAPI({
      i: id
    })

    if (res.code === 1 && res.data) {
      productDetail.value = res.data
    } else {
      Taro.showToast({
        title: res.msg || '获取产品详情失败',
        icon: 'none',
        duration: 2000
      })
    }
  } catch (err) {
    console.error('获取产品详情失败:', err)
    Taro.showToast({
      title: '网络错误,请重试',
      icon: 'none',
      duration: 2000
    })
  } finally {
    loading.value = false
  }
}

/**
 * 查看文档
 *
 * @description 打开文档预览,支持图片、视频、PDF 等多种格式
 * @param {Object} doc - 文档对象
 * @param {string} doc.file_name - 文档名称
 * @param {string} doc.file_url - 文档 URL
 */
const viewDocument = (doc) => {
  // 打开文件预览(自动判断文件类型)
  viewFile({
    fileName: doc.file_name,
    downloadUrl: doc.file_url
  })
}

/**
 * 打开计划书弹窗
 *
 * @description 检查登录权限后,打开当前产品的计划书表单
 */
const { checkPlanPermission } = usePlanPermission()

const openPlanPopup = () => {
    showPlanPopup.value = true
}

// 使用 composable 统一处理计划书提交后逻辑
const { handlePlanSubmit } = usePlanSubmit({
  getPopupState: () => showPlanPopup.value,
  setPopupState: (state) => { showPlanPopup.value = state },
  useGoUtil: false,
  pageName: 'Product Detail'
})

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

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

    // 获取产品详情数据
    fetchProductDetail(options.id)
  } else {
    console.warn('未接收到产品ID')
    loading.value = false
    Taro.showToast({
      title: '产品ID不存在',
      icon: 'none',
      duration: 2000
    })
  }
})

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