diff --git a/pages/goods-detail/goods-detail.js b/pages/goods-detail/goods-detail.js index c1da01f..55f3a6b 100644 --- a/pages/goods-detail/goods-detail.js +++ b/pages/goods-detail/goods-detail.js @@ -1,5 +1,6 @@ // pages/goods-detail/goods-detail.js const API = require('../../utils/api.js') +const timeUtils = require('../../utils/time.js') // 根据sourceType获取对应的颜色 function getSourceTypeColor(sourceType) { @@ -618,6 +619,116 @@ Page({ }); }, 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: { goodsDetail: {}, // 当前商品详情 @@ -652,6 +763,20 @@ Page({ isExtractingCover: false, // 是否正在提取封面 videoCoverCache: {}, // 视频封面缓存 {videoUrl: coverUrl} videoSrcForSnapshot: '', // 用于截图的视频URL + // 评论相关数据 + comments: [], // 评论列表 + averageRating: 0, // 平均评分 + newCommentContent: '', // 新评论内容 + newCommentRating: 0, // 新评论评分 + inputValue: '', // 输入框内容(用于兼容) + showContent: '', // 显示内容(用于兼容) + // 评论弹窗相关数据 + showCommentModal: false, // 是否显示评论弹窗 + // 删除评论相关数据 + showDeleteConfirmModal: false, // 是否显示删除确认弹窗 + commentToDelete: null, // 要删除的评论对象 + // 当前用户信息 + currentUserPhone: '' // 当前用户的手机号,用于判断评论是否属于当前用户 }, // 点击对比价格列表中的商品,跳转到对应的商品详情页 @@ -714,6 +839,26 @@ Page({ 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参数明确要求登录时才显示登录弹窗 if (!isLoggedIn && needLoginFromUrl) { console.log('检测到需要登录且用户未登录,显示登录提示弹窗'); @@ -1352,6 +1497,15 @@ Page({ if (!preloadedData || preloadedData.isFavorite === undefined) { this.loadGoodsFavoriteStatus(productIdStr); } + + // 加载评论列表 + this.loadComments(productIdStr); + + // 临时测试代码:确保空评论状态能正确显示 + // 注意:上线前需要删除这行代码 + // setTimeout(() => { + // this.setData({ comments: [] }); + // }, 100); } else { wx.showToast({ 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 () { diff --git a/pages/goods-detail/goods-detail.wxml b/pages/goods-detail/goods-detail.wxml index 07e08d0..4c6cb2a 100644 --- a/pages/goods-detail/goods-detail.wxml +++ b/pages/goods-detail/goods-detail.wxml @@ -140,7 +140,6 @@ {{displayRegion}} - 新鲜程度 @@ -168,7 +167,6 @@ - @@ -191,6 +189,52 @@ + + + + + 商品评价 + ({{comments.length}}条) + + + + + + + + + + + + + + + + + 删除 + + + + {{item.comments}} + + + + + + 暂无评论,快来抢沙发吧~ + + + + + + +1 + @@ -380,6 +424,70 @@ + + + + + + 发表评论 + + + + + + + + + + + + + + + + + + 确认删除 + + + + 您确定要删除这条评论吗? + + + + + + + + + + diff --git a/pages/goods-detail/goods-detail.wxss b/pages/goods-detail/goods-detail.wxss index 9a21122..4cda2b2 100644 --- a/pages/goods-detail/goods-detail.wxss +++ b/pages/goods-detail/goods-detail.wxss @@ -1039,4 +1039,544 @@ video.slider-media .wx-video-volume-icon { font-size: 36rpx; font-weight: bold; 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); + } } \ No newline at end of file diff --git a/pages/index/index.wxml b/pages/index/index.wxml index 9e280a9..ab51958 100644 --- a/pages/index/index.wxml +++ b/pages/index/index.wxml @@ -179,7 +179,7 @@ 招商 - 🚹 + 🚻 我们 diff --git a/pages/profile/index.wxml b/pages/profile/index.wxml index 29677eb..64e1692 100644 --- a/pages/profile/index.wxml +++ b/pages/profile/index.wxml @@ -30,9 +30,6 @@ 我的标签 - - {{item}} - diff --git a/server-example/server-mysql.js b/server-example/server-mysql.js index 4172672..ccfc266 100644 --- a/server-example/server-mysql.js +++ b/server-example/server-mysql.js @@ -494,6 +494,49 @@ const ChatMessage = sequelize.define('ChatMessage', { 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 = { APPID: process.env.WECHAT_APPID || 'your-wechat-appid', @@ -963,6 +1006,58 @@ Favorite.init({ 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 { } 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) => { try { diff --git a/utils/api.js b/utils/api.js index 2cc0f0a..b99eca5 100644 --- a/utils/api.js +++ b/utils/api.js @@ -252,9 +252,11 @@ function request(url, method, data, requestContext = {}) { } }, fail: function (err) { - console.error('请求网络错误:', { + console.error('请求网络错误详细信息:', { 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); }, complete: function (res) { - console.log('请求完成:', { + console.log('请求完成详细信息:', { url: BASE_URL + url, 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问题) publishProduct: function (product) { console.log('===== publishProduct调用开始 =====') diff --git a/utils/time.js b/utils/time.js index 1b1ef0f..157177a 100644 --- a/utils/time.js +++ b/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时间戳 - * @param {Date|number} date - 可选,日期对象或时间戳 - * @returns {number} UTC+8时间戳 + * 格式化日期时间为相对时间(如:刚刚、1分钟前、2小时前等) + * @param {string|Date} dateTime - 日期时间(可以是ISO字符串或Date对象) + * @returns {string} 格式化后的相对时间 */ -function getUtc8Timestamp(date) { - let baseTimestamp; - if (isFixedTimeMode) { - baseTimestamp = FIXED_UTC8_TIME; - } else if (!date) { - baseTimestamp = Date.now(); - } else if (typeof date === 'number') { - baseTimestamp = date; - } else { - baseTimestamp = date.getTime(); +function formatRelativeTime(dateTime) { + if (!dateTime) { + return '刚刚'; } - 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(); - const month = String(utcDate.getUTCMonth() + 1).padStart(2, '0'); - const day = String(utcDate.getUTCDate()).padStart(2, '0'); - const hours = String(utcDate.getUTCHours()).padStart(2, '0'); - const minutes = String(utcDate.getUTCMinutes()).padStart(2, '0'); - const seconds = String(utcDate.getUTCSeconds()).padStart(2, '0'); - const milliseconds = String(utcDate.getUTCMilliseconds()).padStart(3, '0'); + // 确保输入是Date对象 + const date = typeof dateTime === 'string' ? new Date(dateTime) : dateTime; + if (isNaN(date.getTime())) { + return '刚刚'; + } - return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}+08:00`; -} - -/** - * 将时间转换为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 now = new Date(); + const diff = now - date; - 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 }); -} - -/** - * 设置固定时间模式 - * @param {boolean} mode - true启用,false禁用 - */ -function setFixedTimeMode(mode) { - isFixedTimeMode = mode; + // 根据时间差返回不同的格式 + if (seconds < 60) { + return '刚刚'; + } else if (minutes < 60) { + return `${minutes}分钟前`; + } else if (hours < 24) { + return `${hours}小时前`; + } else if (days < 7) { + 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}`; + } } /** - * 获取固定时间模式状态 - * @returns {boolean} 当前固定时间模式状态 + * 格式化日期为YYYY-MM-DD HH:MM:SS格式 + * @param {string|Date} dateTime - 日期时间(可以是ISO字符串或Date对象) + * @returns {string} 格式化后的日期时间 */ -function getFixedTimeMode() { - return isFixedTimeMode; +function formatDateTime(dateTime) { + 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 = { - getUtc8Timestamp, - getUtc8Date, - toUtc8ISOString, - toUtc8String, - setFixedTimeMode, - getFixedTimeMode, - // 向后兼容别名 - getBeijingTimeTimestamp: getUtc8Timestamp, - getBeijingTimeDate: getUtc8Date, - toBeijingTimeISOString: toUtc8ISOString, - toBeijingTimeString: toUtc8String -}; \ No newline at end of file + formatRelativeTime, + formatDateTime +};