6 changed files with 1066 additions and 5 deletions
@ -0,0 +1,366 @@ |
|||||
|
// pages/favorites/index.js
|
||||
|
const API = require('../../utils/api.js'); |
||||
|
|
||||
|
Page({ |
||||
|
|
||||
|
/** |
||||
|
* 页面的初始数据 |
||||
|
*/ |
||||
|
data: { |
||||
|
favoritesList: [], |
||||
|
loading: true, |
||||
|
hasFavorites: false, |
||||
|
|
||||
|
// 图片预览相关状态
|
||||
|
showImagePreview: false, // 控制图片预览弹窗显示
|
||||
|
previewImageUrls: [], // 预览的图片URL列表
|
||||
|
previewImageIndex: 0, // 当前预览图片的索引
|
||||
|
|
||||
|
// 图片缩放相关状态
|
||||
|
scale: 1, // 当前缩放比例
|
||||
|
lastScale: 1, // 上一次缩放比例
|
||||
|
startDistance: 0, // 双指起始距离
|
||||
|
doubleTapTimer: null, // 双击计时器
|
||||
|
lastTapTime: 0, // 上一次单击时间
|
||||
|
isScaling: false, // 是否正在缩放中
|
||||
|
offsetX: 0, // X轴偏移量
|
||||
|
offsetY: 0, // Y轴偏移量
|
||||
|
initialTouch: null // 初始触摸点
|
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 生命周期函数--监听页面加载 |
||||
|
*/ |
||||
|
onLoad(options) { |
||||
|
this.loadFavorites(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 生命周期函数--监听页面初次渲染完成 |
||||
|
*/ |
||||
|
onReady() { |
||||
|
|
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 生命周期函数--监听页面显示 |
||||
|
*/ |
||||
|
onShow() { |
||||
|
// 每次显示页面时重新加载收藏列表
|
||||
|
this.loadFavorites(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 生命周期函数--监听页面隐藏 |
||||
|
*/ |
||||
|
onHide() { |
||||
|
|
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 生命周期函数--监听页面卸载 |
||||
|
*/ |
||||
|
onUnload() { |
||||
|
|
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 页面相关事件处理函数--监听用户下拉动作 |
||||
|
*/ |
||||
|
onPullDownRefresh() { |
||||
|
// 下拉刷新时重新加载收藏列表
|
||||
|
this.loadFavorites(true); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 页面上拉触底事件的处理函数 |
||||
|
*/ |
||||
|
onReachBottom() { |
||||
|
|
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 用户点击右上角分享 |
||||
|
*/ |
||||
|
onShareAppMessage() { |
||||
|
|
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 加载收藏列表 |
||||
|
*/ |
||||
|
loadFavorites: function (isPullDown = false) { |
||||
|
this.setData({ |
||||
|
loading: true |
||||
|
}); |
||||
|
|
||||
|
// 获取手机号码
|
||||
|
const phoneNumber = wx.getStorageSync('phoneNumber') || '18482694520'; // 默认使用测试手机号
|
||||
|
|
||||
|
if (!phoneNumber) { |
||||
|
// 用户未登录,显示提示
|
||||
|
wx.showToast({ |
||||
|
title: '请先登录', |
||||
|
icon: 'none' |
||||
|
}); |
||||
|
this.setData({ |
||||
|
loading: false, |
||||
|
hasFavorites: false |
||||
|
}); |
||||
|
if (isPullDown) { |
||||
|
wx.stopPullDownRefresh(); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
API.getFavorites(phoneNumber).then(res => { |
||||
|
console.log('获取收藏列表成功:', res); |
||||
|
const favorites = res.data && res.data.favorites ? res.data.favorites : []; |
||||
|
this.setData({ |
||||
|
favoritesList: favorites, |
||||
|
hasFavorites: favorites.length > 0, |
||||
|
loading: false |
||||
|
}); |
||||
|
}).catch(err => { |
||||
|
console.error('获取收藏列表失败:', err); |
||||
|
wx.showToast({ |
||||
|
title: '获取收藏列表失败', |
||||
|
icon: 'none' |
||||
|
}); |
||||
|
this.setData({ |
||||
|
loading: false, |
||||
|
hasFavorites: false |
||||
|
}); |
||||
|
}).finally(() => { |
||||
|
// 停止下拉刷新
|
||||
|
if (isPullDown) { |
||||
|
wx.stopPullDownRefresh(); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 取消收藏 |
||||
|
*/ |
||||
|
cancelFavorite: function (e) { |
||||
|
const productId = e.currentTarget.dataset.productid; |
||||
|
wx.showLoading({ |
||||
|
title: '正在取消收藏', |
||||
|
mask: true |
||||
|
}); |
||||
|
|
||||
|
API.cancelFavorite(productId).then(res => { |
||||
|
console.log('取消收藏成功:', res); |
||||
|
// 从收藏列表中移除该商品
|
||||
|
const updatedList = this.data.favoritesList.filter(item => item.productId !== productId); |
||||
|
this.setData({ |
||||
|
favoritesList: updatedList, |
||||
|
hasFavorites: updatedList.length > 0 |
||||
|
}); |
||||
|
wx.showToast({ |
||||
|
title: '取消收藏成功', |
||||
|
icon: 'success' |
||||
|
}); |
||||
|
}).catch(err => { |
||||
|
console.error('取消收藏失败:', err); |
||||
|
wx.showToast({ |
||||
|
title: '取消收藏失败', |
||||
|
icon: 'none' |
||||
|
}); |
||||
|
}).finally(() => { |
||||
|
wx.hideLoading(); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 跳转到商品详情页 |
||||
|
*/ |
||||
|
goToGoodsDetail: function (e) { |
||||
|
const productId = e.currentTarget.dataset.productid; |
||||
|
wx.navigateTo({ |
||||
|
url: '/pages/goods-detail/goods-detail?productId=' + productId |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 轮播图切换
|
||||
|
swiperChange(e) { |
||||
|
const current = e.detail.current; |
||||
|
const itemId = e.currentTarget.dataset.itemId; |
||||
|
|
||||
|
// 更新对应商品项的currentImageIndex
|
||||
|
this.setData({ |
||||
|
[`favoritesList[${itemId}].currentImageIndex`]: current |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 预览图片
|
||||
|
previewImage(e) { |
||||
|
// 登录验证
|
||||
|
const userInfo = wx.getStorageSync('userInfo') || null; |
||||
|
const userId = wx.getStorageSync('userId') || null; |
||||
|
if (!userInfo || !userId) { |
||||
|
// 未登录,显示授权登录弹窗
|
||||
|
this.setData({ |
||||
|
showAuthModal: true, |
||||
|
pendingUserType: 'buyer' |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 已登录,执行图片预览
|
||||
|
const { urls, index } = e.currentTarget.dataset; |
||||
|
this.setData({ |
||||
|
showImagePreview: true, |
||||
|
previewImageUrls: urls, |
||||
|
previewImageIndex: parseInt(index) |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 关闭图片预览
|
||||
|
closeImagePreview() { |
||||
|
this.setData({ |
||||
|
showImagePreview: false |
||||
|
}); |
||||
|
this.resetZoom(); |
||||
|
}, |
||||
|
|
||||
|
// 重置缩放状态
|
||||
|
resetZoom() { |
||||
|
this.setData({ |
||||
|
scale: 1, |
||||
|
lastScale: 1, |
||||
|
offsetX: 0, |
||||
|
offsetY: 0, |
||||
|
initialTouch: null |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 处理图片点击事件(单击/双击判断)
|
||||
|
handleImageTap(e) { |
||||
|
const currentTime = Date.now(); |
||||
|
const lastTapTime = this.data.lastTapTime; |
||||
|
|
||||
|
// 判断是否为双击(300ms内连续点击)
|
||||
|
if (currentTime - lastTapTime < 300) { |
||||
|
// 双击事件
|
||||
|
if (this.data.doubleTapTimer) { |
||||
|
clearTimeout(this.data.doubleTapTimer); |
||||
|
} |
||||
|
|
||||
|
// 切换放大/缩小状态
|
||||
|
const newScale = this.data.scale === 1 ? 2 : 1; |
||||
|
this.setData({ |
||||
|
scale: newScale, |
||||
|
lastScale: newScale, |
||||
|
offsetX: 0, |
||||
|
offsetY: 0, |
||||
|
lastTapTime: 0 // 重置双击状态
|
||||
|
}); |
||||
|
} else { |
||||
|
// 单击事件,设置延迟来检测是否会成为双击
|
||||
|
if (this.data.doubleTapTimer) { |
||||
|
clearTimeout(this.data.doubleTapTimer); |
||||
|
} |
||||
|
|
||||
|
this.setData({ |
||||
|
lastTapTime: currentTime, |
||||
|
doubleTapTimer: setTimeout(() => { |
||||
|
// 确认是单击,关闭图片预览
|
||||
|
this.closeImagePreview(); |
||||
|
}, 300) |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 处理触摸开始事件
|
||||
|
handleTouchStart(e) { |
||||
|
const touches = e.touches; |
||||
|
|
||||
|
if (touches.length === 1) { |
||||
|
// 单指:准备拖动
|
||||
|
this.setData({ |
||||
|
initialTouch: { |
||||
|
x: touches[0].clientX, |
||||
|
y: touches[0].clientY |
||||
|
} |
||||
|
}); |
||||
|
} else if (touches.length === 2) { |
||||
|
// 双指:记录起始距离,准备缩放
|
||||
|
const distance = this.calculateDistance(touches[0], touches[1]); |
||||
|
this.setData({ |
||||
|
startDistance: distance, |
||||
|
isScaling: true, |
||||
|
lastScale: this.data.scale |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 处理触摸移动事件
|
||||
|
handleTouchMove(e) { |
||||
|
const touches = e.touches; |
||||
|
|
||||
|
if (touches.length === 1 && this.data.initialTouch && this.data.scale !== 1) { |
||||
|
// 单指拖动(只有在缩放状态下才允许拖动)
|
||||
|
const deltaX = touches[0].clientX - this.data.initialTouch.x; |
||||
|
const deltaY = touches[0].clientY - this.data.initialTouch.y; |
||||
|
|
||||
|
// 计算新的偏移量
|
||||
|
let newOffsetX = this.data.offsetX + deltaX; |
||||
|
let newOffsetY = this.data.offsetY + deltaY; |
||||
|
|
||||
|
// 边界限制
|
||||
|
const windowWidth = wx.getSystemInfoSync().windowWidth; |
||||
|
const windowHeight = wx.getSystemInfoSync().windowHeight; |
||||
|
const maxOffsetX = (windowWidth * (this.data.scale - 1)) / 2; |
||||
|
const maxOffsetY = (windowHeight * (this.data.scale - 1)) / 2; |
||||
|
|
||||
|
newOffsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, newOffsetX)); |
||||
|
newOffsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, newOffsetY)); |
||||
|
|
||||
|
this.setData({ |
||||
|
offsetX: newOffsetX, |
||||
|
offsetY: newOffsetY, |
||||
|
initialTouch: { |
||||
|
x: touches[0].clientX, |
||||
|
y: touches[0].clientY |
||||
|
} |
||||
|
}); |
||||
|
} else if (touches.length === 2) { |
||||
|
// 双指缩放
|
||||
|
const currentDistance = this.calculateDistance(touches[0], touches[1]); |
||||
|
const scale = (currentDistance / this.data.startDistance) * this.data.lastScale; |
||||
|
|
||||
|
// 限制缩放范围在0.5倍到3倍之间
|
||||
|
const newScale = Math.max(0.5, Math.min(3, scale)); |
||||
|
|
||||
|
this.setData({ |
||||
|
scale: newScale, |
||||
|
isScaling: true |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 处理触摸结束事件
|
||||
|
handleTouchEnd(e) { |
||||
|
this.setData({ |
||||
|
isScaling: false, |
||||
|
lastScale: this.data.scale, |
||||
|
initialTouch: null |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 计算两点之间的距离
|
||||
|
calculateDistance(touch1, touch2) { |
||||
|
const dx = touch2.clientX - touch1.clientX; |
||||
|
const dy = touch2.clientY - touch1.clientY; |
||||
|
return Math.sqrt(dx * dx + dy * dy); |
||||
|
}, |
||||
|
|
||||
|
// 切换预览图片
|
||||
|
onPreviewImageChange(e) { |
||||
|
this.setData({ |
||||
|
previewImageIndex: e.detail.current |
||||
|
}); |
||||
|
this.resetZoom(); |
||||
|
} |
||||
|
}) |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"usingComponents": {}, |
||||
|
"navigationBarTitleText": "我的收藏" |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
<!--pages/favorites/index.wxml--> |
||||
|
<view class="container" style="align-items: flex-start; padding: 20rpx; width: 100%; max-width: 100vw; overflow-x: hidden; position: relative; box-sizing: border-box;"> |
||||
|
<!-- 加载状态 --> |
||||
|
<view wx:if="{{loading}}" class="loading-container"> |
||||
|
<loading> |
||||
|
加载中... |
||||
|
</loading> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 空状态 --> |
||||
|
<view wx:elif="{{!hasFavorites}}" class="empty-container"> |
||||
|
<view class="empty-icon">💔</view> |
||||
|
<text class="empty-text">您还没有收藏任何商品</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 收藏列表 --> |
||||
|
<view wx:else class="goods-list" style="width: 100%; display: flex; flex-direction: column; align-items: flex-start; min-height: 400rpx; margin-top: 120rpx;"> |
||||
|
<view wx:for="{{favoritesList}}" wx:key="productId" class="card" style="width: 100%; margin-top: 0; margin-bottom: 20rpx;"> |
||||
|
<!-- 图片和信息2:3比例并排显示,整体高度固定 --> |
||||
|
<view style="display: flex; width: 100%; height: 200rpx; border-radius: 8rpx; overflow: hidden; background-color: #f5f5f5;"> |
||||
|
<!-- 左侧图片区域 40%宽度(2/5),高度固定 --> |
||||
|
<view style="width: 40%; position: relative; height: 200rpx;"> |
||||
|
<!-- 第一张图片 --> |
||||
|
<view wx:if="{{item.Product && item.Product.imageUrls && item.Product.imageUrls.length > 0}}" style="width: 100%; height: 100%;"> |
||||
|
<image src="{{item.Product.imageUrls[0]}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.Product.imageUrls}}" data-index="0"></image> |
||||
|
</view> |
||||
|
<view wx:else style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999;"> |
||||
|
<text>暂无图片</text> |
||||
|
</view> |
||||
|
<!-- 剩余图片可滑动区域 --> |
||||
|
<view wx:if="{{item.Product && item.Product.imageUrls && item.Product.imageUrls.length > 0}}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"> |
||||
|
<swiper |
||||
|
class="image-swiper" |
||||
|
style="width: 100%; height: 100%;" |
||||
|
current="{{item.currentImageIndex || 0}}" |
||||
|
bindchange="swiperChange" |
||||
|
data-item-id="{{index}}"> |
||||
|
<block wx:for="{{item.Product.imageUrls}}" wx:for-item="img" wx:for-index="idx" wx:key="idx"> |
||||
|
<swiper-item> |
||||
|
<image src="{{img}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.Product.imageUrls}}" data-index="{{idx}}"></image> |
||||
|
</swiper-item> |
||||
|
</block> |
||||
|
</swiper> |
||||
|
<!-- 显示页码指示器 --> |
||||
|
<view style="position: absolute; bottom: 10rpx; right: 10rpx; background-color: rgba(0,0,0,0.5); color: white; padding: 5rpx 10rpx; border-radius: 15rpx; font-size: 20rpx;"> |
||||
|
{{(item.currentImageIndex || 0) + 1}}/{{item.Product.imageUrls.length}} |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 右侧信息区域 60%宽度(3/5),相应调整 --> |
||||
|
<view style="width: 60%; display: flex; flex-direction: column; background-color: white; border-left: 1rpx solid #f0f0f0;"> |
||||
|
<!-- 上半部分商品信息区域(60%高度),可点击查看详情 --> |
||||
|
<view style="flex: 0.6; padding: 0rpx 15rpx 15rpx 15rpx; cursor: pointer;" bindtap="goToGoodsDetail" data-productid="{{item.productId}}"> |
||||
|
<view> |
||||
|
<view style="margin-bottom: 15rpx; margin-top: -5rpx;"> |
||||
|
<view style="display: inline-block; margin-right: 10rpx; font-size: 18rpx; color: #fff; background-color: #DAA520; padding: 2rpx 8rpx; border-radius: 10rpx; vertical-align: middle;">金标蛋</view> |
||||
|
<span style="vertical-align: middle; font-size: 36rpx; font-weight: bold;">{{item.Product.productName || '未命名商品'}}</span> |
||||
|
</view> |
||||
|
<view style="font-size: 28rpx; font-weight: bold; margin-top: 30rpx;"> |
||||
|
{{(item.Product.spec && item.Product.spec !== '无') ? item.Product.spec : (item.Product.specification && item.Product.specification !== '无') ? item.Product.specification : '无'}} | {{item.Product.yolk || '无'}} | {{item.Product.minOrder || item.Product.quantity || 1}}件 |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 下半部分按钮区域(40%高度) --> |
||||
|
<view style="flex: 0.4; display: flex; justify-content: space-between; align-items: center; padding: 0 20rpx;"> |
||||
|
<!-- 价格显示 --> |
||||
|
<text style="color: #52c41a; font-size: 28rpx; font-weight: bold;">¥{{item.Product.price || 0}}</text> |
||||
|
<!-- 取消收藏按钮 --> |
||||
|
<button |
||||
|
style="background-color: #91c41aff; color: white; font-size: 24rpx; font-weight: bold; width: 150rpx; height: 60rpx; border-radius: 999rpx; display: flex; justify-content: center; align-items: center; padding: 0; border: none; margin: 0; background: none; background-color: #91c41aff;" |
||||
|
bindtap="cancelFavorite" |
||||
|
data-productid="{{item.productId}}" |
||||
|
> |
||||
|
取消收藏 |
||||
|
</button> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 图片预览弹窗 --> |
||||
|
<view class="image-preview-mask" wx:if="{{showImagePreview}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; z-index: 10001;" catchtouchmove="true"> |
||||
|
<view style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;"> |
||||
|
<swiper |
||||
|
style="width: 100%; height: 100%;" |
||||
|
current="{{previewImageIndex}}" |
||||
|
bindchange="onPreviewImageChange" |
||||
|
indicator-dots="true" |
||||
|
indicator-color="rgba(255,255,255,0.5)" |
||||
|
indicator-active-color="#fff"> |
||||
|
<block wx:for="{{previewImageUrls}}" wx:key="*this"> |
||||
|
<swiper-item> |
||||
|
<image |
||||
|
src="{{item}}" |
||||
|
mode="aspectFit" |
||||
|
style="width: 100%; height: 100%; transform: scale({{scale}}) translate({{offsetX}}px, {{offsetY}}px); transition: {{isScaling ? 'none' : 'transform 0.3s'}};" |
||||
|
bindtap="handleImageTap" |
||||
|
bindtouchstart="handleTouchStart" |
||||
|
bindtouchmove="handleTouchMove" |
||||
|
bindtouchend="handleTouchEnd" |
||||
|
bindtouchcancel="handleTouchEnd" |
||||
|
></image> |
||||
|
</swiper-item> |
||||
|
</block> |
||||
|
</swiper> |
||||
|
<view style="position: absolute; top: 40rpx; right: 40rpx; color: white; font-size: 40rpx;"> |
||||
|
<text bindtap="closeImagePreview" style="background: rgba(0,0,0,0.5); padding: 10rpx 20rpx; border-radius: 50%;">×</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
@ -0,0 +1,36 @@ |
|||||
|
/* pages/favorites/index.wxss */ |
||||
|
.container { |
||||
|
min-height: 100vh; |
||||
|
background-color: #f5f5f5; |
||||
|
} |
||||
|
|
||||
|
.loading-container { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
height: 50vh; |
||||
|
} |
||||
|
|
||||
|
.empty-container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
height: 50vh; |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
.empty-icon { |
||||
|
font-size: 100rpx; |
||||
|
margin-bottom: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.empty-text { |
||||
|
font-size: 28rpx; |
||||
|
} |
||||
|
|
||||
|
/* 图片轮播样式 */ |
||||
|
.image-swiper { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
Loading…
Reference in new issue