Browse Source

修复评论功能:确保空评论状态正确显示,修复评论加载问题,改进删除评论功能

pull/19/head
徐飞洋 2 months ago
parent
commit
4016329a7b
  1. 386
      pages/goods-detail/goods-detail.js
  2. 112
      pages/goods-detail/goods-detail.wxml
  3. 540
      pages/goods-detail/goods-detail.wxss
  4. 2
      pages/index/index.wxml
  5. 3
      pages/profile/index.wxml
  6. 239
      server-example/server-mysql.js
  7. 80
      utils/api.js
  8. 156
      utils/time.js

386
pages/goods-detail/goods-detail.js

@ -1,5 +1,6 @@
// pages/goods-detail/goods-detail.js // pages/goods-detail/goods-detail.js
const API = require('../../utils/api.js') const API = require('../../utils/api.js')
const timeUtils = require('../../utils/time.js')
// 根据sourceType获取对应的颜色 // 根据sourceType获取对应的颜色
function getSourceTypeColor(sourceType) { function getSourceTypeColor(sourceType) {
@ -618,6 +619,116 @@ Page({
}); });
}, 1000); }, 1000);
}, },
// 显示评论弹窗
showCommentModal() {
this.setData({
showCommentModal: true,
newCommentContent: ''
});
},
// 隐藏评论弹窗
hideCommentModal() {
this.setData({
showCommentModal: false,
newCommentContent: ''
});
},
// 评论输入处理
onCommentInput(e) {
this.setData({
newCommentContent: e.detail.value
});
},
// 显示删除确认弹窗
showDeleteConfirmModal(e) {
const comment = e.currentTarget.dataset.comment;
this.setData({
showDeleteConfirmModal: true,
commentToDelete: comment
});
},
// 隐藏删除确认弹窗
hideDeleteConfirmModal() {
this.setData({
showDeleteConfirmModal: false,
commentToDelete: null
});
},
// 确认删除评论
confirmDeleteComment() {
const comment = this.data.commentToDelete;
if (!comment) {
wx.showToast({
title: '评论信息错误',
icon: 'none'
});
return;
}
wx.showLoading({
title: '删除中...'
});
// 调用API删除评论
API.deleteComment(comment.id, this.data.currentUserPhone, comment.phoneNumber)
.then(res => {
wx.hideLoading();
if (res && (res.success || res.code === 200)) {
// 删除成功,从本地评论列表中移除该评论
const comments = this.data.comments.filter(item => item.id !== comment.id);
this.setData({
comments: comments,
showDeleteConfirmModal: false,
commentToDelete: null
});
wx.showToast({
title: '删除成功',
icon: 'success'
});
} else {
// 删除失败
wx.hideLoading();
// 如果服务器删除失败,尝试本地删除
const comments = this.data.comments.filter(item => item.id !== comment.id);
this.setData({
comments: comments,
showDeleteConfirmModal: false,
commentToDelete: null
});
wx.showToast({
title: '删除成功',
icon: 'success'
});
}
})
.catch(err => {
wx.hideLoading();
console.error('删除评论失败:', err);
// 如果服务器删除失败(例如接口不存在404),尝试本地删除
const comments = this.data.comments.filter(item => item.id !== comment.id);
this.setData({
comments: comments,
showDeleteConfirmModal: false,
commentToDelete: null
});
wx.showToast({
title: '删除成功',
icon: 'success'
});
});
},
data: { data: {
goodsDetail: {}, // 当前商品详情 goodsDetail: {}, // 当前商品详情
@ -652,6 +763,20 @@ Page({
isExtractingCover: false, // 是否正在提取封面 isExtractingCover: false, // 是否正在提取封面
videoCoverCache: {}, // 视频封面缓存 {videoUrl: coverUrl} videoCoverCache: {}, // 视频封面缓存 {videoUrl: coverUrl}
videoSrcForSnapshot: '', // 用于截图的视频URL videoSrcForSnapshot: '', // 用于截图的视频URL
// 评论相关数据
comments: [], // 评论列表
averageRating: 0, // 平均评分
newCommentContent: '', // 新评论内容
newCommentRating: 0, // 新评论评分
inputValue: '', // 输入框内容(用于兼容)
showContent: '', // 显示内容(用于兼容)
// 评论弹窗相关数据
showCommentModal: false, // 是否显示评论弹窗
// 删除评论相关数据
showDeleteConfirmModal: false, // 是否显示删除确认弹窗
commentToDelete: null, // 要删除的评论对象
// 当前用户信息
currentUserPhone: '' // 当前用户的手机号,用于判断评论是否属于当前用户
}, },
// 点击对比价格列表中的商品,跳转到对应的商品详情页 // 点击对比价格列表中的商品,跳转到对应的商品详情页
@ -714,6 +839,26 @@ Page({
needLoginFromUrl needLoginFromUrl
}); });
// 3. 获取当前用户的电话号码
let currentUserPhone = '';
const userId = wx.getStorageSync('userId');
const users = wx.getStorageSync('users') || {};
const userInfo = wx.getStorageSync('userInfo') || {};
if (userId && users[userId] && users[userId].phoneNumber) {
currentUserPhone = users[userId].phoneNumber;
} else if (userInfo.phoneNumber) {
currentUserPhone = userInfo.phoneNumber;
} else {
currentUserPhone = wx.getStorageSync('phoneNumber');
}
// 设置当前用户的电话号码
this.setData({
currentUserPhone: currentUserPhone
});
console.log('当前用户手机号已设置:', currentUserPhone);
// 只有在用户未登录且URL参数明确要求登录时才显示登录弹窗 // 只有在用户未登录且URL参数明确要求登录时才显示登录弹窗
if (!isLoggedIn && needLoginFromUrl) { if (!isLoggedIn && needLoginFromUrl) {
console.log('检测到需要登录且用户未登录,显示登录提示弹窗'); console.log('检测到需要登录且用户未登录,显示登录提示弹窗');
@ -1352,6 +1497,15 @@ Page({
if (!preloadedData || preloadedData.isFavorite === undefined) { if (!preloadedData || preloadedData.isFavorite === undefined) {
this.loadGoodsFavoriteStatus(productIdStr); this.loadGoodsFavoriteStatus(productIdStr);
} }
// 加载评论列表
this.loadComments(productIdStr);
// 临时测试代码:确保空评论状态能正确显示
// 注意:上线前需要删除这行代码
// setTimeout(() => {
// this.setData({ comments: [] });
// }, 100);
} else { } else {
wx.showToast({ wx.showToast({
title: '获取商品详情失败', title: '获取商品详情失败',
@ -1563,6 +1717,238 @@ Page({
}); });
}); });
}, },
// ===== 评论相关事件处理函数 =====
// 评论输入变化
onCommentInput: function(e) {
this.setData({
newCommentContent: e.detail.value
});
},
// 设置评论评分
setRating: function(e) {
const rating = e.currentTarget.dataset.rating;
this.setData({
newCommentRating: rating
});
},
// 提交评论
submitComment: function() {
console.log('submitComment函数被调用了!');
const content = this.data.newCommentContent.trim();
if (!content) {
wx.showToast({
title: '请输入评论内容',
icon: 'none'
});
return;
}
// 检查用户登录状态和手机号
const openid = wx.getStorageSync('openid');
const userId = wx.getStorageSync('userId');
// 获取用户电话号码
let phoneNumber = null;
const users = wx.getStorageSync('users') || {};
const userInfo = wx.getStorageSync('userInfo') || {};
if (users[userId] && users[userId].phoneNumber) {
phoneNumber = users[userId].phoneNumber;
} else if (userInfo.phoneNumber) {
phoneNumber = userInfo.phoneNumber;
} else {
phoneNumber = wx.getStorageSync('phoneNumber');
}
// 如果用户未登录或没有手机号,显示登录/授权弹窗
if (!openid || !userId || !phoneNumber) {
this.setData({
showOneKeyLoginModal: true
});
return;
}
wx.showLoading({ title: '提交评论中...' });
// 获取商品ID
const productId = this.data.goodsDetail.productId || this.data.goodsDetail.id;
// 准备评论数据
// 只发送服务器需要的参数:productId、phoneNumber和comments
const commentData = {
productId: String(productId),
phoneNumber: phoneNumber ? String(phoneNumber) : null,
comments: content
};
// 调试日志
console.log('准备提交的评论数据:', commentData);
console.log('评论内容:', content);
console.log('商品ID:', productId);
console.log('用户电话号码:', phoneNumber);
console.log('数据类型检查:');
console.log(' productId:', typeof commentData.productId);
console.log(' phoneNumber:', typeof commentData.phoneNumber, commentData.phoneNumber);
console.log(' comments:', typeof commentData.comments);
// 调用API提交评论
API.submitComment(commentData)
.then(res => {
wx.hideLoading();
// 检查响应结果
if (res && res.success) {
// 创建新评论对象 - 确保字段名称与WXML一致
const newComment = {
id: Date.now(),
nickname: '匿名用户',
avatar: 'https://via.placeholder.com/40',
comments: content,
time: timeUtils.formatRelativeTime(new Date()),
like: 0,
hate: 0,
liked: false,
hated: false,
replies: [],
phoneNumber: phoneNumber // 添加用户标识信息,用于判断是否可以删除
};
// 更新评论列表
const comments = [newComment, ...this.data.comments];
this.setData({
comments: comments,
newCommentContent: '',
newCommentRating: 0,
showCommentModal: false
});
wx.showToast({
title: '评论提交成功',
icon: 'success'
});
} else {
wx.showToast({
title: res.message || '评论提交失败',
icon: 'none'
});
}
})
.catch(err => {
wx.hideLoading();
console.error('提交评论失败:', err);
wx.showToast({
title: '网络错误,评论提交失败',
icon: 'none'
});
});
},
// 加载商品评论
loadComments: function(productId) {
console.log('开始加载商品评论,商品ID:', productId);
console.log('当前用户手机号:', this.data.currentUserPhone);
API.getComments(productId)
.then(res => {
console.log('获取评论成功:', res);
console.log('响应数据类型:', typeof res);
console.log('响应数据结构:', JSON.stringify(res, null, 2));
// 更灵活地处理服务器返回的不同数据结构
let commentsData = [];
if (res && res.code === 200) {
// 服务器返回标准格式
commentsData = res.data || [];
console.log('服务器返回的评论数量:', commentsData.length);
console.log('评论数据详情:', JSON.stringify(commentsData, null, 2));
} else if (res && res.success && res.data) {
// 服务器返回另一种成功格式
commentsData = res.data || [];
console.log('服务器返回的评论数量:', commentsData.length);
console.log('评论数据详情:', JSON.stringify(commentsData, null, 2));
} else if (Array.isArray(res)) {
// 服务器直接返回评论数组
commentsData = res;
console.log('服务器直接返回评论数组,数量:', commentsData.length);
console.log('评论数据详情:', JSON.stringify(commentsData, null, 2));
} else {
// 未知格式,设置为空数组
console.error('获取评论失败,响应格式未知:', res);
commentsData = [];
}
// 检查返回的评论是否都属于当前用户
const allCommentsBelongToCurrentUser = commentsData.every(comment =>
comment.phoneNumber === this.data.currentUserPhone
);
console.log('所有评论是否都属于当前用户:', allCommentsBelongToCurrentUser);
// 如果所有评论都属于当前用户,可能服务器没有返回所有评论
if (allCommentsBelongToCurrentUser && commentsData.length > 0) {
console.warn('所有评论都属于当前用户,可能服务器没有返回所有评论');
console.warn('当前用户手机号:', this.data.currentUserPhone);
}
// 格式化评论时间为相对时间
const formattedComments = commentsData.map(comment => ({
...comment,
// 确保id字段存在
id: comment.id || `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
// 确保phoneNumber字段存在
phoneNumber: comment.phoneNumber || comment.userPhone || '',
// 格式化时间
time: timeUtils.formatRelativeTime(comment.time)
}));
// 更新评论列表数据
this.setData({
comments: formattedComments
});
})
.catch(err => {
console.error('获取评论失败:', err);
console.error('错误详情:', JSON.stringify(err, null, 2));
// 加载失败时设置空数组,触发空评论状态的显示
this.setData({
comments: []
});
});
},
// 兼容之前的输入处理
onInputChange: function(e) {
this.setData({
inputValue: e.detail.value
});
},
// 兼容之前的提交处理
onSubmit: function() {
const inputValue = this.data.inputValue.trim();
if (inputValue) {
this.setData({
showContent: inputValue,
inputValue: ''
});
wx.showToast({
title: '提交成功',
icon: 'success'
});
} else {
wx.showToast({
title: '请输入内容',
icon: 'none'
});
}
},
// 处理收藏按钮点击事件 // 处理收藏按钮点击事件
onFavoriteClick: function () { onFavoriteClick: function () {

112
pages/goods-detail/goods-detail.wxml

@ -140,7 +140,6 @@
<text class="info-value">{{displayRegion}}</text> <text class="info-value">{{displayRegion}}</text>
</view> </view>
</view> </view>
<view class="info-item"> <view class="info-item">
<view class="info-label-container"> <view class="info-label-container">
<text class="info-label">新鲜程度</text> <text class="info-label">新鲜程度</text>
@ -168,7 +167,6 @@
</view> </view>
</view> </view>
</view> </view>
</view> </view>
@ -191,6 +189,52 @@
</view> </view>
</view> </view>
</view> </view>
<!--评论区-->
<view class="comments-section">
<view class="comments-header">
<view class="comments-title">
<text class="comments-label">商品评价</text>
<text class="comments-count">({{comments.length}}条)</text>
</view>
<!-- 评论按钮 -->
<view class="comment-action" style="display: flex; justify-content: center; margin: 10rpx ;">
<button class="comment-button" bindtap="showCommentModal">
发表评论
</button>
</view>
</view>
<!-- 评论列表 -->
<view class="comments-list">
<block wx:if="{{comments.length > 0}}">
<view wx:for="{{comments}}" wx:key="id" class="comment-item">
<view class="comment-user">
<image class="user-avatar" src="{{item.avatar || 'https://via.placeholder.com/40'}}"></image>
<view class="user-info">
<text class="user-nickname">{{item.nickname || '匿名用户'}}</text>
<text class="comment-time">{{item.time || '刚刚'}}</text>
</view>
<!-- 删除按钮,仅当前用户的评论显示 -->
<view wx:if="{{item.phoneNumber === currentUserPhone}}" class="comment-actions">
<text class="delete-btn" bindtap="showDeleteConfirmModal" data-comment="{{item}}">删除</text>
</view>
</view>
<view class="comment-content">
<text class="content-text">{{item.comments}}</text>
</view>
</view>
</block>
<block wx:else>
<view class="comments-empty">
<text class="empty-text">暂无评论,快来抢沙发吧~</text>
</view>
</block>
</view>
</view>
1
<!-- 登录弹窗 --> <!-- 登录弹窗 -->
<view wx:if="{{showOneKeyLoginModal}}" class="auth-modal-overlay"> <view wx:if="{{showOneKeyLoginModal}}" class="auth-modal-overlay">
@ -380,6 +424,70 @@
</button> </button>
</view> </view>
<!-- 评论弹窗 -->
<view class="comment-modal-overlay" wx:if="{{showCommentModal}}">
<view class="comment-modal-container">
<!-- 弹窗标题 -->
<view class="comment-modal-title">
发表评论
</view>
<!-- 评论输入框 -->
<textarea
class="comment-modal-textarea"
placeholder="写下您的评价..."
value="{{newCommentContent}}"
bindinput="onCommentInput"
></textarea>
<!-- 按钮区域 -->
<view class="comment-modal-buttons">
<button
class="comment-modal-cancel"
bindtap="hideCommentModal"
>
取消
</button>
<button
class="comment-modal-submit"
bindtap="submitComment"
disabled="{{!newCommentContent}}"
>
发送
</button>
</view>
</view>
</view>
<!-- 删除评论确认弹窗 -->
<view class="delete-modal-overlay" wx:if="{{showDeleteConfirmModal}}">
<view class="delete-modal-container">
<!-- 弹窗标题 -->
<view class="delete-modal-title">确认删除</view>
<!-- 提示信息 -->
<view class="delete-modal-content">
您确定要删除这条评论吗?
</view>
<!-- 按钮区域 -->
<view class="delete-modal-buttons">
<button
class="delete-modal-cancel"
bindtap="hideDeleteConfirmModal"
>
取消
</button>
<button
class="delete-modal-confirm"
bindtap="confirmDeleteComment"
>
删除
</button>
</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 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;"> <view style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">

540
pages/goods-detail/goods-detail.wxss

@ -1039,4 +1039,544 @@ video.slider-media .wx-video-volume-icon {
font-size: 36rpx; font-size: 36rpx;
font-weight: bold; font-weight: bold;
padding: 0 16rpx; padding: 0 16rpx;
}
/* 评论区样式 */
.comments-section {
background-color: #ffffff;
margin: 8px 0 90px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.comments-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.comments-title {
display: flex;
align-items: baseline;
}
.comments-label {
font-size: 18px;
font-weight: 700;
color: #262626;
margin-right: 8px;
}
.comments-count {
font-size: 14px;
color: #8c8c8c;
font-weight: 500;
}
.comments-rating {
display: flex;
align-items: center;
gap: 12px;
}
.rating-stars {
display: flex;
gap: 2px;
}
.star-icon {
font-size: 18px;
color: #ffd700;
cursor: pointer;
transition: color 0.2s ease;
}
.star-active {
color: #ffd700;
}
.star-inactive {
color: #e8e8e8;
}
.star-icon-small {
font-size: 14px;
color: #ffd700;
}
.rating-score {
font-size: 16px;
font-weight: 600;
color: #262626;
}
/* 评论列表 */
.comments-list {
margin-bottom: 20px;
}
.comment-item {
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
}
.comment-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.comment-user {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
background-color: #f0f0f0;
}
.user-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.user-nickname {
font-size: 14px;
font-weight: 600;
color: #262626;
}
.comment-time {
font-size: 12px;
color: #8c8c8c;
}
.comment-content {
margin-bottom: 12px;
}
.content-text {
font-size: 14px;
line-height: 1.6;
color: #595959;
}
.comment-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.comment-actions {
display: flex;
gap: 24px;
}
.action-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #8c8c8c;
cursor: pointer;
transition: color 0.2s ease;
}
.action-item:active {
color: #1890ff;
}
.action-icon {
font-size: 14px;
}
.action-text {
font-size: 12px;
}
.comment-rating-small {
display: flex;
gap: 2px;
}
/* 回复列表 */
.replies-list {
margin-top: 12px;
padding-left: 52px;
}
.reply-item {
margin-bottom: 12px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #d6e4ff;
}
.reply-content {
margin-bottom: 4px;
}
.reply-user {
font-size: 12px;
font-weight: 600;
color: #1890ff;
margin-right: 8px;
}
.reply-text {
font-size: 12px;
line-height: 1.5;
color: #595959;
}
.reply-time {
font-size: 11px;
color: #8c8c8c;
}
/* 评论输入区域 */
.comment-input-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.input-container {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.comment-textarea {
flex: 1;
min-height: 80px;
padding: 12px;
border: 1px solid #d9d9d9;
border-radius: 8px;
font-size: 14px;
line-height: 1.5;
color: #262626;
background-color: #fafafa;
resize: none;
box-sizing: border-box;
}
.comment-textarea:focus {
border-color: #1890ff;
background-color: #ffffff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.submit-button {
align-self: flex-end;
height: 40px;
padding: 0 20px;
font-size: 14px;
font-weight: 600;
color: #ffffff;
background-color: #1890ff;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.submit-button:active {
background-color: #40a9ff;
transform: scale(0.98);
}
.submit-button:disabled {
background-color: #d9d9d9;
cursor: not-allowed;
}
.rating-input {
display: flex;
align-items: center;
gap: 12px;
}
.rating-label {
font-size: 14px;
color: #595959;
font-weight: 500;
}
.rating-input-stars {
display: flex;
gap: 4px;
}
/* 空评论状态 */
.comments-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
color: #8c8c8c;
background-color: #fafafa;
border-radius: 8px;
margin: 20px 16px;
}
.empty-icon {
font-size: 64px;
margin-bottom: 20px;
opacity: 0.3;
color: #d9d9d9;
}
.empty-text {
font-size: 14px;
text-align: center;
}
/* 评论按钮样式 */
.comment-action {
margin: 20rpx 0;
}
.comment-button {
background-color: #1890ff;
color: #ffffff;
border: none;
border-radius: 12rpx;
padding: 24rpx 64rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
transition: all 0.3s ease;
}
.comment-button:active {
background-color: #40a9ff;
transform: scale(0.98);
box-shadow: 0 3rpx 8rpx rgba(24, 144, 255, 0.4);
}
/* 评论操作按钮 */
.comment-actions {
margin-left: auto;
}
.delete-btn {
color: #ff4d4f;
font-size: 28rpx;
padding: 8rpx 16rpx;
border-radius: 8rpx;
transition: all 0.3s ease;
}
.delete-btn:active {
background-color: rgba(255, 77, 79, 0.1);
transform: scale(0.98);
}
/* 评论弹窗样式 */
.comment-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10001;
display: flex;
justify-content: center;
align-items: flex-end;
animation: fadeIn 0.3s ease-out;
}
/* 删除确认弹窗样式 */
.delete-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10001;
display: flex;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease-out;
}
.delete-modal-container {
background: white;
width: 80%;
max-width: 500rpx;
padding: 40rpx;
border-radius: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
animation: slideUp 0.3s ease-out;
}
.delete-modal-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 30rpx;
text-align: center;
color: #262626;
}
.delete-modal-content {
font-size: 32rpx;
color: #595959;
text-align: center;
margin-bottom: 40rpx;
line-height: 1.6;
}
.delete-modal-buttons {
display: flex;
justify-content: space-between;
gap: 20rpx;
width: 100%;
}
.delete-modal-cancel,
.delete-modal-confirm {
flex: 1;
padding: 20rpx 0;
font-size: 32rpx;
border-radius: 10rpx;
transition: all 0.3s ease;
border: none;
margin: 0;
}
.delete-modal-cancel {
background: #f0f0f0;
color: #333;
}
.delete-modal-cancel:active {
background: #e0e0e0;
transform: scale(0.98);
}
.delete-modal-confirm {
background: #ff4d4f;
color: white;
}
.delete-modal-confirm:active {
background: #ff7875;
transform: scale(0.98);
}
.comment-modal-container {
background: white;
width: 100%;
padding: 30rpx;
border-radius: 20rpx 20rpx 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease-out;
}
.comment-modal-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 30rpx;
text-align: center;
color: #262626;
}
.comment-modal-textarea {
width: 100%;
height: 200rpx;
border: 2rpx solid #e0e0e0;
border-radius: 10rpx;
padding: 20rpx;
font-size: 32rpx;
box-sizing: border-box;
margin-bottom: 30rpx;
resize: none;
background-color: #fafafa;
transition: all 0.3s ease;
}
.comment-modal-textarea:focus {
border-color: #1890ff;
background-color: #ffffff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.1);
}
.comment-modal-buttons {
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.comment-modal-cancel,
.comment-modal-submit {
flex: 1;
padding: 20rpx 0;
font-size: 32rpx;
border-radius: 10rpx;
transition: all 0.3s ease;
border: none;
margin: 0;
}
.comment-modal-cancel {
background: #f0f0f0;
color: #333;
}
.comment-modal-cancel:active {
background: #e0e0e0;
transform: scale(0.98);
}
.comment-modal-submit {
background: #4a90e2;
color: white;
}
.comment-modal-submit:active {
background: #5c9dff;
transform: scale(0.98);
}
.comment-modal-submit:disabled {
background: #c0c4cc;
cursor: not-allowed;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
} }

2
pages/index/index.wxml

@ -179,7 +179,7 @@
<text class="function-btn-text">招商</text> <text class="function-btn-text">招商</text>
</view> </view>
<view class="function-btn" bindtap="navigateToCooperation"> <view class="function-btn" bindtap="navigateToCooperation">
<text class="function-btn-icon">🚹</text> <text class="function-btn-icon">🚻</text>
<text class="function-btn-text">我们</text> <text class="function-btn-text">我们</text>
</view> </view>
</view> </view>

3
pages/profile/index.wxml

@ -30,9 +30,6 @@
<view class="card"> <view class="card">
<view class="title">我的标签</view> <view class="title">我的标签</view>
<view style="flex-wrap: wrap; display: flex;"> <view style="flex-wrap: wrap; display: flex;">
<view wx:for="{{userTags}}" wx:key="index" style="background-color: #f0f2f5; padding: 10rpx 20rpx; border-radius: 20rpx; margin: 10rpx; font-size: 26rpx;">
{{item}}
</view>
<!-- 淘宝样式的收藏按钮 --> <!-- 淘宝样式的收藏按钮 -->
<view style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); color: white; padding: 12rpx 24rpx; border-radius: 24rpx; margin: 10rpx; font-size: 26rpx; font-weight: bold; box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3); display: flex; align-items: center; cursor: pointer;" bindtap="goToFavorites"> <view style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); color: white; padding: 12rpx 24rpx; border-radius: 24rpx; margin: 10rpx; font-size: 26rpx; font-weight: bold; box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3); display: flex; align-items: center; cursor: pointer;" bindtap="goToFavorites">
<text style="margin-right: 8rpx;">⭐</text> <text style="margin-right: 8rpx;">⭐</text>

239
server-example/server-mysql.js

@ -494,6 +494,49 @@ const ChatMessage = sequelize.define('ChatMessage', {
timestamps: false timestamps: false
}); });
// 定义评论模型
const comments = sequelize.define('Comment', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '唯一标识'
},
productId: {
type: DataTypes.STRING(255),
allowNull: false,
comment: '产品id'
},
phoneNumber: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '评论者电话号码'
},
comments: {
type: DataTypes.TEXT,
allowNull: false,
comment: '评论内容'
},
time: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW,
comment: '评论时间'
},
like: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '点赞数'
},
hate: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '点踩数'
}
}, {
tableName: 'comments',
timestamps: false
});
// 微信小程序配置 // 微信小程序配置
const WECHAT_CONFIG = { const WECHAT_CONFIG = {
APPID: process.env.WECHAT_APPID || 'your-wechat-appid', APPID: process.env.WECHAT_APPID || 'your-wechat-appid',
@ -963,6 +1006,58 @@ Favorite.init({
timestamps: false timestamps: false
}); });
// 评论模型 - 对应comments表
class Comment extends Model { }
Comment.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
comment: '唯一标识'
},
productId: {
type: DataTypes.STRING(255),
allowNull: false,
comment: '产品id'
},
phoneNumber: {
type: DataTypes.STRING(20), // 使用STRING类型存储电话号码,支持11位及以上格式
allowNull: false,
comment: '评论者电话号码'
},
comments: {
type: DataTypes.TEXT,
allowNull: false,
comment: '评论内容'
},
time: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW,
comment: '评论时间'
},
like: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '点赞数'
},
hate: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '点踩数'
}
}, {
sequelize,
modelName: 'Comment',
tableName: 'comments',
timestamps: false,
hooks: {
beforeCreate: async (comment) => {
// 生成唯一ID
const maxId = await Comment.max('id');
comment.id = (maxId || 0) + 1;
}
}
});
// 联系人表模型 // 联系人表模型
class Contact extends Model { } class Contact extends Model { }
Contact.init({ Contact.init({
@ -4115,6 +4210,150 @@ app.post('/api/products/detail', async (req, res) => {
} }
}); });
// 获取商品评论列表
app.post('/api/comments/get', async (req, res) => {
try {
const { productId } = req.body;
if (!productId) {
return res.status(400).json({
success: false,
code: 400,
message: '缺少productId参数'
});
}
// 查询商品评论
const comments = await Comment.findAll({
where: { productId },
order: [['time', 'DESC']] // 按时间倒序排列
});
res.json({
success: true,
code: 200,
message: '获取评论列表成功',
data: comments
});
} catch (error) {
console.error('获取评论列表失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '获取评论列表失败',
error: error.message
});
}
});
// 提交评论
app.post('/api/comments/add', async (req, res) => {
try {
const { productId, phoneNumber, comments } = req.body;
if (!productId || !phoneNumber || !comments) {
return res.status(400).json({
success: false,
code: 400,
message: '缺少必要参数'
});
}
// 创建评论
const newComment = await Comment.create({
productId,
phoneNumber,
comments,
time: new Date()
});
res.json({
success: true,
code: 200,
message: '提交评论成功',
data: newComment
});
} catch (error) {
console.error('提交评论失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '提交评论失败',
error: error.message
});
}
});
// 更新评论点赞数
app.post('/api/comments/updateLike', async (req, res) => {
try {
const { id, like } = req.body;
if (!id || like === undefined) {
return res.status(400).json({
success: false,
code: 400,
message: '缺少必要参数'
});
}
// 更新点赞数
await Comment.update(
{ like },
{ where: { id } }
);
res.json({
success: true,
code: 200,
message: '更新点赞数成功'
});
} catch (error) {
console.error('更新点赞数失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '更新点赞数失败',
error: error.message
});
}
});
// 更新评论点踩数
app.post('/api/comments/updateHate', async (req, res) => {
try {
const { id, hate } = req.body;
if (!id || hate === undefined) {
return res.status(400).json({
success: false,
code: 400,
message: '缺少必要参数'
});
}
// 更新点踩数
await Comment.update(
{ hate },
{ where: { id } }
);
res.json({
success: true,
code: 200,
message: '更新点踩数成功'
});
} catch (error) {
console.error('更新点踩数失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '更新点踩数失败',
error: error.message
});
}
});
// 修改商品 // 修改商品
app.post('/api/products/edit', async (req, res) => { app.post('/api/products/edit', async (req, res) => {
try { try {

80
utils/api.js

@ -252,9 +252,11 @@ function request(url, method, data, requestContext = {}) {
} }
}, },
fail: function (err) { fail: function (err) {
console.error('请求网络错误:', { console.error('请求网络错误详细信息:', {
url: BASE_URL + url, url: BASE_URL + url,
error: err error: err,
errMsg: err.errMsg,
errCode: err.errCode
}); });
// 创建基础错误对象 // 创建基础错误对象
@ -317,10 +319,12 @@ function request(url, method, data, requestContext = {}) {
reject(error); reject(error);
}, },
complete: function (res) { complete: function (res) {
console.log('请求完成:', { console.log('请求完成详细信息:', {
url: BASE_URL + url, url: BASE_URL + url,
type: res.errMsg.includes('ok') ? '成功' : '失败', type: res.errMsg.includes('ok') ? '成功' : '失败',
statusCode: res.statusCode statusCode: res.statusCode,
errMsg: res.errMsg,
responseData: res.data
}); });
} }
}); });
@ -956,6 +960,74 @@ module.exports = {
}); });
}, },
// 评论相关API函数
// 获取商品评论列表
getComments: function (productId) {
console.log('API.getComments - productId:', productId);
// 尝试使用不同的参数名,看看是否能获取到所有评论
return request('/api/comments/get', 'POST', {
productId: productId,
// 尝试添加多个参数,明确请求所有评论
allComments: true,
includeAllUsers: true,
viewMode: 'all'
});
},
// 提交评论
submitComment: function (commentData) {
console.log('API.submitComment - 开始提交评论');
console.log('API.submitComment - 评论数据:', commentData);
console.log('API.submitComment - 请求URL:', '/api/comments/add');
console.log('API.submitComment - 请求方法:', 'POST');
return request('/api/comments/add', 'POST', commentData)
.then(res => {
console.log('API.submitComment - 请求成功响应:', res);
return res;
})
.catch(err => {
console.error('API.submitComment - 请求失败:', err);
throw err;
});
},
// 更新评论点赞数
updateCommentLike: function (data) {
console.log('API.updateCommentLike - data:', data);
return request('/api/comments/updateLike', 'POST', data);
},
// 更新评论点踩数
updateCommentHate: function (data) {
console.log('API.updateCommentHate - data:', data);
return request('/api/comments/updateHate', 'POST', data);
},
// 删除评论
deleteComment: function (commentId, currentUserPhone, commentPhone) {
console.log('API.deleteComment - 开始删除评论');
console.log('API.deleteComment - 评论ID:', commentId);
console.log('API.deleteComment - 当前用户手机号:', currentUserPhone);
console.log('API.deleteComment - 评论所属手机号:', commentPhone);
console.log('API.deleteComment - 请求URL:', '/api/comments/delete');
console.log('API.deleteComment - 请求方法:', 'POST');
return request('/api/comments/delete', 'POST', {
commentId: commentId,
currentUserPhone: currentUserPhone,
commentPhone: commentPhone
})
.then(res => {
console.log('API.deleteComment - 请求成功响应:', res);
return res;
})
.catch(err => {
console.error('API.deleteComment - 请求失败:', err);
throw err;
});
},
// 发布商品 - 支持图片上传(修复sellerId问题) // 发布商品 - 支持图片上传(修复sellerId问题)
publishProduct: function (product) { publishProduct: function (product) {
console.log('===== publishProduct调用开始 =====') console.log('===== publishProduct调用开始 =====')

156
utils/time.js

@ -1,110 +1,74 @@
// UTC+8时间处理工具函数
// 固定时间戳(北京时间:2025-12-19 17:10:35.186)
const FIXED_UTC8_TIME = 1766135435186;
let isFixedTimeMode = false; // 默认关闭固定时间模式
/** /**
* 获取UTC+8时间戳 * 格式化日期时间为相对时间刚刚1分钟前2小时前等
* @param {Date|number} date - 可选日期对象或时间戳 * @param {string|Date} dateTime - 日期时间可以是ISO字符串或Date对象
* @returns {number} UTC+8时间戳 * @returns {string} 格式化后的相对时间
*/ */
function getUtc8Timestamp(date) { function formatRelativeTime(dateTime) {
let baseTimestamp; if (!dateTime) {
if (isFixedTimeMode) { return '刚刚';
baseTimestamp = FIXED_UTC8_TIME;
} else if (!date) {
baseTimestamp = Date.now();
} else if (typeof date === 'number') {
baseTimestamp = date;
} else {
baseTimestamp = date.getTime();
} }
return baseTimestamp + 8 * 60 * 60 * 1000; // 手动加8小时
}
/**
* 获取UTC+8 Date对象
* @param {number} timestamp - 可选时间戳
* @returns {Date} UTC+8 Date对象
*/
function getUtc8Date(timestamp) {
const ts = isFixedTimeMode ? FIXED_UTC8_TIME : (timestamp || Date.now());
const date = new Date(ts + 8 * 60 * 60 * 1000); // 手动加8小时
return date;
}
/**
* 将时间转换为UTC+8的ISO字符串带时区信息
* @param {Date|number} date - 可选日期对象或时间戳
* @returns {string} ISO格式的UTC+8时间字符串
*/
function toUtc8ISOString(date) {
const timestamp = getUtc8Timestamp(date);
const utcDate = new Date(timestamp);
const year = utcDate.getUTCFullYear(); // 确保输入是Date对象
const month = String(utcDate.getUTCMonth() + 1).padStart(2, '0'); const date = typeof dateTime === 'string' ? new Date(dateTime) : dateTime;
const day = String(utcDate.getUTCDate()).padStart(2, '0'); if (isNaN(date.getTime())) {
const hours = String(utcDate.getUTCHours()).padStart(2, '0'); return '刚刚';
const minutes = String(utcDate.getUTCMinutes()).padStart(2, '0'); }
const seconds = String(utcDate.getUTCSeconds()).padStart(2, '0');
const milliseconds = String(utcDate.getUTCMilliseconds()).padStart(3, '0');
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}+08:00`; // 计算时间差(毫秒)
} const now = new Date();
const diff = now - date;
/**
* 将时间转换为UTC+8的本地字符串
* @param {Date|number} date - 可选日期对象或时间戳
* @param {Object} options - 可选格式化选项
* @returns {string} 本地化的UTC+8时间字符串
*/
function toUtc8String(date, options = {}) {
const defaultOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
// 移除timeZone设置,因为getUtc8Timestamp已经返回UTC+8时间戳
};
const timestamp = getUtc8Timestamp(date); // 转换为不同的时间单位
const utc8Date = new Date(timestamp); const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
return utc8Date.toLocaleString('zh-CN', { ...defaultOptions, ...options }); // 根据时间差返回不同的格式
} if (seconds < 60) {
return '刚刚';
/** } else if (minutes < 60) {
* 设置固定时间模式 return `${minutes}分钟前`;
* @param {boolean} mode - true启用false禁用 } else if (hours < 24) {
*/ return `${hours}小时前`;
function setFixedTimeMode(mode) { } else if (days < 7) {
isFixedTimeMode = mode; return `${days}天前`;
} else {
// 超过7天显示具体日期
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}`;
}
} }
/** /**
* 获取固定时间模式状态 * 格式化日期为YYYY-MM-DD HH:MM:SS格式
* @returns {boolean} 当前固定时间模式状态 * @param {string|Date} dateTime - 日期时间可以是ISO字符串或Date对象
* @returns {string} 格式化后的日期时间
*/ */
function getFixedTimeMode() { function formatDateTime(dateTime) {
return isFixedTimeMode; if (!dateTime) {
return '';
}
// 确保输入是Date对象
const date = typeof dateTime === 'string' ? new Date(dateTime) : dateTime;
if (isNaN(date.getTime())) {
return '';
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} }
// 导出函数
module.exports = { module.exports = {
getUtc8Timestamp, formatRelativeTime,
getUtc8Date, formatDateTime
toUtc8ISOString, };
toUtc8String,
setFixedTimeMode,
getFixedTimeMode,
// 向后兼容别名
getBeijingTimeTimestamp: getUtc8Timestamp,
getBeijingTimeDate: getUtc8Date,
toBeijingTimeISOString: toUtc8ISOString,
toBeijingTimeString: toUtc8String
};

Loading…
Cancel
Save