init
This commit is contained in:
BIN
miniprogram/pages/.DS_Store
vendored
Normal file
BIN
miniprogram/pages/.DS_Store
vendored
Normal file
Binary file not shown.
59
miniprogram/pages/cart/components/cart-bar/index.js
Normal file
59
miniprogram/pages/cart/components/cart-bar/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
Component({
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
},
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
isAllSelected: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
totalAmount: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
totalGoodsNum: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
observer(num) {
|
||||
const isDisabled = num == 0;
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
isDisabled,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
totalDiscountAmount: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
bottomHeight: {
|
||||
type: Number,
|
||||
value: 100,
|
||||
},
|
||||
fixed: Boolean,
|
||||
},
|
||||
data: {
|
||||
isDisabled: false,
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleSelectAll() {
|
||||
const { isAllSelected } = this.data;
|
||||
this.setData({
|
||||
isAllSelected: !isAllSelected,
|
||||
});
|
||||
this.triggerEvent('handleSelectAll', {
|
||||
isAllSelected: isAllSelected,
|
||||
});
|
||||
},
|
||||
|
||||
handleToSettle() {
|
||||
if (this.data.isDisabled) return;
|
||||
this.triggerEvent('handleToSettle');
|
||||
},
|
||||
},
|
||||
});
|
||||
7
miniprogram/pages/cart/components/cart-bar/index.json
Normal file
7
miniprogram/pages/cart/components/cart-bar/index.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"price": "/components/price/index",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
31
miniprogram/pages/cart/components/cart-bar/index.wxml
Normal file
31
miniprogram/pages/cart/components/cart-bar/index.wxml
Normal file
@@ -0,0 +1,31 @@
|
||||
<view class="cart-bar__placeholder" wx:if="{{fixed}}" />
|
||||
<view class="cart-bar {{fixed ? 'cart-bar--fixed' : ''}} flex flex-v-center" style="bottom: {{fixed ? 'calc(' + bottomHeight + 'rpx + env(safe-area-inset-bottom))' : ''}};">
|
||||
<t-icon
|
||||
size="40rpx"
|
||||
color="{{isAllSelected ? '#FA4126' : '#BBBBBB'}}"
|
||||
name="{{isAllSelected ? 'check-circle-filled' : 'circle'}}"
|
||||
class="cart-bar__check"
|
||||
catchtap="handleSelectAll"
|
||||
/>
|
||||
<text>全选</text>
|
||||
<view class="cart-bar__total flex1">
|
||||
<view>
|
||||
<text class="cart-bar__total--bold text-padding-right">总计</text>
|
||||
<price
|
||||
price="{{totalAmount || '0'}}"
|
||||
fill="{{false}}"
|
||||
decimalSmaller
|
||||
class="cart-bar__total--bold cart-bar__total--price"
|
||||
/>
|
||||
<text class="cart-bar__total--normal">(不含运费)</text>
|
||||
</view>
|
||||
<view wx:if="{{totalDiscountAmount}}">
|
||||
<text class="cart-bar__total--normal text-padding-right">已优惠</text>
|
||||
<price class="cart-bar__total--normal" price="{{totalDiscountAmount || '0'}}" fill="{{false}}" />
|
||||
</view>
|
||||
</view>
|
||||
<view catchtap="handleToSettle" class="{{!isDisabled ? '' : 'disabled-btn'}} account-btn" hover-class="{{!isDisabled ? '' : 'hover-btn'}}">
|
||||
去结算({{totalGoodsNum}})
|
||||
</view>
|
||||
</view>
|
||||
|
||||
80
miniprogram/pages/cart/components/cart-bar/index.wxss
Normal file
80
miniprogram/pages/cart/components/cart-bar/index.wxss
Normal file
@@ -0,0 +1,80 @@
|
||||
.cart-bar__placeholder {
|
||||
height: 100rpx;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.flex-v-center {
|
||||
align-items: center;
|
||||
}
|
||||
.flex1 {
|
||||
flex: 1;
|
||||
}
|
||||
.algin-bottom {
|
||||
text-align: end;
|
||||
}
|
||||
.cart-bar--fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
bottom: calc(100rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.cart-bar {
|
||||
height: 112rpx;
|
||||
background-color: #fff;
|
||||
border-top: 1rpx solid #e5e5e5;
|
||||
padding: 16rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
font-size: 24rpx;
|
||||
line-height: 36rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cart-bar .cart-bar__check {
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.cart-bar .cart-bar__total {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
.cart-bar .account-btn {
|
||||
width: 192rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background-color: #fa4126;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
line-height: 80rpx;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
.cart-bar .disabled-btn {
|
||||
background-color: #cccccc !important;
|
||||
}
|
||||
.cart-bar .hover-btn {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cart-bar__total .cart-bar__total--bold {
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.cart-bar__total .cart-bar__total--normal {
|
||||
font-size: 24rpx;
|
||||
line-height: 32rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.cart-bar__total .cart-bar__total--price {
|
||||
color: #fa4126;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-padding-right {
|
||||
padding-right: 4rpx;
|
||||
}
|
||||
22
miniprogram/pages/cart/components/cart-empty/index.js
Normal file
22
miniprogram/pages/cart/components/cart-empty/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
Component({
|
||||
properties: {
|
||||
imgUrl: {
|
||||
type: String,
|
||||
value: 'https://tdesign.gtimg.com/miniprogram/template/retail/template/empty-cart.png',
|
||||
},
|
||||
tip: {
|
||||
type: String,
|
||||
value: '购物车是空的',
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
value: '去首页',
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.triggerEvent('handleClick');
|
||||
},
|
||||
},
|
||||
});
|
||||
6
miniprogram/pages/cart/components/cart-empty/index.json
Normal file
6
miniprogram/pages/cart/components/cart-empty/index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-image": "/components/webp-image/index"
|
||||
}
|
||||
}
|
||||
6
miniprogram/pages/cart/components/cart-empty/index.wxml
Normal file
6
miniprogram/pages/cart/components/cart-empty/index.wxml
Normal file
@@ -0,0 +1,6 @@
|
||||
<view class="cart-empty">
|
||||
<t-image t-class="cart-img" src="{{imgUrl}}" />
|
||||
<view class="tip">{{tip}}</view>
|
||||
<view class="btn" bind:tap="handleClick">{{btnText}}</view>
|
||||
</view>
|
||||
|
||||
33
miniprogram/pages/cart/components/cart-empty/index.wxss
Normal file
33
miniprogram/pages/cart/components/cart-empty/index.wxss
Normal file
@@ -0,0 +1,33 @@
|
||||
.cart-empty {
|
||||
padding: 64rpx 0rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - 100rpx);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.cart-empty .cart-img {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.cart-empty .tip {
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
color: #999;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.cart-empty .btn {
|
||||
width: 240rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
text-align: center;
|
||||
line-height: 72rpx;
|
||||
border: 2rpx solid #fa4126;
|
||||
color: #fa4126;
|
||||
background-color: transparent;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
182
miniprogram/pages/cart/components/cart-group/index.js
Normal file
182
miniprogram/pages/cart/components/cart-group/index.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import Toast from 'tdesign-miniprogram/toast/index';
|
||||
|
||||
const shortageImg = 'https://tdesign.gtimg.com/miniprogram/template/retail/cart/shortage.png';
|
||||
|
||||
Component({
|
||||
isSpecsTap: false, // 标记本次点击事件是否因为点击specs触发(由于底层goods-card组件没有catch specs点击事件,只能在此处加状态来避免点击specs时触发跳转商品详情)
|
||||
externalClasses: ['wr-class'],
|
||||
properties: {
|
||||
storeGoods: {
|
||||
type: Array,
|
||||
observer(storeGoods) {
|
||||
for (const store of storeGoods) {
|
||||
for (const activity of store.promotionGoodsList) {
|
||||
for (const goods of activity.goodsPromotionList) {
|
||||
goods.specs = goods.specInfo.map((item) => item.specValue); // 目前仅展示商品已选规格的值
|
||||
}
|
||||
}
|
||||
for (const goods of store.shortageGoodsList) {
|
||||
goods.specs = goods.specInfo.map((item) => item.specValue); // 目前仅展示商品已选规格的值
|
||||
}
|
||||
}
|
||||
|
||||
this.setData({ _storeGoods: storeGoods });
|
||||
},
|
||||
},
|
||||
invalidGoodItems: {
|
||||
type: Array,
|
||||
observer(invalidGoodItems) {
|
||||
invalidGoodItems.forEach((goods) => {
|
||||
goods.specs = goods.specInfo.map((item) => item.specValue); // 目前仅展示商品已选规格的值
|
||||
});
|
||||
this.setData({ _invalidGoodItems: invalidGoodItems });
|
||||
},
|
||||
},
|
||||
thumbWidth: { type: null },
|
||||
thumbHeight: { type: null },
|
||||
},
|
||||
|
||||
data: {
|
||||
shortageImg,
|
||||
isShowSpecs: false,
|
||||
currentGoods: {},
|
||||
isShowToggle: false,
|
||||
_storeGoods: [],
|
||||
_invalidGoodItems: [],
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 删除商品
|
||||
deleteGoods(e) {
|
||||
const { goods } = e.currentTarget.dataset;
|
||||
this.triggerEvent('delete', { goods });
|
||||
},
|
||||
|
||||
// 清空失效商品
|
||||
clearInvalidGoods() {
|
||||
this.triggerEvent('clearinvalidgoods');
|
||||
},
|
||||
|
||||
// 选中商品
|
||||
selectGoods(e) {
|
||||
const { goods } = e.currentTarget.dataset;
|
||||
this.triggerEvent('selectgoods', {
|
||||
goods,
|
||||
isSelected: !goods.isSelected,
|
||||
});
|
||||
},
|
||||
|
||||
changeQuantity(num, goods) {
|
||||
this.triggerEvent('changequantity', {
|
||||
goods,
|
||||
quantity: num,
|
||||
});
|
||||
},
|
||||
changeStepper(e) {
|
||||
const { value } = e.detail;
|
||||
const { goods } = e.currentTarget.dataset;
|
||||
let num = value;
|
||||
if (value > goods.stack) {
|
||||
num = goods.stack;
|
||||
}
|
||||
this.changeQuantity(num, goods);
|
||||
},
|
||||
|
||||
input(e) {
|
||||
const { value } = e.detail;
|
||||
const { goods } = e.currentTarget.dataset;
|
||||
const num = value;
|
||||
this.changeQuantity(num, goods);
|
||||
},
|
||||
|
||||
overlimit(e) {
|
||||
const text = e.detail.type === 'minus' ? '该商品数量不能减少了哦' : '同一商品最多购买999件';
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: text,
|
||||
});
|
||||
},
|
||||
|
||||
// 去凑单/再逛逛
|
||||
gotoBuyMore(e) {
|
||||
const { promotion, storeId = '' } = e.currentTarget.dataset;
|
||||
// 仅当需要凑单时才触发跳转,“再逛逛”点击不跳转
|
||||
if (promotion && promotion.isNeedAddOnShop == 1) {
|
||||
this.triggerEvent('gocollect', { promotion, storeId });
|
||||
}
|
||||
},
|
||||
|
||||
// 选中门店
|
||||
selectStore(e) {
|
||||
const { storeIndex } = e.currentTarget.dataset;
|
||||
const store = this.data.storeGoods[storeIndex];
|
||||
const isSelected = !store.isSelected;
|
||||
if (store.storeStockShortage && isSelected) {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '部分商品库存不足',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.triggerEvent('selectstore', {
|
||||
store,
|
||||
isSelected,
|
||||
});
|
||||
},
|
||||
|
||||
// 展开/收起切换
|
||||
showToggle() {
|
||||
this.setData({
|
||||
isShowToggle: !this.data.isShowToggle,
|
||||
});
|
||||
},
|
||||
|
||||
// 展示规格popup
|
||||
specsTap(e) {
|
||||
this.isSpecsTap = true;
|
||||
const { goods } = e.currentTarget.dataset;
|
||||
this.setData({
|
||||
isShowSpecs: true,
|
||||
currentGoods: goods,
|
||||
});
|
||||
},
|
||||
|
||||
hideSpecsPopup() {
|
||||
this.setData({
|
||||
isShowSpecs: false,
|
||||
});
|
||||
},
|
||||
|
||||
// 确认选择SKU
|
||||
onSpecsConfirm(e) {
|
||||
const { skuId, spuId, quantity } = e.detail;
|
||||
const currentGoods = this.data.currentGoods;
|
||||
|
||||
// 触发父组件事件,通知更新SKU
|
||||
this.triggerEvent('changesku', {
|
||||
oldSpuId: currentGoods.spuId,
|
||||
oldSkuId: currentGoods.skuId,
|
||||
newSpuId: spuId,
|
||||
newSkuId: skuId,
|
||||
quantity: quantity || 1,
|
||||
});
|
||||
|
||||
this.hideSpecsPopup();
|
||||
},
|
||||
|
||||
goGoodsDetail(e) {
|
||||
if (this.isSpecsTap) {
|
||||
this.isSpecsTap = false;
|
||||
return;
|
||||
}
|
||||
const { goods } = e.currentTarget.dataset;
|
||||
this.triggerEvent('goodsclick', { goods });
|
||||
},
|
||||
|
||||
gotoCoupons() {
|
||||
wx.navigateTo({ url: '/pages/coupon/coupon-list/index' });
|
||||
},
|
||||
},
|
||||
});
|
||||
11
miniprogram/pages/cart/components/cart-group/index.json
Normal file
11
miniprogram/pages/cart/components/cart-group/index.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-toast": "tdesign-miniprogram/toast/toast",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-stepper": "tdesign-miniprogram/stepper/stepper",
|
||||
"swipeout": "/components/swipeout/index",
|
||||
"goods-card": "../../components/goods-card/index",
|
||||
"specs-popup": "../../components/specs-popup/index"
|
||||
}
|
||||
}
|
||||
155
miniprogram/pages/cart/components/cart-group/index.wxml
Normal file
155
miniprogram/pages/cart/components/cart-group/index.wxml
Normal file
@@ -0,0 +1,155 @@
|
||||
<wxs src="./index.wxs" module="handlePromotion" />
|
||||
|
||||
<wxs src="./utils.wxs" module="utils" />
|
||||
|
||||
<view class="cart-group">
|
||||
<view class="goods-wrap" wx:for="{{_storeGoods}}" wx:for-item="store" wx:for-index="si" wx:key="storeId">
|
||||
<view class="cart-store">
|
||||
<t-icon
|
||||
size="40rpx"
|
||||
color="{{store.isSelected ? '#FA4126' : '#BBBBBB'}}"
|
||||
name="{{store.isSelected ? 'check-circle-filled' : 'circle'}}"
|
||||
class="cart-store__check"
|
||||
bindtap="selectStore"
|
||||
data-store-index="{{si}}"
|
||||
/>
|
||||
<view class="cart-store__content">
|
||||
<view class="store-title">
|
||||
<t-icon prefix="wr" size="40rpx" color="#333333" name="store" />
|
||||
<view class="store-name">{{store.storeName}}</view>
|
||||
</view>
|
||||
<view class="get-coupon" catch:tap="gotoCoupons">优惠券</view>
|
||||
</view>
|
||||
</view>
|
||||
<block wx:for="{{store.promotionGoodsList}}" wx:for-item="promotion" wx:for-index="promoindex" wx:key="promoindex">
|
||||
<view
|
||||
class="promotion-wrap"
|
||||
wx:if="{{handlePromotion.hasPromotion(promotion.promotionCode)}}"
|
||||
bindtap="gotoBuyMore"
|
||||
data-promotion="{{promotion}}"
|
||||
data-store-id="{{store.storeId}}"
|
||||
>
|
||||
<view class="promotion-title">
|
||||
<view class="promotion-icon">{{promotion.tag}}</view>
|
||||
<view class="promotion-text">{{promotion.description}}</view>
|
||||
</view>
|
||||
<view class="promotion-action action-btn" hover-class="action-btn--active">
|
||||
<view class="promotion-action-label"> {{promotion.isNeedAddOnShop == 1 ? '去凑单' : '再逛逛'}} </view>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#BBBBBB" />
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="goods-item"
|
||||
wx:for="{{promotion.goodsPromotionList}}"
|
||||
wx:for-item="goods"
|
||||
wx:for-index="gi"
|
||||
wx:key="extKey"
|
||||
>
|
||||
<swipeout right-width="{{ 72 }}">
|
||||
<view class="goods-item-info">
|
||||
<view class="check-wrap" catchtap="selectGoods" data-goods="{{goods}}">
|
||||
<t-icon
|
||||
size="40rpx"
|
||||
color="{{goods.isSelected ? '#FA4126' : '#BBBBBB'}}"
|
||||
name="{{goods.isSelected ? 'check-circle-filled' : 'circle'}}"
|
||||
class="check"
|
||||
/>
|
||||
</view>
|
||||
<view class="goods-sku-info">
|
||||
<goods-card
|
||||
layout="horizontal-wrap"
|
||||
thumb-width="{{thumbWidth}}"
|
||||
thumb-height="{{thumbHeight}}"
|
||||
centered="{{true}}"
|
||||
data="{{goods}}"
|
||||
data-goods="{{goods}}"
|
||||
catchspecs="specsTap"
|
||||
catchclick="goGoodsDetail"
|
||||
>
|
||||
<view slot="thumb-cover" class="stock-mask" wx:if="{{goods.shortageStock || goods.stockQuantity <= 3}}">
|
||||
仅剩{{goods.stockQuantity}}件
|
||||
</view>
|
||||
<view slot="append-body" class="goods-stepper">
|
||||
<view class="stepper-tip" wx:if="{{goods.shortageStock}}">库存不足</view>
|
||||
<t-stepper
|
||||
classname="stepper-info"
|
||||
value="{{goods.quantity}}"
|
||||
min="{{1}}"
|
||||
max="{{999}}"
|
||||
data-goods="{{goods}}"
|
||||
data-gi="{{gi}}"
|
||||
data-si="{{si}}"
|
||||
catchchange="changeStepper"
|
||||
catchblur="input"
|
||||
catchoverlimit="overlimit"
|
||||
theme="filled"
|
||||
/>
|
||||
</view>
|
||||
</goods-card>
|
||||
</view>
|
||||
</view>
|
||||
<view slot="right" class="swiper-right-del" bindtap="deleteGoods" data-goods="{{goods}}"> 删除 </view>
|
||||
</swipeout>
|
||||
</view>
|
||||
<view
|
||||
class="promotion-line-wrap"
|
||||
wx:if="{{handlePromotion.hasPromotion(promotion.promotionCode) && promoindex != (store.promotionGoodsList.length - 2)}}"
|
||||
>
|
||||
<view class="promotion-line" />
|
||||
</view>
|
||||
</block>
|
||||
<block wx:if="{{store.shortageGoodsList.length>0}}">
|
||||
<view
|
||||
class="goods-item"
|
||||
wx:for="{{store.shortageGoodsList}}"
|
||||
wx:for-item="goods"
|
||||
wx:for-index="gi"
|
||||
wx:key="extKey"
|
||||
>
|
||||
<swipeout right-width="{{ 72 }}">
|
||||
<view class="goods-item-info">
|
||||
<view class="check-wrap">
|
||||
<view class="unCheck-icon" />
|
||||
</view>
|
||||
<view class="goods-sku-info">
|
||||
<goods-card
|
||||
layout="horizontal-wrap"
|
||||
thumb-width="{{thumbWidth}}"
|
||||
thumb-height="{{thumbHeight}}"
|
||||
centered="{{true}}"
|
||||
data="{{goods}}"
|
||||
data-goods="{{goods}}"
|
||||
catchspecs="specsTap"
|
||||
catchclick="goGoodsDetail"
|
||||
>
|
||||
<view slot="thumb-cover" class="no-storage-mask" wx:if="{{goods.stockQuantity <=0}}">
|
||||
<view class="no-storage-content">无货</view>
|
||||
</view>
|
||||
</goods-card>
|
||||
</view>
|
||||
</view>
|
||||
<view slot="right" class="swiper-right-del" bindtap="deleteGoods" data-goods="{{goods}}"> 删除 </view>
|
||||
</swipeout>
|
||||
</view>
|
||||
<view
|
||||
class="promotion-line-wrap"
|
||||
wx:if="{{handlePromotion.hasPromotion(promotion.promotionCode) && promoindex != (store.promotionGoodsList.length - 2)}}"
|
||||
>
|
||||
<view class="promotion-line" />
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<specs-popup
|
||||
show="{{isShowSpecs}}"
|
||||
title="{{currentGoods.title || ''}}"
|
||||
price="{{currentGoods.price || ''}}"
|
||||
thumb="{{utils.imgCut(currentGoods.thumb, 180, 180)}}"
|
||||
spuId="{{currentGoods.spuId}}"
|
||||
skuId="{{currentGoods.skuId}}"
|
||||
zIndex="{{11001}}"
|
||||
bindclose="hideSpecsPopup"
|
||||
bindconfirm="onSpecsConfirm"
|
||||
/>
|
||||
|
||||
<t-toast id="t-toast" />
|
||||
5
miniprogram/pages/cart/components/cart-group/index.wxs
Normal file
5
miniprogram/pages/cart/components/cart-group/index.wxs
Normal file
@@ -0,0 +1,5 @@
|
||||
var hasPromotion = function (code) {
|
||||
return code && code !== 'EMPTY_PROMOTION';
|
||||
};
|
||||
|
||||
module.exports.hasPromotion = hasPromotion;
|
||||
335
miniprogram/pages/cart/components/cart-group/index.wxss
Normal file
335
miniprogram/pages/cart/components/cart-group/index.wxss
Normal file
@@ -0,0 +1,335 @@
|
||||
.cart-group {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.cart-group .goods-wrap {
|
||||
margin-top: 40rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cart-group .goods-wrap:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
.cart-group .cart-store {
|
||||
height: 96rpx;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0rpx 24rpx 0rpx 36rpx;
|
||||
}
|
||||
.cart-group .cart-store .cart-store__check {
|
||||
padding: 28rpx 32rpx 28rpx 0rpx;
|
||||
}
|
||||
.cart-group .cart-store__content {
|
||||
box-sizing: border-box;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.cart-group .cart-store__content .store-title {
|
||||
flex: auto;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
color: #333333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cart-group .cart-store__content .store-title .wr-store {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.cart-group .cart-store__content .store-title .store-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
.cart-group .cart-store__content .get-coupon {
|
||||
width: 112rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffecf9;
|
||||
line-height: 40rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #fa4126;
|
||||
}
|
||||
|
||||
.cart-group .promotion-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0rpx 24rpx 32rpx 36rpx;
|
||||
background-color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
line-height: 36rpx;
|
||||
color: #222427;
|
||||
}
|
||||
.cart-group .promotion-wrap .promotion-title {
|
||||
font-weight: bold;
|
||||
flex: auto;
|
||||
overflow: hidden;
|
||||
margin-right: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.cart-group .promotion-wrap .promotion-title .promotion-icon {
|
||||
flex: none;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
padding: 0 8rpx;
|
||||
color: #ffffff;
|
||||
background: #fa4126;
|
||||
font-size: 20rpx;
|
||||
height: 32rpx;
|
||||
line-height: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.cart-group .promotion-wrap .promotion-title .promotion-text {
|
||||
flex: auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cart-group .promotion-wrap .promotion-action {
|
||||
flex: none;
|
||||
color: #333333;
|
||||
}
|
||||
.cart-group .promotion-line-wrap {
|
||||
background-color: #fff;
|
||||
height: 2rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.cart-group .promotion-line-wrap .promotion-line {
|
||||
width: 684rpx;
|
||||
height: 2rpx;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
.cart-group .goods-item-info {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.cart-group .goods-item-info .check-wrap {
|
||||
margin-top: 56rpx;
|
||||
padding: 20rpx 28rpx 20rpx 36rpx;
|
||||
}
|
||||
|
||||
.cart-group .goods-item-info .check-wrap .unCheck-icon {
|
||||
box-sizing: border-box;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #f5f5f5;
|
||||
border: 2rpx solid #bbbbbb;
|
||||
}
|
||||
|
||||
.cart-group .goods-item-info .goods-sku-info {
|
||||
padding: 0rpx 32rpx 40rpx 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.cart-group .goods-item-info .goods-sku-info .stock-mask {
|
||||
position: absolute;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
bottom: 0rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
width: 100%;
|
||||
height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.cart-group .goods-item-info .goods-sku-info .goods-stepper {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 8rpx;
|
||||
}
|
||||
.cart-group .goods-item-info .goods-sku-info .goods-stepper .stepper-tip {
|
||||
position: absolute;
|
||||
top: -36rpx;
|
||||
right: 0;
|
||||
height: 28rpx;
|
||||
color: #ff2525;
|
||||
font-size: 20rpx;
|
||||
line-height: 28rpx;
|
||||
}
|
||||
|
||||
.cart-group .shortage-line {
|
||||
width: 662rpx;
|
||||
height: 2rpx;
|
||||
background-color: #e6e6e6;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.cart-group .shortage-goods-wrap {
|
||||
background-color: #fff;
|
||||
}
|
||||
.cart-group .shortage-goods-wrap .shortage-tip-title {
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
padding-left: 28rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.stepper-info {
|
||||
margin-left: auto;
|
||||
}
|
||||
.invalid-goods-wrap {
|
||||
background-color: #fff;
|
||||
border-radius: 8rpx;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
.invalid-goods-wrap .invalid-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 20rpx;
|
||||
font-size: 24rpx;
|
||||
border-bottom: 2rpx solid #f6f6f6;
|
||||
}
|
||||
.invalid-goods-wrap .invalid-head .invalid-title {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.invalid-goods-wrap .invalid-head .invalid-clear {
|
||||
color: #fa4126;
|
||||
}
|
||||
.invalid-goods-wrap .toggle {
|
||||
display: flex;
|
||||
height: 80rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: #fa4126;
|
||||
}
|
||||
.invalid-goods-wrap .toggle .m-r-6 {
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
.invalid-goods-wrap .toggle .top-icon {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10rpx solid transparent;
|
||||
border-right: 10rpx solid transparent;
|
||||
border-bottom: 10rpx solid #fa4126;
|
||||
}
|
||||
.invalid-goods-wrap .toggle .down-icon {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10rpx solid transparent;
|
||||
border-right: 10rpx solid transparent;
|
||||
border-top: 10rpx solid #fa4126;
|
||||
}
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.action-btn .action-btn-arrow {
|
||||
font-size: 20rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
.action-btn--active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.swiper-right-del {
|
||||
height: calc(100% - 40rpx);
|
||||
width: 144rpx;
|
||||
background-color: #fa4126;
|
||||
font-size: 28rpx;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.goods-stepper .stepper {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
height: auto;
|
||||
width: 168rpx;
|
||||
overflow: visible;
|
||||
}
|
||||
.goods-stepper .stepper .stepper__minus,
|
||||
.goods-stepper .stepper .stepper__plus {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.goods-stepper .stepper .stepper__minus--hover,
|
||||
.goods-stepper .stepper .stepper__plus--hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.goods-stepper .stepper .stepper__minus .wr-icon,
|
||||
.goods-stepper .stepper .stepper__plus .wr-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.goods-stepper .stepper .stepper__minus {
|
||||
position: relative;
|
||||
}
|
||||
.goods-stepper .stepper .stepper__minus::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: ' ';
|
||||
left: -20rpx;
|
||||
right: -5rpx;
|
||||
top: -20rpx;
|
||||
bottom: -20rpx;
|
||||
background-color: transparent;
|
||||
}
|
||||
.goods-stepper .stepper .stepper__plus {
|
||||
position: relative;
|
||||
}
|
||||
.goods-stepper .stepper .stepper__plus::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: ' ';
|
||||
left: -5rpx;
|
||||
right: -20rpx;
|
||||
top: -20rpx;
|
||||
bottom: -20rpx;
|
||||
background-color: transparent;
|
||||
}
|
||||
.goods-stepper .stepper .stepper__input {
|
||||
width: 72rpx;
|
||||
height: 44rpx;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 24rpx;
|
||||
color: #222427;
|
||||
font-weight: 600;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
min-height: 40rpx;
|
||||
margin: 0 4rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.goods-sku-info .no-storage-mask {
|
||||
position: absolute;
|
||||
color: #fff;
|
||||
bottom: 0rpx;
|
||||
left: 0rpx;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
height: 192rpx;
|
||||
width: 192rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-storage-mask .no-storage-content {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 64rpx;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
text-align: center;
|
||||
line-height: 128rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
20
miniprogram/pages/cart/components/cart-group/utils.wxs
Normal file
20
miniprogram/pages/cart/components/cart-group/utils.wxs
Normal file
@@ -0,0 +1,20 @@
|
||||
module.exports.slice = function(arr) {
|
||||
return arr.slice(0, 2);
|
||||
};
|
||||
module.exports.imgCut = function(url, width, height) {
|
||||
if (url && (url.slice(0, 5) === 'http:' || url.slice(0, 6) === 'https:' || url.slice(0, 2) === '//')) {
|
||||
var argsStr = 'imageMogr2/thumbnail/!' + width + 'x' + height + 'r';
|
||||
if (url.indexOf('?') > -1) {
|
||||
url = url + '&' + argsStr;
|
||||
} else {
|
||||
url = url + '?' + argsStr;
|
||||
}
|
||||
if (url.slice(0, 5) === 'http:') {
|
||||
url = 'https://' + url.slice(5)
|
||||
}
|
||||
if (url.slice(0, 2) === '//') {
|
||||
url = 'https:' + url
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
248
miniprogram/pages/cart/components/goods-card/index.js
Normal file
248
miniprogram/pages/cart/components/goods-card/index.js
Normal file
@@ -0,0 +1,248 @@
|
||||
Component({
|
||||
options: {
|
||||
multipleSlots: true, // 在组件定义时的选项中启用多slot支持
|
||||
addGlobalClass: true,
|
||||
},
|
||||
intersectionObserverContext: null,
|
||||
|
||||
externalClasses: [
|
||||
'card-class',
|
||||
'title-class',
|
||||
'desc-class',
|
||||
'num-class',
|
||||
'thumb-class',
|
||||
'specs-class',
|
||||
'price-class',
|
||||
'origin-price-class',
|
||||
'price-prefix-class',
|
||||
],
|
||||
|
||||
properties: {
|
||||
hidden: {
|
||||
// 设置为null代表不做类型转换
|
||||
type: null,
|
||||
value: false,
|
||||
observer(hidden) {
|
||||
// null就是代表没有设置,没有设置的话不setData,防止祖先组件触发的setHidden操作被覆盖
|
||||
if (hidden !== null) {
|
||||
this.setHidden(!!hidden);
|
||||
}
|
||||
},
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
// `goods-card-88888888`
|
||||
// 不能在这里写生成逻辑,如果在这里写,那么假设有多个goods-list时,他们将共享这个值
|
||||
value: '',
|
||||
observer: (id) => {
|
||||
this.genIndependentID(id);
|
||||
if (this.properties.thresholds?.length) {
|
||||
this.createIntersectionObserverHandle();
|
||||
}
|
||||
},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
observer(goods) {
|
||||
// 有ID的商品才渲染
|
||||
if (!goods) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** 划线价是否有效 */
|
||||
let isValidityLinePrice = true;
|
||||
// 判断一次划线价格是否合理
|
||||
if (
|
||||
goods.originPrice &&
|
||||
goods.price &&
|
||||
goods.originPrice < goods.price
|
||||
) {
|
||||
isValidityLinePrice = false;
|
||||
}
|
||||
|
||||
// 敲定换行数量默认值
|
||||
if (goods.lineClamp === undefined || goods.lineClamp <= 0) {
|
||||
// tag数组长度 大于0 且 可见
|
||||
// 指定换行为1行
|
||||
if ((goods.tags?.length || 0) > 0 && !goods.hideKey?.tags) {
|
||||
goods.lineClamp = 1;
|
||||
} else {
|
||||
goods.lineClamp = 2;
|
||||
}
|
||||
}
|
||||
|
||||
this.setData({ goods, isValidityLinePrice });
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
value: 'horizontal',
|
||||
},
|
||||
thumbMode: {
|
||||
type: String,
|
||||
value: 'aspectFill',
|
||||
},
|
||||
priceFill: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
currency: {
|
||||
type: String,
|
||||
value: '¥',
|
||||
},
|
||||
lazyLoad: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
centered: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
pricePrefix: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 元素可见监控阈值, 数组长度大于0就创建 */
|
||||
thresholds: {
|
||||
type: Array,
|
||||
value: [],
|
||||
observer(current) {
|
||||
if (current && current.length) {
|
||||
this.createIntersectionObserverHandle();
|
||||
} else {
|
||||
this.clearIntersectionObserverHandle();
|
||||
}
|
||||
},
|
||||
},
|
||||
specsIconClassPrefix: {
|
||||
type: String,
|
||||
value: 'wr',
|
||||
},
|
||||
specsIcon: {
|
||||
type: String,
|
||||
value: 'expand_more',
|
||||
},
|
||||
addCartIconClassPrefix: {
|
||||
type: String,
|
||||
value: 'wr',
|
||||
},
|
||||
addCartIcon: {
|
||||
type: String,
|
||||
value: 'cart',
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
hiddenInData: false,
|
||||
independentID: '',
|
||||
goods: { id: '' },
|
||||
/** 保证划线价格不小于原价,否则不渲染划线价 */
|
||||
isValidityLinePrice: false,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
ready() {
|
||||
this.init();
|
||||
},
|
||||
detached() {
|
||||
this.clear();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
clickHandle() {
|
||||
this.triggerEvent('click', { goods: this.data.goods });
|
||||
},
|
||||
clickThumbHandle() {
|
||||
this.triggerEvent('thumb', { goods: this.data.goods });
|
||||
},
|
||||
clickSpecsHandle() {
|
||||
this.triggerEvent('specs', { goods: this.data.goods });
|
||||
},
|
||||
clickTagHandle(evt) {
|
||||
const { index } = evt.currentTarget.dataset;
|
||||
this.triggerEvent('tag', { goods: this.data.goods, index });
|
||||
},
|
||||
// 加入购物车
|
||||
addCartHandle(e) {
|
||||
const { id } = e.currentTarget;
|
||||
const { id: cardID } = e.currentTarget.dataset;
|
||||
this.triggerEvent('add-cart', {
|
||||
...e.detail,
|
||||
id,
|
||||
cardID,
|
||||
goods: this.data.goods,
|
||||
});
|
||||
},
|
||||
|
||||
handleImageError(e) {
|
||||
console.warn('购物车商品卡片图片加载失败:', e.detail);
|
||||
// 可以在这里设置默认图片或其他错误处理逻辑
|
||||
},
|
||||
genIndependentID(id, cb) {
|
||||
let independentID;
|
||||
if (id) {
|
||||
independentID = id;
|
||||
} else {
|
||||
independentID = `goods-card-${~~(Math.random() * 10 ** 8)}`;
|
||||
}
|
||||
this.setData({ independentID }, cb);
|
||||
},
|
||||
|
||||
init() {
|
||||
const { thresholds, id, hidden } = this.properties;
|
||||
if (hidden !== null) {
|
||||
this.setHidden(!!hidden);
|
||||
}
|
||||
|
||||
this.genIndependentID(id || '', () => {
|
||||
if (thresholds && thresholds.length) {
|
||||
this.createIntersectionObserverHandle();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.clearIntersectionObserverHandle();
|
||||
},
|
||||
|
||||
setHidden(hidden) {
|
||||
this.setData({ hiddenInData: !!hidden });
|
||||
},
|
||||
|
||||
createIntersectionObserverHandle() {
|
||||
if (this.intersectionObserverContext || !this.data.independentID) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.intersectionObserverContext = wx
|
||||
.createIntersectionObserver(this, {
|
||||
thresholds: this.properties.thresholds,
|
||||
})
|
||||
.relativeToViewport();
|
||||
|
||||
this.intersectionObserverContext.observe(
|
||||
`#${this.data.independentID}`,
|
||||
(res) => {
|
||||
this.intersectionObserverCB(res);
|
||||
},
|
||||
);
|
||||
},
|
||||
intersectionObserverCB(ob) {
|
||||
this.triggerEvent('ob', {
|
||||
goods: this.data.goods,
|
||||
context: this.intersectionObserverContext,
|
||||
ob,
|
||||
});
|
||||
},
|
||||
clearIntersectionObserverHandle() {
|
||||
if (this.intersectionObserverContext) {
|
||||
try {
|
||||
this.intersectionObserverContext.disconnect();
|
||||
} catch (e) {}
|
||||
|
||||
this.intersectionObserverContext = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
9
miniprogram/pages/cart/components/goods-card/index.json
Normal file
9
miniprogram/pages/cart/components/goods-card/index.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"price": "/components/price/index",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-image": "/components/webp-image/index",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
76
miniprogram/pages/cart/components/goods-card/index.wxml
Normal file
76
miniprogram/pages/cart/components/goods-card/index.wxml
Normal file
@@ -0,0 +1,76 @@
|
||||
<view
|
||||
id="{{independentID}}"
|
||||
class="wr-goods-card card-class {{ layout }} {{ centered ? 'center' : ''}}"
|
||||
bind:tap="clickHandle"
|
||||
data-goods="{{ goods }}"
|
||||
hidden="{{hiddenInData}}"
|
||||
>
|
||||
<view class="wr-goods-card__main">
|
||||
<view class="wr-goods-card__thumb thumb-class" bind:tap="clickThumbHandle">
|
||||
<!-- data-src 是方便加购动画读取图片用的 -->
|
||||
<t-image
|
||||
t-class="wr-goods-card__thumb-com"
|
||||
wx:if="{{ !!goods.thumb && !goods.hideKey.thumb }}"
|
||||
src="{{ goods.thumb }}"
|
||||
mode="{{ thumbMode }}"
|
||||
lazy-load="{{ lazyLoad }}"
|
||||
bind:error="handleImageError"
|
||||
/>
|
||||
<slot name="thumb-cover" />
|
||||
</view>
|
||||
<view class="wr-goods-card__body">
|
||||
<view class="wr-goods-card__long_content">
|
||||
<view wx:if="{{ goods.title && !goods.hideKey.title }}" class="wr-goods-card__title title-class" style="-webkit-line-clamp: {{ goods.lineClamp }};">
|
||||
<slot name="before-title" />
|
||||
{{ goods.title }}
|
||||
</view>
|
||||
<slot name="after-title" />
|
||||
<view wx:if="{{ goods.desc && !goods.hideKey.desc }}" class="wr-goods-card__desc desc-class">{{ goods.desc }}</view>
|
||||
<slot name="after-desc" />
|
||||
<view wx:if="{{ goods.specs && goods.specs.length > 0 && !goods.hideKey.specs }}" class="wr-goods-card__specs__desc specs-class" bind:tap="clickSpecsHandle">
|
||||
<view class="wr-goods-card__specs__desc-text">{{ goods.specs }}</view>
|
||||
<t-icon name="chevron-down" size="32rpx" color="#999999" />
|
||||
</view>
|
||||
<view class="goods_tips" wx:if="{{goods.stockQuantity !== 0 && goods.quantity >= goods.stockQuantity}}">库存不足</view>
|
||||
</view>
|
||||
<view class="wr-goods-card__short_content">
|
||||
<block wx:if="{{goods.stockQuantity !== 0}}">
|
||||
<view wx:if="{{ pricePrefix }}" class="wr-goods-card__price__prefix price-prefix-class">{{ pricePrefix }}</view>
|
||||
<slot name="price-prefix" />
|
||||
<view wx:if="{{ goods.price && !goods.hideKey.price }}" class="wr-goods-card__price">
|
||||
<price
|
||||
wr-class="price-class"
|
||||
symbol="{{currency}}"
|
||||
price="{{goods.price}}"
|
||||
fill="{{priceFill}}"
|
||||
decimalSmaller
|
||||
/>
|
||||
</view>
|
||||
<view wx:if="{{ goods.originPrice && !goods.hideKey.originPrice && isValidityLinePrice }}" class="wr-goods-card__origin-price">
|
||||
<price
|
||||
wr-class="origin-price-class"
|
||||
symbol="{{currency}}"
|
||||
price="{{goods.originPrice}}"
|
||||
fill="{{priceFill}}"
|
||||
/>
|
||||
</view>
|
||||
<slot name="origin-price" />
|
||||
<view wx:if="{{goods.num && !goods.hideKey.num}}" class="wr-goods-card__num num-class">
|
||||
<text class="wr-goods-card__num__prefix">x </text>
|
||||
{{ goods.num }}
|
||||
</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="no_storage">
|
||||
<view>请重新选择商品规格</view>
|
||||
<view class="no_storage__right">重选</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<slot name="append-body" />
|
||||
</view>
|
||||
<slot name="footer" />
|
||||
</view>
|
||||
<slot name="append-card" />
|
||||
</view>
|
||||
|
||||
260
miniprogram/pages/cart/components/goods-card/index.wxss
Normal file
260
miniprogram/pages/cart/components/goods-card/index.wxss
Normal file
@@ -0,0 +1,260 @@
|
||||
.wr-goods-card {
|
||||
box-sizing: border-box;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
/* */
|
||||
.wr-goods-card__main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wr-goods-card.center .wr-goods-card__main {
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wr-goods-card__thumb {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
}
|
||||
|
||||
.wr-goods-card__thumb-com {
|
||||
width: 192rpx;
|
||||
height: 192rpx;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wr-goods-card__thumb:empty {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wr-goods-card__body {
|
||||
display: flex;
|
||||
margin: 0 0 0 20rpx;
|
||||
flex-direction: row;
|
||||
flex: 1 1 auto;
|
||||
min-height: 192rpx;
|
||||
}
|
||||
|
||||
.wr-goods-card__long_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.wr-goods-card__long_content .goods_tips {
|
||||
width: 100%;
|
||||
margin-top: 16rpx;
|
||||
text-align: right;
|
||||
color: #fa4126;
|
||||
font-size: 24rpx;
|
||||
line-height: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.wr-goods-card__title {
|
||||
flex-shrink: 0;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 40rpx;
|
||||
font-weight: 400;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.wr-goods-card__title__prefix-tags {
|
||||
display: inline-flex;
|
||||
}
|
||||
.wr-goods-card__title__prefix-tags .prefix-tag {
|
||||
margin: 0 8rpx 0 0;
|
||||
}
|
||||
.wr-goods-card__desc {
|
||||
font-size: 24rpx;
|
||||
color: #f5f5f5;
|
||||
line-height: 40rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wr-goods-card__specs__desc,
|
||||
.wr-goods-card__specs__text {
|
||||
font-size: 24rpx;
|
||||
height: 32rpx;
|
||||
line-height: 32rpx;
|
||||
color: #999999;
|
||||
margin: 8rpx 0;
|
||||
}
|
||||
.wr-goods-card__specs__desc {
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
flex-direction: row;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
}
|
||||
.wr-goods-card__specs__desc-text {
|
||||
height: 100%;
|
||||
max-width: 380rpx;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
.wr-goods-card__specs__desc-icon {
|
||||
line-height: inherit;
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
.wr-goods-card__specs__text {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
}
|
||||
.wr-goods-card__tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 16rpx 0 0 0;
|
||||
}
|
||||
.wr-goods-card__tag {
|
||||
color: #fa550f;
|
||||
background: transparent;
|
||||
font-size: 20rpx;
|
||||
border: 1rpx solid #fa550f;
|
||||
padding: 0 8rpx;
|
||||
height: 30rpx;
|
||||
line-height: 30rpx;
|
||||
margin: 0 8rpx 8rpx 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
word-break: keep-all;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.wr-goods-card__short_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
margin: 0 0 0 46rpx;
|
||||
}
|
||||
.wr-goods-card__price__prefix {
|
||||
order: 0;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
.wr-goods-card__price {
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
order: 1;
|
||||
color: #fa4126;
|
||||
font-size: 36rpx;
|
||||
margin: 0;
|
||||
line-height: 48rpx;
|
||||
}
|
||||
.wr-goods-card__origin-price {
|
||||
white-space: nowrap;
|
||||
font-weight: normal;
|
||||
order: 2;
|
||||
color: #aaaaaa;
|
||||
font-size: 24rpx;
|
||||
margin: 0;
|
||||
}
|
||||
.wr-goods-card__num {
|
||||
white-space: nowrap;
|
||||
order: 4;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin: 20rpx 0 0 auto;
|
||||
}
|
||||
.wr-goods-card__num__prefix {
|
||||
color: inherit;
|
||||
}
|
||||
.wr-goods-card__add-cart {
|
||||
order: 3;
|
||||
margin: auto 0 0 auto;
|
||||
}
|
||||
.wr-goods-card.horizontal-wrap .wr-goods-card__thumb {
|
||||
width: 192rpx;
|
||||
height: 192rpx;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wr-goods-card.horizontal-wrap .wr-goods-card__body {
|
||||
flex-direction: column;
|
||||
}
|
||||
.wr-goods-card.horizontal-wrap .wr-goods-card__short_content {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 16rpx 0 0 0;
|
||||
}
|
||||
|
||||
.wr-goods-card.horizontal-wrap .wr-goods-card__num {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__main {
|
||||
padding: 0 0 22rpx 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__thumb {
|
||||
width: 340rpx;
|
||||
height: 340rpx;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__body {
|
||||
margin: 20rpx 20rpx 0 20rpx;
|
||||
flex-direction: column;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__long_content {
|
||||
overflow: hidden;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__title {
|
||||
line-height: 36rpx;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__short_content {
|
||||
margin: 20rpx 0 0 0;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__price {
|
||||
order: 2;
|
||||
color: #fa4126;
|
||||
margin: 20rpx 0 0 0;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__origin-price {
|
||||
order: 1;
|
||||
}
|
||||
.wr-goods-card.vertical .wr-goods-card__add-cart {
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
}
|
||||
|
||||
.wr-goods-card__short_content .no_storage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40rpx;
|
||||
color: #333;
|
||||
font-size: 24rpx;
|
||||
line-height: 32rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no_storage .no_storage__right {
|
||||
width: 80rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 2rpx solid #fa4126;
|
||||
line-height: 40rpx;
|
||||
text-align: center;
|
||||
color: #fa4126;
|
||||
}
|
||||
273
miniprogram/pages/cart/components/specs-popup/index.js
Normal file
273
miniprogram/pages/cart/components/specs-popup/index.js
Normal file
@@ -0,0 +1,273 @@
|
||||
import { fetchGood } from '../../../../services/good/fetchGood';
|
||||
import Toast from 'tdesign-miniprogram/toast/index';
|
||||
|
||||
Component({
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true, // 在组件定义时的选项中启用多slot支持
|
||||
},
|
||||
|
||||
properties: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer(newVal) {
|
||||
if (newVal && this.data.spuId) {
|
||||
this.loadProductDetail();
|
||||
}
|
||||
},
|
||||
},
|
||||
spuId: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
skuId: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
observer(newVal) {
|
||||
this.setData({ 'goods.title': newVal });
|
||||
},
|
||||
},
|
||||
price: {
|
||||
type: String,
|
||||
value: '',
|
||||
observer(newVal) {
|
||||
this.setData({ 'goods.price': newVal });
|
||||
},
|
||||
},
|
||||
thumb: {
|
||||
type: String,
|
||||
value: '',
|
||||
observer(newVal) {
|
||||
this.setData({ 'goods.thumb': newVal });
|
||||
},
|
||||
},
|
||||
thumbMode: {
|
||||
type: String,
|
||||
value: 'aspectFit',
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
value: 99,
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
goods: {
|
||||
title: '',
|
||||
thumb: '',
|
||||
price: '',
|
||||
hideKey: {
|
||||
originPrice: true,
|
||||
tags: true,
|
||||
specs: true,
|
||||
num: true,
|
||||
},
|
||||
},
|
||||
specList: [],
|
||||
skuList: [],
|
||||
selectedSku: {},
|
||||
isAllSelectedSku: false,
|
||||
selectSkuSellsPrice: 0,
|
||||
currentPrice: '',
|
||||
buyNum: 1,
|
||||
loading: false,
|
||||
},
|
||||
methods: {
|
||||
async loadProductDetail() {
|
||||
const { spuId, skuId } = this.data;
|
||||
if (!spuId) return;
|
||||
|
||||
this.setData({ loading: true });
|
||||
|
||||
try {
|
||||
const detail = await fetchGood(spuId);
|
||||
const { specList = [], skuList = [], minSalePrice } = detail;
|
||||
|
||||
// 处理SKU数据
|
||||
const skuArray = skuList.map((sku) => ({
|
||||
...sku,
|
||||
specInfo: sku.specInfo || [],
|
||||
quantity: Number(
|
||||
(sku.stockInfo && sku.stockInfo.stockQuantity) || sku.stock || sku.quantity || 0
|
||||
),
|
||||
price: Number((sku.priceInfo && sku.priceInfo[0] && sku.priceInfo[0].price) || sku.price || 0),
|
||||
skuId: sku.skuId || sku.id,
|
||||
}));
|
||||
|
||||
// 初始化规格选择,默认选中当前SKU
|
||||
this.initSpecSelection(specList, skuArray, skuId);
|
||||
|
||||
this.setData({
|
||||
specList,
|
||||
skuList: skuArray,
|
||||
selectSkuSellsPrice: minSalePrice,
|
||||
currentPrice: minSalePrice,
|
||||
loading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载商品详情失败:', error);
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '加载失败,请重试',
|
||||
theme: 'error',
|
||||
});
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
initSpecSelection(specList, skuArray, currentSkuId) {
|
||||
const selectedSku = {};
|
||||
|
||||
// 查找当前SKU
|
||||
const currentSku = skuArray.find(sku => String(sku.skuId) === String(currentSkuId));
|
||||
|
||||
console.log('[初始化规格] 当前SKU ID:', currentSkuId);
|
||||
console.log('[初始化规格] 匹配的SKU:', currentSku);
|
||||
|
||||
if (currentSku && Array.isArray(currentSku.specInfo)) {
|
||||
// 标记当前SKU的规格为选中
|
||||
currentSku.specInfo.forEach((spec) => {
|
||||
const specId = spec.specId;
|
||||
const specValueId = spec.specValueId;
|
||||
|
||||
specList.forEach((group) => {
|
||||
if (String(group.specId) === String(specId)) {
|
||||
group.specValueList.forEach((sv) => {
|
||||
sv.isSelected = String(sv.specValueId) === String(specValueId);
|
||||
});
|
||||
selectedSku[group.specId] = specValueId;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const isAllSelectedSku = specList.every((spec) => !!selectedSku[spec.specId]);
|
||||
|
||||
// 更新价格
|
||||
const initPrice = currentSku ? currentSku.price : (skuArray[0] ? skuArray[0].price : 0);
|
||||
console.log('[初始化规格] 初始价格:', initPrice);
|
||||
|
||||
this.setData({
|
||||
selectedSku,
|
||||
isAllSelectedSku,
|
||||
selectSkuSellsPrice: initPrice,
|
||||
currentPrice: initPrice,
|
||||
'goods.price': initPrice,
|
||||
});
|
||||
},
|
||||
|
||||
toChooseItem(e) {
|
||||
const { specid, id } = e.currentTarget.dataset;
|
||||
const { specList, skuList } = this.data;
|
||||
|
||||
// 更新选中状态
|
||||
specList.forEach((spec) => {
|
||||
if (String(spec.specId) === String(specid)) {
|
||||
spec.specValueList.forEach((specValue) => {
|
||||
specValue.isSelected = String(specValue.specValueId) === String(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新selectedSku
|
||||
const selectedSku = { ...this.data.selectedSku };
|
||||
selectedSku[specid] = id;
|
||||
|
||||
const isAllSelectedSku = specList.every((spec) => !!selectedSku[spec.specId]);
|
||||
|
||||
this.setData({
|
||||
specList,
|
||||
selectedSku,
|
||||
isAllSelectedSku,
|
||||
});
|
||||
|
||||
// 查找SKU并更新价格
|
||||
if (isAllSelectedSku) {
|
||||
const matchedSku = this.findMatchedSku(selectedSku, skuList);
|
||||
if (matchedSku) {
|
||||
console.log('[SKU选择] 匹配SKU:', matchedSku);
|
||||
console.log('[SKU选择] SKU价格:', matchedSku.price);
|
||||
this.setData({
|
||||
selectSkuSellsPrice: matchedSku.price,
|
||||
currentPrice: matchedSku.price,
|
||||
'goods.price': matchedSku.price,
|
||||
});
|
||||
} else {
|
||||
console.warn('[SKU选择] 未找到匹配SKU');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
findMatchedSku(selectedSku, skuList) {
|
||||
return skuList.find((sku) => {
|
||||
if (!sku.specInfo || sku.specInfo.length === 0) return false;
|
||||
|
||||
return sku.specInfo.every((spec) => {
|
||||
const specId = String(spec.specId);
|
||||
const specValueId = String(spec.specValueId);
|
||||
const selectedSpecValueId = String(selectedSku[specId] || '');
|
||||
return selectedSpecValueId === specValueId;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onClose() {
|
||||
this.triggerEvent('close');
|
||||
},
|
||||
|
||||
onConfirm() {
|
||||
const { isAllSelectedSku, selectedSku, skuList, spuId, buyNum } = this.data;
|
||||
|
||||
if (!isAllSelectedSku) {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '请选择完整的商品规格',
|
||||
theme: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找匹配SKU
|
||||
const matchedSku = this.findMatchedSku(selectedSku, skuList);
|
||||
|
||||
if (!matchedSku) {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '该规格不存在',
|
||||
theme: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 触发confirm事件,传递数量
|
||||
this.triggerEvent('confirm', {
|
||||
spuId,
|
||||
skuId: matchedSku.skuId,
|
||||
price: matchedSku.price,
|
||||
quantity: buyNum,
|
||||
});
|
||||
},
|
||||
|
||||
handleBuyNumChange(e) {
|
||||
const { value } = e.detail;
|
||||
this.setData({
|
||||
buyNum: value,
|
||||
});
|
||||
},
|
||||
|
||||
onCloseOver() {
|
||||
this.triggerEvent('closeover');
|
||||
},
|
||||
},
|
||||
});
|
||||
7
miniprogram/pages/cart/components/specs-popup/index.json
Normal file
7
miniprogram/pages/cart/components/specs-popup/index.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-popup": "tdesign-miniprogram/popup/popup",
|
||||
"goods-card": "../../components/goods-card/index"
|
||||
}
|
||||
}
|
||||
55
miniprogram/pages/cart/components/specs-popup/index.wxml
Normal file
55
miniprogram/pages/cart/components/specs-popup/index.wxml
Normal file
@@ -0,0 +1,55 @@
|
||||
<t-popup visible="{{show}}" placement="bottom" z-index="{{zIndex}}" bind:visible-change="onClose">
|
||||
<view class="specs-popup">
|
||||
<view class="popup-close" bindtap="onClose">
|
||||
<t-icon name="close" size="36rpx" />
|
||||
</view>
|
||||
|
||||
<!-- 商品信息 -->
|
||||
<view class="popup-header">
|
||||
<goods-card data="{{goods}}" layout="horizontal-wrap" thumb-mode="{{thumbMode}}" />
|
||||
</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view wx:if="{{loading}}" class="loading-container">
|
||||
<t-loading theme="circular" size="40rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 规格选择 -->
|
||||
<view wx:else class="spec-selection">
|
||||
<view class="spec-group" wx:for="{{specList}}" wx:key="specId">
|
||||
<view class="spec-group-title">{{item.title}}</view>
|
||||
<view class="spec-options">
|
||||
<view
|
||||
wx:for="{{item.specValueList}}"
|
||||
wx:for-item="specValue"
|
||||
wx:key="specValueId"
|
||||
class="spec-option {{specValue.isSelected ? 'spec-option--active' : ''}}"
|
||||
data-specid="{{item.specId}}"
|
||||
data-id="{{specValue.specValueId}}"
|
||||
bindtap="toChooseItem"
|
||||
>
|
||||
{{specValue.specValue}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数量选择 -->
|
||||
<view class="quantity-section">
|
||||
<view class="quantity-label">购买数量</view>
|
||||
<t-stepper
|
||||
value="{{buyNum}}"
|
||||
min="{{1}}"
|
||||
max="{{999}}"
|
||||
theme="filled"
|
||||
bind:change="handleBuyNumChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<view class="bottom-btn {{!isAllSelectedSku ? 'bottom-btn--disabled' : ''}}" hover-class="bottom-btn--active" bindtap="onConfirm">
|
||||
确认
|
||||
</view>
|
||||
</view>
|
||||
</t-popup>
|
||||
<t-toast id="t-toast" />
|
||||
126
miniprogram/pages/cart/components/specs-popup/index.wxss
Normal file
126
miniprogram/pages/cart/components/specs-popup/index.wxss
Normal file
@@ -0,0 +1,126 @@
|
||||
.specs-popup {
|
||||
width: 100vw;
|
||||
box-sizing: border-box;
|
||||
padding: 32rpx 32rpx calc(20rpx + env(safe-area-inset-bottom)) 32rpx;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
top: 30rpx;
|
||||
z-index: 9;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.price-section {
|
||||
margin-top: 24rpx;
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 36rpx;
|
||||
color: #fa4126;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
padding: 80rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spec-selection {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.spec-group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.spec-group-title {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
margin-bottom: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.spec-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.spec-option {
|
||||
min-width: 128rpx;
|
||||
height: 56rpx;
|
||||
line-height: 56rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: #f5f5f5;
|
||||
border: 2rpx solid #f5f5f5;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.spec-option--active {
|
||||
background-color: rgba(250, 65, 38, 0.04);
|
||||
border-color: #fa4126;
|
||||
color: #fa4126;
|
||||
}
|
||||
|
||||
.quantity-section {
|
||||
margin-top: 40rpx;
|
||||
padding-top: 32rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.quantity-label {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bottom-btn {
|
||||
margin-top: 42rpx;
|
||||
position: relative;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
background-color: #fa4126;
|
||||
color: white;
|
||||
border-radius: 40rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bottom-btn--disabled {
|
||||
background-color: #dddddd;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.bottom-btn--active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
555
miniprogram/pages/cart/index.js
Normal file
555
miniprogram/pages/cart/index.js
Normal file
@@ -0,0 +1,555 @@
|
||||
import Dialog from 'tdesign-miniprogram/dialog/index';
|
||||
import Toast from 'tdesign-miniprogram/toast/index';
|
||||
import {
|
||||
fetchCartGroupData,
|
||||
updateCartItem,
|
||||
removeFromCart,
|
||||
addToCart,
|
||||
selectCartItem,
|
||||
selectAllCartItems
|
||||
} from '../../services/cart/cart';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
cartGroupData: null,
|
||||
refresherTriggered: false, // 下拉刷新状态
|
||||
},
|
||||
|
||||
// 调用自定义tabbar的init函数,使页面与tabbar激活状态保持一致
|
||||
onShow() {
|
||||
this.getTabBar().init();
|
||||
// 每次显示页面时刷新购物车数据,确保数据是最新的
|
||||
this.refreshData();
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.refreshData();
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
this.getCartGroupData().then((res) => {
|
||||
let isEmpty = true;
|
||||
const cartGroupData = res.data;
|
||||
// 一些组件中需要的字段可能接口并没有返回,或者返回的数据结构与预期不一致,需要在此先对数据做一些处理
|
||||
// 统计门店下加购的商品是否全选、是否存在缺货/无货
|
||||
if (cartGroupData && cartGroupData.storeGoods) {
|
||||
for (const store of cartGroupData.storeGoods) {
|
||||
store.isSelected = true; // 该门店已加购商品是否全选
|
||||
store.storeStockShortage = false; // 该门店已加购商品是否存在库存不足
|
||||
if (!store.shortageGoodsList) {
|
||||
store.shortageGoodsList = []; // 该门店已加购商品如果库存为0需单独分组
|
||||
}
|
||||
for (const activity of store.promotionGoodsList) {
|
||||
activity.goodsPromotionList = activity.goodsPromotionList.filter((goods) => {
|
||||
goods.originPrice = undefined;
|
||||
|
||||
// 统计是否有加购数大于库存数的商品
|
||||
if (goods.quantity > goods.stockQuantity) {
|
||||
store.storeStockShortage = true;
|
||||
}
|
||||
// 统计是否全选
|
||||
if (!goods.isSelected) {
|
||||
store.isSelected = false;
|
||||
}
|
||||
// 库存为0(无货)的商品单独分组
|
||||
if (goods.stockQuantity > 0) {
|
||||
return true;
|
||||
}
|
||||
store.shortageGoodsList.push(goods);
|
||||
return false;
|
||||
});
|
||||
|
||||
if (activity.goodsPromotionList.length > 0) {
|
||||
isEmpty = false;
|
||||
}
|
||||
}
|
||||
if (store.shortageGoodsList.length > 0) {
|
||||
isEmpty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cartGroupData && cartGroupData.invalidGoodItems) {
|
||||
cartGroupData.invalidGoodItems = cartGroupData.invalidGoodItems.map((goods) => {
|
||||
goods.originPrice = undefined;
|
||||
return goods;
|
||||
});
|
||||
}
|
||||
if (cartGroupData) {
|
||||
cartGroupData.isNotEmpty = !isEmpty;
|
||||
}
|
||||
this.setData({ cartGroupData });
|
||||
});
|
||||
},
|
||||
|
||||
findGoods(spuId, skuId) {
|
||||
let currentStore;
|
||||
let currentActivity;
|
||||
let currentGoods;
|
||||
if (!this.data.cartGroupData || !this.data.cartGroupData.storeGoods) {
|
||||
return {
|
||||
currentStore: null,
|
||||
currentActivity: null,
|
||||
currentGoods: null,
|
||||
};
|
||||
}
|
||||
const { storeGoods } = this.data.cartGroupData;
|
||||
for (const store of storeGoods) {
|
||||
for (const activity of store.promotionGoodsList) {
|
||||
for (const goods of activity.goodsPromotionList) {
|
||||
if (goods.spuId === spuId && goods.skuId === skuId) {
|
||||
currentStore = store;
|
||||
currentActivity = activity;
|
||||
currentGoods = goods;
|
||||
return {
|
||||
currentStore,
|
||||
currentActivity,
|
||||
currentGoods,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
currentStore,
|
||||
currentActivity,
|
||||
currentGoods,
|
||||
};
|
||||
},
|
||||
|
||||
// 获取购物车数据 - 每次都从后端获取最新数据
|
||||
getCartGroupData() {
|
||||
return fetchCartGroupData();
|
||||
},
|
||||
|
||||
// 选择单个商品
|
||||
selectGoodsService({ spuId, skuId, isSelected }) {
|
||||
// 先更新本地状态
|
||||
const { currentGoods } = this.findGoods(spuId, skuId);
|
||||
if (currentGoods) {
|
||||
currentGoods.isSelected = isSelected ? 1 : 0;
|
||||
}
|
||||
|
||||
// 调用API更新后端状态
|
||||
const cartId = currentGoods ? currentGoods.uid : spuId;
|
||||
return selectCartItem(cartId, isSelected).catch(err => {
|
||||
console.error('选择商品失败:', err);
|
||||
// 如果API调用失败,恢复本地状态
|
||||
if (currentGoods) {
|
||||
currentGoods.isSelected = isSelected ? 0 : 1;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
// 全选门店
|
||||
// 注:实际场景时应该调用接口更改选中状态
|
||||
selectStoreService({ storeId, isSelected }) {
|
||||
if (!this.data.cartGroupData || !this.data.cartGroupData.storeGoods) {
|
||||
return;
|
||||
}
|
||||
const currentStore = this.data.cartGroupData.storeGoods.find((s) => s.storeId === storeId);
|
||||
if (currentStore) {
|
||||
currentStore.isSelected = isSelected;
|
||||
// 添加数据验证,确保数组存在
|
||||
if (Array.isArray(currentStore.promotionGoodsList)) {
|
||||
currentStore.promotionGoodsList.forEach((activity) => {
|
||||
if (Array.isArray(activity.goodsPromotionList)) {
|
||||
activity.goodsPromotionList.forEach((goods) => {
|
||||
goods.isSelected = isSelected;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
// 加购数量变更
|
||||
async changeQuantityService(spuId, skuId, quantity) {
|
||||
try {
|
||||
const goods = this.findGoods(spuId, skuId);
|
||||
if (!goods) {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '商品不存在',
|
||||
theme: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用后端API,传递skuId
|
||||
await updateCartItem(spuId, quantity, skuId);
|
||||
|
||||
// 更新本地数据
|
||||
goods.quantity = quantity;
|
||||
this.setData({
|
||||
cartGroupData: this.data.cartGroupData,
|
||||
});
|
||||
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '修改成功',
|
||||
theme: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('修改数量失败:', error);
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: error.message || '修改失败',
|
||||
theme: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 删除商品
|
||||
async deleteGoodsService(spuId, skuId) {
|
||||
try {
|
||||
const goods = this.findGoods(spuId, skuId);
|
||||
if (!goods.currentGoods) {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '商品不存在',
|
||||
theme: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用后端API,传递skuId
|
||||
await removeFromCart(spuId, skuId);
|
||||
|
||||
// 重新获取购物车数据
|
||||
this.refreshData();
|
||||
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '删除成功',
|
||||
theme: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除商品失败:', error);
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: error.message || '删除失败',
|
||||
theme: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 清空失效商品
|
||||
// 注:实际场景时应该调用接口
|
||||
clearInvalidGoodsService() {
|
||||
this.data.cartGroupData.invalidGoodItems = [];
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
onGoodsSelect(e) {
|
||||
const {
|
||||
goods: { spuId, skuId },
|
||||
isSelected,
|
||||
} = e.detail;
|
||||
const { currentGoods } = this.findGoods(spuId, skuId);
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: `${isSelected ? '选择' : '取消'}"${
|
||||
currentGoods.title.length > 5 ? `${currentGoods.title.slice(0, 5)}...` : currentGoods.title
|
||||
}"`,
|
||||
icon: '',
|
||||
});
|
||||
this.selectGoodsService({ spuId, skuId, isSelected }).then(() => this.refreshData());
|
||||
},
|
||||
|
||||
onStoreSelect(e) {
|
||||
const {
|
||||
store: { storeId },
|
||||
isSelected,
|
||||
} = e.detail;
|
||||
this.selectStoreService({ storeId, isSelected }).then(() => this.refreshData());
|
||||
},
|
||||
|
||||
onQuantityChange(e) {
|
||||
const {
|
||||
goods: { spuId, skuId },
|
||||
quantity,
|
||||
} = e.detail;
|
||||
|
||||
const { currentGoods } = this.findGoods(spuId, skuId);
|
||||
const stockQuantity = currentGoods.stockQuantity > 0 ? currentGoods.stockQuantity : 0; // 避免后端返回的是-1
|
||||
// 加购数量超过库存数量
|
||||
if (quantity > stockQuantity) {
|
||||
// 加购数量等于库存数量的情况下继续加购
|
||||
if (currentGoods.quantity === stockQuantity && quantity - stockQuantity === 1) {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '当前商品库存不足',
|
||||
});
|
||||
return;
|
||||
}
|
||||
Dialog.confirm({
|
||||
title: '商品库存不足',
|
||||
content: `当前商品库存不足,最大可购买数量为${stockQuantity}件`,
|
||||
confirmBtn: '修改为最大可购买数量',
|
||||
cancelBtn: '取消',
|
||||
})
|
||||
.then(() => {
|
||||
this.changeQuantityService(spuId, skuId, stockQuantity).then(() => this.refreshData());
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
this.changeQuantityService(spuId, skuId, quantity).then(() => this.refreshData());
|
||||
},
|
||||
|
||||
// SKU变更
|
||||
async onSkuChange(e) {
|
||||
const { oldSpuId, oldSkuId, newSpuId, newSkuId, quantity } = e.detail;
|
||||
|
||||
try {
|
||||
// 先删除旧SKU
|
||||
await removeFromCart(oldSpuId, oldSkuId);
|
||||
|
||||
// 再添加新SKU,使用用户选择的数量
|
||||
await addToCart(newSpuId, newSkuId, quantity || 1);
|
||||
|
||||
// 刷新购物车数据
|
||||
this.refreshData();
|
||||
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '规格修改成功',
|
||||
theme: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('SKU变更失败:', error);
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: error.message || '修改规格失败',
|
||||
theme: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
goCollect() {
|
||||
/** 活动肯定有一个活动ID,用来获取活动banner,活动商品列表等 */
|
||||
const promotionID = '123';
|
||||
wx.navigateTo({
|
||||
url: `/pages/promotion/promotion-detail/index?promotion_id=${promotionID}`,
|
||||
});
|
||||
},
|
||||
|
||||
goGoodsDetail(e) {
|
||||
const { spuId, storeId } = e.detail.goods;
|
||||
wx.navigateTo({
|
||||
url: `/pages/goods/details/index?spuId=${spuId}&storeId=${storeId}`,
|
||||
});
|
||||
},
|
||||
|
||||
clearInvalidGoods() {
|
||||
// 实际场景时应该调用接口清空失效商品
|
||||
this.clearInvalidGoodsService().then(() => this.refreshData());
|
||||
},
|
||||
|
||||
onGoodsDelete(e) {
|
||||
const {
|
||||
goods: { spuId, skuId },
|
||||
} = e.detail;
|
||||
Dialog.confirm({
|
||||
content: '确认删除该商品吗?',
|
||||
confirmBtn: '确定',
|
||||
cancelBtn: '取消',
|
||||
}).then(() => {
|
||||
// deleteGoodsService 内部已经包含了Toast提示和数据刷新,无需重复操作
|
||||
this.deleteGoodsService(spuId, skuId);
|
||||
});
|
||||
},
|
||||
|
||||
onSelectAll(event) {
|
||||
const { isAllSelected } = event?.detail ?? {};
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: `${isAllSelected ? '取消' : '点击'}了全选按钮`,
|
||||
});
|
||||
|
||||
// 调用接口改变全选
|
||||
selectAllCartItems(!isAllSelected).then(() => {
|
||||
this.refreshData();
|
||||
}).catch(err => {
|
||||
console.error('全选操作失败:', err);
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '操作失败,请重试',
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onToSettle() {
|
||||
const goodsRequestList = [];
|
||||
let totalAmount = 0;
|
||||
let selectedCount = 0;
|
||||
|
||||
// 添加数据验证,确保数据结构存在
|
||||
if (this.data.cartGroupData && Array.isArray(this.data.cartGroupData.storeGoods)) {
|
||||
this.data.cartGroupData.storeGoods.forEach((store) => {
|
||||
if (Array.isArray(store.promotionGoodsList)) {
|
||||
store.promotionGoodsList.forEach((promotion) => {
|
||||
if (Array.isArray(promotion.goodsPromotionList)) {
|
||||
promotion.goodsPromotionList.forEach((m) => {
|
||||
if (m.isSelected == 1) {
|
||||
// 检查库存
|
||||
if (m.quantity > m.stockQuantity) {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: `${m.title}库存不足,请调整数量`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
goodsRequestList.push({
|
||||
...m,
|
||||
// 确保价格格式正确
|
||||
price: parseInt(m.price) || 0,
|
||||
originPrice: parseInt(m.originPrice) || parseInt(m.price) || 0,
|
||||
});
|
||||
selectedCount += m.quantity;
|
||||
totalAmount += (parseInt(m.price) || 0) * m.quantity;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 验证是否有选中的商品
|
||||
if (goodsRequestList.length === 0) {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '请选择要结算的商品',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证用户是否登录
|
||||
const token = wx.getStorageSync('token');
|
||||
if (!token) {
|
||||
Dialog.confirm({
|
||||
title: '提示',
|
||||
content: '请先登录后再进行结算',
|
||||
confirmBtn: '去登录',
|
||||
cancelBtn: '取消',
|
||||
}).then(() => {
|
||||
wx.navigateTo({ url: '/pages/login/index' });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存结算数据到本地存储
|
||||
const orderData = {
|
||||
goodsRequestList,
|
||||
totalAmount,
|
||||
selectedCount,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
try {
|
||||
wx.setStorageSync('order.goodsRequestList', JSON.stringify(goodsRequestList));
|
||||
wx.setStorageSync('order.settlementData', JSON.stringify(orderData));
|
||||
|
||||
// 跳转到订单确认页面
|
||||
wx.navigateTo({
|
||||
url: '/pages/order/order-confirm/index?type=cart',
|
||||
success: () => {
|
||||
console.log('跳转到订单确认页面成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('跳转失败:', err);
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '跳转失败,请重试',
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存订单数据失败:', error);
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '数据保存失败,请重试',
|
||||
});
|
||||
}
|
||||
},
|
||||
onGotoHome() {
|
||||
wx.switchTab({ url: '/pages/home/home' });
|
||||
},
|
||||
|
||||
// 下拉刷新相关方法
|
||||
onRefresh() {
|
||||
console.log('开始下拉刷新');
|
||||
this.setData({
|
||||
refresherTriggered: true,
|
||||
});
|
||||
|
||||
// 重新获取购物车数据
|
||||
this.refreshData();
|
||||
|
||||
// 模拟网络请求延迟,确保用户能看到刷新动画
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
refresherTriggered: false,
|
||||
});
|
||||
|
||||
// 显示刷新成功提示
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '刷新成功',
|
||||
theme: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
onRefresherPulling(e) {
|
||||
console.log('下拉中', e.detail);
|
||||
},
|
||||
|
||||
onRefresherRestore(e) {
|
||||
console.log('刷新恢复', e.detail);
|
||||
},
|
||||
|
||||
onRefresherAbort(e) {
|
||||
console.log('刷新中止', e.detail);
|
||||
this.setData({
|
||||
refresherTriggered: false,
|
||||
});
|
||||
},
|
||||
|
||||
// 分享功能
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '购物车 - 查看我的心仪好物',
|
||||
path: '/pages/cart/index'
|
||||
};
|
||||
},
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '购物车 - 查看我的心仪好物'
|
||||
};
|
||||
}
|
||||
});
|
||||
10
miniprogram/pages/cart/index.json
Normal file
10
miniprogram/pages/cart/index.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "购物车",
|
||||
"usingComponents": {
|
||||
"cart-group": "./components/cart-group/index",
|
||||
"cart-empty": "./components/cart-empty/index",
|
||||
"cart-bar": "./components/cart-bar/index",
|
||||
"t-toast": "tdesign-miniprogram/toast/toast",
|
||||
"t-dialog": "tdesign-miniprogram/dialog/dialog"
|
||||
}
|
||||
}
|
||||
52
miniprogram/pages/cart/index.wxml
Normal file
52
miniprogram/pages/cart/index.wxml
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- 下拉刷新容器 -->
|
||||
<scroll-view
|
||||
class="cart-scroll-view"
|
||||
scroll-y="{{true}}"
|
||||
refresher-enabled="{{true}}"
|
||||
refresher-threshold="{{45}}"
|
||||
refresher-default-style="black"
|
||||
refresher-background="white"
|
||||
refresher-triggered="{{refresherTriggered}}"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
bindrefresherpulling="onRefresherPulling"
|
||||
bindrefresherrestore="onRefresherRestore"
|
||||
bindrefresherabort="onRefresherAbort"
|
||||
>
|
||||
<!-- 分层购物车 -->
|
||||
<block wx:if="{{cartGroupData.isNotEmpty}}">
|
||||
<cart-group
|
||||
store-goods="{{ cartGroupData.storeGoods }}"
|
||||
invalid-good-items="{{ cartGroupData.invalidGoodItems }}"
|
||||
bindselectgoods="onGoodsSelect"
|
||||
bindselectstore="onStoreSelect"
|
||||
bindchangequantity="onQuantityChange"
|
||||
bindchangesku="onSkuChange"
|
||||
bindgocollect="goCollect"
|
||||
bindgoodsclick="goGoodsDetail"
|
||||
bindclearinvalidgoods="clearInvalidGoods"
|
||||
binddelete="onGoodsDelete"
|
||||
/>
|
||||
|
||||
<view class="gap" />
|
||||
</block>
|
||||
<!-- 购物车空态 -->
|
||||
<cart-empty wx:else bind:handleClick="onGotoHome" />
|
||||
|
||||
<!-- 底部占位,避免被底部栏遮挡 -->
|
||||
<view class="bottom-placeholder"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 商品小计以及结算按钮 -->
|
||||
<cart-bar
|
||||
wx:if="{{cartGroupData.isNotEmpty}}"
|
||||
is-all-selected="{{cartGroupData.isAllSelected}}"
|
||||
total-amount="{{cartGroupData.totalAmount}}"
|
||||
total-goods-num="{{cartGroupData.selectedGoodsCount}}"
|
||||
total-discount-amount="{{cartGroupData.totalDiscountAmount}}"
|
||||
fixed="{{true}}"
|
||||
bottomHeight="{{110}}"
|
||||
bindhandleSelectAll="onSelectAll"
|
||||
bindhandleToSettle="onToSettle"
|
||||
/>
|
||||
<t-toast id="t-toast" />
|
||||
<t-dialog id="t-dialog" />
|
||||
26
miniprogram/pages/cart/index.wxss
Normal file
26
miniprogram/pages/cart/index.wxss
Normal file
@@ -0,0 +1,26 @@
|
||||
:host {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cart-scroll-view {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.gap {
|
||||
height: 100rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 120rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.t-button {
|
||||
--td-button-default-color: #000;
|
||||
--td-button-primary-text-color: #fa4126;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# Sidebar 侧边导航
|
||||
|
||||
### 引入
|
||||
|
||||
全局引入,在miniprogram根目录下的`app.json`中配置,局部引入,在需要引入的页面或组件的`index.json`中配置。
|
||||
|
||||
```json
|
||||
// app.json 或 index.json
|
||||
"usingComponents": {
|
||||
"wr-sidebar": "path/to/components/goods-category/wr-sidebar/index",
|
||||
"wr-sidebar-item": "path/to/component/goods-category/wr-sidebar/wr-sidebar-item/index"
|
||||
}
|
||||
```
|
||||
|
||||
## 代码演示
|
||||
|
||||
### 基础用法
|
||||
|
||||
通过在`wr-sidebar`上设置`activeKey`属性来控制选中项
|
||||
|
||||
```html
|
||||
<wr-sidebar active-key="{{ activeKey }}" bind:change="onChange">
|
||||
<wr-sidebar-item title="标签名称" />
|
||||
<wr-sidebar-item title="标签名称" />
|
||||
<wr-sidebar-item title="标签名称" />
|
||||
</wr-sidebar>
|
||||
```
|
||||
|
||||
``` javascript
|
||||
Page({
|
||||
data: {
|
||||
activeKey: 0
|
||||
},
|
||||
|
||||
onChange(event) {
|
||||
wx.showToast({
|
||||
icon: 'none',
|
||||
title: `切换至第${event.detail}项`
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 提示气泡(暂未实现)
|
||||
|
||||
设置`dot`属性后,会在右上角展示一个小红点。设置`info`属性后,会在右上角展示相应的徽标
|
||||
|
||||
```html
|
||||
<wr-sidebar active-key="{{ activeKey }}">
|
||||
<wr-sidebar-item title="标签名称" dot />
|
||||
<wr-sidebar-item title="标签名称" info="5" />
|
||||
<wr-sidebar-item title="标签名称" info="99+" />
|
||||
</wr-sidebar>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Sidebar Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
|-----------|-----------|-----------|-------------|-------------|
|
||||
| activeKey | 选中项的索引 | *string \| number* | `0` | - |
|
||||
|
||||
### Sidebar Event
|
||||
|
||||
| 事件名 | 说明 | 参数 |
|
||||
|------|------|------|
|
||||
| change | 切换选项时触发 | 当前选中选项的索引 |
|
||||
|
||||
### Sidebar 外部样式类
|
||||
|
||||
| 类名 | 说明 |
|
||||
|-----------|-----------|
|
||||
| custom-class | 根节点样式类 |
|
||||
|
||||
### SidebarItem Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
|-----------|-----------|-----------|-------------|-------------|
|
||||
| title | 内容 | *string* | `''` | - |
|
||||
| disabled | 是否禁用 | | *boolean* | `false` | - |
|
||||
| dot | 是否显示右上角小红点 | *boolean* | `false` | - |
|
||||
| info | 提示消息 | *string \| number* | `''` | - |
|
||||
|
||||
### SidebarItem Event
|
||||
|
||||
| 事件名 | 说明 | 参数 |
|
||||
|------|------|------|
|
||||
| click | 点击徽章时触发 | 当前徽章的索引 |
|
||||
|
||||
### SidebarItem 外部样式类
|
||||
|
||||
| 类名 | 说明 |
|
||||
|-----------|-----------|
|
||||
| custom-class | 根节点样式类 |
|
||||
@@ -0,0 +1,51 @@
|
||||
Component({
|
||||
relations: {
|
||||
'../../c-sidebar/index': {
|
||||
type: 'ancestor',
|
||||
linked(target) {
|
||||
this.parent = target;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
externalClasses: ['custom-class'],
|
||||
properties: {
|
||||
title: String,
|
||||
disabled: Boolean,
|
||||
},
|
||||
|
||||
data: {
|
||||
topRightRadius: false,
|
||||
bottomRightRadius: false,
|
||||
},
|
||||
|
||||
methods: {
|
||||
setActive(selected) {
|
||||
return this.setData({ selected });
|
||||
},
|
||||
onClick() {
|
||||
const { parent } = this;
|
||||
|
||||
if (!parent || this.properties.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parent.children.indexOf(this);
|
||||
|
||||
parent.setActive(index).then(() => {
|
||||
this.triggerEvent('click', index);
|
||||
parent.triggerEvent('change', { index });
|
||||
});
|
||||
},
|
||||
setTopRightRadius(val) {
|
||||
return this.setData({
|
||||
topRightRadius: val,
|
||||
});
|
||||
},
|
||||
setBottomRightRadius(val) {
|
||||
return this.setData({
|
||||
bottomRightRadius: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<view class="c-sidebar-item-container">
|
||||
<view
|
||||
class="c-sidebar-item {{ selected ? 'active' : '' }} {{ disabled ? 'disabled' : '' }} {{topRightRadius ? 'top-right-radius' : ''}} {{bottomRightRadius ? 'bottom-right-radius' : ''}} custom-class"
|
||||
hover-class="c-sidebar-item--hover"
|
||||
hover-stay-time="70"
|
||||
bind:tap="onClick"
|
||||
>
|
||||
<view class="c-sidebar-item__text text-overflow"> {{ title }} </view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,60 @@
|
||||
.c-sidebar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
background-color: #f5f5f5;
|
||||
color: #222427;
|
||||
padding: 20rpx 0;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.c-sidebar-item.active {
|
||||
position: relative;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.c-sidebar-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 6rpx;
|
||||
height: 48rpx;
|
||||
background-color: #fa4126;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
border-radius: 64rpx;
|
||||
}
|
||||
|
||||
.c-sidebar-item__text {
|
||||
width: 136rpx;
|
||||
height: 36rpx;
|
||||
padding: 8rpx 0;
|
||||
line-height: 36rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.c-sidebar-item.active .c-sidebar-item__text {
|
||||
background-color: white;
|
||||
border-radius: 36rpx;
|
||||
color: #fa4126;
|
||||
}
|
||||
|
||||
.text-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.top-right-radius {
|
||||
border-top-right-radius: 16rpx;
|
||||
}
|
||||
|
||||
.bottom-right-radius {
|
||||
border-bottom-right-radius: 16rpx;
|
||||
}
|
||||
|
||||
.c-sidebar-item-container {
|
||||
background-color: white;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
Component({
|
||||
relations: {
|
||||
'./c-sidebar-item/index': {
|
||||
type: 'descendant',
|
||||
linked(target) {
|
||||
this.children.push(target);
|
||||
this.setActive(this.properties.activeKey, true);
|
||||
},
|
||||
unlinked(target) {
|
||||
this.children = this.children.filter((item) => item !== target);
|
||||
this.setActive(this.properties.activeKey, true);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
externalClasses: ['custom-class'],
|
||||
|
||||
properties: {
|
||||
activeKey: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
observers: {
|
||||
activeKey(newVal) {
|
||||
this.setActive(newVal);
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.children = [];
|
||||
this.currentActive = -1;
|
||||
this.topRightRadiusItemIndexs = [];
|
||||
this.bottomRightRadiusItemIndexs = [];
|
||||
},
|
||||
|
||||
methods: {
|
||||
setActive(activeKey, isChildrenChange) {
|
||||
const {
|
||||
children,
|
||||
currentActive,
|
||||
topRightRadiusItemIndexs: preTopRightRadiusItemIndexs,
|
||||
bottomRightRadiusItemIndexs: preBottomRightRadiusItemIndexs,
|
||||
} = this;
|
||||
|
||||
if (!children.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (activeKey === currentActive && !isChildrenChange) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.currentActive = activeKey;
|
||||
this.topRightRadiusItemIndexs = this.getTopRightRadiusItemIndexs(activeKey, children);
|
||||
this.bottomRightRadiusItemIndexs = this.getBottomRightRadiusItemIndexs(activeKey, children);
|
||||
|
||||
const stack = []; // 任务列表,存放调用子组件的setActive后返回的一堆promise
|
||||
|
||||
// 将旧的选中项改为false
|
||||
if (currentActive !== activeKey && currentActive >= 0 && currentActive < children.length && children[currentActive]) {
|
||||
stack.push(children[currentActive].setActive(false));
|
||||
}
|
||||
|
||||
// 将新的选中项改为true
|
||||
if (activeKey >= 0 && activeKey < children.length && children[activeKey]) {
|
||||
stack.push(children[activeKey].setActive(true));
|
||||
}
|
||||
|
||||
preTopRightRadiusItemIndexs.forEach((item) => {
|
||||
if (item >= 0 && item < children.length && children[item]) {
|
||||
stack.push(children[item].setTopRightRadius(false));
|
||||
}
|
||||
});
|
||||
|
||||
preBottomRightRadiusItemIndexs.forEach((item) => {
|
||||
if (item >= 0 && item < children.length && children[item]) {
|
||||
stack.push(children[item].setBottomRightRadius(false));
|
||||
}
|
||||
});
|
||||
|
||||
this.topRightRadiusItemIndexs.forEach((item) => {
|
||||
if (item >= 0 && item < children.length && children[item]) {
|
||||
stack.push(children[item].setTopRightRadius(true));
|
||||
}
|
||||
});
|
||||
|
||||
this.bottomRightRadiusItemIndexs.forEach((item) => {
|
||||
if (item >= 0 && item < children.length && children[item]) {
|
||||
stack.push(children[item].setBottomRightRadius(true));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(stack);
|
||||
},
|
||||
getTopRightRadiusItemIndexs(activeKey, children) {
|
||||
const { length } = children;
|
||||
if (activeKey !== 0 && activeKey < length - 1) return [0, activeKey + 1];
|
||||
if (activeKey !== 0) return [0];
|
||||
if (activeKey < length - 1) return [activeKey + 1];
|
||||
return [];
|
||||
},
|
||||
getBottomRightRadiusItemIndexs(activeKey) {
|
||||
if (activeKey !== 0) return [activeKey - 1];
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<scroll-view class="c-sidebar custom-class" scroll-y>
|
||||
<slot />
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,9 @@
|
||||
.c-sidebar {
|
||||
width: 176rpx;
|
||||
height: 100%;
|
||||
}
|
||||
.c-sidebar::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
Component({
|
||||
externalClasses: ['custom-class'],
|
||||
|
||||
properties: {
|
||||
tabList: Array,
|
||||
},
|
||||
|
||||
data: {
|
||||
unfolded: false,
|
||||
boardMaxHeight: null,
|
||||
},
|
||||
attached() {
|
||||
wx.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('.c-tabbar-more')
|
||||
.boundingClientRect((rect) => {
|
||||
this.setData({ boardMaxHeight: rect.height });
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeFold() {
|
||||
this.setData({
|
||||
unfolded: !this.data.unfolded,
|
||||
});
|
||||
const { unfolded } = this.data;
|
||||
this.triggerEvent('change', { unfolded });
|
||||
},
|
||||
|
||||
onSelect(event) {
|
||||
const activeKey = event.currentTarget.dataset.index;
|
||||
this.triggerEvent('select', activeKey);
|
||||
this.changeFold();
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<view class="c-tabbar-more">
|
||||
<view class="c-tabbar-more__btn" bind:tap="changeFold">
|
||||
<view class="wr {{unfolded ? 'wr-arrow-up':'wr-arrow-down'}}"></view>
|
||||
</view>
|
||||
<view class="t-tabbar-more__boardwrapper" wx:if="{{ unfolded }}">
|
||||
<view class="t-tabbar-more__mask"></view>
|
||||
<scroll-view
|
||||
class="c-tabbar-more__board"
|
||||
style="{{ boardMaxHeight ? 'height:' + boardMaxHeight + 'px;' : '' }}"
|
||||
scroll-y
|
||||
>
|
||||
<view class="c-tabbar-more__boardinner">
|
||||
<view
|
||||
class="c-tabbar-more__item text-overflow"
|
||||
wx:for="{{ tabList }}"
|
||||
wx:key="index"
|
||||
data-index="{{ index }}"
|
||||
bind:tap="onSelect"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,63 @@
|
||||
.c-tabbar-more {
|
||||
width: 100%;
|
||||
height: calc(100% - var(--tabbar-height, 100rpx));
|
||||
position: absolute;
|
||||
top: var(--tabbar-height, 100rpx);
|
||||
}
|
||||
.c-tabbar-more__btn {
|
||||
position: absolute;
|
||||
top: calc(0rpx - var(--tabbar-height, 100rpx));
|
||||
right: 0;
|
||||
width: 80rpx;
|
||||
height: var(--tabbar-height, 100rpx);
|
||||
line-height: var(--tabbar-height, 100rpx);
|
||||
background-color: var(--tabbar-background-color, white);
|
||||
box-shadow: -20rpx 0 20rpx -10rpx var(--tabbar-background-color, white);
|
||||
text-align: center;
|
||||
}
|
||||
.c-tabbar-more__btn .market {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.t-tabbar-more__boardwrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.t-tabbar-more__mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.c-tabbar-more__board {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
.c-tabbar-more__boardinner {
|
||||
padding: 20rpx 0 20rpx 20rpx;
|
||||
background-color: var(--tabbar-background-color, white);
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.c-tabbar-more__item {
|
||||
margin: 0 20rpx 20rpx 0;
|
||||
flex: 0 0 calc((100% - 60rpx) / 3);
|
||||
box-sizing: border-box;
|
||||
padding: 0 10rpx;
|
||||
border-radius: 30rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #5d5d5d;
|
||||
background-color: #eee;
|
||||
}
|
||||
.text-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
Component({
|
||||
externalClasses: ['custom-class'],
|
||||
|
||||
properties: {
|
||||
activeKey: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
tabList: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
showMore: Boolean, // 是否需要下拉功能
|
||||
},
|
||||
observers: {
|
||||
activeKey(newVal) {
|
||||
if (this.properties.tabList && newVal) {
|
||||
this.setActive(newVal).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
currentActive: -1,
|
||||
},
|
||||
attached() {
|
||||
this.setActive(this.properties.activeKey).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
setActive(activeKey) {
|
||||
if (!this.properties.tabList || !Array.isArray(this.properties.tabList) ||
|
||||
activeKey < 0 || activeKey >= this.properties.tabList.length ||
|
||||
!this.properties.tabList[activeKey] || this.properties.tabList[activeKey].disabled) {
|
||||
return Promise.reject('数据异常或不可操作');
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.setData(
|
||||
{
|
||||
currentActive: activeKey,
|
||||
},
|
||||
() => resolve(),
|
||||
);
|
||||
});
|
||||
},
|
||||
onClick(event) {
|
||||
let activeKey;
|
||||
if (event.type === 'select') {
|
||||
activeKey = event.detail;
|
||||
} else {
|
||||
activeKey = event.currentTarget.dataset.index;
|
||||
}
|
||||
this.setActive(activeKey)
|
||||
.then(() => {
|
||||
const { currentActive } = this.data;
|
||||
this.triggerEvent('change', { index: currentActive });
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"c-tabbar-more": "./c-tabbar-more/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<view class="c-tabbar custom-class">
|
||||
<scroll-view
|
||||
wx:if="{{ tabList.length > 0 }}"
|
||||
class="c-tabbar__scroll"
|
||||
scroll-x="true"
|
||||
scroll-into-view="{{ 'id-' + currentActive }}"
|
||||
>
|
||||
<view class="c-tabbar__inner {{showMore && tabList.length > 4 ? 'c-tabbar__inner_more' : ''}}">
|
||||
<view
|
||||
wx:for="{{ tabList }}"
|
||||
wx:key="index"
|
||||
id="{{ 'id-' + index }}"
|
||||
class="c-tabbar-item {{ currentActive === index ? 'active' : '' }} {{ item.disabled ? 'disabled' : '' }}"
|
||||
bind:tap="onClick"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<view class="c-tabbar-item__text"> {{ item.name }} </view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<c-tabbar-more wx:if="{{ showMore && tabList.length > 4 }}" tabList="{{tabList}}" bindselect="onClick" />
|
||||
<slot />
|
||||
</view>
|
||||
@@ -0,0 +1,53 @@
|
||||
.c-tabbar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
--tabbar-height: 100rpx;
|
||||
--tabbar-fontsize: 28rpx;
|
||||
--tabbar-background-color: white;
|
||||
}
|
||||
.c-tabbar__inner {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
.c-tabbar__scroll {
|
||||
position: relative;
|
||||
}
|
||||
.c-tabbar__scroll::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
bottom: -1px;
|
||||
height: 1px;
|
||||
background-color: #eee;
|
||||
z-index: 1;
|
||||
}
|
||||
.c-tabbar__inner.c-tabbar__inner_more::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
flex: none;
|
||||
}
|
||||
.c-tabbar-item {
|
||||
flex: none;
|
||||
height: 100rpx;
|
||||
color: #282828;
|
||||
font-size: 28rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
.c-tabbar-item.active:not(.disabled) {
|
||||
color: #0071ce;
|
||||
position: relative;
|
||||
}
|
||||
.c-tabbar-item.disabled {
|
||||
color: #ccc;
|
||||
}
|
||||
.c-tabbar-item__text {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
}
|
||||
109
miniprogram/pages/category/components/goods-category/index.js
Normal file
109
miniprogram/pages/category/components/goods-category/index.js
Normal file
@@ -0,0 +1,109 @@
|
||||
Component({
|
||||
options: {
|
||||
multipleSlots: true,
|
||||
},
|
||||
externalClasses: ['custom-class'],
|
||||
|
||||
properties: {
|
||||
category: {
|
||||
type: Array,
|
||||
observer(newVal) {
|
||||
// 分类数据到达后,若为二级分类并首次进入,则默认触发“全部”子项
|
||||
if (newVal && Array.isArray(newVal) && newVal.length > 0) {
|
||||
const activeKey = this.data.activeKey || 0;
|
||||
const subActiveKey = this.data.subActiveKey || 0;
|
||||
const parent = newVal[activeKey] || null;
|
||||
const children = (parent && parent.children) ? parent.children : [];
|
||||
if (!this._hasDefaultEmitted && this.properties.level === 2 && children.length > 0) {
|
||||
this._hasDefaultEmitted = true;
|
||||
const item = children[subActiveKey] || children[0];
|
||||
this.triggerEvent('changeCategory', { item, source: 'parent' });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
initActive: {
|
||||
type: Array,
|
||||
value: [],
|
||||
observer(newVal, oldVal) {
|
||||
if (newVal && oldVal && newVal.length > 0 && oldVal.length > 0 && newVal[0] !== oldVal[0]) {
|
||||
this.setActiveKey(newVal[0], newVal[1] || 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
isSlotRight: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
value: 3,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
activeKey: 0,
|
||||
subActiveKey: 0,
|
||||
},
|
||||
attached() {
|
||||
if (this.properties.initActive && Array.isArray(this.properties.initActive) && this.properties.initActive.length > 0) {
|
||||
this.setData({
|
||||
activeKey: this.properties.initActive[0],
|
||||
subActiveKey: this.properties.initActive.length > 1 ? this.properties.initActive[1] : 0,
|
||||
});
|
||||
}
|
||||
// 默认触发守卫标记
|
||||
this._hasDefaultEmitted = false;
|
||||
},
|
||||
methods: {
|
||||
onParentChange(event) {
|
||||
this.setActiveKey(event.detail.index, 0).then(() => {
|
||||
const { category } = this.properties;
|
||||
const parent = category && category[this.data.activeKey];
|
||||
const children = (parent && parent.children) ? parent.children : [];
|
||||
const item = children[0] || null;
|
||||
if (item) {
|
||||
this.triggerEvent('changeCategory', { item, source: 'parent' });
|
||||
}
|
||||
this.triggerEvent('change', [this.data.activeKey, this.data.subActiveKey]);
|
||||
});
|
||||
},
|
||||
onChildChange(event) {
|
||||
this.setActiveKey(this.data.activeKey, event.detail.index).then(() => {
|
||||
const { category } = this.properties;
|
||||
const parent = category && category[this.data.activeKey];
|
||||
const children = (parent && parent.children) ? parent.children : [];
|
||||
const item = children[this.data.subActiveKey] || null;
|
||||
if (item) {
|
||||
this.triggerEvent('changeCategory', { item, source: 'child' });
|
||||
}
|
||||
this.triggerEvent('change', [this.data.activeKey, this.data.subActiveKey]);
|
||||
});
|
||||
},
|
||||
changCategory(event) {
|
||||
const { item, index } = event.currentTarget.dataset;
|
||||
const nextIndex = typeof index === 'number' ? index : 0;
|
||||
// 更新当前子分类选中态
|
||||
this.setActiveKey(this.data.activeKey, nextIndex).then(() => {
|
||||
// 触发分类变更事件,传递完整的item数据
|
||||
this.triggerEvent('changeCategory', { item, source: 'child' });
|
||||
this.triggerEvent('change', [this.data.activeKey, this.data.subActiveKey]);
|
||||
});
|
||||
},
|
||||
setActiveKey(key, subKey) {
|
||||
return new Promise((resolve) => {
|
||||
this.setData(
|
||||
{
|
||||
activeKey: key,
|
||||
subActiveKey: subKey,
|
||||
},
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
onRightScrollToLower() {
|
||||
this.triggerEvent('reachbottom');
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"c-tabbar": "./components/c-tabbar/index",
|
||||
"c-sidebar": "./components/c-sidebar/index",
|
||||
"c-sidebar-item": "./components/c-sidebar/c-sidebar-item/index",
|
||||
"t-image": "/components/webp-image/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<view class="goods-category custom-class">
|
||||
<c-sidebar custom-class="custom-sidebar" bindchange="onParentChange" activeKey="{{activeKey}}">
|
||||
<c-sidebar-item wx:for="{{ category }}" wx:key="index" title="{{ item.name }}" disabled="{{ item.disabled }}" />
|
||||
</c-sidebar>
|
||||
<view class="goods-category__right">
|
||||
<view class="goods-category__right-header">
|
||||
<c-tabbar wx:if="{{isSlotRight}}" activeKey="{{subActiveKey}}" bindchange="onChildChange" showMore>
|
||||
<slot />
|
||||
</c-tabbar>
|
||||
<view wx:if="{{!isSlotRight}}" class="goods-category-normal">
|
||||
<view
|
||||
class="goods-category-normal-item"
|
||||
wx:if="{{category && category[activeKey] && category[activeKey].children && category[activeKey].children.length > 0}}"
|
||||
>
|
||||
<block
|
||||
wx:for="{{category && category[activeKey] && category[activeKey].children || []}}"
|
||||
wx:key="index"
|
||||
wx:if="{{level === 3 && item.children && item.children.length > 0}}"
|
||||
>
|
||||
<view class="flex goods-category-normal-item-title"> {{item.name}} </view>
|
||||
<view class="goods-category-normal-item-container">
|
||||
<view
|
||||
class="goods-category-normal-item-container-item category-button"
|
||||
wx:for="{{item.children}}"
|
||||
wx:for-index="subIndex"
|
||||
wx:key="subIndex"
|
||||
wx:for-item="subItem"
|
||||
bindtap="changCategory"
|
||||
data-item="{{subItem}}"
|
||||
>
|
||||
<view class="category-button-text">{{subItem.name}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<scroll-view class="goods-category-second-scroll" scroll-x="true" show-scrollbar="false" wx:if="{{level === 2}}">
|
||||
<view class="goods-category-normal-item-second-container">
|
||||
<block wx:for="{{category && category[activeKey] && category[activeKey].children || []}}" wx:key="index">
|
||||
<view
|
||||
class="goods-category-normal-item-second-container-item category-button {{ index === subActiveKey ? 'active' : '' }}"
|
||||
bindtap="changCategory"
|
||||
data-item="{{item}}"
|
||||
data-index="{{index}}"
|
||||
data-is-all="{{item.isAll || false}}"
|
||||
>
|
||||
<view class="category-button-text">{{item.name}}</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="goods-category__right-scroll" scroll-y show-scrollbar="false" bindscrolltolower="onRightScrollToLower" style="height: 100%;">
|
||||
<slot name="goodsList" />
|
||||
<view
|
||||
class="goods-category-empty"
|
||||
wx:if="{{category && category[activeKey] && (!category[activeKey].children || category[activeKey].children.length === 0)}}"
|
||||
>
|
||||
<view class="goods-category-empty-icon">📦</view>
|
||||
<view class="goods-category-empty-text">暂无分类</view>
|
||||
<view class="goods-category-empty-desc">该分类下暂时没有子分类</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
185
miniprogram/pages/category/components/goods-category/index.wxss
Normal file
185
miniprogram/pages/category/components/goods-category/index.wxss
Normal file
@@ -0,0 +1,185 @@
|
||||
.goods-category {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.custom-sidebar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.goods-category__right {
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
width: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.goods-category-normal {
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
padding: 16rpx 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-container {
|
||||
background-color: #fff;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 0;
|
||||
margin-bottom: 30rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-container-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-container-item .image {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-container-item-title {
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.goods-category .custom-sidebar {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.custom-sidebar {
|
||||
width: 180rpx;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.custom-sidebar::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-second-container {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16rpx;
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
padding: 24rpx 24rpx;
|
||||
gap: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
border: 1rpx solid #f1f3f4;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-second-container-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-second-container-item .image {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
}
|
||||
|
||||
.goods-category-normal-item-second-container-item-title {
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
color: #222427;
|
||||
}
|
||||
|
||||
/* 二级分类横向滚动容器,保持区域吸顶 */
|
||||
.goods-category-second-scroll {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 无分类提示样式 */
|
||||
.goods-category-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.goods-category-empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.goods-category-empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.goods-category-empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 右侧内容滚动区域占满剩余空间并可滚动 */
|
||||
.goods-category__right-scroll {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 优化后的按钮样式 */
|
||||
.category-button {
|
||||
background: #fff;
|
||||
border: 1rpx solid #e6e8ea;
|
||||
border-radius: 24rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
margin: 0;
|
||||
min-width: 120rpx;
|
||||
box-shadow: none;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.category-button.active {
|
||||
border-color: #fa4126;
|
||||
}
|
||||
.category-button.active .category-button-text {
|
||||
color: #fa4126;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 全部按钮使用柔和的主色描边 */
|
||||
/* 取消“全部”按钮的特殊样式,统一使用选中态样式 */
|
||||
|
||||
.category-button-text {
|
||||
color: #495057;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
142
miniprogram/pages/category/index.js
Normal file
142
miniprogram/pages/category/index.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { getCategoryList } from '../../services/good/fetchCategoryList';
|
||||
import { fetchGoodsList } from '../../services/good/fetchGoodsList';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
list: [],
|
||||
goodsList: [],
|
||||
hasLoaded: false,
|
||||
loadMoreStatus: 0,
|
||||
categoryId: '',
|
||||
categoryName: '',
|
||||
},
|
||||
pageNum: 1,
|
||||
pageSize: 30,
|
||||
total: 0,
|
||||
async init() {
|
||||
try {
|
||||
const result = await getCategoryList();
|
||||
this.setData({
|
||||
list: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('err:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.getTabBar().init();
|
||||
},
|
||||
onChange(e) {
|
||||
const { item, source } = e.detail;
|
||||
let categoryId = item.groupId || item.categoryId || '';
|
||||
const categoryName = item.name || '';
|
||||
|
||||
// 兼容“全部”按钮的 groupId: `${id}_all`
|
||||
if (categoryId && categoryId.toString().endsWith('_all')) {
|
||||
categoryId = categoryId.toString().replace('_all', '');
|
||||
}
|
||||
|
||||
// 二级分类:跳转到商品列表页;一级分类:当前页加载数据
|
||||
if (source === 'child') {
|
||||
const url = `/pages/goods/list/index?categoryId=${encodeURIComponent(categoryId)}&categoryName=${encodeURIComponent(categoryName)}`;
|
||||
wx.navigateTo({ url });
|
||||
return;
|
||||
}
|
||||
|
||||
// 一级分类只在当前页加载商品
|
||||
this.pageNum = 1;
|
||||
this.setData({ categoryId, categoryName, loadMoreStatus: 0 });
|
||||
this.loadCategoryGoods(true);
|
||||
},
|
||||
|
||||
// 组装查询参数
|
||||
buildQueryParams(reset = true) {
|
||||
const params = {
|
||||
pageNum: reset ? 1 : this.pageNum + 1,
|
||||
pageSize: this.pageSize,
|
||||
category_id: this.data.categoryId || undefined,
|
||||
};
|
||||
return params;
|
||||
},
|
||||
|
||||
async loadCategoryGoods(reset = true) {
|
||||
const { loadMoreStatus, goodsList = [] } = this.data;
|
||||
if (loadMoreStatus !== 0) return;
|
||||
|
||||
const params = this.buildQueryParams(reset);
|
||||
this.setData({ loadMoreStatus: 1 });
|
||||
try {
|
||||
const result = await fetchGoodsList(params);
|
||||
|
||||
if (!result || !result.spuList) {
|
||||
this.setData({ hasLoaded: true, loadMoreStatus: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
const { spuList, totalCount = 0 } = result;
|
||||
const merged = reset ? spuList : goodsList.concat(spuList);
|
||||
const status = merged.length === totalCount ? 2 : 0;
|
||||
|
||||
this.pageNum = params.pageNum || 1;
|
||||
this.total = totalCount;
|
||||
|
||||
this.setData({
|
||||
goodsList: merged,
|
||||
loadMoreStatus: status,
|
||||
hasLoaded: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取分类商品失败:', error);
|
||||
this.setData({ loadMoreStatus: 0, hasLoaded: true });
|
||||
}
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
const { goodsList } = this.data;
|
||||
const total = this.total || 0;
|
||||
if (goodsList.length >= total && total > 0) {
|
||||
this.setData({ loadMoreStatus: 2 });
|
||||
return;
|
||||
}
|
||||
this.loadCategoryGoods(false);
|
||||
},
|
||||
|
||||
handleAddCart() {
|
||||
wx.showToast({ title: '点击加购', icon: 'none' });
|
||||
},
|
||||
|
||||
|
||||
onLoad() {
|
||||
this.init(true);
|
||||
},
|
||||
|
||||
// 分享功能
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '商品分类 - 浏览更多精选商品',
|
||||
path: '/pages/category/index'
|
||||
};
|
||||
},
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '商品分类 - 浏览更多精选商品'
|
||||
};
|
||||
},
|
||||
|
||||
handleClickGoods(e) {
|
||||
const { index } = e.detail || {};
|
||||
const { goodsList = [] } = this.data;
|
||||
const item = goodsList[index];
|
||||
const spuId = item && (item.spuId || item.id || item.skuId);
|
||||
if (!spuId) {
|
||||
wx.showToast({ title: '无法识别商品ID', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
wx.navigateTo({
|
||||
url: `/pages/goods/details/index?spuId=${encodeURIComponent(spuId)}`
|
||||
});
|
||||
},
|
||||
});
|
||||
9
miniprogram/pages/category/index.json
Normal file
9
miniprogram/pages/category/index.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "分类",
|
||||
"usingComponents": {
|
||||
"goods-category": "./components/goods-category/index",
|
||||
"goods-list": "/components/goods-list/index",
|
||||
"load-more": "/components/load-more/index",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty"
|
||||
}
|
||||
}
|
||||
28
miniprogram/pages/category/index.wxml
Normal file
28
miniprogram/pages/category/index.wxml
Normal file
@@ -0,0 +1,28 @@
|
||||
<view class="wrap">
|
||||
<goods-category
|
||||
level="{{2}}"
|
||||
custom-class="goods-category-class"
|
||||
category="{{list}}"
|
||||
initActive="{{[0,0]}}"
|
||||
bind:changeCategory="onChange"
|
||||
bind:reachbottom="onReachBottom"
|
||||
>
|
||||
<!-- 当前分类商品列表:插入到二级分类组件下方 -->
|
||||
<view slot="goodsList" class="category-goods-container">
|
||||
<view class="empty-wrap" wx:if="{{goodsList.length === 0 && hasLoaded}}">
|
||||
<t-empty t-class="empty-tips" size="240rpx" description="暂无相关商品" />
|
||||
</view>
|
||||
|
||||
<view class="category-goods-list" wx:if="{{goodsList.length}}">
|
||||
<goods-list
|
||||
wr-class="wr-goods-list"
|
||||
goodsList="{{goodsList}}"
|
||||
show-cart="{{false}}"
|
||||
bind:click="handleClickGoods"
|
||||
bind:addcart="handleAddCart"
|
||||
/>
|
||||
</view>
|
||||
<load-more wx:if="{{goodsList.length > 0}}" status="{{loadMoreStatus}}" no-more-text="没有更多了" />
|
||||
</view>
|
||||
</goods-category>
|
||||
</view>
|
||||
43
miniprogram/pages/category/index.wxss
Normal file
43
miniprogram/pages/category/index.wxss
Normal file
@@ -0,0 +1,43 @@
|
||||
.tabbar-position {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.goods-category-class {
|
||||
background-color: #f6f6f6 !important;
|
||||
height: calc(100vh - 180rpx);
|
||||
}
|
||||
.goods-category-class .goods-category-normal-item-container-item {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 商品列表区域样式 */
|
||||
.category-goods-container {
|
||||
background-color: transparent;
|
||||
padding: 0 24rpx 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.category-goods-list {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.wr-goods-list {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.empty-wrap {
|
||||
margin-top: 40rpx;
|
||||
margin-bottom: 40rpx;
|
||||
height: 300rpx;
|
||||
}
|
||||
BIN
miniprogram/pages/coupon/.DS_Store
vendored
Normal file
BIN
miniprogram/pages/coupon/.DS_Store
vendored
Normal file
Binary file not shown.
57
miniprogram/pages/coupon/components/coupon-card/index.js
Normal file
57
miniprogram/pages/coupon/components/coupon-card/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const statusMap = {
|
||||
default: { text: '去使用', theme: 'primary' },
|
||||
useless: { text: '已使用', theme: 'default' },
|
||||
disabled: { text: '已过期', theme: 'default' },
|
||||
};
|
||||
Component({
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true, // 在组件定义时的选项中启用多slot支持
|
||||
},
|
||||
|
||||
externalClasses: ['coupon-class'],
|
||||
|
||||
properties: {
|
||||
couponDTO: {
|
||||
type: Object,
|
||||
value: {}, // 优惠券数据
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
btnText: '',
|
||||
btnTheme: '',
|
||||
},
|
||||
|
||||
observers: {
|
||||
couponDTO: function (couponDTO) {
|
||||
if (!couponDTO) {
|
||||
return;
|
||||
}
|
||||
const statusInfo = statusMap[couponDTO.status] || statusMap.default;
|
||||
|
||||
this.setData({
|
||||
btnText: statusInfo.text,
|
||||
btnTheme: statusInfo.theme,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
attached() {},
|
||||
|
||||
methods: {
|
||||
// 跳转到详情页
|
||||
gotoDetail() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/coupon/coupon-detail/index?id=${this.data.couponDTO.key}`,
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到商品列表
|
||||
gotoGoodsList() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/coupon/coupon-activity-goods/index?id=${this.data.couponDTO.key}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-coupon-card": "/components/promotion/ui-coupon-card/index",
|
||||
"t-button": "tdesign-miniprogram/button/button"
|
||||
}
|
||||
}
|
||||
23
miniprogram/pages/coupon/components/coupon-card/index.wxml
Normal file
23
miniprogram/pages/coupon/components/coupon-card/index.wxml
Normal file
@@ -0,0 +1,23 @@
|
||||
<ui-coupon-card
|
||||
title="{{couponDTO.title || ''}}"
|
||||
type="{{couponDTO.type || ''}}"
|
||||
value="{{couponDTO.value || '0'}}"
|
||||
tag="{{couponDTO.tag || ''}}"
|
||||
desc="{{couponDTO.desc || ''}}"
|
||||
currency="{{couponDTO.currency || ''}}"
|
||||
timeLimit="{{couponDTO.timeLimit || ''}}"
|
||||
status="{{couponDTO.status || ''}}"
|
||||
bind:tap="gotoDetail"
|
||||
>
|
||||
<view slot="operator" class="coupon-btn-slot">
|
||||
<t-button
|
||||
t-class="coupon-btn-{{btnTheme}}"
|
||||
theme="{{btnTheme}}"
|
||||
variant="outline"
|
||||
shape="round"
|
||||
size="extra-small"
|
||||
bind:tap="gotoGoodsList"
|
||||
>{{btnText}}
|
||||
</t-button>
|
||||
</view>
|
||||
</ui-coupon-card>
|
||||
@@ -0,0 +1,9 @@
|
||||
.coupon-btn-default {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.coupon-btn-primary {
|
||||
--td-button-extra-small-padding-horizontal: 26rpx;
|
||||
--td-button-primary-outline-color: #fa4126;
|
||||
--td-button-primary-outline-border-color: #fa4126;
|
||||
}
|
||||
17
miniprogram/pages/coupon/components/floating-button/index.js
Normal file
17
miniprogram/pages/coupon/components/floating-button/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
Component({
|
||||
data: { icon: 'cart' },
|
||||
|
||||
properties: {
|
||||
count: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
goToCart() {
|
||||
wx.switchTab({
|
||||
url: '/pages/cart/index',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<view class="floating-button" bind:tap="goToCart">
|
||||
<view class="floating-inner-container">
|
||||
<t-icon
|
||||
prefix="wr"
|
||||
name="{{icon}}"
|
||||
size="42rpx"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</view>
|
||||
<view class="floating-right-top">
|
||||
{{count}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.floating-button {
|
||||
position: fixed;
|
||||
right: 20rpx;
|
||||
bottom: 108rpx;
|
||||
}
|
||||
|
||||
.floating-button .floating-inner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 96rpx;
|
||||
width: 96rpx;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
opacity: 0.7;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.floating-button .floating-right-top {
|
||||
position: absolute;
|
||||
right: 0rpx;
|
||||
top: 0rpx;
|
||||
height: 28rpx;
|
||||
background: #fa4126;
|
||||
border-radius: 64rpx;
|
||||
font-weight: bold;
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
color: #fff;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
78
miniprogram/pages/coupon/coupon-activity-goods/index.js
Normal file
78
miniprogram/pages/coupon/coupon-activity-goods/index.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { fetchCouponDetail } from '../../../services/coupon/index';
|
||||
import { fetchGoodsList } from '../../../services/good/fetchGoods';
|
||||
import Toast from 'tdesign-miniprogram/toast/index';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
goods: [],
|
||||
detail: {},
|
||||
couponTypeDesc: '',
|
||||
showStoreInfoList: false,
|
||||
cartNum: 2,
|
||||
},
|
||||
|
||||
id: '',
|
||||
|
||||
onLoad(query) {
|
||||
const id = parseInt(query.id);
|
||||
this.id = id;
|
||||
|
||||
this.getCouponDetail(id);
|
||||
this.getGoodsList(id);
|
||||
},
|
||||
|
||||
getCouponDetail(id) {
|
||||
fetchCouponDetail(id).then(({ detail }) => {
|
||||
this.setData({ detail });
|
||||
if (detail.type === 2) {
|
||||
if (detail.base > 0) {
|
||||
this.setData({
|
||||
couponTypeDesc: `满${detail.base / 100}元${detail.value}折`,
|
||||
});
|
||||
} else {
|
||||
this.setData({ couponTypeDesc: `${detail.value}折` });
|
||||
}
|
||||
} else if (detail.type === 1) {
|
||||
if (detail.base > 0) {
|
||||
this.setData({
|
||||
couponTypeDesc: `满${detail.base / 100}元减${detail.value / 100}元`,
|
||||
});
|
||||
} else {
|
||||
this.setData({ couponTypeDesc: `减${detail.value / 100}元` });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getGoodsList(id) {
|
||||
fetchGoodsList(id).then((goods) => {
|
||||
this.setData({ goods });
|
||||
});
|
||||
},
|
||||
|
||||
openStoreList() {
|
||||
this.setData({
|
||||
showStoreInfoList: true,
|
||||
});
|
||||
},
|
||||
|
||||
closeStoreList() {
|
||||
this.setData({
|
||||
showStoreInfoList: false,
|
||||
});
|
||||
},
|
||||
|
||||
goodClickHandle(e) {
|
||||
const { index } = e.detail;
|
||||
const { spuId } = this.data.goods[index];
|
||||
wx.navigateTo({ url: `/pages/goods/details/index?spuId=${spuId}` });
|
||||
},
|
||||
|
||||
cartClickHandle() {
|
||||
Toast({
|
||||
context: this,
|
||||
selector: '#t-toast',
|
||||
message: '点击加入购物车',
|
||||
});
|
||||
},
|
||||
});
|
||||
10
miniprogram/pages/coupon/coupon-activity-goods/index.json
Normal file
10
miniprogram/pages/coupon/coupon-activity-goods/index.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "活动商品",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-popup": "tdesign-miniprogram/popup/popup",
|
||||
"t-toast": "tdesign-miniprogram/toast/toast",
|
||||
"goods-list": "/components/goods-list/index",
|
||||
"floating-button": "../components/floating-button/index"
|
||||
}
|
||||
}
|
||||
41
miniprogram/pages/coupon/coupon-activity-goods/index.wxml
Normal file
41
miniprogram/pages/coupon/coupon-activity-goods/index.wxml
Normal file
@@ -0,0 +1,41 @@
|
||||
<view class="coupon-page-container">
|
||||
<view class="notice-bar-content">
|
||||
<view class="notice-bar-text">
|
||||
以下商品可使用
|
||||
<text class="height-light">{{couponTypeDesc}}</text>
|
||||
优惠券
|
||||
</view>
|
||||
<t-icon name="help-circle" size="32rpx" color="#AAAAAA" bind:tap="openStoreList" />
|
||||
</view>
|
||||
<view class="goods-list-container">
|
||||
<goods-list
|
||||
wr-class="goods-list-wrap"
|
||||
goodsList="{{goods}}"
|
||||
show-cart="{{false}}"
|
||||
bind:click="goodClickHandle"
|
||||
bind:addcart="cartClickHandle"
|
||||
/>
|
||||
</view>
|
||||
<floating-button count="{{cartNum}}" />
|
||||
<t-popup visible="{{showStoreInfoList}}" placement="bottom" bind:visible-change="closeStoreList">
|
||||
<t-icon slot="closeBtn" name="close" size="40rpx" bind:tap="closeStoreList" />
|
||||
<view class="popup-content-wrap">
|
||||
<view class="popup-content-title"> 规则详情 </view>
|
||||
<view class="desc-group-wrap">
|
||||
<view wx:if="{{detail && detail.timeLimit}}" class="item-wrap">
|
||||
<view class="item-title">优惠券有效时间</view>
|
||||
<view class="item-label">{{detail.timeLimit}}</view>
|
||||
</view>
|
||||
<view wx:if="{{detail && detail.desc}}" class="item-wrap">
|
||||
<view class="item-title">优惠券说明</view>
|
||||
<view class="item-label">{{detail.desc}}</view>
|
||||
</view>
|
||||
<view wx:if="{{detail && detail.useNotes}}" class="item-wrap">
|
||||
<view class="item-title">使用须知</view>
|
||||
<view class="item-label">{{detail.useNotes}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</t-popup>
|
||||
</view>
|
||||
<t-toast id="t-toast" />
|
||||
69
miniprogram/pages/coupon/coupon-activity-goods/index.wxss
Normal file
69
miniprogram/pages/coupon/coupon-activity-goods/index.wxss
Normal file
@@ -0,0 +1,69 @@
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.coupon-page-container .notice-bar-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.coupon-page-container .notice-bar-text {
|
||||
font-size: 26rpx;
|
||||
line-height: 36rpx;
|
||||
font-weight: 400;
|
||||
color: #666666;
|
||||
margin-left: 24rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.coupon-page-container .notice-bar-text .height-light {
|
||||
color: #fa550f;
|
||||
}
|
||||
|
||||
.coupon-page-container .popup-content-wrap {
|
||||
background-color: #fff;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
}
|
||||
|
||||
.coupon-page-container .popup-content-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
height: 104rpx;
|
||||
line-height: 104rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-page-container .desc-group-wrap {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.coupon-page-container .desc-group-wrap .item-wrap {
|
||||
margin: 0 30rpx 30rpx;
|
||||
}
|
||||
|
||||
.coupon-page-container .desc-group-wrap .item-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.coupon-page-container .desc-group-wrap .item-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 12rpx;
|
||||
white-space: pre-line;
|
||||
word-break: break-all;
|
||||
line-height: 34rpx;
|
||||
}
|
||||
|
||||
.coupon-page-container .goods-list-container {
|
||||
margin: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.coupon-page-container .goods-list-wrap {
|
||||
background: #f5f5f5 !important;
|
||||
}
|
||||
345
miniprogram/pages/coupon/coupon-center/index-browser.js
Normal file
345
miniprogram/pages/coupon/coupon-center/index-browser.js
Normal file
@@ -0,0 +1,345 @@
|
||||
// 浏览器环境下的优惠券中心页面逻辑
|
||||
|
||||
// API配置
|
||||
const API_CONFIG = {
|
||||
// 生产环境
|
||||
production: 'https://tral.cc/api/v1',
|
||||
// 开发环境
|
||||
development: 'http://localhost:8080/api/v1'
|
||||
};
|
||||
|
||||
// 当前环境 - 根据域名自动判断
|
||||
const CURRENT_ENV = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||
? 'development'
|
||||
: 'production';
|
||||
|
||||
const config = {
|
||||
apiBase: API_CONFIG[CURRENT_ENV]
|
||||
};
|
||||
|
||||
// 模拟请求函数
|
||||
async function request(options) {
|
||||
const { url, method = 'GET', data } = options;
|
||||
const fullUrl = config.apiBase + url;
|
||||
|
||||
try {
|
||||
const response = await fetch(fullUrl, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': localStorage.getItem('token') || ''
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面数据
|
||||
let pageData = {
|
||||
availableCoupons: [],
|
||||
loading: false
|
||||
};
|
||||
|
||||
// 页面初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 设置测试用的JWT token
|
||||
const testToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3NjEwMzE0ODQsImlhdCI6MTc2MDk0NTA4NH0.JAi7VnVTsA-qPegR9DCINr1nISrxakaQxf6aY4EZykU';
|
||||
localStorage.setItem('token', testToken);
|
||||
|
||||
fetchAvailableCoupons();
|
||||
});
|
||||
|
||||
// 获取可领取优惠券列表
|
||||
async function fetchAvailableCoupons() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 调用真实API获取优惠券数据
|
||||
const response = await request({
|
||||
url: '/coupons',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
const coupons = response.data || [];
|
||||
const formattedCoupons = formatCoupons(coupons);
|
||||
pageData.availableCoupons = formattedCoupons;
|
||||
|
||||
renderCouponList(formattedCoupons);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('获取可领取优惠券失败:', error);
|
||||
setLoading(false);
|
||||
showToast('获取优惠券失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化优惠券数据
|
||||
function formatCoupons(coupons) {
|
||||
return coupons.map(coupon => {
|
||||
const remainingCount = coupon.total_count > 0 ? coupon.total_count - coupon.used_count : -1;
|
||||
|
||||
return {
|
||||
id: coupon.id,
|
||||
name: coupon.name,
|
||||
type: coupon.type,
|
||||
value: coupon.value,
|
||||
minAmount: coupon.min_amount,
|
||||
desc: getCouponDesc(coupon),
|
||||
timeLimit: formatTimeLimit(coupon.start_time, coupon.end_time),
|
||||
status: 'default',
|
||||
totalCount: coupon.total_count,
|
||||
usedCount: coupon.used_count,
|
||||
remainingCount: remainingCount
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 获取优惠券描述
|
||||
function getCouponDesc(coupon) {
|
||||
if (coupon.type === 1) { // 满减券
|
||||
if (coupon.min_amount > 0) {
|
||||
return `满${(coupon.min_amount / 100).toFixed(0)}元减${(coupon.value / 100).toFixed(0)}元`;
|
||||
}
|
||||
return `立减${(coupon.value / 100).toFixed(0)}元`;
|
||||
} else if (coupon.type === 2) { // 折扣券
|
||||
return `${coupon.value / 10}折`;
|
||||
} else if (coupon.type === 3) { // 免邮券
|
||||
return '免运费';
|
||||
}
|
||||
return coupon.description || '优惠券';
|
||||
}
|
||||
|
||||
// 格式化时间限制
|
||||
function formatTimeLimit(startTime, endTime) {
|
||||
return `${startTime} - ${endTime}`;
|
||||
}
|
||||
|
||||
// 渲染优惠券列表
|
||||
function renderCouponList(coupons) {
|
||||
const couponListElement = document.getElementById('couponList');
|
||||
const emptyStateElement = document.getElementById('emptyState');
|
||||
|
||||
if (!coupons || coupons.length === 0) {
|
||||
couponListElement.innerHTML = '';
|
||||
emptyStateElement.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
|
||||
emptyStateElement.style.display = 'none';
|
||||
|
||||
const couponHTML = coupons.map(coupon => {
|
||||
const valueDisplay = coupon.type === 1
|
||||
? `<span class="value-number">${coupon.value / 100}</span><span class="value-unit">元</span>`
|
||||
: coupon.type === 2
|
||||
? `<span class="value-number">${coupon.value / 10}</span><span class="value-unit">折</span>`
|
||||
: `<span class="value-text">免邮</span>`;
|
||||
|
||||
const stockText = coupon.remainingCount > 0
|
||||
? `剩余 ${coupon.remainingCount} 张`
|
||||
: '已抢完';
|
||||
|
||||
const isDisabled = coupon.remainingCount === 0;
|
||||
|
||||
return `
|
||||
<div class="coupon-item">
|
||||
<div class="coupon-card">
|
||||
<div class="coupon-main">
|
||||
<div class="coupon-left">
|
||||
<div class="coupon-value">
|
||||
${valueDisplay}
|
||||
</div>
|
||||
<div class="coupon-desc">${coupon.desc}</div>
|
||||
</div>
|
||||
|
||||
<div class="coupon-right">
|
||||
<div class="coupon-name">${coupon.name}</div>
|
||||
<div class="coupon-time">${coupon.timeLimit}</div>
|
||||
<div class="coupon-stock">${stockText}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="coupon-action">
|
||||
<button class="receive-btn ${isDisabled ? 'disabled' : ''}"
|
||||
data-coupon-id="${coupon.id}"
|
||||
${isDisabled ? 'disabled' : ''}>
|
||||
${isDisabled ? '已抢完' : '立即领取'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
couponListElement.innerHTML = couponHTML;
|
||||
|
||||
// 添加事件委托处理按钮点击
|
||||
couponListElement.removeEventListener('click', handleCouponListClick);
|
||||
couponListElement.addEventListener('click', handleCouponListClick);
|
||||
}
|
||||
|
||||
// 处理优惠券列表点击事件
|
||||
function handleCouponListClick(event) {
|
||||
const target = event.target;
|
||||
|
||||
// 检查是否点击了领取按钮
|
||||
if (target.classList.contains('receive-btn') && !target.disabled) {
|
||||
const couponId = target.getAttribute('data-coupon-id');
|
||||
if (couponId) {
|
||||
receiveCoupon(event, parseInt(couponId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 领取优惠券
|
||||
async function receiveCoupon(event, couponId) {
|
||||
// 阻止事件冒泡和默认行为
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
if (!couponId) return;
|
||||
|
||||
// 检查优惠券是否还有库存
|
||||
const coupon = pageData.availableCoupons.find(c => c.id == couponId);
|
||||
if (!coupon || coupon.remainingCount === 0) {
|
||||
showToast('优惠券已抢完', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复点击
|
||||
const button = event.target;
|
||||
if (button.disabled) return;
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = '领取中...';
|
||||
|
||||
try {
|
||||
showLoading('正在领取优惠券...');
|
||||
|
||||
const response = await request({
|
||||
url: `/coupons/${couponId}/receive`,
|
||||
method: 'POST'
|
||||
});
|
||||
console.log('优惠券领取成功:', response);
|
||||
|
||||
hideLoading();
|
||||
|
||||
// 显示成功提示
|
||||
showToast(`🎉 ${coupon.name} 领取成功!`, 'success');
|
||||
|
||||
// 重新获取优惠券列表以确保数据同步
|
||||
await fetchAvailableCoupons();
|
||||
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('领取优惠券失败:', error);
|
||||
showToast(error.message || '领取失败,请重试', 'error');
|
||||
|
||||
// 重新获取列表以确保状态正确
|
||||
await fetchAvailableCoupons();
|
||||
}
|
||||
}
|
||||
|
||||
// 查看优惠券详情
|
||||
function viewCouponDetail(couponId) {
|
||||
showToast(`查看优惠券详情: ${couponId}`);
|
||||
}
|
||||
|
||||
// 返回
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
showToast('返回上一页');
|
||||
}
|
||||
}
|
||||
|
||||
// 设置加载状态
|
||||
function setLoading(loading) {
|
||||
pageData.loading = loading;
|
||||
const loadingElement = document.getElementById('loadingState');
|
||||
const couponListElement = document.getElementById('couponList');
|
||||
|
||||
if (loading) {
|
||||
loadingElement.style.display = 'flex';
|
||||
couponListElement.style.display = 'none';
|
||||
} else {
|
||||
loadingElement.style.display = 'none';
|
||||
couponListElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
function showToast(message, type = 'info') {
|
||||
// 创建toast元素
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#333'};
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
z-index: 10000;
|
||||
font-size: 14px;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(toast);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 显示加载中
|
||||
function showLoading(message = '加载中...') {
|
||||
const loading = document.createElement('div');
|
||||
loading.id = 'globalLoading';
|
||||
loading.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10001;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
loading.innerHTML = `
|
||||
<div style="width: 40px; height: 40px; border: 3px solid #f3f3f3; border-top: 3px solid #fff; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 16px;"></div>
|
||||
<div>${message}</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(loading);
|
||||
}
|
||||
|
||||
// 隐藏加载中
|
||||
function hideLoading() {
|
||||
const loading = document.getElementById('globalLoading');
|
||||
if (loading) {
|
||||
document.body.removeChild(loading);
|
||||
}
|
||||
}
|
||||
377
miniprogram/pages/coupon/coupon-center/index.css
Normal file
377
miniprogram/pages/coupon/coupon-center/index.css
Normal file
@@ -0,0 +1,377 @@
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
html {
|
||||
/* 防止iOS Safari缩放 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 防止横屏时字体缩放 */
|
||||
text-size-adjust: 100%;
|
||||
/* 平滑滚动 */
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
line-height: 1.4;
|
||||
/* 支持安全区域 */
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
/* 移动端触摸优化 */
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
/* 防止橡皮筋效果 */
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
/* 主容器 */
|
||||
.coupon-center-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
/* 适配移动端安全区域 */
|
||||
min-height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
|
||||
padding-bottom: max(20px, env(safe-area-inset-bottom));
|
||||
/* 为固定标题栏留出空间 */
|
||||
padding-top: calc(44px + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
background: #ffffff;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
/* 状态栏高度适配 - 模拟微信小程序标准导航栏位置 */
|
||||
padding-top: env(safe-area-inset-top);
|
||||
height: calc(44px + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
height: 44px;
|
||||
margin-top: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-back {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.header-back:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header-back svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.header-back svg path {
|
||||
stroke: #333333;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.header-placeholder {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
/* 优惠券列表容器 */
|
||||
.coupon-list-container {
|
||||
padding: 12px;
|
||||
/* 适配移动端安全区域 */
|
||||
padding-left: max(12px, env(safe-area-inset-left));
|
||||
padding-right: max(12px, env(safe-area-inset-right));
|
||||
padding-bottom: max(20px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 优惠券项 */
|
||||
.coupon-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.coupon-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.coupon-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 优惠券主体 */
|
||||
.coupon-main {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-main::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
background: linear-gradient(to bottom, transparent, #e5e5e5, transparent);
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
flex: 0 0 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 12px 8px;
|
||||
margin-right: 16px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.coupon-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.value-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 11px;
|
||||
opacity: 0.9;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.coupon-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.coupon-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.coupon-stock {
|
||||
font-size: 12px;
|
||||
color: #ff6b6b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 领取按钮区域 */
|
||||
.coupon-action {
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.receive-btn {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.receive-btn:hover {
|
||||
background: linear-gradient(135deg, #ff5252, #ff7979);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.receive-btn.disabled {
|
||||
background: #ccc;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.receive-btn.disabled:hover {
|
||||
transform: none;
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.receive-btn:not(.disabled):active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #ff6b6b;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 480px) {
|
||||
.coupon-left {
|
||||
flex: 0 0 80px;
|
||||
padding: 8px 6px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.value-number {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.coupon-main {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.coupon-action {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.header-content {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.coupon-list-container {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
flex: 0 0 70px;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.value-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
49
miniprogram/pages/coupon/coupon-center/index.html
Normal file
49
miniprogram/pages/coupon/coupon-center/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="light-content">
|
||||
<title>领券中心</title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="coupon-center-container">
|
||||
<!-- 页面标题 -->
|
||||
<header class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-back" onclick="goBack()">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 18L9 12L15 6" stroke="#333333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="header-title">领券中心</div>
|
||||
<div class="header-placeholder">
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 优惠券列表 -->
|
||||
<div class="coupon-list-container" id="couponListContainer">
|
||||
<!-- 加载状态 -->
|
||||
<div class="loading-state" id="loadingState" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 优惠券列表将通过JavaScript动态生成 -->
|
||||
<div id="couponList"></div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="empty-state" id="emptyState" style="display: none;">
|
||||
<div class="empty-icon">🎫</div>
|
||||
<div class="empty-text">暂无可领取的优惠券</div>
|
||||
<div class="empty-desc">请稍后再来看看吧</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="index-browser.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
173
miniprogram/pages/coupon/coupon-center/index.js
Normal file
173
miniprogram/pages/coupon/coupon-center/index.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import { request } from '../../../services/_utils/request';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
availableCoupons: [], // 可领取的优惠券列表
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.fetchAvailableCoupons();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时刷新数据,以防用户已领取某些优惠券
|
||||
this.fetchAvailableCoupons();
|
||||
},
|
||||
|
||||
// 刷新数据
|
||||
onRefresh() {
|
||||
this.setData({ refreshing: true });
|
||||
this.fetchAvailableCoupons().finally(() => {
|
||||
this.setData({ refreshing: false });
|
||||
});
|
||||
},
|
||||
|
||||
// 获取可领取优惠券列表
|
||||
async fetchAvailableCoupons() {
|
||||
try {
|
||||
this.setData({ loading: true });
|
||||
|
||||
const response = await request({
|
||||
url: '/coupons',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
const coupons = response.data || [];
|
||||
const formattedCoupons = this.formatCoupons(coupons);
|
||||
this.setData({
|
||||
availableCoupons: formattedCoupons,
|
||||
loading: false
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取可领取优惠券失败:', error);
|
||||
this.setData({ loading: false });
|
||||
wx.showToast({
|
||||
title: '获取优惠券失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化优惠券数据
|
||||
formatCoupons(coupons) {
|
||||
return coupons.map(coupon => {
|
||||
return {
|
||||
id: coupon.id,
|
||||
name: coupon.name,
|
||||
type: coupon.type,
|
||||
value: coupon.value,
|
||||
minAmount: coupon.min_amount,
|
||||
desc: this.getCouponDesc(coupon),
|
||||
timeLimit: this.formatTimeLimit(coupon.start_time, coupon.end_time),
|
||||
status: 'default', // 可领取状态
|
||||
totalCount: coupon.total_count,
|
||||
usedCount: coupon.used_count,
|
||||
remainingCount: coupon.total_count > 0 ? coupon.total_count - coupon.used_count : -1,
|
||||
isReceived: coupon.is_received || false // 是否已领取
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// 获取优惠券描述
|
||||
getCouponDesc(coupon) {
|
||||
if (coupon.type === 1) { // 满减券
|
||||
if (coupon.min_amount > 0) {
|
||||
return `满${(coupon.min_amount / 100).toFixed(0)}元减${(coupon.value / 100).toFixed(0)}元`;
|
||||
}
|
||||
return `立减${(coupon.value / 100).toFixed(0)}元`;
|
||||
} else if (coupon.type === 2) { // 折扣券
|
||||
return `${coupon.value / 10}折`;
|
||||
} else if (coupon.type === 3) { // 免邮券
|
||||
return '免运费';
|
||||
}
|
||||
return coupon.description || '优惠券';
|
||||
},
|
||||
|
||||
// 格式化时间限制
|
||||
formatTimeLimit(startTime, endTime) {
|
||||
const start = new Date(startTime);
|
||||
const end = new Date(endTime);
|
||||
const formatDate = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}.${month}.${day}`;
|
||||
};
|
||||
return `${formatDate(start)} - ${formatDate(end)}`;
|
||||
},
|
||||
|
||||
// 领取优惠券
|
||||
async receiveCoupon(e) {
|
||||
const { couponid } = e.currentTarget.dataset;
|
||||
if (!couponid) return;
|
||||
|
||||
// 检查优惠券是否还有库存
|
||||
const coupon = this.data.availableCoupons.find(c => c.id == couponid);
|
||||
if (!coupon || coupon.remainingCount === 0) {
|
||||
wx.showToast({
|
||||
title: '优惠券已抢完',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已领取
|
||||
if (coupon.isReceived) {
|
||||
wx.showToast({
|
||||
title: '您已领取过该优惠券',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
wx.showLoading({ title: '领取中...' });
|
||||
|
||||
const response = await request({
|
||||
url: `/coupons/${couponid}/receive`,
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
|
||||
wx.showToast({
|
||||
title: '领取成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 设置刷新标识,确保返回其他页面时能刷新优惠券数据
|
||||
wx.setStorageSync('shouldRefreshCoupons', true);
|
||||
|
||||
// 重新获取优惠券列表
|
||||
this.fetchAvailableCoupons();
|
||||
} catch (error) {
|
||||
wx.hideLoading();
|
||||
console.error('领取优惠券失败:', error);
|
||||
wx.showToast({
|
||||
title: error.message || '领取失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.setData({ refreshing: true });
|
||||
this.fetchAvailableCoupons().finally(() => {
|
||||
this.setData({ refreshing: false });
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
|
||||
// 查看优惠券详情
|
||||
viewCouponDetail(e) {
|
||||
const { couponid } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
url: `/pages/coupon/coupon-detail/index?id=${couponid}`
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
9
miniprogram/pages/coupon/coupon-center/index.json
Normal file
9
miniprogram/pages/coupon/coupon-center/index.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "领券中心",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
128
miniprogram/pages/coupon/coupon-center/index.wxml
Normal file
128
miniprogram/pages/coupon/coupon-center/index.wxml
Normal file
@@ -0,0 +1,128 @@
|
||||
<view class="coupon-center-container">
|
||||
|
||||
<!-- 优惠券列表 -->
|
||||
<view class="coupon-list-container">
|
||||
<block wx:if="{{!loading && availableCoupons.length > 0}}">
|
||||
<view class="coupon-item" wx:for="{{availableCoupons}}" wx:key="id">
|
||||
<view class="coupon-card {{item.isReceived ? 'received-card' : ''}} {{item.remainingCount === 0 ? 'soldout-card' : ''}}">
|
||||
<!-- 状态标签 - 移除已领取标签 -->
|
||||
<view class="coupon-status-tag soldout" wx:if="{{item.remainingCount === 0}}">
|
||||
<text class="status-icon">!</text>
|
||||
<text class="status-text">已抢完</text>
|
||||
</view>
|
||||
<view class="coupon-status-tag hot" wx:elif="{{item.remainingCount <= 10 && item.remainingCount > 0}}">
|
||||
<text class="status-icon">🔥</text>
|
||||
<text class="status-text">仅剩{{item.remainingCount}}张</text>
|
||||
</view>
|
||||
|
||||
<!-- 优惠券主体 -->
|
||||
<view class="coupon-main">
|
||||
<view class="coupon-left {{item.type === 1 ? 'type-amount' : item.type === 2 ? 'type-discount' : 'type-shipping'}}">
|
||||
<!-- 装饰元素 -->
|
||||
<view class="value-decoration"></view>
|
||||
|
||||
<view class="coupon-value">
|
||||
<block wx:if="{{item.type === 1}}">
|
||||
<text class="value-symbol">¥</text>
|
||||
<text class="value-number">{{item.value / 100}}</text>
|
||||
<text class="value-unit">元</text>
|
||||
</block>
|
||||
<block wx:elif="{{item.type === 2}}">
|
||||
<text class="value-number">{{item.value / 10}}</text>
|
||||
<text class="value-unit">折</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="value-text">免邮</text>
|
||||
</block>
|
||||
</view>
|
||||
<view class="coupon-desc">{{item.desc}}</view>
|
||||
</view>
|
||||
|
||||
<view class="coupon-right">
|
||||
<view class="coupon-header">
|
||||
<view class="coupon-name">{{item.name}}</view>
|
||||
<view class="coupon-type-badge" wx:if="{{item.type === 1}}">满减券</view>
|
||||
<view class="coupon-type-badge discount" wx:elif="{{item.type === 2}}">折扣券</view>
|
||||
<view class="coupon-type-badge shipping" wx:else>包邮券</view>
|
||||
</view>
|
||||
|
||||
<view class="coupon-details">
|
||||
<view class="detail-item">
|
||||
<text class="detail-icon">⏰</text>
|
||||
<text class="detail-text">{{item.timeLimit}}</text>
|
||||
</view>
|
||||
<view class="detail-item" wx:if="{{item.totalCount > 0}}">
|
||||
<text class="detail-icon">📦</text>
|
||||
<text class="detail-text">剩余 {{item.remainingCount}} 张</text>
|
||||
</view>
|
||||
<view class="detail-item" wx:if="{{item.minAmount > 0}}">
|
||||
<text class="detail-icon">💰</text>
|
||||
<text class="detail-text">满{{item.minAmount / 100}}元可用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 领取按钮 -->
|
||||
<view class="coupon-action">
|
||||
<view
|
||||
class="receive-btn {{item.remainingCount === 0 ? 'disabled' : ''}} {{item.isReceived ? 'received' : ''}}"
|
||||
catchtap="{{item.isReceived ? '' : 'receiveCoupon'}}"
|
||||
data-couponid="{{item.id}}"
|
||||
>
|
||||
<view class="btn-content">
|
||||
<block wx:if="{{item.isReceived}}">
|
||||
<text class="btn-icon">✓</text>
|
||||
<text class="btn-text">已领取</text>
|
||||
</block>
|
||||
<block wx:elif="{{item.remainingCount === 0}}">
|
||||
<text class="btn-icon">😔</text>
|
||||
<text class="btn-text">已抢完</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="btn-icon">🎁</text>
|
||||
<text class="btn-text">立即领取</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && availableCoupons.length === 0}}">
|
||||
<view class="empty-animation">
|
||||
<view class="empty-icon">🎫</view>
|
||||
<view class="empty-sparkles">
|
||||
<view class="sparkle sparkle-1">✨</view>
|
||||
<view class="sparkle sparkle-2">⭐</view>
|
||||
<view class="sparkle sparkle-3">💫</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-content">
|
||||
<view class="empty-text">暂无可领取的优惠券</view>
|
||||
<view class="empty-desc">精彩活动即将上线,敬请期待</view>
|
||||
<view class="empty-action">
|
||||
<view class="refresh-btn" bindtap="onRefresh">
|
||||
<text class="refresh-icon">🔄</text>
|
||||
<text class="refresh-text">刷新试试</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" wx:if="{{loading}}">
|
||||
<view class="loading-animation">
|
||||
<t-loading theme="circular" size="40rpx" />
|
||||
<view class="loading-dots">
|
||||
<view class="dot dot-1"></view>
|
||||
<view class="dot dot-2"></view>
|
||||
<view class="dot dot-3"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="loading-text">正在为您寻找最优惠券...</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
817
miniprogram/pages/coupon/coupon-center/index.wxss
Normal file
817
miniprogram/pages/coupon/coupon-center/index.wxss
Normal file
@@ -0,0 +1,817 @@
|
||||
.coupon-center-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 50%, #ffa8a8 100%);
|
||||
padding: 20rpx 0 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.header-bg-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.decoration-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
top: -60rpx;
|
||||
right: 50rpx;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
top: 100rpx;
|
||||
right: -40rpx;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
top: 50rpx;
|
||||
left: 30rpx;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20rpx) rotate(180deg); }
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
margin-top: 20rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-main {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.title-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.header-placeholder {
|
||||
width: 48rpx;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.header-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
animation: slideInUp 0.8s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||||
animation: fadeIn 1s ease-out 0.5s both;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 8rpx;
|
||||
animation: fadeIn 1s ease-out 0.7s both;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 2rpx;
|
||||
height: 60rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
margin: 0 40rpx;
|
||||
}
|
||||
|
||||
/* 优惠券列表容器 */
|
||||
.coupon-list-container {
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* 优惠券项 */
|
||||
.coupon-item {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
animation: slideInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 40rpx rgba(255, 107, 107, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.coupon-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
|
||||
position: relative;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.coupon-card:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 卡片状态样式 */
|
||||
.coupon-card.received-card {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-color: #0ea5e9;
|
||||
}
|
||||
|
||||
.coupon-card.soldout-card {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-color: #cbd5e1;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.coupon-status-tag {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: #fff;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 18rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
box-shadow: 0 2rpx 8rpx rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.coupon-status-tag.soldout {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
box-shadow: 0 2rpx 8rpx rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.coupon-status-tag.hot {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.3);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
margin-right: 4rpx;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 18rpx;
|
||||
}
|
||||
|
||||
/* 优惠券主体 */
|
||||
.coupon-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 20rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
margin-right: 20rpx;
|
||||
width: 160rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 不同类型的优惠券背景 */
|
||||
.coupon-left.type-amount {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 50%, #ffa8a8 100%);
|
||||
}
|
||||
|
||||
.coupon-left.type-discount {
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c4b5fd 100%);
|
||||
}
|
||||
|
||||
.coupon-left.type-shipping {
|
||||
background: linear-gradient(135deg, #10b981 0%, #34d399 50%, #6ee7b7 100%);
|
||||
}
|
||||
|
||||
/* 装饰元素 */
|
||||
.value-decoration {
|
||||
position: absolute;
|
||||
top: -20rpx;
|
||||
right: -20rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: rotate 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.coupon-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 12rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.value-symbol {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.value-number {
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 22rpx;
|
||||
opacity: 0.9;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.coupon-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.coupon-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
flex: 1;
|
||||
margin-right: 12rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.coupon-type-badge {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: #fff;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 18rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.coupon-type-badge.discount {
|
||||
background: linear-gradient(135deg, #8b5cf6, #a78bfa);
|
||||
}
|
||||
|
||||
.coupon-type-badge.shipping {
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.coupon-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
margin-right: 6rpx;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 领取按钮区域 */
|
||||
.coupon-action {
|
||||
padding: 16rpx 20rpx 20rpx;
|
||||
background: linear-gradient(180deg, rgba(248, 249, 250, 0.5) 0%, rgba(248, 249, 250, 0.8) 100%);
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.receive-btn {
|
||||
width: 100%;
|
||||
height: 64rpx;
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 50%, #ffa8a8 100%);
|
||||
color: #fff;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.receive-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
.receive-btn:active::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.receive-btn.disabled {
|
||||
background: linear-gradient(135deg, #e5e7eb, #d1d5db);
|
||||
color: #9ca3af;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.receive-btn.received {
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
color: #fff;
|
||||
border: 2rpx solid #059669;
|
||||
box-shadow: 0 4rpx 16rpx rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.receive-btn:not(.disabled):active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.2);
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 24rpx;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-4rpx); }
|
||||
60% { transform: translateY(-2rpx); }
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-animation {
|
||||
position: relative;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
opacity: 0.8;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.empty-sparkles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.sparkle {
|
||||
position: absolute;
|
||||
font-size: 32rpx;
|
||||
animation: sparkle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sparkle-1 {
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.sparkle-2 {
|
||||
bottom: 30rpx;
|
||||
left: 10rpx;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
|
||||
.sparkle-3 {
|
||||
top: 60rpx;
|
||||
left: -10rpx;
|
||||
animation-delay: 1.4s;
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0%, 100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #374151;
|
||||
margin-bottom: 16rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #6b7280;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: #fff;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.2);
|
||||
}
|
||||
|
||||
.refresh-icon {
|
||||
font-size: 24rpx;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.refresh-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.loading-animation {
|
||||
position: relative;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
margin-top: 16rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff6b6b;
|
||||
animation: loadingDots 1.4s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
.dot-1 { animation-delay: -0.32s; }
|
||||
.dot-2 { animation-delay: -0.16s; }
|
||||
.dot-3 { animation-delay: 0s; }
|
||||
|
||||
@keyframes loadingDots {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 750rpx) {
|
||||
.header {
|
||||
padding: 60rpx 24rpx 40rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
padding: 0 24rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.coupon-list {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.coupon-card {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.coupon-value {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.coupon-unit {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 24rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.coupon-status-tag {
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
}
|
||||
|
||||
.coupon-type-badge {
|
||||
font-size: 18rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕适配 */
|
||||
@media (max-width: 600rpx) {
|
||||
.header {
|
||||
padding: 50rpx 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex-direction: row;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.coupon-list {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.coupon-card {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.coupon-value {
|
||||
font-size: 42rpx;
|
||||
}
|
||||
|
||||
.coupon-right {
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 22rpx;
|
||||
padding: 14rpx 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 375px) {
|
||||
.coupon-left {
|
||||
flex: 0 0 160rpx;
|
||||
padding: 20rpx 12rpx;
|
||||
}
|
||||
|
||||
.value-number {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
32
miniprogram/pages/coupon/coupon-detail/index.js
Normal file
32
miniprogram/pages/coupon/coupon-detail/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { fetchCouponDetail } from '../../../services/coupon/index';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
detail: null,
|
||||
storeInfoList: [],
|
||||
storeInfoStr: '',
|
||||
showStoreInfoList: false,
|
||||
},
|
||||
|
||||
id: '',
|
||||
|
||||
onLoad(query) {
|
||||
const id = parseInt(query.id);
|
||||
this.id = id;
|
||||
this.getGoodsList(id);
|
||||
},
|
||||
|
||||
getGoodsList(id) {
|
||||
fetchCouponDetail(id).then(({ detail }) => {
|
||||
this.setData({
|
||||
detail,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
navGoodListHandle() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/coupon/coupon-activity-goods/index?id=${this.id}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
10
miniprogram/pages/coupon/coupon-detail/index.json
Normal file
10
miniprogram/pages/coupon/coupon-detail/index.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "优惠券详情",
|
||||
"usingComponents": {
|
||||
"coupon-card": "../components/coupon-card/index",
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
45
miniprogram/pages/coupon/coupon-detail/index.wxml
Normal file
45
miniprogram/pages/coupon/coupon-detail/index.wxml
Normal file
@@ -0,0 +1,45 @@
|
||||
<!-- 优惠券 -->
|
||||
<view class="coupon-card-wrap">
|
||||
<coupon-card couponDTO="{{detail}}" />
|
||||
</view>
|
||||
<!-- 说明 -->
|
||||
<view class="desc-wrap">
|
||||
<t-cell-group t-class="desc-group-wrap">
|
||||
<t-cell
|
||||
wx:if="{{detail && detail.desc}}"
|
||||
t-class="t-class-cell"
|
||||
t-class-title="t-class-title"
|
||||
t-class-note="t-class-note"
|
||||
title="规则说明"
|
||||
note="{{detail && detail.desc}}"
|
||||
/>
|
||||
<t-cell
|
||||
wx:if="{{detail && detail.timeLimit}}"
|
||||
t-class="t-class-cell"
|
||||
t-class-title="t-class-title"
|
||||
t-class-note="t-class-note"
|
||||
title="有效时间"
|
||||
note="{{detail && detail.timeLimit}}"
|
||||
/>
|
||||
<t-cell
|
||||
wx:if="{{detail && detail.storeAdapt}}"
|
||||
t-class="t-class-cell"
|
||||
t-class-title="t-class-title"
|
||||
t-class-note="t-class-note"
|
||||
title="适用范围"
|
||||
note="{{detail && detail.storeAdapt}}"
|
||||
/>
|
||||
<t-cell
|
||||
wx:if="{{detail && detail.useNotes}}"
|
||||
t-class="t-class-cell"
|
||||
t-class-title="t-class-title"
|
||||
t-class-note="t-class-note"
|
||||
title="使用须知"
|
||||
note="{{detail && detail.useNotes}}"
|
||||
/>
|
||||
</t-cell-group>
|
||||
<!-- 查看可用商品 -->
|
||||
<view class="button-wrap">
|
||||
<t-button shape="round" block bindtap="navGoodListHandle"> 查看可用商品 </t-button>
|
||||
</view>
|
||||
</view>
|
||||
92
miniprogram/pages/coupon/coupon-detail/index.wxss
Normal file
92
miniprogram/pages/coupon/coupon-detail/index.wxss
Normal file
@@ -0,0 +1,92 @@
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.coupon-card-wrap {
|
||||
background-color: #fff;
|
||||
padding: 32rpx 32rpx 1rpx;
|
||||
}
|
||||
.desc-wrap {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.desc-wrap .button-wrap {
|
||||
margin: 50rpx 32rpx 0;
|
||||
}
|
||||
|
||||
.desc-group-wrap .t-class-cell {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.desc-group-wrap .t-class-title {
|
||||
font-size: 26rpx;
|
||||
width: 140rpx;
|
||||
flex: none;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.desc-group-wrap .t-class-note {
|
||||
font-size: 26rpx;
|
||||
word-break: break-all;
|
||||
white-space: pre-line;
|
||||
justify-content: flex-start;
|
||||
color: #333;
|
||||
width: 440rpx;
|
||||
}
|
||||
|
||||
.desc-group-wrap {
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
|
||||
--cell-label-font-size: 26rpx;
|
||||
--cell-label-line-height: 36rpx;
|
||||
--cell-label-color: #999;
|
||||
}
|
||||
|
||||
.desc-group-wrap.in-popup {
|
||||
border-radius: 0;
|
||||
overflow: auto;
|
||||
max-height: 828rpx;
|
||||
}
|
||||
|
||||
.desc-group-wrap .wr-cell__title {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* .desc-group-wrap .max-width-cell {
|
||||
overflow: hidden;
|
||||
} */
|
||||
|
||||
/* .desc-group-wrap .signal-line-label {
|
||||
word-break: keep-all;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.desc-group-wrap .multi-line-label {
|
||||
word-break: break-all;
|
||||
white-space: pre-line;
|
||||
} */
|
||||
|
||||
.popup-content-wrap {
|
||||
background-color: #fff;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
}
|
||||
|
||||
.popup-content-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
|
||||
text-align: center;
|
||||
height: 104rpx;
|
||||
line-height: 104rpx;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup-content-title .close-icon {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
}
|
||||
225
miniprogram/pages/coupon/coupon-list/index.js
Normal file
225
miniprogram/pages/coupon/coupon-list/index.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import { fetchUserCoupons } from '../../../services/coupon/index';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
status: 0,
|
||||
list: [
|
||||
{
|
||||
text: '可使用',
|
||||
key: 0,
|
||||
},
|
||||
{
|
||||
text: '已使用',
|
||||
key: 1,
|
||||
},
|
||||
{
|
||||
text: '已失效',
|
||||
key: 2,
|
||||
},
|
||||
],
|
||||
|
||||
couponList: [],
|
||||
loading: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时刷新数据,确保从领券中心返回后显示最新的优惠券
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
init() {
|
||||
this.fetchList();
|
||||
},
|
||||
|
||||
fetchList(status = this.data.status) {
|
||||
this.setData({ loading: true });
|
||||
|
||||
// 状态映射:前端页面状态 -> API状态
|
||||
// 前端: 0=可使用, 1=已使用, 2=已失效
|
||||
// API: 1=未使用, 2=已使用, 3=已过期
|
||||
const statusMap = {
|
||||
0: 1, // 可使用 -> 未使用(API状态1)
|
||||
1: 2, // 已使用 -> 已使用(API状态2)
|
||||
2: 3, // 已失效 -> 已过期(API状态3)
|
||||
};
|
||||
|
||||
const apiStatus = statusMap[status] || 1;
|
||||
|
||||
fetchUserCoupons(apiStatus)
|
||||
.then((result) => {
|
||||
console.log('获取用户优惠券成功:', result);
|
||||
console.log('API返回的原始数据:', JSON.stringify(result.data, null, 2));
|
||||
|
||||
// 检查第一个优惠券的数据结构
|
||||
if (result.data && result.data.length > 0) {
|
||||
const firstCoupon = result.data[0];
|
||||
console.log('第一个优惠券数据:', firstCoupon);
|
||||
console.log('优惠券详情:', firstCoupon.coupon);
|
||||
console.log('优惠券面值:', firstCoupon.coupon?.value);
|
||||
}
|
||||
|
||||
// 转换API数据格式为页面所需格式
|
||||
const couponList = this.transformCouponData(result.data || [], status);
|
||||
|
||||
this.setData({
|
||||
couponList,
|
||||
loading: false
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('获取用户优惠券失败:', error);
|
||||
wx.showToast({
|
||||
title: '获取优惠券失败',
|
||||
icon: 'none'
|
||||
});
|
||||
this.setData({
|
||||
couponList: [],
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 转换API数据格式
|
||||
transformCouponData(apiData, status) {
|
||||
const statusMap = {
|
||||
0: 'default', // 可使用
|
||||
1: 'useless', // 已使用
|
||||
2: 'disabled' // 已失效
|
||||
};
|
||||
|
||||
return apiData.map((item, index) => {
|
||||
const coupon = item.coupon || item;
|
||||
|
||||
return {
|
||||
key: `coupon_${coupon.id || index}`,
|
||||
title: coupon.name || '优惠券',
|
||||
type: coupon.type || 1,
|
||||
value: coupon.value, // 直接使用原始值,让ui-coupon-card组件处理
|
||||
currency: this.getCurrency(coupon.type),
|
||||
tag: this.getCouponTag(coupon.type),
|
||||
desc: this.getCouponDesc(coupon),
|
||||
timeLimit: this.formatTimeLimit(coupon, status),
|
||||
status: statusMap[status] || 'default',
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化优惠券金额
|
||||
formatCouponValue(coupon) {
|
||||
console.log('formatCouponValue 输入参数:', coupon);
|
||||
console.log('优惠券类型:', coupon?.type, '面值:', coupon?.value);
|
||||
|
||||
if (!coupon || coupon.value === undefined) {
|
||||
console.log('优惠券数据无效,返回0');
|
||||
return '0';
|
||||
}
|
||||
|
||||
// 直接返回原始值,让ui-coupon-card组件处理单位转换
|
||||
// ui-coupon-card会自动将满减券除以100(分转元),折扣券除以10(85转8.5折)
|
||||
if (coupon.type === 1) { // 满减券
|
||||
console.log('满减券,返回原始值:', coupon.value);
|
||||
return coupon.value.toString(); // 返回原始分值,ui-coupon-card会除以100
|
||||
} else if (coupon.type === 2) { // 折扣券
|
||||
console.log('折扣券,返回原始值:', coupon.value);
|
||||
return coupon.value.toString(); // 返回原始值,ui-coupon-card会除以10
|
||||
} else if (coupon.type === 3) { // 免邮券
|
||||
console.log('免邮券,返回0');
|
||||
return '0';
|
||||
}
|
||||
console.log('未知类型,返回0');
|
||||
return '0';
|
||||
},
|
||||
|
||||
// 获取货币符号
|
||||
getCurrency(type) {
|
||||
if (type === 1) return '¥';
|
||||
if (type === 2) return '折';
|
||||
if (type === 3) return '';
|
||||
return '¥';
|
||||
},
|
||||
|
||||
// 获取优惠券标签
|
||||
getCouponTag(type) {
|
||||
if (type === 1) return '满减券';
|
||||
if (type === 2) return '折扣券';
|
||||
if (type === 3) return '免邮券';
|
||||
return '优惠券';
|
||||
},
|
||||
|
||||
// 获取优惠券描述
|
||||
getCouponDesc(coupon) {
|
||||
if (!coupon) {
|
||||
return '优惠券';
|
||||
}
|
||||
|
||||
if (coupon.type === 1) { // 满减券
|
||||
if (coupon.min_amount > 0) {
|
||||
return `满${(coupon.min_amount / 100).toFixed(0)}元可用`;
|
||||
}
|
||||
return '无门槛使用';
|
||||
} else if (coupon.type === 2) { // 折扣券
|
||||
if (coupon.min_amount > 0) {
|
||||
return `满${(coupon.min_amount / 100).toFixed(0)}元可用`;
|
||||
}
|
||||
return '全场商品可用';
|
||||
} else if (coupon.type === 3) { // 免邮券
|
||||
return '全场包邮';
|
||||
}
|
||||
return '优惠券';
|
||||
},
|
||||
|
||||
// 格式化时间限制
|
||||
formatTimeLimit(coupon, status) {
|
||||
if (status === 1) { // 已使用
|
||||
return `已于${this.formatDate(coupon.updated_at || new Date())}使用`;
|
||||
} else if (status === 2) { // 已失效
|
||||
return `已于${this.formatDate(coupon.end_time)}过期`;
|
||||
} else { // 可使用
|
||||
return `${this.formatDate(coupon.end_time)}前有效`;
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}.${month}.${day}`;
|
||||
},
|
||||
|
||||
tabChange(e) {
|
||||
const { value } = e.detail;
|
||||
|
||||
this.setData({ status: value });
|
||||
this.fetchList(value);
|
||||
},
|
||||
|
||||
goCouponCenterHandle() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/coupon/coupon-center/index',
|
||||
});
|
||||
},
|
||||
|
||||
onPullDownRefresh_(e) {
|
||||
// 安全获取callback
|
||||
const callback = e && e.detail && e.detail.callback;
|
||||
|
||||
this.setData(
|
||||
{
|
||||
couponList: [],
|
||||
},
|
||||
() => {
|
||||
this.fetchList();
|
||||
// 确保callback存在且是函数
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
10
miniprogram/pages/coupon/coupon-list/index.json
Normal file
10
miniprogram/pages/coupon/coupon-list/index.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "优惠券",
|
||||
"usingComponents": {
|
||||
"t-pull-down-refresh": "tdesign-miniprogram/pull-down-refresh/pull-down-refresh",
|
||||
"t-tabs": "tdesign-miniprogram/tabs/tabs",
|
||||
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"coupon-card": "../components/coupon-card/index"
|
||||
}
|
||||
}
|
||||
74
miniprogram/pages/coupon/coupon-list/index.wxml
Normal file
74
miniprogram/pages/coupon/coupon-list/index.wxml
Normal file
@@ -0,0 +1,74 @@
|
||||
<t-tabs
|
||||
defaultValue="{{status}}"
|
||||
bind:change="tabChange"
|
||||
tabList="{{list}}"
|
||||
t-class="tabs-external__inner"
|
||||
t-class-item="tabs-external__item"
|
||||
t-class-active="tabs-external__active"
|
||||
t-class-track="tabs-external__track"
|
||||
>
|
||||
<t-tab-panel
|
||||
wx:for="{{list}}"
|
||||
wx:for-index="index"
|
||||
wx:for-item="tab"
|
||||
wx:key="key"
|
||||
label="{{tab.text}}"
|
||||
value="{{tab.key}}"
|
||||
/>
|
||||
</t-tabs>
|
||||
<view class="coupon-list-wrap">
|
||||
<t-pull-down-refresh
|
||||
t-class-indicator="t-class-indicator"
|
||||
id="t-pull-down-refresh"
|
||||
bind:refresh="onPullDownRefresh_"
|
||||
background="#fff"
|
||||
>
|
||||
<!-- 优惠券列表 -->
|
||||
<view wx:if="{{!loading && couponList.length > 0}}">
|
||||
<view class="coupon-list-item" wx:for="{{couponList}}" wx:key="key">
|
||||
<coupon-card couponDTO="{{item}}" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && couponList.length === 0}}">
|
||||
<view class="empty-icon">
|
||||
<block wx:if="{{status === 0}}">🎫</block>
|
||||
<block wx:elif="{{status === 1}}">✅</block>
|
||||
<block wx:else>⏰</block>
|
||||
</view>
|
||||
<view class="empty-text">
|
||||
<block wx:if="{{status === 0}}">暂无可用优惠券</block>
|
||||
<block wx:elif="{{status === 1}}">暂无已使用优惠券</block>
|
||||
<block wx:else>暂无已失效优惠券</block>
|
||||
</view>
|
||||
<view class="empty-desc">
|
||||
<block wx:if="{{status === 0}}">去领券中心看看吧</block>
|
||||
<block wx:elif="{{status === 1}}">快去使用优惠券吧</block>
|
||||
<block wx:else>关注最新优惠活动</block>
|
||||
</view>
|
||||
<view class="empty-action" wx:if="{{status === 0}}" bind:tap="goCouponCenterHandle">
|
||||
<view class="empty-action-btn">去领券中心</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" wx:if="{{loading}}">
|
||||
<t-loading theme="circular" size="40rpx" />
|
||||
<view class="loading-text">加载中...</view>
|
||||
</view>
|
||||
</t-pull-down-refresh>
|
||||
|
||||
<view class="center-entry">
|
||||
<view class="center-entry-btn" bind:tap="goCouponCenterHandle">
|
||||
<view>领券中心</view>
|
||||
<t-icon
|
||||
name="chevron-right"
|
||||
color="#fa4126"
|
||||
size="40rpx"
|
||||
style="line-height: 28rpx;"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
146
miniprogram/pages/coupon/coupon-list/index.wxss
Normal file
146
miniprogram/pages/coupon/coupon-list/index.wxss
Normal file
@@ -0,0 +1,146 @@
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tabs-external__inner {
|
||||
height: 88rpx;
|
||||
width: 100%;
|
||||
line-height: 88rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
.tabs-external__inner {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tabs-external__inner .tabs-external__track {
|
||||
background: #fa4126 !important;
|
||||
}
|
||||
|
||||
.tabs-external__inner .tabs-external__item {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tabs-external__inner .tabs-external__active {
|
||||
font-size: 28rpx;
|
||||
color: #fa4126 !important;
|
||||
}
|
||||
|
||||
.tabs-external__inner.order-nav .order-nav-item .bottom-line {
|
||||
bottom: 12rpx;
|
||||
}
|
||||
|
||||
.coupon-list-wrap {
|
||||
margin-top: 32rpx;
|
||||
margin-left: 32rpx;
|
||||
margin-right: 32rpx;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 100rpx;
|
||||
padding-bottom: calc(constant(safe-area-inset-top) + 100rpx);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 100rpx);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.center-entry {
|
||||
box-sizing: content-box;
|
||||
border-top: 1rpx solid #dce0e4;
|
||||
background-color: #fff;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100rpx;
|
||||
padding-bottom: 0;
|
||||
padding-bottom: constant(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.center-entry-btn {
|
||||
color: #fa4126;
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
line-height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.coupon-list-wrap .t-pull-down-refresh__bar {
|
||||
background: #fff !important;
|
||||
}
|
||||
.t-class-indicator {
|
||||
color: #b9b9b9 !important;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
text-align: center;
|
||||
min-height: 400rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 32rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.empty-action-btn {
|
||||
background: #fa4126;
|
||||
color: #fff;
|
||||
padding: 20rpx 40rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4rpx 12rpx rgba(250, 65, 38, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.empty-action-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 2rpx 8rpx rgba(250, 65, 38, 0.2);
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
text-align: center;
|
||||
min-height: 400rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
71
miniprogram/pages/debug/README.md
Normal file
71
miniprogram/pages/debug/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 调试页面使用说明
|
||||
|
||||
## 硬编码评论测试页面 (hardcoded-comment-test)
|
||||
|
||||
### 页面路径
|
||||
`pages/debug/hardcoded-comment-test`
|
||||
|
||||
### 功能说明
|
||||
这个页面用于测试和调试评论相关功能,特别是商品ID验证逻辑。
|
||||
|
||||
### 主要功能
|
||||
|
||||
#### 1. 商品ID验证测试
|
||||
- **输入框测试**: 可以手动输入任意商品ID进行验证测试
|
||||
- **快速测试按钮**: 提供常见的测试用例
|
||||
- 正常ID(1): 测试正常的正整数
|
||||
- 零值(0): 测试零值是否被正确处理
|
||||
- 空字符串: 测试空值情况
|
||||
- 非数字(abc): 测试非数字字符串
|
||||
- 负数(-1): 测试负数情况
|
||||
- 小数(1.5): 测试小数情况
|
||||
|
||||
#### 2. 测试结果显示
|
||||
- 显示每次测试的详细结果
|
||||
- 包含测试时间、输入值、成功/失败状态
|
||||
- 成功时显示返回的数据
|
||||
- 失败时显示具体错误信息
|
||||
|
||||
#### 3. 评论组件显示测试
|
||||
- 可以切换"有评论"和"无评论"模式
|
||||
- 验证评论组件的显示逻辑
|
||||
- 显示硬编码的评论数据用于UI测试
|
||||
|
||||
### 使用方法
|
||||
|
||||
1. 在微信开发者工具中打开小程序
|
||||
2. 在控制台中输入以下代码跳转到调试页面:
|
||||
```javascript
|
||||
wx.navigateTo({
|
||||
url: '/pages/debug/hardcoded-comment-test'
|
||||
})
|
||||
```
|
||||
3. 或者在任意页面的JS文件中添加跳转代码
|
||||
|
||||
### 测试建议
|
||||
|
||||
1. **基础验证测试**: 使用快速测试按钮测试所有预设场景
|
||||
2. **边界值测试**: 测试特殊值如 "0", "", "null", "undefined"
|
||||
3. **类型测试**: 测试不同数据类型的输入
|
||||
4. **观察日志**: 在开发者工具控制台查看详细的调试日志
|
||||
|
||||
### 调试日志说明
|
||||
|
||||
页面会在控制台输出详细的调试信息:
|
||||
- `[调试页面]` 前缀: 调试页面相关日志
|
||||
- `[fetchCommentsCount]` 前缀: 评论统计服务相关日志
|
||||
- `[fetchComments]` 前缀: 评论列表服务相关日志
|
||||
|
||||
### 常见问题排查
|
||||
|
||||
1. **"商品ID必须是有效的正整数"错误**:
|
||||
- 检查传入的productId值和类型
|
||||
- 确认验证逻辑是否正确处理各种输入情况
|
||||
|
||||
2. **页面无法加载**:
|
||||
- 确认页面已在app.json中注册
|
||||
- 检查文件路径是否正确
|
||||
|
||||
3. **测试结果不显示**:
|
||||
- 检查控制台是否有JavaScript错误
|
||||
- 确认服务文件路径是否正确
|
||||
189
miniprogram/pages/debug/api-test.js
Normal file
189
miniprogram/pages/debug/api-test.js
Normal file
@@ -0,0 +1,189 @@
|
||||
// pages/debug/api-test.js
|
||||
const { fetchCommentsCount } = require('../../services/comments/fetchCommentsCount');
|
||||
const { fetchComments } = require('../../services/comments/fetchComments');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
spuId: 1, // 测试用的商品ID
|
||||
testResults: {
|
||||
statsApiCalled: false,
|
||||
statsApiSuccess: false,
|
||||
statsApiError: '',
|
||||
statsRawData: null,
|
||||
statsProcessedData: null,
|
||||
|
||||
listApiCalled: false,
|
||||
listApiSuccess: false,
|
||||
listApiError: '',
|
||||
listRawData: null,
|
||||
listProcessedData: null,
|
||||
|
||||
finalCommentsStatistics: null,
|
||||
finalCommentsList: null,
|
||||
shouldDisplay: false
|
||||
}
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
console.log('API测试页面加载');
|
||||
if (options.spuId) {
|
||||
this.setData({
|
||||
spuId: parseInt(options.spuId)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 测试评论统计API
|
||||
async testCommentsStatistics() {
|
||||
console.log('开始测试评论统计API...');
|
||||
|
||||
this.setData({
|
||||
'testResults.statsApiCalled': true,
|
||||
'testResults.statsApiSuccess': false,
|
||||
'testResults.statsApiError': ''
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await fetchCommentsCount(this.data.spuId);
|
||||
console.log('评论统计API原始响应:', result);
|
||||
|
||||
// 模拟商品详情页的数据处理逻辑
|
||||
const processedData = {
|
||||
total_count: result.total_count || 0,
|
||||
good_count: result.good_count || 0,
|
||||
medium_count: result.medium_count || 0,
|
||||
bad_count: result.bad_count || 0,
|
||||
average_rating: result.average_rating || 0,
|
||||
image_count: result.image_count || 0
|
||||
};
|
||||
|
||||
const goodRate = processedData.total_count > 0 ?
|
||||
Math.round((processedData.good_count / processedData.total_count) * 100) : 0;
|
||||
|
||||
const commentsStatistics = {
|
||||
commentCount: processedData.total_count || 0,
|
||||
goodCount: processedData.good_count || 0,
|
||||
middleCount: processedData.medium_count || 0,
|
||||
badCount: processedData.bad_count || 0,
|
||||
goodRate: goodRate,
|
||||
hasImageCount: processedData.image_count || 0,
|
||||
};
|
||||
|
||||
this.setData({
|
||||
'testResults.statsApiSuccess': true,
|
||||
'testResults.statsRawData': result,
|
||||
'testResults.statsProcessedData': processedData,
|
||||
'testResults.finalCommentsStatistics': commentsStatistics,
|
||||
'testResults.shouldDisplay': commentsStatistics.commentCount > 0
|
||||
});
|
||||
|
||||
console.log('评论统计处理完成:', commentsStatistics);
|
||||
|
||||
} catch (error) {
|
||||
console.error('评论统计API调用失败:', error);
|
||||
this.setData({
|
||||
'testResults.statsApiError': error.message || '未知错误'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 测试评论列表API
|
||||
async testCommentsList() {
|
||||
console.log('开始测试评论列表API...');
|
||||
|
||||
this.setData({
|
||||
'testResults.listApiCalled': true,
|
||||
'testResults.listApiSuccess': false,
|
||||
'testResults.listApiError': ''
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await fetchComments({
|
||||
productId: this.data.spuId,
|
||||
page: 1,
|
||||
pageSize: 3
|
||||
});
|
||||
console.log('评论列表API原始响应:', result);
|
||||
|
||||
// 模拟商品详情页的数据处理逻辑
|
||||
let commentsList = [];
|
||||
if (result.comments && Array.isArray(result.comments)) {
|
||||
commentsList = result.comments.map(comment => ({
|
||||
id: comment.id,
|
||||
userName: comment.is_anonymous ? '匿名用户' : (comment.user_name || '用户'),
|
||||
userHeadUrl: comment.user_avatar || 'https://tdesign.gtimg.com/miniprogram/template/retail/avatar/avatar1.png',
|
||||
commentScore: comment.rating || 5,
|
||||
commentContent: comment.content || '',
|
||||
commentTime: comment.created_at ? this.formatTime(comment.created_at) : '',
|
||||
specInfo: comment.product_spec || '',
|
||||
commentResources: comment.images || [],
|
||||
isAnonymity: comment.is_anonymous || false,
|
||||
sellerReply: comment.reply_content || '',
|
||||
goodsDetailInfo: ''
|
||||
}));
|
||||
}
|
||||
|
||||
this.setData({
|
||||
'testResults.listApiSuccess': true,
|
||||
'testResults.listRawData': result,
|
||||
'testResults.listProcessedData': commentsList,
|
||||
'testResults.finalCommentsList': commentsList
|
||||
});
|
||||
|
||||
console.log('评论列表处理完成:', commentsList);
|
||||
|
||||
} catch (error) {
|
||||
console.error('评论列表API调用失败:', error);
|
||||
this.setData({
|
||||
'testResults.listApiError': error.message || '未知错误'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 运行完整测试
|
||||
async runFullTest() {
|
||||
console.log('开始运行完整API测试...');
|
||||
await this.testCommentsStatistics();
|
||||
await this.testCommentsList();
|
||||
console.log('完整API测试完成');
|
||||
},
|
||||
|
||||
// 清除测试结果
|
||||
clearResults() {
|
||||
this.setData({
|
||||
testResults: {
|
||||
statsApiCalled: false,
|
||||
statsApiSuccess: false,
|
||||
statsApiError: '',
|
||||
statsRawData: null,
|
||||
statsProcessedData: null,
|
||||
|
||||
listApiCalled: false,
|
||||
listApiSuccess: false,
|
||||
listApiError: '',
|
||||
listRawData: null,
|
||||
listProcessedData: null,
|
||||
|
||||
finalCommentsStatistics: null,
|
||||
finalCommentsList: null,
|
||||
shouldDisplay: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
|
||||
// 修改测试商品ID
|
||||
onSpuIdInput(e) {
|
||||
this.setData({
|
||||
spuId: parseInt(e.detail.value) || 1
|
||||
});
|
||||
}
|
||||
});
|
||||
6
miniprogram/pages/debug/api-test.json
Normal file
6
miniprogram/pages/debug/api-test.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "评论API测试",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
147
miniprogram/pages/debug/api-test.wxml
Normal file
147
miniprogram/pages/debug/api-test.wxml
Normal file
@@ -0,0 +1,147 @@
|
||||
<!--pages/debug/api-test.wxml-->
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">评论API测试页面</text>
|
||||
</view>
|
||||
|
||||
<!-- 测试配置 -->
|
||||
<view class="section">
|
||||
<view class="section-title">测试配置</view>
|
||||
<view class="config-item">
|
||||
<text>商品ID (spuId): </text>
|
||||
<input class="spu-input" type="number" value="{{spuId}}" bindinput="onSpuIdInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="section">
|
||||
<view class="section-title">测试操作</view>
|
||||
<view class="button-group">
|
||||
<button class="test-btn" bindtap="testCommentsStatistics">测试评论统计API</button>
|
||||
<button class="test-btn" bindtap="testCommentsList">测试评论列表API</button>
|
||||
<button class="test-btn primary" bindtap="runFullTest">运行完整测试</button>
|
||||
<button class="test-btn secondary" bindtap="clearResults">清除结果</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评论统计API测试结果 -->
|
||||
<view class="section">
|
||||
<view class="section-title">评论统计API测试结果</view>
|
||||
<view class="test-result">
|
||||
<view class="result-item">
|
||||
<text class="label">API调用状态:</text>
|
||||
<text class="value {{testResults.statsApiCalled ? 'called' : 'not-called'}}">
|
||||
{{testResults.statsApiCalled ? '已调用' : '未调用'}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.statsApiCalled}}">
|
||||
<text class="label">调用结果:</text>
|
||||
<text class="value {{testResults.statsApiSuccess ? 'success' : 'error'}}">
|
||||
{{testResults.statsApiSuccess ? '成功' : '失败'}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.statsApiError}}">
|
||||
<text class="label">错误信息:</text>
|
||||
<text class="value error">{{testResults.statsApiError}}</text>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.statsRawData}}">
|
||||
<text class="label">原始数据:</text>
|
||||
<view class="json-data">{{testResults.statsRawData}}</view>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.finalCommentsStatistics}}">
|
||||
<text class="label">处理后数据:</text>
|
||||
<view class="json-data">{{testResults.finalCommentsStatistics}}</view>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.statsApiCalled}}">
|
||||
<text class="label">显示条件:</text>
|
||||
<text class="value {{testResults.shouldDisplay ? 'success' : 'error'}}">
|
||||
commentCount > 0 = {{testResults.shouldDisplay ? 'true (应该显示)' : 'false (不应该显示)'}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评论列表API测试结果 -->
|
||||
<view class="section">
|
||||
<view class="section-title">评论列表API测试结果</view>
|
||||
<view class="test-result">
|
||||
<view class="result-item">
|
||||
<text class="label">API调用状态:</text>
|
||||
<text class="value {{testResults.listApiCalled ? 'called' : 'not-called'}}">
|
||||
{{testResults.listApiCalled ? '已调用' : '未调用'}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.listApiCalled}}">
|
||||
<text class="label">调用结果:</text>
|
||||
<text class="value {{testResults.listApiSuccess ? 'success' : 'error'}}">
|
||||
{{testResults.listApiSuccess ? '成功' : '失败'}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.listApiError}}">
|
||||
<text class="label">错误信息:</text>
|
||||
<text class="value error">{{testResults.listApiError}}</text>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.listRawData}}">
|
||||
<text class="label">原始数据:</text>
|
||||
<view class="json-data">{{testResults.listRawData}}</view>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:if="{{testResults.finalCommentsList}}">
|
||||
<text class="label">处理后评论列表 ({{testResults.finalCommentsList.length}}条):</text>
|
||||
<view class="json-data">{{testResults.finalCommentsList}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 模拟评论组件显示 -->
|
||||
<view class="section" wx:if="{{testResults.finalCommentsStatistics}}">
|
||||
<view class="section-title">模拟评论组件显示</view>
|
||||
<view class="comment-simulation">
|
||||
<view class="display-condition">
|
||||
<text>显示条件: commentCount({{testResults.finalCommentsStatistics.commentCount}}) > 0 = {{testResults.shouldDisplay}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 模拟的评论组件 -->
|
||||
<view class="comments-wrap" wx:if="{{testResults.shouldDisplay}}">
|
||||
<view class="comments-head">
|
||||
<view class="comments-title">
|
||||
<text class="comments-title-label">商品评价</text>
|
||||
<text class="comments-title-count">({{testResults.finalCommentsStatistics.commentCount}})</text>
|
||||
</view>
|
||||
<view class="comments-rate-wrap">
|
||||
<text class="comments-rate-label">好评率</text>
|
||||
<text class="comments-rate-num">{{testResults.finalCommentsStatistics.goodRate}}%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="comment-item-wrap" wx:for="{{testResults.finalCommentsList}}" wx:key="id" wx:if="{{index < 3}}">
|
||||
<view class="comment-item-head">
|
||||
<image class="comment-item-avatar" src="{{item.userHeadUrl}}" />
|
||||
<view class="comment-item-info">
|
||||
<text class="comment-item-name">{{item.userName}}</text>
|
||||
<view class="comment-item-rate">
|
||||
<text class="comment-item-score">{{item.commentScore}}分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="comment-item-content">
|
||||
<text>{{item.commentContent}}</text>
|
||||
</view>
|
||||
<view class="comment-item-time">{{item.commentTime}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="no-comments" wx:else>
|
||||
<text>评论组件不显示 (commentCount = {{testResults.finalCommentsStatistics.commentCount}})</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
255
miniprogram/pages/debug/api-test.wxss
Normal file
255
miniprogram/pages/debug/api-test.wxss
Normal file
@@ -0,0 +1,255 @@
|
||||
/* pages/debug/api-test.wxss */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: white;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
border-bottom: 2rpx solid #eee;
|
||||
padding-bottom: 10rpx;
|
||||
}
|
||||
|
||||
/* 配置区域 */
|
||||
.config-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.spu-input {
|
||||
border: 2rpx solid #ddd;
|
||||
padding: 10rpx;
|
||||
border-radius: 5rpx;
|
||||
margin-left: 10rpx;
|
||||
width: 200rpx;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
flex: 1;
|
||||
min-width: 200rpx;
|
||||
margin: 5rpx;
|
||||
padding: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.test-btn.primary {
|
||||
background-color: #07c160;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.secondary {
|
||||
background-color: #ff6b6b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 测试结果 */
|
||||
.test-result {
|
||||
background-color: #f9f9f9;
|
||||
padding: 15rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
margin-bottom: 15rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.value.called {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.value.not-called {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.value.success {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.value.error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.json-data {
|
||||
background-color: #f0f0f0;
|
||||
padding: 10rpx;
|
||||
border-radius: 5rpx;
|
||||
font-family: monospace;
|
||||
font-size: 24rpx;
|
||||
word-break: break-all;
|
||||
max-height: 300rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 评论组件模拟 */
|
||||
.comment-simulation {
|
||||
border: 2rpx dashed #ddd;
|
||||
padding: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.display-condition {
|
||||
background-color: #e3f2fd;
|
||||
padding: 10rpx;
|
||||
border-radius: 5rpx;
|
||||
margin-bottom: 15rpx;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40rpx;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
/* 评论组件样式 (复制自商品详情页) */
|
||||
.comments-wrap {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.comments-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.comments-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comments-title-label {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comments-title-count {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.comments-rate-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comments-rate-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.comments-rate-num {
|
||||
font-size: 28rpx;
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.comment-item-wrap {
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
padding-bottom: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.comment-item-wrap:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.comment-item-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.comment-item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-item-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.comment-item-rate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment-item-score {
|
||||
font-size: 24rpx;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.comment-item-content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.comment-item-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
186
miniprogram/pages/debug/comment-display-test.js
Normal file
186
miniprogram/pages/debug/comment-display-test.js
Normal file
@@ -0,0 +1,186 @@
|
||||
const { fetchComments } = require('../../services/comments/fetchComments');
|
||||
const { fetchCommentsCount } = require('../../services/comments/fetchCommentsCount');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
spuId: 1,
|
||||
commentsList: [],
|
||||
commentsStatistics: {
|
||||
badCount: 0,
|
||||
commentCount: 0,
|
||||
goodCount: 0,
|
||||
goodRate: 0,
|
||||
hasImageCount: 0,
|
||||
middleCount: 0,
|
||||
},
|
||||
isLoading: false,
|
||||
debugInfo: {
|
||||
apiCalled: false,
|
||||
dataReceived: false,
|
||||
errorMessage: '',
|
||||
rawData: null,
|
||||
processedData: null
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('评论显示测试页面加载');
|
||||
this.loadCommentData();
|
||||
},
|
||||
|
||||
async loadCommentData() {
|
||||
this.setData({
|
||||
isLoading: true,
|
||||
'debugInfo.apiCalled': false,
|
||||
'debugInfo.dataReceived': false,
|
||||
'debugInfo.errorMessage': '',
|
||||
'debugInfo.rawData': null,
|
||||
'debugInfo.processedData': null
|
||||
});
|
||||
|
||||
try {
|
||||
// 获取评论统计
|
||||
await this.getCommentsStatistics(this.data.spuId);
|
||||
// 获取评论列表
|
||||
await this.getCommentsList(this.data.spuId);
|
||||
|
||||
this.setData({
|
||||
isLoading: false,
|
||||
'debugInfo.dataReceived': true
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载评论数据失败:', error);
|
||||
this.setData({
|
||||
isLoading: false,
|
||||
'debugInfo.errorMessage': error.message || '未知错误'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取评论统计数据 - 复制商品详情页面的逻辑
|
||||
async getCommentsStatistics(spuId) {
|
||||
try {
|
||||
this.setData({ 'debugInfo.apiCalled': true });
|
||||
console.log('调用 fetchCommentsCount,spuId:', spuId);
|
||||
|
||||
const res = await fetchCommentsCount(spuId);
|
||||
console.log('fetchCommentsCount 返回结果:', res);
|
||||
|
||||
this.setData({ 'debugInfo.rawData': res });
|
||||
|
||||
if (res) {
|
||||
const goodRate = res.total_count > 0 ? Math.round((res.good_count / res.total_count) * 100) : 0;
|
||||
|
||||
const commentsStatistics = {
|
||||
commentCount: res.total_count || 0,
|
||||
goodCount: res.good_count || 0,
|
||||
middleCount: res.medium_count || 0,
|
||||
badCount: res.bad_count || 0,
|
||||
goodRate: goodRate,
|
||||
hasImageCount: res.image_count || 0,
|
||||
};
|
||||
|
||||
console.log('处理后的统计数据:', commentsStatistics);
|
||||
console.log('显示条件 (commentCount > 0):', commentsStatistics.commentCount > 0);
|
||||
|
||||
this.setData({
|
||||
commentsStatistics: commentsStatistics,
|
||||
'debugInfo.processedData': commentsStatistics
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评论统计失败:', error);
|
||||
this.setData({
|
||||
'debugInfo.errorMessage': error.message || '获取评论统计失败',
|
||||
commentsStatistics: {
|
||||
commentCount: 0,
|
||||
goodCount: 0,
|
||||
middleCount: 0,
|
||||
badCount: 0,
|
||||
goodRate: 0,
|
||||
hasImageCount: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取评论列表数据 - 复制商品详情页面的逻辑
|
||||
async getCommentsList(spuId) {
|
||||
try {
|
||||
console.log('调用 fetchComments,参数:', { productId: spuId, page: 1, pageSize: 3 });
|
||||
|
||||
const res = await fetchComments({ productId: spuId, page: 1, pageSize: 3 });
|
||||
console.log('fetchComments 返回结果:', res);
|
||||
|
||||
if (res && res.comments) {
|
||||
// 转换评论数据格式
|
||||
const commentsList = res.comments.map(comment => ({
|
||||
id: comment.id,
|
||||
userName: comment.is_anonymous ? '匿名用户' : (comment.user_name || '用户'),
|
||||
userHeadUrl: comment.user_avatar || 'https://tdesign.gtimg.com/miniprogram/template/retail/avatar/avatar1.png',
|
||||
commentScore: comment.rating || 5,
|
||||
commentContent: comment.content || '',
|
||||
commentTime: comment.created_at ? this.formatTime(comment.created_at) : '',
|
||||
specInfo: comment.product_spec || '',
|
||||
commentResources: (comment.images || []).map(imageUrl => ({
|
||||
src: imageUrl,
|
||||
type: 'image'
|
||||
})),
|
||||
isAnonymity: comment.is_anonymous || false,
|
||||
sellerReply: comment.reply_content || '',
|
||||
goodsDetailInfo: ''
|
||||
}));
|
||||
|
||||
console.log('处理后的评论列表:', commentsList);
|
||||
|
||||
this.setData({
|
||||
commentsList: commentsList
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评论列表失败:', error);
|
||||
this.setData({
|
||||
commentsList: []
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(dateString) {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
},
|
||||
|
||||
// 重新加载数据
|
||||
reloadData() {
|
||||
this.loadCommentData();
|
||||
},
|
||||
|
||||
// 清除数据
|
||||
clearData() {
|
||||
this.setData({
|
||||
commentsList: [],
|
||||
commentsStatistics: {
|
||||
badCount: 0,
|
||||
commentCount: 0,
|
||||
goodCount: 0,
|
||||
goodRate: 0,
|
||||
hasImageCount: 0,
|
||||
middleCount: 0,
|
||||
},
|
||||
debugInfo: {
|
||||
apiCalled: false,
|
||||
dataReceived: false,
|
||||
errorMessage: '',
|
||||
rawData: null,
|
||||
processedData: null
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
4
miniprogram/pages/debug/comment-display-test.json
Normal file
4
miniprogram/pages/debug/comment-display-test.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "评论组件显示测试",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
92
miniprogram/pages/debug/comment-display-test.wxml
Normal file
92
miniprogram/pages/debug/comment-display-test.wxml
Normal file
@@ -0,0 +1,92 @@
|
||||
<!--评论显示测试页面-->
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">评论组件显示测试</text>
|
||||
<text class="subtitle">模拟商品详情页面的评论组件</text>
|
||||
</view>
|
||||
|
||||
<!-- 调试信息 -->
|
||||
<view class="debug-section">
|
||||
<text class="section-title">调试信息</text>
|
||||
<view class="debug-item">
|
||||
<text class="debug-label">API已调用:</text>
|
||||
<text class="debug-value {{debugInfo.apiCalled ? 'success' : 'error'}}">{{debugInfo.apiCalled ? '是' : '否'}}</text>
|
||||
</view>
|
||||
<view class="debug-item">
|
||||
<text class="debug-label">数据已接收:</text>
|
||||
<text class="debug-value {{debugInfo.dataReceived ? 'success' : 'error'}}">{{debugInfo.dataReceived ? '是' : '否'}}</text>
|
||||
</view>
|
||||
<view class="debug-item" wx:if="{{debugInfo.errorMessage}}">
|
||||
<text class="debug-label">错误信息:</text>
|
||||
<text class="debug-value error">{{debugInfo.errorMessage}}</text>
|
||||
</view>
|
||||
<view class="debug-item">
|
||||
<text class="debug-label">评论数量:</text>
|
||||
<text class="debug-value">{{commentsStatistics.commentCount}}</text>
|
||||
</view>
|
||||
<view class="debug-item">
|
||||
<text class="debug-label">显示条件:</text>
|
||||
<text class="debug-value {{commentsStatistics.commentCount > 0 ? 'success' : 'error'}}">
|
||||
commentCount > 0 = {{commentsStatistics.commentCount > 0}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-section">
|
||||
<button class="action-btn" bindtap="reloadData" disabled="{{isLoading}}">
|
||||
{{isLoading ? '加载中...' : '重新加载'}}
|
||||
</button>
|
||||
<button class="action-btn secondary" bindtap="clearData">清除数据</button>
|
||||
</view>
|
||||
|
||||
<!-- 评论组件 - 复制商品详情页面的结构 -->
|
||||
<view class="comment-component-section">
|
||||
<text class="section-title">评论组件 ({{commentsStatistics.commentCount > 0 ? '应该显示' : '应该隐藏'}})</text>
|
||||
|
||||
<!-- 这里是关键的显示条件 -->
|
||||
<view wx:if="{{commentsStatistics.commentCount > 0}}" class="comments-wrap">
|
||||
<view class="comments-head">
|
||||
<view class="comments-title-wrap">
|
||||
<text class="comments-title-label">商品评价</text>
|
||||
<text class="comments-title-count">({{commentsStatistics.commentCount}})</text>
|
||||
</view>
|
||||
<view class="comments-rate-wrap">
|
||||
<text class="comments-good-rate">{{commentsStatistics.goodRate}}%好评</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
<view wx:for="{{commentsList}}" wx:key="id" class="comment-item-wrap">
|
||||
<view class="comment-item-head">
|
||||
<image class="comment-item-avatar" src="{{item.userHeadUrl}}" mode="aspectFill"></image>
|
||||
<view class="comment-head-right">
|
||||
<text class="comment-username">{{item.userName}}</text>
|
||||
<view class="comment-rating">
|
||||
<text wx:for="{{[1,2,3,4,5]}}" wx:for-item="star" wx:key="star"
|
||||
class="star {{star <= item.commentScore ? 'filled' : ''}}">★</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="comment-item-content">{{item.commentContent}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 当没有评论时显示的内容 -->
|
||||
<view wx:else class="no-comments">
|
||||
<text class="no-comments-text">暂无评论 (commentCount = {{commentsStatistics.commentCount}})</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 原始数据显示 -->
|
||||
<view class="raw-data-section" wx:if="{{debugInfo.rawData}}">
|
||||
<text class="section-title">原始API数据</text>
|
||||
<view class="data-display">{{debugInfo.rawData}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 处理后数据显示 -->
|
||||
<view class="processed-data-section" wx:if="{{debugInfo.processedData}}">
|
||||
<text class="section-title">处理后数据</text>
|
||||
<view class="data-display">{{debugInfo.processedData}}</view>
|
||||
</view>
|
||||
</view>
|
||||
265
miniprogram/pages/debug/comment-display-test.wxss
Normal file
265
miniprogram/pages/debug/comment-display-test.wxss
Normal file
@@ -0,0 +1,265 @@
|
||||
/* 评论显示测试页面样式 */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 10rpx;
|
||||
border-bottom: 2rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* 调试信息样式 */
|
||||
.debug-section {
|
||||
background: #fff;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
border-left: 4rpx solid #2196F3;
|
||||
}
|
||||
|
||||
.debug-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.debug-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.debug-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.debug-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.debug-value.success {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.debug-value.error {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
/* 操作按钮样式 */
|
||||
.action-section {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #2196F3;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.action-btn[disabled] {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
/* 评论组件样式 */
|
||||
.comment-component-section {
|
||||
background: #fff;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
/* 复制商品详情页面的评论样式 */
|
||||
.comments-wrap {
|
||||
margin: 20rpx 0;
|
||||
padding: 28rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
border: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.comments-wrap .comments-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.comments-wrap .comments-head .comments-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comments-title-label {
|
||||
color: #222222;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.comments-title-count {
|
||||
color: #666666;
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.comments-rate-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #fff5f5;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid #ffe5e5;
|
||||
}
|
||||
|
||||
.comments-rate-wrap .comments-good-rate {
|
||||
color: #fa4126;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.comment-item-wrap {
|
||||
margin-top: 24rpx;
|
||||
padding: 20rpx;
|
||||
background: #fafafa;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.comment-item-wrap .comment-item-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment-item-wrap .comment-item-head .comment-item-avatar {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
border: 2rpx solid #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.comment-item-wrap .comment-item-head .comment-head-right {
|
||||
margin-left: 20rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-head-right .comment-username {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.comment-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.star {
|
||||
font-size: 24rpx;
|
||||
color: #ddd;
|
||||
margin-right: 2rpx;
|
||||
}
|
||||
|
||||
.star.filled {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.comment-item-wrap .comment-item-content {
|
||||
margin-top: 16rpx;
|
||||
color: #555555;
|
||||
line-height: 1.5;
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
padding: 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
border-left: 3rpx solid #fa4126;
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
text-align: center;
|
||||
padding: 60rpx 20rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed #ddd;
|
||||
}
|
||||
|
||||
.no-comments-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 数据显示样式 */
|
||||
.raw-data-section,
|
||||
.processed-data-section {
|
||||
background: #fff;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.data-display {
|
||||
background: #f5f5f5;
|
||||
padding: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
106
miniprogram/pages/debug/comment-test.js
Normal file
106
miniprogram/pages/debug/comment-test.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// 评论服务测试页面
|
||||
const { fetchComments } = require('../../services/comments/fetchComments');
|
||||
const { fetchCommentsCount } = require('../../services/comments/fetchCommentsCount');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
testResults: [],
|
||||
isLoading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('评论测试页面加载');
|
||||
},
|
||||
|
||||
// 测试评论统计
|
||||
async testCommentsCount() {
|
||||
this.setData({ isLoading: true });
|
||||
const results = [...this.data.testResults];
|
||||
|
||||
try {
|
||||
console.log('开始测试 fetchCommentsCount...');
|
||||
const result = await fetchCommentsCount(1);
|
||||
console.log('fetchCommentsCount 结果:', result);
|
||||
|
||||
// 模拟商品详情页面的数据处理逻辑
|
||||
const goodRate = result.total_count > 0 ? Math.round((result.good_count / result.total_count) * 100) : 0;
|
||||
const commentsStatistics = {
|
||||
commentCount: result.total_count || 0,
|
||||
goodCount: result.good_count || 0,
|
||||
middleCount: result.medium_count || 0,
|
||||
badCount: result.bad_count || 0,
|
||||
goodRate: goodRate,
|
||||
hasImageCount: result.image_count || 0,
|
||||
};
|
||||
|
||||
console.log('处理后的统计数据:', commentsStatistics);
|
||||
console.log('显示条件 (commentCount > 0):', commentsStatistics.commentCount > 0);
|
||||
|
||||
results.push({
|
||||
test: 'fetchCommentsCount',
|
||||
status: 'success',
|
||||
data: {
|
||||
raw: result,
|
||||
processed: commentsStatistics,
|
||||
shouldShow: commentsStatistics.commentCount > 0
|
||||
},
|
||||
time: new Date().toLocaleTimeString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('fetchCommentsCount 错误:', error);
|
||||
results.push({
|
||||
test: 'fetchCommentsCount',
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
time: new Date().toLocaleTimeString()
|
||||
});
|
||||
}
|
||||
|
||||
this.setData({ testResults: results, isLoading: false });
|
||||
},
|
||||
|
||||
// 测试评论列表
|
||||
async testCommentsList() {
|
||||
this.setData({ isLoading: true });
|
||||
const results = [...this.data.testResults];
|
||||
|
||||
try {
|
||||
console.log('开始测试 fetchComments...');
|
||||
const result = await fetchComments({
|
||||
productId: 1,
|
||||
page: 1,
|
||||
page_size: 3
|
||||
});
|
||||
console.log('fetchComments 结果:', result);
|
||||
|
||||
results.push({
|
||||
test: 'fetchComments',
|
||||
status: 'success',
|
||||
data: result,
|
||||
time: new Date().toLocaleTimeString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('fetchComments 错误:', error);
|
||||
results.push({
|
||||
test: 'fetchComments',
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
time: new Date().toLocaleTimeString()
|
||||
});
|
||||
}
|
||||
|
||||
this.setData({ testResults: results, isLoading: false });
|
||||
},
|
||||
|
||||
// 清空测试结果
|
||||
clearResults() {
|
||||
this.setData({ testResults: [] });
|
||||
},
|
||||
|
||||
// 运行所有测试
|
||||
async runAllTests() {
|
||||
this.clearResults();
|
||||
await this.testCommentsCount();
|
||||
await this.testCommentsList();
|
||||
}
|
||||
});
|
||||
3
miniprogram/pages/debug/comment-test.json
Normal file
3
miniprogram/pages/debug/comment-test.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
51
miniprogram/pages/debug/comment-test.wxml
Normal file
51
miniprogram/pages/debug/comment-test.wxml
Normal file
@@ -0,0 +1,51 @@
|
||||
<!--评论服务测试页面-->
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">评论服务测试</text>
|
||||
<text class="subtitle">测试 fetchComments 和 fetchCommentsCount 导入修复</text>
|
||||
</view>
|
||||
|
||||
<view class="test-buttons">
|
||||
<button class="test-btn" bindtap="testCommentsCount" disabled="{{isLoading}}">
|
||||
测试评论统计
|
||||
</button>
|
||||
<button class="test-btn" bindtap="testCommentsList" disabled="{{isLoading}}">
|
||||
测试评论列表
|
||||
</button>
|
||||
<button class="test-btn primary" bindtap="runAllTests" disabled="{{isLoading}}">
|
||||
运行所有测试
|
||||
</button>
|
||||
<button class="test-btn secondary" bindtap="clearResults">
|
||||
清空结果
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="loading" wx:if="{{isLoading}}">
|
||||
<text>测试中...</text>
|
||||
</view>
|
||||
|
||||
<view class="results" wx:if="{{testResults.length > 0}}">
|
||||
<text class="results-title">测试结果:</text>
|
||||
<view class="result-item" wx:for="{{testResults}}" wx:key="index">
|
||||
<view class="result-header">
|
||||
<text class="test-name">{{item.test}}</text>
|
||||
<text class="test-status {{item.status}}">{{item.status}}</text>
|
||||
<text class="test-time">{{item.time}}</text>
|
||||
</view>
|
||||
|
||||
<view class="result-content" wx:if="{{item.status === 'success'}}">
|
||||
<text class="data-label">返回数据:</text>
|
||||
<text class="data-content">{{item.data}}</text>
|
||||
</view>
|
||||
|
||||
<view class="result-content error" wx:if="{{item.status === 'error'}}">
|
||||
<text class="error-label">错误信息:</text>
|
||||
<text class="error-content">{{item.error}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:if="{{testResults.length === 0 && !isLoading}}">
|
||||
<text>点击上方按钮开始测试</text>
|
||||
</view>
|
||||
</view>
|
||||
162
miniprogram/pages/debug/comment-test.wxss
Normal file
162
miniprogram/pages/debug/comment-test.wxss
Normal file
@@ -0,0 +1,162 @@
|
||||
/* 评论服务测试页面样式 */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
padding: 30rpx;
|
||||
background-color: white;
|
||||
border-radius: 10rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.test-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
padding: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 30rpx;
|
||||
border: none;
|
||||
background-color: white;
|
||||
color: #333;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-btn.primary {
|
||||
background-color: #07c160;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.secondary {
|
||||
background-color: #ff6b6b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
background-color: white;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.results {
|
||||
background-color: white;
|
||||
border-radius: 10rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.results-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.test-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-status.success {
|
||||
background-color: #07c160;
|
||||
}
|
||||
|
||||
.test-status.error {
|
||||
background-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.test-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.result-content.error {
|
||||
background-color: #fff5f5;
|
||||
}
|
||||
|
||||
.data-label, .error-label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.data-content {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
font-size: 24rpx;
|
||||
color: #e53e3e;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 60rpx;
|
||||
background-color: white;
|
||||
border-radius: 10rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
230
miniprogram/pages/debug/hardcoded-comment-test.js
Normal file
230
miniprogram/pages/debug/hardcoded-comment-test.js
Normal file
@@ -0,0 +1,230 @@
|
||||
// pages/debug/hardcoded-comment-test.js
|
||||
Page({
|
||||
data: {
|
||||
// 商品ID测试相关
|
||||
testProductId: '1',
|
||||
testResults: [],
|
||||
|
||||
// 硬编码的评论统计数据 - 确保有评论
|
||||
commentsStatistics: {
|
||||
commentCount: 5,
|
||||
goodCount: 4,
|
||||
middleCount: 1,
|
||||
badCount: 0,
|
||||
goodRate: 80,
|
||||
hasImageCount: 2,
|
||||
},
|
||||
|
||||
// 硬编码的评论列表数据
|
||||
commentsList: [
|
||||
{
|
||||
id: 1,
|
||||
userName: '测试用户1',
|
||||
userHeadUrl: 'https://tdesign.gtimg.com/miniprogram/template/retail/avatar/avatar1.png',
|
||||
commentScore: 5,
|
||||
commentContent: '商品质量很好,物流也很快!',
|
||||
commentTime: '2024-01-15',
|
||||
specInfo: '颜色:红色 尺寸:M',
|
||||
commentResources: [],
|
||||
isAnonymity: false,
|
||||
sellerReply: '',
|
||||
goodsDetailInfo: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userName: '测试用户2',
|
||||
userHeadUrl: 'https://tdesign.gtimg.com/miniprogram/template/retail/avatar/avatar2.png',
|
||||
commentScore: 4,
|
||||
commentContent: '整体不错,性价比很高,推荐购买!',
|
||||
commentTime: '2024-01-14',
|
||||
specInfo: '颜色:蓝色 尺寸:L',
|
||||
commentResources: ['https://example.com/image1.jpg'],
|
||||
isAnonymity: false,
|
||||
sellerReply: '感谢您的好评!',
|
||||
goodsDetailInfo: ''
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
userName: '匿名用户',
|
||||
userHeadUrl: 'https://tdesign.gtimg.com/miniprogram/template/retail/avatar/avatar3.png',
|
||||
commentScore: 3,
|
||||
commentContent: '一般般,还可以吧。',
|
||||
commentTime: '2024-01-13',
|
||||
specInfo: '颜色:绿色 尺寸:S',
|
||||
commentResources: [],
|
||||
isAnonymity: true,
|
||||
sellerReply: '',
|
||||
goodsDetailInfo: ''
|
||||
}
|
||||
],
|
||||
|
||||
// 测试状态
|
||||
testMode: 'with-comments', // 'with-comments' 或 'no-comments'
|
||||
debugInfo: {
|
||||
commentCount: 5,
|
||||
displayCondition: true,
|
||||
timestamp: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
console.log('硬编码评论测试页面加载');
|
||||
this.updateDebugInfo();
|
||||
},
|
||||
|
||||
// 更新调试信息
|
||||
updateDebugInfo() {
|
||||
const { commentsStatistics } = this.data;
|
||||
this.setData({
|
||||
'debugInfo.commentCount': commentsStatistics.commentCount,
|
||||
'debugInfo.displayCondition': commentsStatistics.commentCount > 0,
|
||||
'debugInfo.timestamp': new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
console.log('调试信息更新:', {
|
||||
commentCount: commentsStatistics.commentCount,
|
||||
displayCondition: commentsStatistics.commentCount > 0,
|
||||
shouldShow: commentsStatistics.commentCount > 0 ? '应该显示' : '不应该显示'
|
||||
});
|
||||
},
|
||||
|
||||
// 切换测试模式
|
||||
switchTestMode() {
|
||||
const newMode = this.data.testMode === 'with-comments' ? 'no-comments' : 'with-comments';
|
||||
|
||||
if (newMode === 'no-comments') {
|
||||
// 设置为无评论状态
|
||||
this.setData({
|
||||
testMode: newMode,
|
||||
commentsStatistics: {
|
||||
commentCount: 0,
|
||||
goodCount: 0,
|
||||
middleCount: 0,
|
||||
badCount: 0,
|
||||
goodRate: 0,
|
||||
hasImageCount: 0,
|
||||
},
|
||||
commentsList: []
|
||||
});
|
||||
} else {
|
||||
// 设置为有评论状态
|
||||
this.setData({
|
||||
testMode: newMode,
|
||||
commentsStatistics: {
|
||||
commentCount: 5,
|
||||
goodCount: 4,
|
||||
middleCount: 1,
|
||||
badCount: 0,
|
||||
goodRate: 80,
|
||||
hasImageCount: 2,
|
||||
},
|
||||
commentsList: [
|
||||
{
|
||||
id: 1,
|
||||
userName: '测试用户1',
|
||||
userHeadUrl: 'https://tdesign.gtimg.com/miniprogram/template/retail/avatar/avatar1.png',
|
||||
commentScore: 5,
|
||||
commentContent: '商品质量很好,物流也很快!',
|
||||
commentTime: '2024-01-15',
|
||||
specInfo: '颜色:红色 尺寸:M',
|
||||
commentResources: [],
|
||||
isAnonymity: false,
|
||||
sellerReply: '',
|
||||
goodsDetailInfo: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userName: '测试用户2',
|
||||
userHeadUrl: 'https://tdesign.gtimg.com/miniprogram/template/retail/avatar/avatar2.png',
|
||||
commentScore: 4,
|
||||
commentContent: '整体不错,性价比很高,推荐购买!',
|
||||
commentTime: '2024-01-14',
|
||||
specInfo: '颜色:蓝色 尺寸:L',
|
||||
commentResources: ['https://example.com/image1.jpg'],
|
||||
isAnonymity: false,
|
||||
sellerReply: '感谢您的好评!',
|
||||
goodsDetailInfo: ''
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
userName: '匿名用户',
|
||||
userHeadUrl: 'https://tdesign.gtimg.com/miniprogram/template/retail/avatar/avatar3.png',
|
||||
commentScore: 3,
|
||||
commentContent: '一般般,还可以吧。',
|
||||
commentTime: '2024-01-13',
|
||||
specInfo: '颜色:绿色 尺寸:S',
|
||||
commentResources: [],
|
||||
isAnonymity: true,
|
||||
sellerReply: '',
|
||||
goodsDetailInfo: ''
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
this.updateDebugInfo();
|
||||
},
|
||||
|
||||
// 跳转到评论列表页面(占位函数)
|
||||
navToCommentsListPage() {
|
||||
console.log('跳转到评论列表页面');
|
||||
wx.showToast({
|
||||
title: '跳转到评论列表',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
// 输入框值变化
|
||||
onProductIdInput(e) {
|
||||
this.setData({
|
||||
testProductId: e.detail.value
|
||||
});
|
||||
},
|
||||
|
||||
// 测试商品ID验证
|
||||
async testProductIdValidation() {
|
||||
const { testProductId } = this.data;
|
||||
console.log(`[调试页面] 开始测试商品ID: "${testProductId}"`);
|
||||
|
||||
const testResult = {
|
||||
productId: testProductId,
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
success: false,
|
||||
error: null,
|
||||
data: null
|
||||
};
|
||||
|
||||
try {
|
||||
// 导入评论服务
|
||||
const { fetchCommentsCount } = require('../../services/comments/fetchCommentsCount');
|
||||
|
||||
console.log(`[调试页面] 调用 fetchCommentsCount,参数: "${testProductId}"`);
|
||||
const result = await fetchCommentsCount(testProductId);
|
||||
|
||||
testResult.success = true;
|
||||
testResult.data = result;
|
||||
console.log(`[调试页面] 测试成功:`, result);
|
||||
|
||||
} catch (error) {
|
||||
testResult.success = false;
|
||||
testResult.error = error.message;
|
||||
console.error(`[调试页面] 测试失败:`, error);
|
||||
}
|
||||
|
||||
// 更新测试结果
|
||||
const testResults = [testResult, ...this.data.testResults.slice(0, 9)]; // 保留最近10条
|
||||
this.setData({ testResults });
|
||||
},
|
||||
|
||||
// 快速测试预设值
|
||||
async quickTest(e) {
|
||||
const testValue = e.currentTarget.dataset.value;
|
||||
this.setData({ testProductId: testValue });
|
||||
await this.testProductIdValidation();
|
||||
},
|
||||
|
||||
// 清空测试结果
|
||||
clearTestResults() {
|
||||
this.setData({ testResults: [] });
|
||||
}
|
||||
});
|
||||
6
miniprogram/pages/debug/hardcoded-comment-test.json
Normal file
6
miniprogram/pages/debug/hardcoded-comment-test.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "硬编码评论测试",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
175
miniprogram/pages/debug/hardcoded-comment-test.wxml
Normal file
175
miniprogram/pages/debug/hardcoded-comment-test.wxml
Normal file
@@ -0,0 +1,175 @@
|
||||
<!--pages/debug/hardcoded-comment-test.wxml-->
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">硬编码评论组件测试</text>
|
||||
<text class="subtitle">使用硬编码数据测试评论组件显示</text>
|
||||
</view>
|
||||
|
||||
<!-- 测试信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">测试信息</view>
|
||||
<view class="test-info">
|
||||
<view class="info-item">
|
||||
<text class="label">当前模式:</text>
|
||||
<text class="value">{{testMode === 'with-comments' ? '有评论模式' : '无评论模式'}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">评论数量:</text>
|
||||
<text class="value">{{debugInfo.commentCount}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">显示条件:</text>
|
||||
<text class="value {{debugInfo.displayCondition ? 'success' : 'error'}}">
|
||||
commentCount({{debugInfo.commentCount}}) > 0 = {{debugInfo.displayCondition ? 'true (应该显示)' : 'false (不应该显示)'}}
|
||||
</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">更新时间:</text>
|
||||
<text class="value">{{debugInfo.timestamp}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品ID测试 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品ID验证测试</view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="test-input-area">
|
||||
<view class="input-row">
|
||||
<text class="input-label">测试商品ID:</text>
|
||||
<input class="test-input"
|
||||
value="{{testProductId}}"
|
||||
bindinput="onProductIdInput"
|
||||
placeholder="输入要测试的商品ID" />
|
||||
</view>
|
||||
<button class="test-btn" bindtap="testProductIdValidation">测试验证</button>
|
||||
</view>
|
||||
|
||||
<!-- 快速测试按钮 -->
|
||||
<view class="quick-test-area">
|
||||
<text class="quick-test-label">快速测试:</text>
|
||||
<view class="quick-test-buttons">
|
||||
<button class="quick-btn" bindtap="quickTest" data-value="1">正常ID(1)</button>
|
||||
<button class="quick-btn" bindtap="quickTest" data-value="0">零值(0)</button>
|
||||
<button class="quick-btn" bindtap="quickTest" data-value="">空字符串</button>
|
||||
<button class="quick-btn" bindtap="quickTest" data-value="abc">非数字(abc)</button>
|
||||
<button class="quick-btn" bindtap="quickTest" data-value="-1">负数(-1)</button>
|
||||
<button class="quick-btn" bindtap="quickTest" data-value="1.5">小数(1.5)</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 测试结果 -->
|
||||
<view class="test-results" wx:if="{{testResults.length > 0}}">
|
||||
<view class="results-header">
|
||||
<text class="results-title">测试结果 ({{testResults.length}}条)</text>
|
||||
<button class="clear-btn" bindtap="clearTestResults">清空</button>
|
||||
</view>
|
||||
|
||||
<view class="result-item" wx:for="{{testResults}}" wx:key="timestamp">
|
||||
<view class="result-header">
|
||||
<text class="result-id">ID: "{{item.productId}}"</text>
|
||||
<text class="result-time">{{item.timestamp}}</text>
|
||||
<text class="result-status {{item.success ? 'success' : 'error'}}">
|
||||
{{item.success ? '✓ 成功' : '✗ 失败'}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="result-content" wx:if="{{item.success}}">
|
||||
<text class="result-data">返回数据: {{item.data}}</text>
|
||||
</view>
|
||||
|
||||
<view class="result-content" wx:else>
|
||||
<text class="result-error">错误信息: {{item.error}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="section">
|
||||
<view class="section-title">评论组件测试</view>
|
||||
<button class="switch-btn" bindtap="switchTestMode">
|
||||
切换到{{testMode === 'with-comments' ? '无评论模式' : '有评论模式'}}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 评论组件显示区域 -->
|
||||
<view class="section">
|
||||
<view class="section-title">评论组件显示区域</view>
|
||||
|
||||
<!-- 显示条件检查 -->
|
||||
<view class="condition-check">
|
||||
<text class="condition-text">
|
||||
条件检查: commentsStatistics.commentCount = {{commentsStatistics.commentCount}}
|
||||
</text>
|
||||
<text class="condition-text">
|
||||
显示条件: {{commentsStatistics.commentCount > 0 ? 'true (显示)' : 'false (隐藏)'}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 评论组件 - 完全复制商品详情页的结构 -->
|
||||
<view class="comments-wrap" wx:if="{{commentsStatistics.commentCount > 0}}">
|
||||
<view class="comments-head">
|
||||
<view class="comments-title">
|
||||
<text class="comments-title-label">商品评价</text>
|
||||
<text class="comments-title-count">({{commentsStatistics.commentCount}})</text>
|
||||
</view>
|
||||
<view class="comments-rate-wrap">
|
||||
<text class="comments-rate-label">好评率</text>
|
||||
<text class="comments-rate-num">{{commentsStatistics.goodRate}}%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
<view class="comment-item-wrap" wx:for="{{commentsList}}" wx:key="id" wx:if="{{index < 3}}">
|
||||
<view class="comment-item-head">
|
||||
<image class="comment-item-avatar" src="{{item.userHeadUrl}}" />
|
||||
<view class="comment-item-info">
|
||||
<text class="comment-item-name">{{item.userName}}</text>
|
||||
<view class="comment-item-rate">
|
||||
<text class="comment-item-score">{{item.commentScore}}分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="comment-item-content">
|
||||
<text>{{item.commentContent}}</text>
|
||||
</view>
|
||||
<view class="comment-item-spec" wx:if="{{item.specInfo}}">
|
||||
<text>{{item.specInfo}}</text>
|
||||
</view>
|
||||
<view class="comment-item-reply" wx:if="{{item.sellerReply}}">
|
||||
<text class="reply-label">商家回复:</text>
|
||||
<text class="reply-content">{{item.sellerReply}}</text>
|
||||
</view>
|
||||
<view class="comment-item-time">{{item.commentTime}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 查看更多按钮 -->
|
||||
<view class="comments-more" bindtap="navToCommentsListPage">
|
||||
<text>查看全部评价</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无评论时的显示 -->
|
||||
<view class="no-comments-placeholder" wx:else>
|
||||
<text class="placeholder-text">评论组件未显示</text>
|
||||
<text class="placeholder-reason">原因: commentCount = {{commentsStatistics.commentCount}} (不满足 > 0 的条件)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 原始数据显示 -->
|
||||
<view class="section">
|
||||
<view class="section-title">原始数据</view>
|
||||
<view class="data-display">
|
||||
<view class="data-item">
|
||||
<text class="data-label">commentsStatistics:</text>
|
||||
<view class="data-content">{{commentsStatistics}}</view>
|
||||
</view>
|
||||
<view class="data-item">
|
||||
<text class="data-label">commentsList ({{commentsList.length}}条):</text>
|
||||
<view class="data-content">{{commentsList}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
463
miniprogram/pages/debug/hardcoded-comment-test.wxss
Normal file
463
miniprogram/pages/debug/hardcoded-comment-test.wxss
Normal file
@@ -0,0 +1,463 @@
|
||||
/* pages/debug/hardcoded-comment-test.wxss */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: white;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
border-bottom: 2rpx solid #eee;
|
||||
padding-bottom: 10rpx;
|
||||
}
|
||||
|
||||
/* 测试信息 */
|
||||
.test-info {
|
||||
background-color: #f9f9f9;
|
||||
padding: 15rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 10rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
min-width: 150rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.value.success {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.value.error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.switch-btn {
|
||||
width: 100%;
|
||||
background-color: #07c160;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
/* 条件检查 */
|
||||
.condition-check {
|
||||
background-color: #e3f2fd;
|
||||
padding: 15rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.condition-text {
|
||||
font-family: monospace;
|
||||
font-size: 26rpx;
|
||||
color: #1976d2;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 无评论占位符 */
|
||||
.no-comments-placeholder {
|
||||
text-align: center;
|
||||
padding: 60rpx 20rpx;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 10rpx;
|
||||
border: 2rpx dashed #ddd;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
color: #999;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.placeholder-reason {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 原始数据显示 */
|
||||
.data-display {
|
||||
background-color: #f0f0f0;
|
||||
padding: 15rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.data-item {
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.data-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.data-content {
|
||||
background-color: white;
|
||||
padding: 10rpx;
|
||||
border-radius: 5rpx;
|
||||
font-family: monospace;
|
||||
font-size: 24rpx;
|
||||
word-break: break-all;
|
||||
max-height: 200rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 评论组件样式 - 完全复制商品详情页 */
|
||||
.comments-wrap {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-top: 20rpx;
|
||||
border: 3rpx solid #07c160; /* 添加绿色边框以便识别 */
|
||||
}
|
||||
|
||||
.comments-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.comments-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comments-title-label {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comments-title-count {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.comments-rate-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comments-rate-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.comments-rate-num {
|
||||
font-size: 28rpx;
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.comment-item-wrap {
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
padding-bottom: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.comment-item-wrap:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.comment-item-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.comment-item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-item-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.comment-item-rate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment-item-score {
|
||||
font-size: 24rpx;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.comment-item-content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.comment-item-spec {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.comment-item-reply {
|
||||
background-color: #f5f5f5;
|
||||
padding: 12rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.reply-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.comment-item-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.comments-more {
|
||||
text-align: center;
|
||||
padding: 20rpx;
|
||||
color: #07c160;
|
||||
font-size: 28rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
/* 商品ID测试样式 */
|
||||
.test-input-area {
|
||||
background-color: #f9f9f9;
|
||||
padding: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-right: 15rpx;
|
||||
min-width: 180rpx;
|
||||
}
|
||||
|
||||
.test-input {
|
||||
flex: 1;
|
||||
background-color: white;
|
||||
border: 2rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 15rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
width: 100%;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
padding: 15rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.quick-test-area {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.quick-test-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.quick-test-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 6rpx;
|
||||
padding: 10rpx 15rpx;
|
||||
font-size: 24rpx;
|
||||
margin: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.quick-btn:active {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
border-top: 2rpx solid #eee;
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
|
||||
.results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background-color: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6rpx;
|
||||
padding: 8rpx 15rpx;
|
||||
font-size: 24rpx;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
padding: 15rpx;
|
||||
margin-bottom: 10rpx;
|
||||
border-left: 4rpx solid #ddd;
|
||||
}
|
||||
|
||||
.result-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.result-id {
|
||||
font-family: monospace;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.result-status {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.result-status.success {
|
||||
color: #07c160;
|
||||
background-color: #e8f5e8;
|
||||
border-left-color: #07c160;
|
||||
}
|
||||
|
||||
.result-status.error {
|
||||
color: #ff6b6b;
|
||||
background-color: #ffe8e8;
|
||||
border-left-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-family: monospace;
|
||||
font-size: 24rpx;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.result-data {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.result-error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
174
miniprogram/pages/debug/payment-test.js
Normal file
174
miniprogram/pages/debug/payment-test.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import { config } from '../../config/index';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
testResult: ''
|
||||
},
|
||||
|
||||
// 测试正确的支付参数(标准格式)
|
||||
testWechatPayment() {
|
||||
const paymentParams = {
|
||||
timeStamp: String(Math.floor(Date.now() / 1000)),
|
||||
nonceStr: 'test_nonce_str_' + Math.random().toString(36).substr(2, 15),
|
||||
package: 'prepay_id=wx_test_prepay_id_123456789',
|
||||
signType: 'MD5',
|
||||
paySign: 'test_pay_sign_123456789'
|
||||
};
|
||||
|
||||
console.log('测试标准支付参数:', paymentParams);
|
||||
console.log('参数详情:');
|
||||
console.log('- timeStamp:', paymentParams.timeStamp, '(类型:', typeof paymentParams.timeStamp, ')');
|
||||
console.log('- nonceStr:', paymentParams.nonceStr, '(长度:', paymentParams.nonceStr.length, ')');
|
||||
console.log('- package:', paymentParams.package, '(格式检查: prepay_id=前缀)');
|
||||
console.log('- signType:', paymentParams.signType);
|
||||
console.log('- paySign:', paymentParams.paySign, '(长度:', paymentParams.paySign.length, ')');
|
||||
|
||||
wx.requestPayment({
|
||||
...paymentParams,
|
||||
success: (res) => {
|
||||
console.log('支付成功:', res);
|
||||
this.setData({
|
||||
testResult: '支付成功: ' + JSON.stringify(res)
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('支付失败:', err);
|
||||
console.log('错误详情:');
|
||||
console.log('- errMsg:', err.errMsg);
|
||||
console.log('- errCode:', err.errCode);
|
||||
this.setData({
|
||||
testResult: '支付失败: ' + JSON.stringify(err) + '\n错误信息: ' + err.errMsg
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 测试错误的package格式(不含prepay_id前缀)
|
||||
testWrongPackageFormat() {
|
||||
const paymentParams = {
|
||||
timeStamp: String(Math.floor(Date.now() / 1000)),
|
||||
nonceStr: 'test_nonce_str_' + Math.random().toString(36).substr(2, 15),
|
||||
package: 'wx_test_prepay_id_123456789', // 错误格式:缺少prepay_id=前缀
|
||||
signType: 'MD5',
|
||||
paySign: 'test_pay_sign_123456789'
|
||||
};
|
||||
|
||||
console.log('测试错误package格式:', paymentParams);
|
||||
|
||||
wx.requestPayment({
|
||||
...paymentParams,
|
||||
success: (res) => {
|
||||
console.log('支付成功:', res);
|
||||
this.setData({
|
||||
testResult: '支付成功: ' + JSON.stringify(res)
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('支付失败:', err);
|
||||
this.setData({
|
||||
testResult: '支付失败: ' + JSON.stringify(err)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 测试空的package参数
|
||||
testEmptyPackage() {
|
||||
const paymentParams = {
|
||||
timeStamp: String(Math.floor(Date.now() / 1000)),
|
||||
nonceStr: 'test_nonce_str_' + Math.random().toString(36).substr(2, 15),
|
||||
package: '', // 空的package
|
||||
signType: 'MD5',
|
||||
paySign: 'test_pay_sign_123456789'
|
||||
};
|
||||
|
||||
console.log('测试空package参数:', paymentParams);
|
||||
|
||||
wx.requestPayment({
|
||||
...paymentParams,
|
||||
success: (res) => {
|
||||
console.log('支付成功:', res);
|
||||
this.setData({
|
||||
testResult: '支付成功: ' + JSON.stringify(res)
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('支付失败:', err);
|
||||
this.setData({
|
||||
testResult: '支付失败: ' + JSON.stringify(err)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 测试真实的后端API调用
|
||||
testRealPaymentAPI() {
|
||||
console.log('开始测试真实后端API...');
|
||||
wx.request({
|
||||
url: `${config.apiBase}/payment/create`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
amount: 1,
|
||||
description: '测试支付'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('后端API返回:', res.data);
|
||||
if (res.data.code === 200 && res.data.data) {
|
||||
const paymentData = res.data.data;
|
||||
console.log('后端返回的支付参数:', paymentData);
|
||||
console.log('参数验证:');
|
||||
console.log('- timeStamp:', paymentData.timeStamp, '(类型:', typeof paymentData.timeStamp, ')');
|
||||
console.log('- nonceStr:', paymentData.nonceStr, '(长度:', paymentData.nonceStr ? paymentData.nonceStr.length : 'undefined', ')');
|
||||
console.log('- package:', paymentData.package, '(格式:', paymentData.package ? (paymentData.package.startsWith('prepay_id=') ? '正确' : '错误') : 'undefined', ')');
|
||||
console.log('- signType:', paymentData.signType);
|
||||
console.log('- paySign:', paymentData.paySign, '(长度:', paymentData.paySign ? paymentData.paySign.length : 'undefined', ')');
|
||||
|
||||
// 检查是否包含total_fee参数
|
||||
if (paymentData.total_fee !== undefined) {
|
||||
console.warn('⚠️ 发现total_fee参数:', paymentData.total_fee);
|
||||
}
|
||||
|
||||
wx.requestPayment({
|
||||
...paymentData,
|
||||
success: (payRes) => {
|
||||
console.log('支付成功:', payRes);
|
||||
this.setData({
|
||||
testResult: '真实API支付成功: ' + JSON.stringify(payRes)
|
||||
});
|
||||
},
|
||||
fail: (payErr) => {
|
||||
console.log('支付失败:', payErr);
|
||||
console.log('错误详情:');
|
||||
console.log('- errMsg:', payErr.errMsg);
|
||||
console.log('- errCode:', payErr.errCode);
|
||||
this.setData({
|
||||
testResult: '真实API支付失败: ' + JSON.stringify(payErr) + '\n错误信息: ' + payErr.errMsg
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('API返回错误:', res.data);
|
||||
this.setData({
|
||||
testResult: 'API调用失败: ' + JSON.stringify(res.data)
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('API请求失败:', err);
|
||||
this.setData({
|
||||
testResult: 'API请求失败: ' + JSON.stringify(err)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 清除测试结果
|
||||
clearResult() {
|
||||
this.setData({
|
||||
testResult: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
3
miniprogram/pages/debug/payment-test.json
Normal file
3
miniprogram/pages/debug/payment-test.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
35
miniprogram/pages/debug/payment-test.wxml
Normal file
35
miniprogram/pages/debug/payment-test.wxml
Normal file
@@ -0,0 +1,35 @@
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">微信支付测试页面</text>
|
||||
<text class="subtitle">用于诊断total_fee参数问题</text>
|
||||
</view>
|
||||
|
||||
<view class="test-section">
|
||||
<view class="section-title">测试用例</view>
|
||||
|
||||
<button class="test-btn" bindtap="testWechatPayment">
|
||||
测试标准支付参数
|
||||
</button>
|
||||
|
||||
<button class="test-btn warning" bindtap="testWrongPackageFormat">
|
||||
测试错误package格式
|
||||
</button>
|
||||
|
||||
<button class="test-btn error" bindtap="testEmptyPackage">
|
||||
测试空package参数
|
||||
</button>
|
||||
|
||||
<button class="test-btn primary" bindtap="testRealPaymentAPI">
|
||||
测试真实后端API
|
||||
</button>
|
||||
|
||||
<button class="test-btn secondary" bindtap="clearResult">
|
||||
清除结果
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="result-section" wx:if="{{testResult}}">
|
||||
<view class="section-title">测试结果</view>
|
||||
<view class="result-content">{{testResult}}</view>
|
||||
</view>
|
||||
</view>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user