Browse Source

feat: 添加商品咨询分享功能

- 新增 goods-card 商品卡片组件,支持在聊天中展示商品信息
- 优化 goods-detail 立即咨询功能,自动发送结构化商品消息
- 完善 chat-detail 页面,支持解析和展示商品卡片消息
- 消息格式支持 JSON 结构化数据,包含商品详情、图片、价格等信息
pull/11/head
徐飞洋 2 months ago
parent
commit
0479b6bbf9
  1. 43
      components/goods-card/goods-card.js
  2. 4
      components/goods-card/goods-card.json
  3. 33
      components/goods-card/goods-card.wxml
  4. 159
      components/goods-card/goods-card.wxss
  5. 18
      pages/chat-detail/index.js
  6. 5
      pages/chat-detail/index.json
  7. 19
      pages/chat-detail/index.wxml
  8. 50
      pages/chat-detail/index.wxss
  9. 161
      pages/goods-detail/goods-detail.js

43
components/goods-card/goods-card.js

@ -0,0 +1,43 @@
Component({
properties: {
id: { type: String, value: '' },
name: { type: String, value: '' },
imageUrl: { type: String, value: '' },
price: { type: [String, Number], value: '' },
priceUnit: { type: String, value: '/件' },
region: { type: String, value: '' },
specification: { type: String, value: '' },
spec: { type: String, value: '' },
specs: { type: String, value: '' },
displaySpecification: { type: String, value: '' },
displayYolk: { type: String, value: '' },
sourceType: { type: String, value: '' },
totalStock: { type: [String, Number], value: '' },
supplyStatus: { type: String, value: '' },
status: { type: String, value: '' },
reservedCount: { type: [String, Number], value: 0 }
},
data: {
isSoldOut: false
},
observers: {
'status': function(status) {
this.setData({
isSoldOut: status === 'sold_out'
});
}
},
methods: {
onTap: function() {
const goodsId = this.properties.id;
if (goodsId) {
wx.navigateTo({
url: `/pages/goods-detail/goods-detail?id=${goodsId}`
});
}
}
}
});

4
components/goods-card/goods-card.json

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

33
components/goods-card/goods-card.wxml

@ -0,0 +1,33 @@
<view class="goods-card {{isSoldOut ? 'sold-out' : ''}}" bindtap="onTap">
<view class="goods-image-area">
<image
wx:if="{{imageUrl}}"
class="goods-image"
src="{{imageUrl}}"
mode="aspectFill"
lazy-load="true"
></image>
<view wx:else class="goods-image-placeholder">
<text>暂无图片</text>
</view>
<view wx:if="{{supplyStatus === '预售'}}" class="promo-tag presale">预售</view>
<view wx:elif="{{supplyStatus === '现货'}}" class="promo-tag in-stock">现货</view>
<view wx:if="{{status === 'sold_out'}}" class="promo-tag sold-out">售空</view>
</view>
<view class="goods-info">
<view class="goods-title">{{name || '商品名称'}}</view>
<view class="goods-spec">{{displaySpecification}}{{displayYolk ? ' | ' + displayYolk : ''}}</view>
<view class="goods-tags">
<view class="tag source-{{sourceType === '三方认证' ? 'third' : (sourceType === '平台货源' ? 'platform' : 'unverified')}}">{{sourceType || ''}}</view>
<view class="tag stock">库存:{{totalStock && totalStock !== '充足' ? totalStock + '件' : (totalStock || '充足')}}</view>
</view>
<view class="goods-bottom">
<view class="goods-price">
<text class="price-unit">¥</text>
<text class="price-value">{{price || '0'}}</text>
<text class="price-unit">{{priceUnit || '/件'}}</text>
</view>
<view class="goods-region">{{region || ''}}</view>
</view>
</view>
</view>

159
components/goods-card/goods-card.wxss

@ -0,0 +1,159 @@
.goods-card {
width: 100%;
max-width: 560rpx;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.goods-card.sold-out {
opacity: 0.7;
}
.goods-image-area {
position: relative;
width: 100%;
height: 280rpx;
background: #f5f5f5;
}
.goods-image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.goods-image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
color: #999;
font-size: 24rpx;
}
.promo-tag {
position: absolute;
top: 0;
left: 0;
padding: 6rpx 12rpx;
font-size: 20rpx;
color: #fff;
border-radius: 0 0 12rpx 0;
z-index: 1;
font-weight: 600;
}
.promo-tag.presale {
background: linear-gradient(135deg, #ff6b00 0%, #ff8c00 100%);
}
.promo-tag.in-stock {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
}
.promo-tag.sold-out {
background: linear-gradient(135deg, #a92a2a 0%, #a6a6a6 100%);
}
.goods-info {
padding: 16rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.goods-title {
font-size: 26rpx;
color: #000000;
line-height: 1.4;
height: 72rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
font-weight: 700;
}
.goods-spec {
font-size: 22rpx;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.goods-tags {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 8rpx;
margin: 6rpx 0;
}
.tag {
font-size: 20rpx;
padding: 4rpx 10rpx;
border-radius: 6rpx;
}
.tag.source-third {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #fff;
}
.tag.source-platform {
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
color: #fff;
}
.tag.source-unverified {
background: rgba(0, 0, 0, 0.08);
color: #666;
}
.tag.stock {
background: rgba(255, 77, 79, 0.08);
color: #666;
}
.goods-bottom {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-top: 6rpx;
}
.goods-price {
display: flex;
align-items: baseline;
}
.price-unit {
font-size: 24rpx;
color: #ff4d4f;
font-weight: 700;
}
.price-value {
font-size: 36rpx;
color: #ff4d4f;
font-weight: 800;
margin: 0 2rpx;
}
.goods-region {
font-size: 20rpx;
color: #999;
max-width: 100rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

18
pages/chat-detail/index.js

@ -172,9 +172,25 @@ Page({
const processedMessages = res.map(message => { const processedMessages = res.map(message => {
const sender = (message.sender_phone === userPhone) ? 'me' : 'other'; const sender = (message.sender_phone === userPhone) ? 'me' : 'other';
const time = this.formatDateTime(message.created_at); const time = this.formatDateTime(message.created_at);
// 尝试解析JSON格式的商品消息
let content = message.content;
let goodsData = null;
try {
const parsed = JSON.parse(message.content);
if (parsed.type === 'goods' && parsed.goodsData) {
goodsData = parsed.goodsData;
content = parsed.text || '';
}
} catch (e) {
// 不是JSON格式,使用原始内容
}
return { return {
id: message.id, id: message.id,
content: message.content, content: content,
goodsData: goodsData,
sender: sender, sender: sender,
time: time, time: time,
originalTime: message.created_at, originalTime: message.created_at,

5
pages/chat-detail/index.json

@ -1,3 +1,6 @@
{ {
"navigationBarTitleText": "聊天详情" "navigationBarTitleText": "聊天详情",
"usingComponents": {
"goods-card": "/components/goods-card/goods-card"
}
} }

19
pages/chat-detail/index.wxml

@ -28,7 +28,24 @@
<image src="https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0" mode="aspectFill"></image> <image src="https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0" mode="aspectFill"></image>
</view> </view>
<view class="message-bubble"> <view class="message-bubble">
<text class="message-content">{{item.content}}</text> <!-- 商品卡片消息 -->
<view wx:if="{{item.goodsData}}" class="goods-message-card">
<goods-card
id="{{item.goodsData.id}}"
name="{{item.goodsData.name}}"
image-url="{{item.goodsData.imageUrl}}"
price="{{item.goodsData.price}}"
region="{{item.goodsData.region}}"
display-specification="{{item.goodsData.displaySpecification}}"
display-yolk="{{item.goodsData.displayYolk}}"
source-type="{{item.goodsData.sourceType}}"
total-stock="{{item.goodsData.totalStock}}"
supply-status="{{item.goodsData.supplyStatus}}"
status="{{item.goodsData.status}}"
></goods-card>
</view>
<!-- 普通文本消息 -->
<text wx:else class="message-content">{{item.content}}</text>
<text class="message-time">{{item.time}}</text> <text class="message-time">{{item.time}}</text>
</view> </view>
<!-- 自己的消息 --> <!-- 自己的消息 -->

50
pages/chat-detail/index.wxss

@ -234,3 +234,53 @@
.send-button:hover { .send-button:hover {
background-color: #FF526D; background-color: #FF526D;
} }
/* 商品消息卡片样式 */
.goods-message-card {
width: 100%;
min-width: 280rpx;
max-width: 400rpx;
}
.goods-message-card .goods-card {
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
/* 商品卡片在小消息气泡中的适配 */
.message-item .message-bubble {
padding: 12rpx;
}
.goods-message-card .goods-image-area {
height: 200rpx;
}
.goods-message-card .goods-info {
padding: 12rpx;
}
.goods-message-card .goods-title {
font-size: 24rpx;
height: 66rpx;
}
.goods-message-card .goods-spec {
font-size: 20rpx;
}
.goods-message-card .goods-tags {
margin: 4rpx 0;
}
.goods-message-card .tag {
font-size: 18rpx;
padding: 2rpx 6rpx;
}
.goods-message-card .price-value {
font-size: 32rpx;
}
.goods-message-card .price-unit {
font-size: 22rpx;
}

161
pages/goods-detail/goods-detail.js

@ -81,6 +81,92 @@ function formatShareTitle(goodsDetail) {
return goodsDetail.name ? `优质鸡蛋 - ${goodsDetail.name}` : '优质鸡蛋货源'; return goodsDetail.name ? `优质鸡蛋 - ${goodsDetail.name}` : '优质鸡蛋货源';
} }
// 构建商品分享消息内容
function buildShareGoodsMessage(goodsDetail) {
const parts = [];
// 商品名称
if (goodsDetail.name) {
parts.push(`【商品】${goodsDetail.name}`);
}
// 规格
const specification = (goodsDetail.specification || goodsDetail.spec || goodsDetail.specs || '').trim();
if (specification) {
parts.push(`【规格】${specification}`);
}
// 地区
const region = (goodsDetail.region || '').trim();
if (region) {
parts.push(`【地区】${region}`);
}
// 数量
const quantity = (goodsDetail.quantity || '').trim();
if (quantity) {
parts.push(`【数量】${quantity}`);
}
// 价格
if (goodsDetail.price) {
parts.push(`【价格】${goodsDetail.price}`);
}
// 货源类型
const sourceType = (goodsDetail.sourceType || '').trim();
if (sourceType) {
parts.push(`【货源】${sourceType}`);
}
// 联系人
if (goodsDetail.product_contact) {
parts.push(`【联系人】${goodsDetail.product_contact}`);
}
// 联系电话
if (goodsDetail.contact_phone) {
parts.push(`【电话】${goodsDetail.contact_phone}`);
}
// 蛋黄颜色
const yolk = (goodsDetail.yolk || '').trim();
if (yolk) {
parts.push(`【蛋黄】${yolk}`);
}
// 蛋壳颜色/品种
const eggshell = (goodsDetail.eggshell || goodsDetail.shell || goodsDetail.breed || '').trim();
if (eggshell) {
parts.push(`【蛋壳/品种】${eggshell}`);
}
// 新鲜程度
const freshness = (goodsDetail.freshness || '').trim();
if (freshness) {
parts.push(`【新鲜程度】${freshness}`);
}
// 备注描述
const description = (goodsDetail.description || '').trim();
if (description) {
parts.push(`【备注】${description}`);
}
// 获取第一张图片作为商品图片
const imageUrls = goodsDetail.imageUrls || [];
const firstImage = imageUrls.find(url => !isVideoUrl(url));
let message = parts.join('\n');
// 如果有图片,添加图片链接
if (firstImage) {
message += `\n\n【商品图片】${firstImage}`;
}
return message;
}
// 获取适合分享的图片 - 优先使用正方形图片填满分享框 // 获取适合分享的图片 - 优先使用正方形图片填满分享框
function getShareImageUrl(goodsDetail) { function getShareImageUrl(goodsDetail) {
const imageUrls = goodsDetail.imageUrls || []; const imageUrls = goodsDetail.imageUrls || [];
@ -1379,19 +1465,68 @@ Page({
// 使用联系人手机号作为聊天会话ID // 使用联系人手机号作为聊天会话ID
const chatSessionId = contactPhone; const chatSessionId = contactPhone;
// 跳转到聊天页面,确保正确传递联系人手机号和用户名 // 构建商品分享消息内容(包含显示文本和结构化数据)
wx.navigateTo({ const goodsDetail = this.data.goodsDetail;
url: `/pages/chat-detail/index?userId=${chatSessionId}&userName=${encodeURIComponent(contactName || '联系人')}&phone=${contactPhone}&isManager=true`,
success: function () { // 构建结构化的商品消息
console.log('成功跳转到聊天详情页'); const goodsData = {
}, id: goodsDetail.id || '',
fail: function (error) { name: goodsDetail.name || '',
console.error('跳转到聊天详情页失败:', error); imageUrl: (goodsDetail.imageUrls && goodsDetail.imageUrls.length > 0) ?
wx.showToast({ (goodsDetail.imageUrls.find(url => !isVideoUrl(url)) || '') : '',
title: '聊天功能开发中', price: goodsDetail.price || '',
icon: 'none' region: goodsDetail.region || '',
}); displaySpecification: goodsDetail.displaySpecification || goodsDetail.specification || goodsDetail.spec || goodsDetail.specs || '',
} displayYolk: goodsDetail.displayYolk || goodsDetail.yolk || '',
sourceType: goodsDetail.sourceType || '',
totalStock: goodsDetail.totalStock || goodsDetail.stock || '',
supplyStatus: goodsDetail.supplyStatus || goodsDetail.status === 'sold_out' ? '' : (goodsDetail.supplyStatus || ''),
status: goodsDetail.status || ''
};
// 构建显示文本(兼容旧版本)
const displayText = buildShareGoodsMessage(goodsDetail);
// 发送结构化商品消息(JSON格式,包含goodsData用于展示卡片)
const structuredMessage = JSON.stringify({
type: 'goods',
text: displayText,
goodsData: goodsData
});
// 发送商品分享消息
API.sendMessage(userPhone, contactPhone, structuredMessage).then(sendRes => {
console.log('商品分享消息发送成功:', sendRes);
// 跳转到聊天页面
wx.navigateTo({
url: `/pages/chat-detail/index?userId=${chatSessionId}&userName=${encodeURIComponent(contactName || '联系人')}&phone=${contactPhone}&isManager=true`,
success: function () {
console.log('成功跳转到聊天详情页');
},
fail: function (error) {
console.error('跳转到聊天详情页失败:', error);
wx.showToast({
title: '聊天功能开发中',
icon: 'none'
});
}
});
}).catch(sendErr => {
console.error('发送商品分享消息失败:', sendErr);
// 即使发送消息失败,也跳转到聊天页面
wx.navigateTo({
url: `/pages/chat-detail/index?userId=${chatSessionId}&userName=${encodeURIComponent(contactName || '联系人')}&phone=${contactPhone}&isManager=true`,
success: function () {
console.log('成功跳转到聊天详情页');
},
fail: function (error) {
console.error('跳转到聊天详情页失败:', error);
wx.showToast({
title: '聊天功能开发中',
icon: 'none'
});
}
});
}); });
}).catch(err => { }).catch(err => {
console.error('建立聊天失败:', err); console.error('建立聊天失败:', err);

Loading…
Cancel
Save