This commit is contained in:
solosw
2026-01-05 14:11:34 +08:00
parent 35ef825371
commit 99b11b04e4
658 changed files with 99266 additions and 0 deletions
+781
View File
@@ -0,0 +1,781 @@
<template>
<view class="container">
<!-- 商品图片轮播 -->
<view class="image-section">
<swiper class="image-swiper" :indicator-dots="imageList.length > 1" indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#FF4444" :autoplay="false" :circular="true">
<swiper-item v-for="(image, index) in imageList" :key="index">
<image :src="getFullImageUrl(image)" class="product-image" mode="aspectFill" @tap="previewImage(index)"></image>
</swiper-item>
</swiper>
<view class="image-count" v-if="imageList.length > 1">
<text>{{ currentImageIndex + 1 }}/{{ imageList.length }}</text>
</view>
</view>
<!-- 商品基本信息 -->
<view class="product-info-section">
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ productInfo.current_price || productInfo.selling_price }}</text>
</view>
<view class="original-price" v-if="productInfo.original_price && productInfo.original_price != productInfo.current_price">
<text class="original-price-text">原价 ¥{{ productInfo.original_price }}</text>
</view>
</view>
<view class="product-title">
<text class="product-name">{{ productInfo.product_name }}</text>
</view>
<view class="product-stats">
<text class="stats-item">销量 {{ productInfo.sales_count || 0 }}</text>
<text class="stats-item">库存 {{ productInfo.stock_quantity || productInfo.quantity || 0 }}</text>
<text class="stats-item">浏览 {{ productInfo.view_count || 0 }}</text>
</view>
</view>
<!-- 卖家信息(仅二级商品显示) -->
<view class="seller-section" v-if="productType === 'secondary' && productInfo.seller">
<view class="section-title">卖家信息</view>
<view class="seller-info">
<image :src="getFullImageUrl(productInfo.seller.avatar)" class="seller-avatar" mode="aspectFill" v-if="productInfo.seller.avatar"></image>
<view class="seller-avatar-placeholder" v-else>
<uni-icons type="person" size="40" color="#ccc"></uni-icons>
</view>
<view class="seller-details">
<text class="seller-name">{{ productInfo.seller.real_name || productInfo.seller.customer_name }}</text>
<text class="seller-code">ID: {{ productInfo.seller.identity_code }}</text>
</view>
</view>
</view>
<!-- 商品描述 -->
<view class="description-section" v-if="productInfo.product_description">
<view class="section-title">商品描述</view>
<view class="description-content">
<text class="description-text">{{ productInfo.product_description }}</text>
</view>
</view>
<!-- 购买数量选择弹窗 -->
<uni-popup ref="quantityPopup" type="bottom" background-color="#fff" border-radius="20rpx 20rpx 0 0">
<view class="quantity-popup">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">选择购买数量</text>
<view class="close-btn" @tap="closeQuantityPopup">
<uni-icons type="close" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="popup-product-info">
<image :src="getFullImageUrl(getFirstImage(productInfo.product_images))" class="popup-product-image" mode="aspectFill"></image>
<view class="popup-product-details">
<text class="popup-product-name">{{ productInfo.product_name }}</text>
<text class="popup-product-price">¥{{ productInfo.current_price || productInfo.selling_price }}</text>
</view>
</view>
<view class="quantity-selector">
<text class="quantity-label">购买数量</text>
<view class="quantity-controls">
<view class="quantity-btn" @tap="decreaseQuantity" :class="{ disabled: selectedQuantity <= 1 }">
<uni-icons type="minus" size="16" color="#666"></uni-icons>
</view>
<input class="quantity-input" type="number" v-model="selectedQuantity" @input="onQuantityInput" />
<view class="quantity-btn" @tap="increaseQuantity" :class="{ disabled: selectedQuantity >= maxQuantity }">
<uni-icons type="plus" size="16" color="#666"></uni-icons>
</view>
</view>
<text class="stock-info">库存{{ maxQuantity }}件</text>
</view>
<view class="total-price">
<text class="total-label">总价:</text>
<text class="total-amount">¥{{ (selectedQuantity * (productInfo.current_price || productInfo.selling_price)).toFixed(2) }}</text>
</view>
</view>
<view class="popup-footer">
<button class="confirm-buy-btn" @tap="confirmPurchase">
立即购买
</button>
</view>
</view>
</uni-popup>
<!-- 底部购买栏 -->
<view class="bottom-bar">
<button class="buy-btn" @tap="showQuantityPopup" :disabled="!canPurchase">
<text v-if="!canPurchase">暂时无法购买</text>
<text v-else>立即购买</text>
</button>
</view>
<!-- 加载状态 -->
<view class="loading-overlay" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
</view>
</template>
<script>
import request from '@/utils/request.js'
export default {
data() {
return {
productId: '',
productType: '', // 'primary' 或 'secondary'
productInfo: {},
imageList: [],
currentImageIndex: 0,
selectedQuantity: 1,
maxQuantity: 0,
loading: false,
canPurchase: true
}
},
onLoad(options) {
// 检查登录状态
const token = uni.getStorageSync('token')
if (!token) {
uni.reLaunch({
url: '/pages/login/login'
})
return
}
// 显示安全提示弹窗
this.showSecurityAlert()
if (options.id && options.type) {
this.productId = options.id
this.productType = options.type
this.loadProductDetail()
} else {
uni.showToast({
title: '商品信息有误',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
},
methods: {
// 显示安全提示弹窗
showSecurityAlert() {
// 检查是否已经显示过,避免重复提示
const hasShownAlert = uni.getStorageSync('security_alert_shown')
if (hasShownAlert) {
return
}
uni.showModal({
title: '安全提示',
content: '为了您的账户安全,请在使用过程中:\n\n• 关闭WiFi连接\n• 关闭位置服务\n• 关闭蓝牙功能\n• 关闭热点功能\n\n这样可以防止信息泄露,保护您的隐私安全。',
showCancel: true,
cancelText: '知道了',
confirmText: '不再提示',
success: (res) => {
if (res.confirm) {
// 用户选择不再提示,记录到本地存储
uni.setStorageSync('security_alert_shown', true)
}
// 无论用户选择什么,都已经显示过提示了
}
})
},
// 获取完整的图片URL
getFullImageUrl(url) {
return request.getImageUrl(url)
},
// 获取商品的第一张图片
getFirstImage(images) {
if (!images) return ''
// 如果是逗号分隔的字符串
if (typeof images === 'string') {
return images.split(',')[0].trim()
}
// 如果是数组
if (Array.isArray(images) && images.length > 0) {
return images[0]
}
return ''
},
// 加载商品详情
async loadProductDetail() {
this.loading = true
try {
let url = ''
if (this.productType === 'primary') {
url = `/back/user/products/primary/${this.productId}`
} else {
url = `/back/user/products/secondary/${this.productId}`
}
const res = await request.get(url)
if (res.success) {
this.productInfo = res.data
this.processProductImages()
this.calculateMaxQuantity()
this.checkCanPurchase()
} else {
throw new Error(res.message || '加载商品详情失败')
}
} catch (error) {
console.error('加载商品详情失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 处理商品图片
processProductImages() {
if (this.productInfo.product_images) {
if (typeof this.productInfo.product_images === 'string') {
this.imageList = this.productInfo.product_images.split(',').map(img => img.trim()).filter(img => img)
} else if (Array.isArray(this.productInfo.product_images)) {
this.imageList = this.productInfo.product_images
}
}
// 如果没有图片,设置默认图片
if (this.imageList.length === 0) {
this.imageList = ['/static/images/default-product.png']
}
},
// 计算最大购买数量
calculateMaxQuantity() {
if (this.productType === 'primary') {
this.maxQuantity = this.productInfo.stock_quantity || 0
} else {
this.maxQuantity = this.productInfo.quantity || 0
}
},
// 检查是否可以购买
checkCanPurchase() {
if (this.maxQuantity <= 0) {
this.canPurchase = false
return
}
// 如果是二级商品,需要检查是否是自己的商品
if (this.productType === 'secondary') {
const userInfo = uni.getStorageSync('userInfo')
if (userInfo && this.productInfo.seller_id === userInfo.id) {
this.canPurchase = false
return
}
}
this.canPurchase = true
},
// 预览图片
previewImage(index) {
const urls = this.imageList.map(img => this.getFullImageUrl(img))
uni.previewImage({
urls: urls,
current: index
})
},
// 显示数量选择弹窗
showQuantityPopup() {
if (!this.canPurchase) return
this.selectedQuantity = 1
this.$refs.quantityPopup.open()
},
// 关闭数量选择弹窗
closeQuantityPopup() {
this.$refs.quantityPopup.close()
},
// 减少数量
decreaseQuantity() {
if (this.selectedQuantity > 1) {
this.selectedQuantity--
}
},
// 增加数量
increaseQuantity() {
if (this.selectedQuantity < this.maxQuantity) {
this.selectedQuantity++
}
},
// 数量输入处理
onQuantityInput(e) {
let value = parseInt(e.detail.value) || 1
if (value < 1) value = 1
if (value > this.maxQuantity) value = this.maxQuantity
this.selectedQuantity = value
},
// 确认购买
async confirmPurchase() {
if (this.selectedQuantity <= 0 || this.selectedQuantity > this.maxQuantity) {
uni.showToast({
title: '购买数量有误',
icon: 'none'
})
return
}
// 先检查是否有收货地址
try {
const addressRes = await request.get('/back/user/addresses')
if (!addressRes.success || !addressRes.data || addressRes.data.length === 0) {
uni.showModal({
title: '提示',
content: '请先设置收货地址',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/address/edit'
})
}
}
})
return
}
// 创建订单
const orderData = {
product_id: parseInt(this.productId),
product_type: this.productType === 'primary' ? 1 : 2,
product_name: this.productInfo.product_name,
product_images: this.getFirstImage(this.productInfo.product_images),
unit_price: this.productInfo.current_price || this.productInfo.selling_price,
quantity: this.selectedQuantity,
total_amount: parseFloat((this.selectedQuantity * (this.productInfo.current_price || this.productInfo.selling_price)).toFixed(2)),
address_id: addressRes.data.find(addr => addr.is_default === 1)?.id || addressRes.data[0].id,
remark: ''
}
uni.showLoading({
title: '创建订单中...',
mask: true
})
const orderRes = await request.post('/back/user/orders/purchase', orderData)
var isSecond=this.productType === 'primary' ? false : true
if (orderRes.success) {
uni.hideLoading()
// 关闭弹窗
this.closeQuantityPopup()
// 跳转到付款页面
uni.navigateTo({
url: `/pages/payment/purchase?order_id=${orderRes.data.order_id}&order_no=${orderRes.data.order_no}&is_second=${isSecond}`
})
uni.showToast({
title: '订单创建成功',
icon: 'success'
})
} else {
throw new Error(orderRes.message || '创建订单失败')
}
} catch (error) {
uni.hideLoading()
console.error('创建订单失败:', error)
uni.showToast({
title: error.message || '创建订单失败',
icon: 'none'
})
}
}
}
}
</script>
<style scoped>
.container {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 120rpx;
}
.image-section {
position: relative;
background: white;
}
.image-swiper {
width: 100%;
height: 750rpx;
}
.product-image {
width: 100%;
height: 100%;
}
.image-count {
position: absolute;
bottom: 20rpx;
right: 20rpx;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 10rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.product-info-section {
background: white;
padding: 30rpx;
margin-bottom: 20rpx;
}
.price-section {
display: flex;
align-items: baseline;
margin-bottom: 20rpx;
}
.current-price {
display: flex;
align-items: baseline;
margin-right: 20rpx;
}
.price-symbol {
font-size: 28rpx;
color: #FF4444;
font-weight: bold;
}
.price-value {
font-size: 48rpx;
color: #FF4444;
font-weight: bold;
}
.original-price {
display: flex;
align-items: center;
}
.original-price-text {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
.product-title {
margin-bottom: 20rpx;
}
.product-name {
font-size: 32rpx;
color: #333;
font-weight: bold;
line-height: 1.4;
}
.product-stats {
display: flex;
gap: 30rpx;
}
.stats-item {
font-size: 26rpx;
color: #666;
}
.seller-section {
background: white;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.seller-info {
display: flex;
align-items: center;
}
.seller-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 20rpx;
}
.seller-avatar-placeholder {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.seller-details {
flex: 1;
}
.seller-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
display: block;
margin-bottom: 8rpx;
}
.seller-code {
font-size: 24rpx;
color: #666;
}
.description-section {
background: white;
padding: 30rpx;
margin-bottom: 20rpx;
}
.description-content {
margin-top: 20rpx;
}
.description-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.buy-btn {
width: 100%;
height: 80rpx;
background: linear-gradient(135deg, #FF4444 0%, #FF6666 100%);
border: none;
border-radius: 40rpx;
color: white;
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
.buy-btn:disabled {
background: #ccc;
}
.quantity-popup {
display: flex;
flex-direction: column;
max-height: 75vh;
background: white;
border-radius: 20rpx 20rpx 0 0;
}
.popup-content {
flex: 1;
padding: 40rpx 30rpx 20rpx;
overflow-y: auto;
}
.popup-footer {
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f5f5f5;
background: white;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.close-btn {
padding: 10rpx;
}
.popup-product-info {
display: flex;
align-items: center;
padding: 20rpx;
background: #f8f8f8;
border-radius: 20rpx;
margin-bottom: 40rpx;
}
.popup-product-image {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.popup-product-details {
flex: 1;
}
.popup-product-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.popup-product-price {
font-size: 32rpx;
color: #FF4444;
font-weight: bold;
}
.quantity-selector {
margin-bottom: 40rpx;
}
.quantity-label {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.quantity-controls {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
.quantity-btn {
width: 60rpx;
height: 60rpx;
border: 1rpx solid #ddd;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
background: white;
}
.quantity-btn.disabled {
background: #f5f5f5;
border-color: #eee;
}
.quantity-input {
width: 120rpx;
height: 60rpx;
text-align: center;
border: 1rpx solid #ddd;
border-left: none;
border-right: none;
font-size: 28rpx;
color: #333;
background: white;
}
.stock-info {
font-size: 24rpx;
color: #999;
text-align: center;
display: block;
}
.total-price {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
}
.total-label {
font-size: 28rpx;
color: #333;
}
.total-amount {
font-size: 36rpx;
color: #FF4444;
font-weight: bold;
}
.confirm-buy-btn {
width: 100%;
height: 80rpx;
background: linear-gradient(135deg, #FF4444 0%, #FF6666 100%);
border: none;
border-radius: 40rpx;
color: white;
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.confirm-buy-btn:disabled {
background: #ccc;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-text {
font-size: 28rpx;
color: #666;
}
</style>