|
|
@@ -14,14 +14,18 @@
|
|
|
<text class="product-name">{{ product.name }}</text>
|
|
|
</view>
|
|
|
<view class="price-row">
|
|
|
- <text class="price">{{ formatPriceWithSymbol(product.price) }}</text>
|
|
|
- <text v-if="product.memberPrice" class="member-price">会员价 {{ formatPriceWithSymbol(product.memberPrice) }}</text>
|
|
|
+ <text class="price">{{ formatPriceWithSymbol(displayPrice) }}</text>
|
|
|
+ <view v-if="displayMemberPrice && displayMemberPrice > 0 && displayMemberPrice !== displayPrice" class="member-price-row">
|
|
|
+ <text v-if="!isMember" class="member-price-hint" @click="goUpgrade">会员价 {{ formatPriceWithSymbol(displayMemberPrice) }} → 点击升级</text>
|
|
|
+ <text v-if="isMember" class="member-price-current">会员价 {{ formatPriceWithSymbol(displayMemberPrice) }}</text>
|
|
|
+ <text v-if="isMember" class="member-price-original">原价 {{ formatPriceWithSymbol(displayPrice) }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
<view v-if="product.giftLevelName" class="gift-tag">购买即送 {{ product.giftLevelName }} 会员</view>
|
|
|
<view class="meta-row">
|
|
|
<text class="meta-item" v-if="product.productType">{{ typeLabel }}</text>
|
|
|
<text class="meta-item" v-if="product.domain">{{ domainLabel }}</text>
|
|
|
- <text class="meta-item">库存 {{ product.stock || 0 }}</text>
|
|
|
+ <text class="meta-item">库存 {{ displayStock }}</text>
|
|
|
</view>
|
|
|
<view class="vendor-row">
|
|
|
<text class="vendor-label">供应商:</text>
|
|
|
@@ -29,6 +33,26 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
+ <!-- SKU 选择器 -->
|
|
|
+ <view v-if="specGroups.length > 0" class="sku-section">
|
|
|
+ <view v-for="group in specGroups" :key="group.groupId" class="spec-group">
|
|
|
+ <text class="spec-group-name">{{ group.groupName }}</text>
|
|
|
+ <view class="spec-options">
|
|
|
+ <view
|
|
|
+ v-for="option in group.options"
|
|
|
+ :key="option.id"
|
|
|
+ :class="getSpecOptionClass(group.groupId, option)"
|
|
|
+ @click="selectSpec(group.groupId, option)"
|
|
|
+ >
|
|
|
+ <text class="spec-option-text">{{ option.name }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view v-if="selectedSku" class="selected-summary">
|
|
|
+ <text class="selected-text">已选: {{ buildSpecDesc() }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
<view class="intro-section" v-if="product.intro">
|
|
|
<text class="section-title">商品介绍</text>
|
|
|
<mp-html :content="product.intro" />
|
|
|
@@ -41,17 +65,18 @@
|
|
|
|
|
|
<view class="bottom-bar">
|
|
|
<view class="price-info">
|
|
|
- <text class="bottom-price">{{ formatPriceWithSymbol(product.price) }}</text>
|
|
|
- <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
|
|
|
+ <text class="bottom-price">{{ formatPriceWithSymbol(displayPrice) }}</text>
|
|
|
+ <text v-if="displayMemberPrice && displayMemberPrice > 0 && !isMember" class="bottom-member">会员{{ formatPriceWithSymbol(displayMemberPrice) }}</text>
|
|
|
+ <text v-if="displayMemberPrice && displayMemberPrice > 0 && isMember" class="bottom-member bottom-member-highlight">会员{{ formatPriceWithSymbol(displayMemberPrice) }}</text>
|
|
|
</view>
|
|
|
<view class="action-btns">
|
|
|
<view class="action-extra">
|
|
|
<button class="btn-share" @click="onShareProduct">🖼</button>
|
|
|
</view>
|
|
|
<view class="action-extra">
|
|
|
- <button class="btn-cart" @click="onAddToCart">加入购物车</button>
|
|
|
+ <button class="btn-cart" :class="buyButtonDisabled ? 'btn-disabled' : ''" :disabled="buyButtonDisabled" @click="onAddToCart">{{ buyButtonDisabled ? '请选规格' : '加入购物车' }}</button>
|
|
|
</view>
|
|
|
- <button class="btn-buy" @click="onBuy">立即购买</button>
|
|
|
+ <button class="btn-buy" :class="buyButtonDisabled ? 'btn-disabled' : ''" :disabled="buyButtonDisabled" @click="onBuy">{{ buyButtonText }}</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
@@ -65,7 +90,7 @@
|
|
|
|
|
|
<script>
|
|
|
import config from '@/config.js'
|
|
|
-import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode } from '@/utils/api.js'
|
|
|
+import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getMyMembership } from '@/utils/api.js'
|
|
|
import MpHtml from '@/components/mp-html/mp-html.vue'
|
|
|
import ContentSharePoster from '@/components/ContentSharePoster.vue'
|
|
|
import shareMixin from '../../../components/share-mixin.js'
|
|
|
@@ -80,7 +105,12 @@ export default {
|
|
|
showPoster: false,
|
|
|
posterQrCode: '',
|
|
|
posterDimension: '',
|
|
|
- posterInviteText: ''
|
|
|
+ posterInviteText: '',
|
|
|
+ // SKU 选择器状态
|
|
|
+ specGroups: [],
|
|
|
+ selectedSpecs: {},
|
|
|
+ selectedSku: null,
|
|
|
+ isMember: false
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -144,6 +174,39 @@ export default {
|
|
|
images = images.concat(this.product.imageList)
|
|
|
}
|
|
|
return images.length ? images : ['/static/default-product.png']
|
|
|
+ },
|
|
|
+ hasSku: function() {
|
|
|
+ return this.specGroups && this.specGroups.length > 0
|
|
|
+ },
|
|
|
+ allSpecsSelected: function() {
|
|
|
+ if (!this.hasSku) return true
|
|
|
+ for (var i = 0; i < this.specGroups.length; i++) {
|
|
|
+ if (!this.selectedSpecs[this.specGroups[i].groupId]) return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ },
|
|
|
+ displayPrice: function() {
|
|
|
+ if (this.selectedSku) return this.selectedSku.price
|
|
|
+ if (this.hasSku) return this.product.price || 0
|
|
|
+ return this.product.price || 0
|
|
|
+ },
|
|
|
+ displayMemberPrice: function() {
|
|
|
+ if (this.selectedSku) {
|
|
|
+ return this.product.memberPrice || 0
|
|
|
+ }
|
|
|
+ return this.product.memberPrice || 0
|
|
|
+ },
|
|
|
+ displayStock: function() {
|
|
|
+ if (this.selectedSku) return this.selectedSku.stock || 0
|
|
|
+ if (this.hasSku) return 0
|
|
|
+ return this.product.stock || 0
|
|
|
+ },
|
|
|
+ buyButtonDisabled: function() {
|
|
|
+ return this.hasSku && !this.allSpecsSelected
|
|
|
+ },
|
|
|
+ buyButtonText: function() {
|
|
|
+ if (this.hasSku && !this.allSpecsSelected) return '请选择完整规格'
|
|
|
+ return '立即购买'
|
|
|
}
|
|
|
},
|
|
|
onLoad(options) {
|
|
|
@@ -159,6 +222,7 @@ export default {
|
|
|
}
|
|
|
if (pid) {
|
|
|
this.loadDetail(pid)
|
|
|
+ this.checkMemberStatus()
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
@@ -174,61 +238,192 @@ export default {
|
|
|
images[idx] = '/static/default-product.png'
|
|
|
}
|
|
|
},
|
|
|
- loadDetail(id) {
|
|
|
+ loadDetail: function(id) {
|
|
|
this.loading = true
|
|
|
- productDetail({ id: parseInt(id) }).then(res => {
|
|
|
- this.loading = false
|
|
|
+ var that = this
|
|
|
+ productDetail({ id: parseInt(id) }).then(function(res) {
|
|
|
+ that.loading = false
|
|
|
if (res.code === 200 && res.data) {
|
|
|
- this.product = res.data
|
|
|
- this.setShareInfo(
|
|
|
- this.product.name,
|
|
|
- '/pages/discover-detail/product-detail/product-detail?id=' + this.product.id
|
|
|
+ that.product = res.data
|
|
|
+ that.setShareInfo(
|
|
|
+ that.product.name,
|
|
|
+ '/pages/discover-detail/product-detail/product-detail?id=' + that.product.id
|
|
|
)
|
|
|
+ that.loadSkuData(id)
|
|
|
}
|
|
|
- }).catch(() => {
|
|
|
- this.loading = false
|
|
|
+ }).catch(function() {
|
|
|
+ that.loading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ loadSkuData: function(productId) {
|
|
|
+ var that = this
|
|
|
+ productSpecMap({ productIds: [parseInt(productId)] }).then(function(res) {
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ var map = res.data
|
|
|
+ var groups = map[productId]
|
|
|
+ if (groups && groups.length > 0) {
|
|
|
+ that.specGroups = groups
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ // 规格加载失败不阻塞页面
|
|
|
})
|
|
|
},
|
|
|
- onBuy() {
|
|
|
- const userId = uni.getStorageSync('userId')
|
|
|
+
|
|
|
+ checkMemberStatus: function() {
|
|
|
+ var that = this
|
|
|
+ getMyMembership().then(function(res) {
|
|
|
+ if (res.data && res.data.memberLevel) {
|
|
|
+ that.isMember = res.data.memberLevel !== 'FREE'
|
|
|
+ }
|
|
|
+ }).catch(function() {})
|
|
|
+ },
|
|
|
+
|
|
|
+ selectSpec: function(groupId, option) {
|
|
|
+ if (this.selectedSpecs[groupId] === option.id) {
|
|
|
+ delete this.selectedSpecs[groupId]
|
|
|
+ this.selectedSku = null
|
|
|
+ } else {
|
|
|
+ this.selectedSpecs[groupId] = option.id
|
|
|
+ this.matchSku()
|
|
|
+ }
|
|
|
+ this.$forceUpdate()
|
|
|
+ },
|
|
|
+
|
|
|
+ matchSku: function() {
|
|
|
+ for (var i = 0; i < this.specGroups.length; i++) {
|
|
|
+ if (!this.selectedSpecs[this.specGroups[i].groupId]) {
|
|
|
+ this.selectedSku = null
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ var commonSkuIds = null
|
|
|
+ for (var j = 0; j < this.specGroups.length; j++) {
|
|
|
+ var group = this.specGroups[j]
|
|
|
+ var selectedOptionId = this.selectedSpecs[group.groupId]
|
|
|
+ var skuIds = []
|
|
|
+ for (var k = 0; k < group.options.length; k++) {
|
|
|
+ if (group.options[k].id === selectedOptionId) {
|
|
|
+ skuIds.push(group.options[k].skuId)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (commonSkuIds === null) {
|
|
|
+ commonSkuIds = skuIds
|
|
|
+ } else {
|
|
|
+ var intersection = []
|
|
|
+ for (var m = 0; m < commonSkuIds.length; m++) {
|
|
|
+ for (var n = 0; n < skuIds.length; n++) {
|
|
|
+ if (commonSkuIds[m] === skuIds[n]) {
|
|
|
+ intersection.push(commonSkuIds[m])
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ commonSkuIds = intersection
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (commonSkuIds && commonSkuIds.length === 1) {
|
|
|
+ var matchedSkuId = commonSkuIds[0]
|
|
|
+ for (var p = 0; p < this.specGroups.length; p++) {
|
|
|
+ var opts = this.specGroups[p].options
|
|
|
+ for (var q = 0; q < opts.length; q++) {
|
|
|
+ if (opts[q].skuId === matchedSkuId) {
|
|
|
+ this.selectedSku = {
|
|
|
+ id: matchedSkuId,
|
|
|
+ price: opts[q].price,
|
|
|
+ stock: opts[q].stock
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.selectedSku = null
|
|
|
+ },
|
|
|
+
|
|
|
+ buildSpecDesc: function() {
|
|
|
+ if (!this.selectedSku) return ''
|
|
|
+ var desc = ''
|
|
|
+ for (var i = 0; i < this.specGroups.length; i++) {
|
|
|
+ var group = this.specGroups[i]
|
|
|
+ var optionId = this.selectedSpecs[group.groupId]
|
|
|
+ var optionName = ''
|
|
|
+ for (var j = 0; j < group.options.length; j++) {
|
|
|
+ if (group.options[j].id === optionId) {
|
|
|
+ optionName = group.options[j].name
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (desc) desc += '; '
|
|
|
+ desc += group.groupName + ':' + optionName
|
|
|
+ }
|
|
|
+ return desc
|
|
|
+ },
|
|
|
+
|
|
|
+ getSpecOptionClass: function(groupId, option) {
|
|
|
+ if (this.selectedSpecs[groupId] === option.id) return 'spec-option spec-option-selected'
|
|
|
+ if (!option.enabled || (option.stock !== undefined && option.stock <= 0)) return 'spec-option spec-option-disabled'
|
|
|
+ return 'spec-option'
|
|
|
+ },
|
|
|
+
|
|
|
+ goUpgrade: function() {
|
|
|
+ uni.navigateTo({ url: '/pages/membership/upgrade' })
|
|
|
+ },
|
|
|
+
|
|
|
+ onBuy: function() {
|
|
|
+ if (this.buyButtonDisabled) {
|
|
|
+ uni.showToast({ title: '请选择完整规格', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var userId = uni.getStorageSync('userId')
|
|
|
if (!userId) {
|
|
|
uni.navigateTo({ url: '/pages/login/login' })
|
|
|
return
|
|
|
}
|
|
|
- // 跳转结算页面,传递商品信息
|
|
|
- const price = this.product.price
|
|
|
- const productData = {
|
|
|
+ var price = this.selectedSku ? this.selectedSku.price : this.product.price
|
|
|
+ var productData = {
|
|
|
productId: this.product.id,
|
|
|
productName: this.product.name,
|
|
|
coverImage: this.product.coverImage,
|
|
|
unitPrice: price,
|
|
|
quantity: 1
|
|
|
}
|
|
|
- // 先存储到 storage,checkout 页面会读取
|
|
|
+ if (this.selectedSku) {
|
|
|
+ productData.skuId = this.selectedSku.id
|
|
|
+ productData.specDesc = this.buildSpecDesc()
|
|
|
+ }
|
|
|
uni.setStorageSync('checkoutItems', [productData])
|
|
|
uni.navigateTo({
|
|
|
url: '/pages/shop/checkout/checkout?from=product&id=' + this.product.id
|
|
|
})
|
|
|
},
|
|
|
- onAddToCart() {
|
|
|
- const userId = uni.getStorageSync('userId')
|
|
|
+ onAddToCart: function() {
|
|
|
+ if (this.buyButtonDisabled) {
|
|
|
+ uni.showToast({ title: '请选择完整规格', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var userId = uni.getStorageSync('userId')
|
|
|
if (!userId) {
|
|
|
uni.navigateTo({ url: '/pages/login/login' })
|
|
|
return
|
|
|
}
|
|
|
uni.showLoading({ title: '加入购物车...' })
|
|
|
- cartAdd({
|
|
|
+ var params = {
|
|
|
userId: userId,
|
|
|
productId: this.product.id,
|
|
|
quantity: 1
|
|
|
- }).then(res => {
|
|
|
+ }
|
|
|
+ if (this.selectedSku) {
|
|
|
+ params.skuId = this.selectedSku.id
|
|
|
+ }
|
|
|
+ cartAdd(params).then(function(res) {
|
|
|
uni.hideLoading()
|
|
|
if (res.code === 200) {
|
|
|
uni.showToast({ title: '已加入购物车', icon: 'success' })
|
|
|
} else {
|
|
|
uni.showToast({ title: res.message || '加入购物车失败', icon: 'none' })
|
|
|
}
|
|
|
- }).catch(() => {
|
|
|
+ }).catch(function() {
|
|
|
uni.hideLoading()
|
|
|
uni.showToast({ title: '网络异常', icon: 'none' })
|
|
|
})
|
|
|
@@ -392,6 +587,85 @@ export default {
|
|
|
line-height: 1.6;
|
|
|
display: block;
|
|
|
}
|
|
|
+/* SKU 选择器 */
|
|
|
+.sku-section {
|
|
|
+ background: #fff;
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.spec-group {
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.spec-group:last-child {
|
|
|
+ margin-bottom: 10rpx;
|
|
|
+}
|
|
|
+.spec-group-name {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.spec-options {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+.spec-option {
|
|
|
+ padding: 12rpx 28rpx;
|
|
|
+ border: 2rpx solid #ddd;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ margin-right: 16rpx;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+.spec-option-selected {
|
|
|
+ border-color: #F97316;
|
|
|
+ background: #F97316;
|
|
|
+}
|
|
|
+.spec-option-selected .spec-option-text {
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.spec-option-disabled {
|
|
|
+ border-color: #eee;
|
|
|
+ background: #f9f9f9;
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+.spec-option-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.selected-summary {
|
|
|
+ margin-top: 10rpx;
|
|
|
+ padding-top: 14rpx;
|
|
|
+ border-top: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+.selected-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #F97316;
|
|
|
+}
|
|
|
+.member-price-row {
|
|
|
+ margin-top: 6rpx;
|
|
|
+}
|
|
|
+.member-price-hint {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #F97316;
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+.member-price-original {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+ text-decoration: line-through;
|
|
|
+}
|
|
|
+.member-price-current {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #F97316;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+.bottom-member-highlight {
|
|
|
+ color: #F97316 !important;
|
|
|
+}
|
|
|
+.btn-disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
.bottom-bar {
|
|
|
position: fixed;
|
|
|
bottom: 0;
|