JewelryMall/miniprogram/pages/product/detail.vue
2026-03-05 00:43:04 +08:00

344 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="product-detail" v-if="product">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="custom-navbar__content" :style="{ height: navBarHeight + 'px' }">
<image class="custom-navbar__back" src="/static/ic_back.png" mode="aspectFit" @click="goBack" />
<text class="custom-navbar__title">商品详情</text>
<view class="custom-navbar__placeholder" />
</view>
</view>
<!-- 导航栏占位 -->
<view :style="{ height: (statusBarHeight + navBarHeight) + 'px' }" />
<!-- Banner 轮播 -->
<view class="banner-wrapper">
<BannerSwiper :images="product.bannerImages || []" :video="product.bannerVideo" />
</view>
<!-- 基础信息 -->
<view class="base-info">
<view class="base-info__top">
<text class="base-info__name">{{ product.name }}</text>
<view class="base-info__price">
<text class="base-info__price-symbol">¥</text>
<text class="base-info__price-num">{{ product.basePrice }}</text>
<text class="base-info__price-unit"></text>
</view>
</view>
<view class="base-info__attrs">
<view class="attr-row">
<text class="attr-label"> </text>
<text class="attr-value">{{ product.styleNo }}</text>
<text class="attr-label">库存</text>
<text class="attr-value">{{ product.stock }}</text>
</view>
<view class="attr-row">
<text class="attr-label"> </text>
<text class="attr-value">{{ product.loss }}%</text>
<text class="attr-label">工费</text>
<text class="attr-value">¥{{ product.laborCost }}</text>
</view>
</view>
</view>
<!-- 发货公告 -->
<ShippingNotice />
<!-- 商品详情大图展示 -->
<view class="detail-section">
<view class="detail-section__title">商品详情</view>
<view class="detail-section__images">
<image
v-for="(img, idx) in (product.detailImages || [])"
:key="idx"
class="detail-section__img"
:src="fullUrl(img)"
mode="widthFix"
/>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="bottom-bar__icons">
<view class="bottom-bar__icon-item" @click="showQrCode = true">
<image class="bottom-bar__icon-img" src="/static/ic_customer.png" mode="aspectFit" />
<text class="bottom-bar__icon-text">客服</text>
</view>
<view class="bottom-bar__icon-item" @click="goCart">
<view class="cart-icon-wrap">
<image class="bottom-bar__icon-img" src="/static/tab/car.png" mode="aspectFit" />
<text v-if="cartCount > 0" class="cart-badge">{{ cartCount > 99 ? '99+' : cartCount }}</text>
</view>
<text class="bottom-bar__icon-text">购物车</text>
</view>
</view>
<view class="bottom-bar__main-btn" @click="showSpecPanel = true">
<text>空托—查看详细参数</text>
</view>
</view>
<!-- 详细参数面板 -->
<SpecPanel v-if="showSpecPanel" :product-id="product.id" :product="product" @close="showSpecPanel = false" />
<!-- 客服二维码弹窗 -->
<CustomerServiceBtn v-if="showQrCode" mode="qrcode" @close="showQrCode = false" />
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import type { Product } from '../../types'
import { getProductDetail } from '../../api/product'
import { BASE_URL } from '../../utils/request'
import { useCartStore } from '../../store/cart'
import BannerSwiper from '../../components/BannerSwiper.vue'
import ShippingNotice from '../../components/ShippingNotice.vue'
import SpecPanel from '../../components/SpecPanel.vue'
import CustomerServiceBtn from '../../components/CustomerServiceBtn.vue'
const cartStore = useCartStore()
const cartCount = computed(() => cartStore.items.length)
const product = ref<Product | null>(null)
const showSpecPanel = ref(false)
const showQrCode = ref(false)
const statusBarHeight = ref(20)
const navBarHeight = ref(44)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 20
// #ifdef MP-WEIXIN
const menuBtn = uni.getMenuButtonBoundingClientRect()
navBarHeight.value = (menuBtn.top - (sysInfo.statusBarHeight || 20)) * 2 + menuBtn.height
// #endif
} catch { /* fallback */ }
function goBack() {
uni.navigateBack({ delta: 1 })
}
function fullUrl(path: string): string {
if (!path) return ''
if (path.startsWith('http')) return path
return BASE_URL + path
}
function goCart() {
uni.switchTab({ url: '/pages/cart/index' })
}
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] as { options?: { id?: string } }
const id = Number(currentPage.options?.id)
if (id) loadProduct(id)
cartStore.fetchCart()
})
async function loadProduct(id: number) {
try {
product.value = await getProductDetail(id)
} catch {
uni.showToast({ title: '加载商品失败', icon: 'none' })
}
}
</script>
<style scoped>
.product-detail {
padding-bottom: 140rpx;
background: #f5f5f5;
}
/* 自定义导航栏 */
.custom-navbar {
background: linear-gradient(to right, #FFCFDE, #FFA6C4);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
.custom-navbar__content {
display: flex;
align-items: center;
padding: 0 24rpx;
}
.custom-navbar__back {
width: 44rpx;
height: 44rpx;
}
.custom-navbar__title {
flex: 1;
text-align: center;
font-size: 34rpx;
font-weight: bold;
color: #333;
}
.custom-navbar__placeholder {
width: 44rpx;
}
/* Banner 容器 */
.banner-wrapper {
margin: 20rpx 24rpx 0;
border-radius: 20rpx;
overflow: hidden;
}
/* 基础信息卡片 */
.base-info {
background: #fff;
margin: 20rpx 24rpx 0;
border-radius: 20rpx;
padding: 30rpx;
}
.base-info__top {
display: flex;
justify-content: space-between;
align-items: center;
}
.base-info__name {
font-size: 32rpx;
color: #333;
font-weight: 600;
flex: 1;
margin-right: 20rpx;
}
.base-info__price {
display: flex;
align-items: baseline;
color: #FF6D9B;
flex-shrink: 0;
}
.base-info__price-symbol {
font-size: 30rpx;
font-weight: bold;
}
.base-info__price-num {
font-size: 48rpx;
font-weight: bold;
}
.base-info__price-unit {
font-size: 24rpx;
margin-left: 4rpx;
}
/* 属性行 - 两列布局 */
.base-info__attrs {
margin-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
padding-top: 20rpx;
}
.attr-row {
display: flex;
align-items: center;
padding: 8rpx 0;
}
.attr-label {
font-size: 26rpx;
color: #999;
width: 80rpx;
flex-shrink: 0;
letter-spacing: 4rpx;
}
.attr-value {
font-size: 28rpx;
color: #333;
min-width: 180rpx;
margin-right: 40rpx;
}
/* 商品详情大图 */
.detail-section {
background: #fff;
margin: 20rpx 24rpx 0;
border-radius: 20rpx;
padding: 30rpx;
}
.detail-section__title {
text-align: center;
font-size: 30rpx;
color: #333;
font-weight: 600;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #eee;
margin-bottom: 24rpx;
}
.detail-section__images {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.detail-section__img {
width: 100%;
border-radius: 12rpx;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 16rpx 24rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 20rpx;
}
.bottom-bar__icons {
display: flex;
gap: 32rpx;
flex-shrink: 0;
}
.bottom-bar__icon-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.bottom-bar__icon-img {
width: 44rpx;
height: 44rpx;
}
.cart-icon-wrap {
position: relative;
}
.cart-badge {
position: absolute;
top: -10rpx;
right: -16rpx;
background: #e91e63;
color: #fff;
font-size: 20rpx;
min-width: 32rpx;
height: 32rpx;
line-height: 32rpx;
text-align: center;
border-radius: 16rpx;
padding: 0 8rpx;
box-sizing: border-box;
}
.bottom-bar__icon-text {
font-size: 20rpx;
color: #666;
}
.bottom-bar__main-btn {
flex: 1;
background: linear-gradient(to right, #f5a0b8, #e4393c);
color: #fff;
text-align: center;
padding: 24rpx 0;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: 500;
}
</style>