Merge remote-tracking branch 'origin/master' into master

This commit is contained in:
zhiyun 2026-02-05 01:02:05 +08:00
parent 54515c6e23
commit 4146309ff2
24 changed files with 1261 additions and 171 deletions

View File

@ -233,14 +233,35 @@ Page({
}, },
fail: () => { fail: () => {
wx.hideLoading() wx.hideLoading()
wx.showToast({ wx.showToast({ title: '下载失败', icon: 'none' })
title: '下载失败',
icon: 'none'
})
} }
}) })
}, },
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
const city = this.data.selectedCity || '深圳市'
return {
title: `${city}同城活动 - 发现身边的精彩`,
path: `/pages/city-activities/city-activities?city=${encodeURIComponent(city)}&referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
const city = this.data.selectedCity || '深圳市'
return {
title: `${city}同城活动 - 发现身边的精彩`,
query: `city=${encodeURIComponent(city)}&referralCode=${referralCode}`
}
},
/** /**
* 点击活动卡片 * 点击活动卡片
*/ */

View File

@ -3,6 +3,7 @@
const api = require('../../utils/api') const api = require('../../utils/api')
const errorHandler = require('../../utils/errorHandler') const errorHandler = require('../../utils/errorHandler')
const imageUrl = require('../../utils/imageUrl')
// 缓存配置 // 缓存配置
const CACHE_CONFIG = { const CACHE_CONFIG = {
@ -45,7 +46,7 @@ Page({
// 缓存状态 // 缓存状态
cacheExpired: false, cacheExpired: false,
lastUpdateTime: '', lastUpdateTime: '',
defaultAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&auto=format&fit=crop&q=60', defaultAvatar: '/images/default-avatar.svg',
cardTitle: '守护会员', cardTitle: '守护会员',
pickerDate: '', // YYYY-MM pickerDate: '', // YYYY-MM
pickerDateDisplay: '' // YYYY年MM月 pickerDateDisplay: '' // YYYY年MM月
@ -251,6 +252,11 @@ Page({
lastUpdateTime: this.formatCacheTime(Date.now()) lastUpdateTime: this.formatCacheTime(Date.now())
}) })
// 关键修复:保存推荐码到本地存储,供全局分享使用
if (statsData.referralCode) {
wx.setStorageSync('referralCode', statsData.referralCode)
}
// 保存到缓存 // 保存到缓存
this.saveStatsToCache(statsData) this.saveStatsToCache(statsData)
@ -372,18 +378,36 @@ Page({
* 转换记录数据格式 * 转换记录数据格式
*/ */
transformRecord(record) { transformRecord(record) {
let titleText = record.fromUserName || record.userName || '用户'; const fromUser = record.fromUser || record.from_user || record.user || record.fromUserInfo || null
let descText = 'VIP月卡'; const titleText =
if (record.amount > 100) descText = 'SVIP年卡'; record.fromUserName ||
record.from_user_name ||
if (record.fromUserLevel) { record.userName ||
descText = this.getUserLevelText(record.fromUserLevel); record.user_name ||
} else if (record.orderType === 'vip' || record.orderType === 'svip') { fromUser?.name ||
descText = record.orderType.toUpperCase() === 'SVIP' ? 'SVIP会员' : 'VIP会员'; fromUser?.nickname ||
} else if (record.orderType === 'identity_card') { '用户'
descText = '身份会员';
} else if (record.orderType === 'companion_chat') { const levelRaw =
descText = '陪伴聊天'; record.fromUserLevel ??
record.from_user_level ??
record.fromUserRole ??
record.from_user_role ??
record.fromUserDistributorRole ??
record.distributorRole ??
record.role ??
record.userLevel ??
record.user_level ??
record.level ??
fromUser?.level ??
fromUser?.role
let descText = this.getUserLevelText(levelRaw)
if (descText === '普通用户') {
if (record.orderType === 'vip') descText = 'VIP月卡'
else if (record.orderType === 'svip') descText = 'SVIP年卡'
else if (record.orderType === 'identity_card') descText = '身份会员'
else if (record.orderType === 'companion_chat') descText = '陪伴聊天'
} }
const dateObj = new Date(record.created_at || record.createdAt); const dateObj = new Date(record.created_at || record.createdAt);
@ -403,7 +427,14 @@ Page({
statusText: record.status === 'pending' ? '待结算' : '已结算', statusText: record.status === 'pending' ? '待结算' : '已结算',
time: this.formatTime(record.created_at || record.createdAt), time: this.formatTime(record.created_at || record.createdAt),
orderNo: record.orderId ? `ORD${record.orderId.substring(0, 12)}` : 'ORD2024012401', orderNo: record.orderId ? `ORD${record.orderId.substring(0, 12)}` : 'ORD2024012401',
userAvatar: record.userAvatar || record.fromUserAvatar || record.avatar || '', userAvatar: imageUrl.getAvatarUrl(
record.userAvatar ||
record.user_avatar ||
record.fromUserAvatar ||
record.from_user_avatar ||
record.avatar ||
fromUser?.avatar
),
listTitle: titleText, listTitle: titleText,
fmtTime: fmtTime, fmtTime: fmtTime,
} }
@ -411,19 +442,29 @@ Page({
getUserLevelText(level) { getUserLevelText(level) {
const levelMap = { const levelMap = {
'vip': 'VIP会员', 'vip_month': 'VIP月卡',
'svip': 'SVIP会员', 'vip_monthly': 'VIP月卡',
'vip_month_card': 'VIP月卡',
'vip': 'VIP月卡',
'svip_year': 'SVIP年卡',
'svip_yearly': 'SVIP年卡',
'svip_year_card': 'SVIP年卡',
'svip': 'SVIP年卡',
'guardian': '守护会员', 'guardian': '守护会员',
'companion': '陪伴会员', 'companion': '陪伴会员',
'partner': '城市合伙人', 'partner': '城市合伙人',
'1': '普通用户', '1': '普通用户',
'2': 'VIP会员', '2': 'VIP月卡',
'3': 'SVIP会员', '3': 'SVIP年卡',
'4': '守护会员', '4': '守护会员',
'5': '陪伴会员', '5': '陪伴会员',
'6': '城市合伙人' '6': '城市合伙人'
}; };
return levelMap[level] || levelMap['1']; if (level === null || level === undefined || level === '') return levelMap['1']
const normalized = typeof level === 'number' ? String(level) : String(level).trim()
if (levelMap[normalized]) return levelMap[normalized]
if (/(会员|月卡|年卡|合伙人)/.test(normalized)) return normalized
return levelMap['1']
}, },
getCardTitle(type) { getCardTitle(type) {

View File

@ -42,5 +42,27 @@ Page({
} finally { } finally {
this.setData({ loading: false }) this.setData({ loading: false })
} }
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '智慧康养 - 守护您的健康生活',
path: `/pages/eldercare/eldercare?referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '智慧康养 - 守护您的健康生活',
query: `referralCode=${referralCode}`
}
} }
}) })

View File

@ -825,5 +825,27 @@ Page({
} }
} }
wx.switchTab({ url: path }) wx.switchTab({ url: path })
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '休闲文娱 - 精彩活动等你来',
path: `/pages/entertainment/entertainment?referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '休闲文娱 - 精彩活动等你来',
query: `referralCode=${referralCode}`
}
} }
}) })

View File

@ -129,7 +129,17 @@ Page({
title: '提示', title: '提示',
content: '请联系在线客服', content: '请联系在线客服',
showCancel: false, showCancel: false,
confirmText: '知道了' confirmText: '知道了',
success: (res) => {
if (res.confirm) {
wx.setClipboardData({
data: 'mmj20259999',
success: () => {
//wx.showToast({ title: '已复制', icon: 'success' })
}
})
}
}
}) })
}, },

View File

@ -343,5 +343,27 @@ Page({
wx.showToast({ title: '保存失败', icon: 'none' }) wx.showToast({ title: '保存失败', icon: 'none' })
} }
} }
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '快乐学堂 - 活到老 学到老',
path: `/pages/happy-school/happy-school?referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '快乐学堂 - 活到老 学到老',
query: `referralCode=${referralCode}`
}
} }
}) })

View File

@ -256,5 +256,29 @@ Page({
} finally { } finally {
wx.hideLoading() wx.hideLoading()
} }
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
const { isReapply } = this.data
return {
title: '家政保洁服务师 - 诚邀您的加入',
path: `/pages/housekeeping-apply/housekeeping-apply?isReapply=${isReapply}&referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
const { isReapply } = this.data
return {
title: '家政保洁服务师 - 诚邀您的加入',
query: `isReapply=${isReapply}&referralCode=${referralCode}`
}
} }
}) })

View File

@ -101,6 +101,27 @@ Page({
this.loadCharacters() this.loadCharacters()
this.loadHeartBalance() this.loadHeartBalance()
this.loadUnlockConfig() this.loadUnlockConfig()
this.ensureReferralCode()
},
/**
* 确保本地有推荐码用于分享
*/
async ensureReferralCode() {
if (!app.globalData.isLoggedIn) return
// 如果本地已有,暂不刷新
if (wx.getStorageSync('referralCode')) return
try {
const res = await api.commission.getStats()
if (res.success && res.data && res.data.referralCode) {
wx.setStorageSync('referralCode', res.data.referralCode)
console.log('[index] 已同步推荐码:', res.data.referralCode)
}
} catch (err) {
console.log('[index] 同步推荐码失败(非阻断)', err)
}
}, },
/** /**

View File

@ -330,5 +330,27 @@ Page({
}) })
} }
}) })
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '兴趣搭子 - 寻找志同道合的伙伴',
path: `/pages/interest-partner/interest-partner?referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '兴趣搭子 - 寻找志同道合的伙伴',
query: `referralCode=${referralCode}`
}
} }
}) })

View File

@ -311,5 +311,29 @@ Page({
} finally { } finally {
wx.hideLoading() wx.hideLoading()
} }
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
const { isReapply } = this.data
return {
title: '陪诊师招募 - 诚邀您的加入',
path: `/pages/medical-apply/medical-apply?isReapply=${isReapply}&referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
const { isReapply } = this.data
return {
title: '陪诊师招募 - 诚邀您的加入',
query: `isReapply=${isReapply}&referralCode=${referralCode}`
}
} }
}) })

View File

@ -48,13 +48,17 @@ Page({
const list = orders.map((o) => ({ const list = orders.map((o) => ({
id: o.id || o.orderNo, id: o.id || o.orderNo,
remark: this.formatOrderType(o.orderType || 'order'), // Use title/desc from API, fallback to existing logic
title: o.title || this.formatOrderType(o.orderType || 'order'),
desc: o.desc || '',
amountText: this.formatAmount(o.amount), amountText: this.formatAmount(o.amount),
status: this.formatStatus(o.status), // Use statusText from API, fallback to local formatting
status: o.statusText || this.formatStatus(o.status),
statusClass: `status-${o.status}`, statusClass: `status-${o.status}`,
// Handle createdAt
createdAtText: this.formatDateTime(new Date(o.createdAt || o.created_at || Date.now())), createdAtText: this.formatDateTime(new Date(o.createdAt || o.created_at || Date.now())),
// visual adjustment: ensure it looks like income/recharge style transactionType: o.orderType || 'recharge',
transactionType: o.orderType || 'recharge' orderNo: o.orderNo
})); }));
this.setData({ list }); this.setData({ list });

View File

@ -17,24 +17,24 @@
</view> </view>
<view class="wrap" style="padding-top: {{totalNavHeight + 60}}px; padding-bottom: 40rpx;"> <view class="wrap" style="padding-top: {{totalNavHeight + 60}}px; padding-bottom: 40rpx;">
<view class="card"> <view wx:if="{{loading}}" class="loading">加载中...</view>
<view class="list"> <view wx:elif="{{list.length === 0}}" class="empty">暂无数据</view>
<view wx:if="{{loading}}" class="loading">加载中...</view> <view wx:else class="order-list">
<view wx:elif="{{list.length === 0}}" class="empty">暂无数据</view> <view class="order-card" wx:for="{{list}}" wx:key="id">
<view wx:else> <view class="card-header">
<view class="row" wx:for="{{list}}" wx:key="id"> <text class="order-time">{{item.createdAtText}}</text>
<view class="row-left"> <text class="order-status {{item.statusClass}}">{{item.status}}</text>
<text class="row-title">{{item.remark || item.transactionType}}</text> </view>
<text class="row-sub">{{item.createdAtText}}</text> <view class="card-body">
</view> <view class="body-left">
<view class="row-right"> <text class="order-title">{{item.title}}</text>
<text class="row-amount">{{item.amountText}}</text> <text class="order-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
<text class="row-status {{item.statusClass}}">{{item.status}}</text> </view>
</view> <view class="body-right">
<text class="order-amount">{{item.amountText}}</text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>

View File

@ -13,27 +13,27 @@
position: fixed; position: fixed;
left: 0; left: 0;
right: 0; right: 0;
height: 120rpx; height: 100rpx;
background: #ffffff; background: #ffffff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
z-index: 100; z-index: 100;
border-bottom: 2rpx solid #f3f4f6; box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.02);
} }
.tab-item { .tab-item {
font-size: 28rpx; font-size: 32rpx;
color: #6b7280; color: #6b7280;
font-weight: 600; font-weight: 500;
position: relative; position: relative;
height: 80rpx; height: 100rpx;
line-height: 100rpx; line-height: 100rpx;
} }
.tab-item.active { .tab-item.active {
color: #b06ab3; color: #b06ab3;
font-weight: 900; font-weight: 700;
} }
.tab-item.active::after { .tab-item.active::after {
@ -43,94 +43,92 @@
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 40rpx; width: 40rpx;
height: 6rpx; height: 10rpx;
background: #b06ab3; background: #b06ab3;
border-radius: 3rpx; border-radius: 3rpx;
} }
.card {
background: #ffffff;
border-radius: 40rpx;
padding: 24rpx;
box-shadow: 0 10rpx 20rpx rgba(17, 24, 39, 0.04);
}
.loading, .loading,
.empty { .empty {
text-align: center; text-align: center;
color: #9ca3af; color: #9ca3af;
font-weight: 800; font-weight: 500;
padding: 80rpx 0; padding: 80rpx 0;
font-size: 28rpx;
} }
.row { /* Order List Styles */
padding: 24rpx 8rpx; .order-list {
padding-bottom: 40rpx;
}
.order-card {
background: #ffffff;
border-radius: 24rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
border-bottom: 2rpx solid #f3f4f6; align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f3f4f6;
} }
.row:last-child { .order-time {
border-bottom: 0; font-size: 32rpx;
color: #353434ff;
} }
.row-left { .order-status {
flex: 1; font-size: 30rpx;
min-width: 0;
}
.row-title {
display: block;
font-size: 34rpx;
font-weight: 900;
color: #111827;
}
.row-sub {
display: block;
margin-top: 10rpx;
font-size: 26rpx;
color: #9ca3af;
font-weight: 600; font-weight: 600;
} }
.row-right { .card-body {
text-align: right; display: flex;
justify-content: space-between;
align-items: center;
} }
.row-amount { .body-left {
display: block; flex: 1;
margin-right: 20rpx;
display: flex;
flex-direction: column;
}
.order-title {
font-size: 36rpx; font-size: 36rpx;
font-weight: 900; font-weight: 700;
color: #111827;
margin-bottom: 8rpx;
line-height: 1.4;
}
.order-desc {
font-size: 30rpx;
color: #6b7280;
line-height: 1.4;
}
.body-right {
flex-shrink: 0;
}
.order-amount {
font-size: 38rpx;
font-weight: 800;
color: #b06ab3; color: #b06ab3;
} }
.row-status { /* Status Colors */
display: block; .status-completed { color: #10B981; }
margin-top: 10rpx; .status-paid { color: #3B82F6; }
font-size: 26rpx; .status-pending { color: #F59E0B; }
color: #6b7280; .status-cancelled { color: #9CA3AF; }
font-weight: 700; .status-refunded { color: #EF4444; }
}
.status-completed {
color: #10B981;
}
.status-paid {
color: #3B82F6;
}
.status-pending {
color: #F59E0B;
}
.status-cancelled {
color: #9CA3AF;
}
.status-refunded {
color: #EF4444;
}

View File

@ -325,5 +325,27 @@ Page({
console.error('保存失败', err) console.error('保存失败', err)
wx.showToast({ title: '保存失败', icon: 'none' }) wx.showToast({ title: '保存失败', icon: 'none' })
} }
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '户外郊游 - 结伴同行 领略自然',
path: `/pages/outdoor-activities/outdoor-activities?referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '户外郊游 - 结伴同行 领略自然',
query: `referralCode=${referralCode}`
}
} }
}) })

View File

@ -52,6 +52,14 @@ Page({
auditStatus: app.globalData.auditStatus auditStatus: app.globalData.auditStatus
}); });
wx.hideTabBar({ animation: false }); wx.hideTabBar({ animation: false });
// Throttle loadAll to prevent frequent calls (e.g. switching tabs quickly)
const now = Date.now();
if (this.lastLoadTime && now - this.lastLoadTime < 2000) {
return;
}
this.lastLoadTime = now;
this.loadAll(); this.loadAll();
}, },
@ -62,15 +70,27 @@ Page({
wx.showNavigationBarLoading(); wx.showNavigationBarLoading();
try { try {
if (isLoggedIn) { if (isLoggedIn) {
// Stage 1: Critical user info and unread counts (Visual priority)
await Promise.all([ await Promise.all([
this.loadMe(), this.loadMe(),
this.loadBalance(), this.loadBalance(),
this.loadCommission(),
this.loadCounts(),
this.loadUnreadCount() this.loadUnreadCount()
]); ]);
this.checkRegistrationReward();
this.checkGf100Popup(); // Stage 2: Financial stats (Slight delay to avoid 429)
setTimeout(async () => {
if (!this.data.isLoggedIn) return;
await this.loadCommission();
// Run loadCounts separately as it triggers multiple sub-requests
await this.loadCounts();
}, 300);
// Stage 3: Popups and Rewards (Lowest priority)
setTimeout(() => {
if (!this.data.isLoggedIn) return;
this.checkRegistrationReward();
this.checkGf100Popup();
}, 800);
} else { } else {
this.setData({ this.setData({
me: { nickname: '未登录', avatar: this.data.defaultAvatar }, me: { nickname: '未登录', avatar: this.data.defaultAvatar },
@ -141,7 +161,7 @@ Page({
const roleMap = { const roleMap = {
'soulmate': { text: '心伴会员', class: 'vip-soulmate' }, 'soulmate': { text: '心伴会员', class: 'vip-soulmate' },
'guardian': { text: '守护会员', class: 'vip-guardian' }, 'guardian': { text: '守护会员', class: 'vip-guardian' },
'companion': { text: '伴会员', class: 'vip-companion' }, 'companion': { text: '伴会员', class: 'vip-companion' },
'listener': { text: '倾听会员', class: 'vip-listener' }, 'listener': { text: '倾听会员', class: 'vip-listener' },
'partner': { text: '城市合伙人', class: 'vip-partner' } 'partner': { text: '城市合伙人', class: 'vip-partner' }
}; };
@ -598,7 +618,13 @@ Page({
try { try {
const res = await api.commission.getStats() const res = await api.commission.getStats()
if (res.success && res.data) { if (res.success && res.data) {
this.setData({ referralCode: res.data.referralCode || '' }) const code = res.data.referralCode || ''
this.setData({ referralCode: code })
// 关键修复:保存推荐码到本地存储,供全局分享使用
if (code) {
wx.setStorageSync('referralCode', code)
}
} }
} catch (err) { } catch (err) {
console.error('加载推荐码失败:', err) console.error('加载推荐码失败:', err)

View File

@ -43,7 +43,7 @@
<view class="vip-header-row"> <view class="vip-header-row">
<text class="vip-label">我的会员</text> <text class="vip-label">我的会员</text>
</view> </view>
<text class="vip-main-text">心伴会员</text> <text class="vip-main-text">{{vip.levelText || '会员'}}</text>
</view> </view>
<button class="btn-reset vip-action-btn">立即充值</button> <button class="btn-reset vip-action-btn">立即充值</button>
<!-- Decorative Circle --> <!-- Decorative Circle -->

View File

@ -289,5 +289,27 @@ Page({
} }
wx.switchTab({ url: path }) wx.switchTab({ url: path })
} }
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '综合服务 - 优质生活服务平台',
path: `/pages/service/service?referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '综合服务 - 优质生活服务平台',
query: `referralCode=${referralCode}`
}
} }
}) })

View File

@ -64,7 +64,7 @@ Page({
try { try {
const { activeTab } = this.data const { activeTab } = this.data
const params = { const params = {
category: 'city', // 单身聚会通常属于同城活动 category: 'singles-party',
limit: 100 limit: 100
} }
@ -276,5 +276,27 @@ Page({
console.error('保存失败', err) console.error('保存失败', err)
wx.showToast({ title: '保存失败', icon: 'none' }) wx.showToast({ title: '保存失败', icon: 'none' })
} }
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '单身聚会 - 遇见心动的TA',
path: `/pages/singles-party/singles-party?referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '单身聚会 - 遇见心动的TA',
query: `referralCode=${referralCode}`
}
} }
}) })

View File

@ -4,6 +4,16 @@ const api = require('../../utils/api')
const util = require('../../utils/util') const util = require('../../utils/util')
const imageUrl = require('../../utils/imageUrl') const imageUrl = require('../../utils/imageUrl')
// 常用表情
const EMOJIS = [
"😊", "😀", "😁", "😃", "😂", "🤣", "😅", "😆", "😉", "😋", "😎", "😍", "😘", "🥰", "😗", "😙",
"🙂", "🤗", "🤩", "🤔", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮", "😯", "😪", "😫", "😴",
"😌", "😛", "😜", "😝", "😒", "😓", "😔", "😕", "🙃", "😲", "😖", "😞", "😟", "😤", "😢", "😭",
"😨", "😩", "😬", "😰", "😱", "😳", "😵", "😡", "😠", "😷", "🤒", "🤕", "😇", "🥳", "🥺",
"👋", "👌", "✌️", "🤞", "👍", "👎", "👏", "🙌", "🤝", "🙏", "💪", "❤️", "🧡", "💛", "💚", "💙",
"💜", "🖤", "💔", "💕", "💖", "💗", "💘", "💝", "🌹", "🌺", "🌻", "🌼", "🌷", "🎉", "🎊", "🎁"
]
Page({ Page({
data: { data: {
statusBarHeight: 44, statusBarHeight: 44,
@ -16,7 +26,17 @@ Page({
ticketId: '', ticketId: '',
scrollIntoView: '', scrollIntoView: '',
scrollTop: 0, scrollTop: 0,
pollingTimer: null pollingTimer: null,
// 新增状态
isVoiceMode: false,
isRecording: false,
showEmoji: false,
showMorePanel: false,
voiceCancelHint: false,
recordingDuration: 0,
emojis: EMOJIS,
playingVoiceId: null
}, },
onLoad() { onLoad() {
@ -74,7 +94,6 @@ Page({
this.setData({ ticketId: latestTicket.id }) this.setData({ ticketId: latestTicket.id })
await this.loadMessages(latestTicket.id) await this.loadMessages(latestTicket.id)
} else { } else {
// 如果没有工单,可以在首次发送消息时创建
console.log('[support] No existing tickets found.') console.log('[support] No existing tickets found.')
} }
} catch (err) { } catch (err) {
@ -95,7 +114,11 @@ Page({
const messages = res.data.messages.map(msg => ({ const messages = res.data.messages.map(msg => ({
id: msg.id, id: msg.id,
isMe: msg.senderType === 'user', isMe: msg.senderType === 'user',
text: msg.content, text: msg.type === 'text' ? msg.content : (msg.type === 'image' ? '[图片]' : (msg.type === 'voice' ? '[语音]' : msg.content)),
type: msg.type || 'text',
imageUrl: msg.type === 'image' ? msg.content : '',
audioUrl: msg.type === 'voice' ? msg.content : '',
duration: msg.duration || 0,
time: util.formatTime(new Date(msg.createdAt), 'HH:mm'), time: util.formatTime(new Date(msg.createdAt), 'HH:mm'),
senderName: msg.senderName senderName: msg.senderName
})) }))
@ -119,7 +142,25 @@ Page({
const content = this.data.inputText.trim() const content = this.data.inputText.trim()
if (!content || this.isSending) return if (!content || this.isSending) return
// 收起键盘和面板
this.setData({
showEmoji: false,
showMorePanel: false
})
await this.sendMessage(content, 'text')
},
/**
* 统一发送消息方法
* @param {string} content - 消息内容文本或URL
* @param {string} type - 消息类型 (text/image/voice)
* @param {number} duration - 语音时长
*/
async sendMessage(content, type = 'text', duration = 0) {
if (this.isSending) return
this.isSending = true this.isSending = true
const tempId = util.generateId() const tempId = util.generateId()
const now = new Date() const now = new Date()
@ -127,39 +168,59 @@ Page({
const userMsg = { const userMsg = {
id: tempId, id: tempId,
isMe: true, isMe: true,
text: content, text: type === 'text' ? content : (type === 'image' ? '[图片]' : '[语音]'),
time: util.formatTime(now, 'HH:mm') type: type,
imageUrl: type === 'image' ? content : '',
audioUrl: type === 'voice' ? content : '',
duration: duration,
time: util.formatTime(now, 'HH:mm'),
uploading: true
} }
this.setData({ this.setData({
messages: [...this.data.messages, userMsg], messages: [...this.data.messages, userMsg],
inputText: '', inputText: '',
inputFocus: true inputFocus: type === 'text' // 仅文本发送后聚焦
}, () => { }, () => {
this.scrollToBottom() this.scrollToBottom()
}) })
try { try {
const payload = {
content: content,
type: type,
duration: duration,
userName: app.globalData.userInfo?.nickname || '访客'
}
if (this.data.ticketId) { if (this.data.ticketId) {
// 回复已有工单 // 回复已有工单
await api.customerService.reply({ await api.customerService.reply({
ticketId: this.data.ticketId, ticketId: this.data.ticketId,
content: content, ...payload
userName: app.globalData.userInfo?.nickname || '访客'
}) })
} else { } else {
// 创建新工单 // 创建新工单
const guestId = wx.getStorageSync('guestId') const guestId = wx.getStorageSync('guestId')
const res = await api.customerService.create({ const res = await api.customerService.create({
category: 'other', category: 'other',
content: content, guestId: guestId,
userName: app.globalData.userInfo?.nickname || '访客', ...payload
guestId: guestId
}) })
if (res.success && res.data) { if (res.success && res.data) {
this.setData({ ticketId: res.data.ticketId }) this.setData({ ticketId: res.data.ticketId })
} }
} }
// 更新本地消息状态
const messages = this.data.messages.map(msg => {
if (msg.id === tempId) {
return { ...msg, uploading: false }
}
return msg
})
this.setData({ messages })
// 发送后立即拉取一次 // 发送后立即拉取一次
if (this.data.ticketId) { if (this.data.ticketId) {
await this.loadMessages(this.data.ticketId) await this.loadMessages(this.data.ticketId)
@ -167,6 +228,15 @@ Page({
} catch (err) { } catch (err) {
console.error('[support] send message error:', err) console.error('[support] send message error:', err)
wx.showToast({ title: '发送失败', icon: 'none' }) wx.showToast({ title: '发送失败', icon: 'none' })
// 更新失败状态
const messages = this.data.messages.map(msg => {
if (msg.id === tempId) {
return { ...msg, uploading: false, error: true }
}
return msg
})
this.setData({ messages })
} finally { } finally {
this.isSending = false this.isSending = false
} }
@ -203,12 +273,222 @@ Page({
}, },
onTapChatArea() { onTapChatArea() {
this.setData({ inputFocus: false }) this.setData({
inputFocus: false,
showEmoji: false,
showMorePanel: false
})
}, },
scrollToBottom() { scrollToBottom() {
this.setData({ this.setData({
scrollIntoView: 'chat-bottom-anchor' scrollIntoView: 'chat-bottom-anchor'
}) })
},
// ==================== 底部功能区逻辑 ====================
onVoiceMode() {
this.setData({
isVoiceMode: !this.data.isVoiceMode,
showEmoji: false,
showMorePanel: false,
inputFocus: false
})
},
onEmojiToggle() {
this.setData({
inputFocus: false
})
setTimeout(() => {
this.setData({
showEmoji: !this.data.showEmoji,
showMorePanel: false,
isVoiceMode: false
})
}, 50)
},
onAddMore() {
this.setData({
inputFocus: false
})
setTimeout(() => {
this.setData({
showMorePanel: !this.data.showMorePanel,
showEmoji: false,
isVoiceMode: false
})
}, 50)
},
onClosePanels() {
this.setData({
showEmoji: false,
showMorePanel: false
})
},
onEmojiSelect(e) {
const emoji = e.currentTarget.dataset.emoji
this.setData({
inputText: this.data.inputText + emoji
})
},
// ==================== 语音录制 ====================
onVoiceTouchStart(e) {
this.touchStartY = e.touches[0].clientY
this.setData({
isRecording: true,
voiceCancelHint: false,
recordingDuration: 0
})
const recorderManager = wx.getRecorderManager()
recorderManager.start({
duration: 60000,
format: 'mp3'
})
this.recorderManager = recorderManager
this.recordingTimer = setInterval(() => {
this.setData({
recordingDuration: this.data.recordingDuration + 1
})
}, 1000)
},
onVoiceTouchMove(e) {
const moveY = e.touches[0].clientY
const diff = this.touchStartY - moveY
this.setData({
voiceCancelHint: diff > 50
})
},
onVoiceTouchEnd() {
clearInterval(this.recordingTimer)
const { voiceCancelHint, recordingDuration } = this.data
this.setData({ isRecording: false })
if (this.recorderManager) {
this.recorderManager.stop()
}
if (voiceCancelHint) {
util.showToast('已取消')
return
}
if (recordingDuration < 1) {
util.showError('录音时间太短')
return
}
this.recorderManager.onStop(async (res) => {
const tempFilePath = res.tempFilePath
// 上传语音
try {
const uploadRes = await api.uploadFile(tempFilePath, 'audio')
if (uploadRes.success && uploadRes.data && uploadRes.data.url) {
await this.sendMessage(uploadRes.data.url, 'voice', recordingDuration)
} else {
throw new Error('Upload failed')
}
} catch (err) {
console.error('语音上传失败', err)
util.showError('语音发送失败')
}
})
},
onVoiceTouchCancel() {
clearInterval(this.recordingTimer)
this.setData({ isRecording: false })
if (this.recorderManager) {
this.recorderManager.stop()
}
},
// ==================== 图片/拍照 ====================
onChooseImage() {
this.setData({ showMorePanel: false })
wx.chooseMedia({
count: 9,
mediaType: ['image'],
sourceType: ['album'],
success: (res) => {
res.tempFiles.forEach(file => {
this.uploadAndSendImage(file.tempFilePath)
})
}
})
},
onTakePhoto() {
this.setData({ showMorePanel: false })
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['camera'],
camera: 'back',
success: (res) => {
this.uploadAndSendImage(res.tempFiles[0].tempFilePath)
}
})
},
async uploadAndSendImage(filePath) {
try {
const uploadRes = await api.uploadFile(filePath, 'uploads')
if (uploadRes.success && uploadRes.data && uploadRes.data.url) {
await this.sendMessage(uploadRes.data.url, 'image')
} else {
throw new Error('Upload failed')
}
} catch (err) {
console.error('图片上传失败', err)
util.showError('图片发送失败')
}
},
// ==================== 预览与播放 ====================
onPreviewImage(e) {
const url = e.currentTarget.dataset.url
wx.previewImage({
current: url,
urls: [url]
})
},
onPlayVoice(e) {
const { id, url } = e.currentTarget.dataset
if (!url) return
const innerAudioContext = wx.createInnerAudioContext()
innerAudioContext.src = url
innerAudioContext.play()
this.setData({ playingVoiceId: id })
innerAudioContext.onEnded(() => {
this.setData({ playingVoiceId: null })
innerAudioContext.destroy()
})
innerAudioContext.onError((res) => {
console.error(res.errMsg)
this.setData({ playingVoiceId: null })
})
} }
}) })

View File

@ -58,9 +58,23 @@
<image class="chat-avatar" src="/images/icon-headphones.png" mode="aspectFit"></image> <image class="chat-avatar" src="/images/icon-headphones.png" mode="aspectFit"></image>
</view> </view>
<view class="message-content"> <view class="message-content">
<view class="chat-bubble other"> <!-- 文字消息 -->
<view class="chat-bubble other" wx:if="{{item.type === 'text'}}">
<text class="chat-text" decode="{{true}}">{{item.text}}</text> <text class="chat-text" decode="{{true}}">{{item.text}}</text>
</view> </view>
<!-- 图片消息 -->
<view class="chat-bubble-image other" wx:elif="{{item.type === 'image'}}">
<image class="message-image" src="{{item.imageUrl}}" mode="widthFix" bindtap="onPreviewImage" data-url="{{item.imageUrl}}"></image>
</view>
<!-- 语音消息 -->
<view class="chat-bubble voice other {{playingVoiceId === item.id ? 'playing' : ''}}" wx:elif="{{item.type === 'voice'}}" bindtap="onPlayVoice" data-id="{{item.id}}" data-url="{{item.audioUrl}}">
<view class="voice-waves">
<view class="voice-wave-bar"></view>
<view class="voice-wave-bar"></view>
<view class="voice-wave-bar"></view>
</view>
<text class="voice-duration">{{item.duration || 1}}″</text>
</view>
<view class="message-actions"> <view class="message-actions">
<text class="message-time">{{item.time}}</text> <text class="message-time">{{item.time}}</text>
</view> </view>
@ -70,9 +84,23 @@
<!-- 用户消息(右侧) --> <!-- 用户消息(右侧) -->
<block wx:else> <block wx:else>
<view class="message-content me"> <view class="message-content me">
<view class="chat-bubble me"> <!-- 文字消息 -->
<view class="chat-bubble me" wx:if="{{item.type === 'text'}}">
<text class="chat-text" decode="{{true}}">{{item.text}}</text> <text class="chat-text" decode="{{true}}">{{item.text}}</text>
</view> </view>
<!-- 图片消息 -->
<view class="chat-bubble-image me" wx:elif="{{item.type === 'image'}}">
<image class="message-image" src="{{item.imageUrl}}" mode="widthFix" bindtap="onPreviewImage" data-url="{{item.imageUrl}}"></image>
</view>
<!-- 语音消息 -->
<view class="chat-bubble voice me {{playingVoiceId === item.id ? 'playing' : ''}}" wx:elif="{{item.type === 'voice'}}" bindtap="onPlayVoice" data-id="{{item.id}}" data-url="{{item.audioUrl}}">
<text class="voice-duration">{{item.duration || 1}}″</text>
<view class="voice-waves">
<view class="voice-wave-bar"></view>
<view class="voice-wave-bar"></view>
<view class="voice-wave-bar"></view>
</view>
</view>
<text class="message-time">{{item.time}}</text> <text class="message-time">{{item.time}}</text>
</view> </view>
<view class="avatar-wrap user-avatar"> <view class="avatar-wrap user-avatar">
@ -101,10 +129,31 @@
</scroll-view> </scroll-view>
</view> </view>
<!-- 面板打开时的透明遮罩层 -->
<view class="panel-overlay" wx:if="{{showEmoji || showMorePanel}}" bindtap="onClosePanels"></view>
<!-- 底部输入区域 --> <!-- 底部输入区域 -->
<view class="bottom-input-area"> <view class="bottom-input-area {{showEmoji || showMorePanel ? 'panel-open' : ''}}">
<view class="input-container figma-input-container"> <view class="input-container figma-input-container">
<view class="figma-input-wrap"> <!-- 语音/键盘切换按钮 -->
<view class="figma-voice-btn" bindtap="onVoiceMode">
<image src="{{isVoiceMode ? '/images/icon-keyboard.png' : '/images/chat-input-voice.png'}}" class="figma-btn-icon" mode="aspectFit"></image>
</view>
<!-- 语音模式:按住说话按钮 -->
<view
wx:if="{{isVoiceMode}}"
class="voice-record-btn {{isRecording ? 'recording' : ''}}"
bindtouchstart="onVoiceTouchStart"
bindtouchmove="onVoiceTouchMove"
bindtouchend="onVoiceTouchEnd"
bindtouchcancel="onVoiceTouchCancel"
>
<text>{{isRecording ? (voiceCancelHint ? '松开 取消' : '松开 发送') : '按住 说话'}}</text>
</view>
<!-- 文字模式:输入框 -->
<view wx:else class="figma-input-wrap">
<input <input
class="figma-text-input" class="figma-text-input"
placeholder="请输入您要咨询的问题..." placeholder="请输入您要咨询的问题..."
@ -113,14 +162,80 @@
confirm-type="send" confirm-type="send"
bindconfirm="onSend" bindconfirm="onSend"
focus="{{inputFocus}}" focus="{{inputFocus}}"
adjust-position="{{!showEmoji}}"
hold-keyboard="{{true}}" hold-keyboard="{{true}}"
/> />
</view> </view>
<view class="figma-send-btn {{inputText.length > 0 ? 'active' : ''}}" bindtap="onSend"> <!-- 表情按钮 -->
<image src="/images/icon-send.png" class="figma-btn-icon" mode="aspectFit"></image> <view class="figma-emoji-btn {{showEmoji ? 'active' : ''}}" bindtap="onEmojiToggle">
<text class="send-text">发送</text> <image src="{{showEmoji ? '/images/icon-keyboard.png' : '/images/chat-input-emoji.png'}}" class="figma-btn-icon" mode="aspectFit"></image>
</view> </view>
<!-- 发送/更多按钮 -->
<view class="figma-send-btn" wx:if="{{inputText.length > 0 && !isVoiceMode}}" bindtap="onSend">
<image src="/images/icon-send.png" class="figma-btn-icon" mode="aspectFit"></image>
</view>
<view class="figma-add-btn {{showMorePanel ? 'active' : ''}}" wx:else bindtap="onAddMore">
<image src="/images/chat-input-plus.png" class="figma-btn-icon" mode="aspectFit"></image>
</view>
</view>
<!-- 表情面板 -->
<view class="emoji-panel" wx:if="{{showEmoji}}">
<scroll-view scroll-y class="emoji-scroll">
<view class="emoji-grid">
<view
class="emoji-item"
wx:for="{{emojis}}"
wx:key="*this"
data-emoji="{{item}}"
bindtap="onEmojiSelect"
>
<text class="emoji-text">{{item}}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 更多功能面板 -->
<view class="more-panel" wx:if="{{showMorePanel}}">
<view class="more-panel-content">
<view class="more-grid ai-chat-grid">
<!-- 照片 -->
<view class="more-item" bindtap="onChooseImage">
<view class="more-icon-wrap figma-style">
<image class="figma-action-icon" src="/images/chat-action-photo.png" mode="aspectFit"></image>
</view>
<text class="more-text">照片</text>
</view>
<!-- 拍摄 -->
<view class="more-item" bindtap="onTakePhoto">
<view class="more-icon-wrap figma-style">
<image class="figma-action-icon" src="/images/chat-action-camera.png" mode="aspectFit"></image>
</view>
<text class="more-text">拍摄</text>
</view>
</view>
</view>
<!-- 底部安全区域 -->
<view class="more-panel-safe"></view>
</view>
</view>
<!-- 语音录制提示浮层 -->
<view class="voice-recording-mask" wx:if="{{isRecording}}">
<view class="voice-recording-popup {{voiceCancelHint ? 'cancel' : ''}}">
<view class="voice-wave" wx:if="{{!voiceCancelHint}}">
<view class="wave-bar"></view>
<view class="wave-bar"></view>
<view class="wave-bar"></view>
<view class="wave-bar"></view>
<view class="wave-bar"></view>
</view>
<image wx:else class="cancel-icon" src="/images/icon-close.png" mode="aspectFit"></image>
<text class="voice-tip">{{voiceCancelHint ? '松开手指,取消发送' : '手指上划,取消发送'}}</text>
<text class="voice-duration-tip" wx:if="{{!voiceCancelHint}}">{{recordingDuration}}″</text>
</view> </view>
</view> </view>
</view> </view>

View File

@ -1,4 +1,5 @@
/* pages/support/support.wxss */ /* pages/support/support.wxss */
/* 样式复用自 AI聊天详情页 (pages/chat-detail/chat-detail.wxss) 以保持一致体验 */
/* 页面容器 */ /* 页面容器 */
.page-container { .page-container {
@ -9,7 +10,7 @@
position: relative; position: relative;
} }
/* 聊天区域包装器 */ /* 聊天区域包装器 - 使用固定定位确保正确布局 */
.chat-area-wrapper { .chat-area-wrapper {
position: fixed; position: fixed;
left: 0; left: 0;
@ -20,6 +21,17 @@
overflow: hidden; overflow: hidden;
} }
/* 面板打开时的透明遮罩层 */
.panel-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
z-index: 98;
}
/* 状态栏区域 */ /* 状态栏区域 */
.status-bar-area { .status-bar-area {
position: fixed; position: fixed;
@ -267,21 +279,46 @@
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
.figma-input-container { .input-container {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16rpx; gap: 16rpx;
padding: 24rpx 32rpx; padding: 24rpx 32rpx;
padding-bottom: 24rpx; padding-bottom: 16rpx;
}
/* Figma设计样式 - 底部输入区域 */
.figma-input-container {
display: flex;
align-items: center;
gap: 16rpx;
padding: 24rpx 20rpx;
padding-bottom: 20rpx;
}
.figma-voice-btn {
width: 80rpx;
height: 80rpx;
background: #F3F4F6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.figma-btn-icon {
width: 80rpx;
height: 80rpx;
} }
.figma-input-wrap { .figma-input-wrap {
flex: 1; flex: 1;
background: #F9FAFB; background: #F9FAFB;
border: 2rpx solid #F3F4F6; border: 2rpx solid #F3F4F6;
border-radius: 40rpx; border-radius: 32rpx;
padding: 0 32rpx; padding: 0 32rpx;
height: 120rpx; height: 96rpx;
display: flex; display: flex;
align-items: center; align-items: center;
} }
@ -289,43 +326,337 @@
.figma-text-input { .figma-text-input {
width: 100%; width: 100%;
height: 100%; height: 100%;
font-size: 38rpx; font-size: 36rpx;
color: #101828; color: #101828;
font-family: Arial, sans-serif;
} }
.figma-send-btn { .figma-emoji-btn {
width: 180rpx; width: 80rpx;
height: 88rpx; height: 80rpx;
background: #F3F4F6; background: transparent;
border-radius: 44rpx; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8rpx;
flex-shrink: 0; flex-shrink: 0;
transition: all 0.3s ease;
} }
.figma-send-btn.active { .figma-emoji-btn.active {
background: #E9D5FF;
}
.figma-send-btn {
width: 80rpx;
height: 80rpx;
background: #914584; background: #914584;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
} }
.figma-btn-icon { .figma-send-btn .figma-btn-icon {
width: 36rpx; width: 44rpx;
height: 36rpx; height: 44rpx;
filter: grayscale(1) opacity(0.5);
} }
.figma-send-btn.active .figma-btn-icon { .figma-add-btn {
width: 80rpx;
height: 80rpx;
background: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.figma-add-btn.active {
background: #FCE7F3;
}
/* 面板面板显示时,移除底部安全区域 */
.bottom-input-area.panel-open {
padding-bottom: 0;
}
/* 语音录制按钮 */
.voice-record-btn {
flex: 1;
background: #F3F4F6;
border: 2rpx solid #E5E7EB;
border-radius: 32rpx;
height: 96rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
color: #374151;
transition: all 0.15s;
}
.voice-record-btn:active,
.voice-record-btn.recording {
background: #E5E7EB;
transform: scale(0.98);
}
/* 录音提示浮层 */
.voice-recording-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
}
.voice-recording-popup {
width: 320rpx;
height: 320rpx;
background: rgba(0, 0, 0, 0.8);
border-radius: 32rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24rpx;
}
.voice-recording-popup.cancel {
background: rgba(220, 38, 38, 0.9);
}
.cancel-icon {
width: 80rpx;
height: 80rpx;
filter: brightness(0) invert(1); filter: brightness(0) invert(1);
} }
.send-text { .voice-wave {
font-size: 28rpx; display: flex;
font-weight: 700; align-items: center;
color: #9CA3AF; justify-content: center;
gap: 12rpx;
height: 100rpx;
} }
.figma-send-btn.active .send-text { .wave-bar {
width: 12rpx;
height: 40rpx;
background: #22C55E;
border-radius: 6rpx;
animation: wave 0.8s ease-in-out infinite;
}
.wave-bar:nth-child(1) { animation-delay: 0s; height: 40rpx; }
.wave-bar:nth-child(2) { animation-delay: 0.1s; height: 60rpx; }
.wave-bar:nth-child(3) { animation-delay: 0.2s; height: 80rpx; }
.wave-bar:nth-child(4) { animation-delay: 0.3s; height: 60rpx; }
.wave-bar:nth-child(5) { animation-delay: 0.4s; height: 40rpx; }
@keyframes wave {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.5); }
}
.voice-tip {
font-size: 28rpx;
color: #FFFFFF; color: #FFFFFF;
} }
.voice-duration-tip {
font-size: 48rpx;
font-weight: 700;
color: #FFFFFF;
}
/* 表情面板 */
.emoji-panel {
background: #FFFFFF;
border-top: 2rpx solid #F3F4F6;
}
.emoji-scroll {
height: 480rpx;
padding: 24rpx;
box-sizing: border-box;
}
.emoji-grid {
display: flex;
flex-wrap: wrap;
}
.emoji-item {
width: 12.5%;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
}
.emoji-text {
font-size: 56rpx;
line-height: 1;
}
.emoji-item:active {
background: #F3F4F6;
border-radius: 16rpx;
}
/* 更多功能面板 */
.more-panel {
background: #F5F5F5;
border-top: 2rpx solid #E5E7EB;
}
.more-panel-content {
padding: 40rpx 32rpx 24rpx;
}
.more-grid {
display: flex;
justify-content: space-between;
}
.more-grid.ai-chat-grid {
justify-content: center;
gap: 90rpx;
padding: 0 48rpx;
}
.more-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
width: 25%;
}
.more-icon-wrap {
width: 112rpx;
height: 112rpx;
background: #FFFFFF;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.more-icon-wrap.figma-style {
width: 128rpx;
height: 128rpx;
background: transparent;
border-radius: 0;
box-shadow: none;
}
.figma-action-icon {
width: 128rpx;
height: 128rpx;
}
.more-text {
font-size: 28rpx;
font-weight: 700;
color: #4A5565;
}
.more-panel-safe {
height: env(safe-area-inset-bottom);
background: #F5F5F5;
}
/* 图片消息气泡 */
.chat-bubble-image {
max-width: 400rpx;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.chat-bubble-image.other {
border-radius: 12rpx 24rpx 24rpx 24rpx;
}
.chat-bubble-image.me {
border-radius: 24rpx 12rpx 24rpx 24rpx;
}
.message-image {
width: 100%;
min-width: 200rpx;
max-width: 400rpx;
display: block;
}
/* 语音消息气泡 */
.chat-bubble.voice {
display: flex;
align-items: center;
gap: 16rpx;
min-width: 160rpx;
padding: 24rpx 32rpx;
}
.chat-bubble.voice.other {
flex-direction: row;
}
.chat-bubble.voice.me {
flex-direction: row-reverse;
}
.voice-waves {
display: flex;
align-items: center;
gap: 6rpx;
height: 40rpx;
}
.voice-wave-bar {
width: 6rpx;
height: 20rpx;
border-radius: 3rpx;
background: #9CA3AF;
}
.chat-bubble.voice.me .voice-wave-bar {
background: rgba(255, 255, 255, 0.7);
}
.voice-wave-bar:nth-child(1) { height: 16rpx; }
.voice-wave-bar:nth-child(2) { height: 28rpx; }
.voice-wave-bar:nth-child(3) { height: 20rpx; }
.chat-bubble.voice.playing .voice-wave-bar {
animation: voiceWave 0.8s ease-in-out infinite;
}
.chat-bubble.voice.playing .voice-wave-bar:nth-child(1) { animation-delay: 0s; }
.chat-bubble.voice.playing .voice-wave-bar:nth-child(2) { animation-delay: 0.2s; }
.chat-bubble.voice.playing .voice-wave-bar:nth-child(3) { animation-delay: 0.4s; }
@keyframes voiceWave {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.8); }
}
.voice-duration {
font-size: 28rpx;
color: #6B7280;
}
.chat-bubble.voice.me .voice-duration {
color: rgba(255, 255, 255, 0.9);
}

View File

@ -75,7 +75,10 @@ Page({
this.setData({ this.setData({
stats: { stats: {
todayReferrals: Number(d.todayReferrals || d.today_referrals || 0), todayReferrals: Number(d.todayReferrals || d.today_referrals || 0),
totalReferrals: Number(d.totalReferrals || d.total_referrals || 0), // 团队总人数:优先使用 teamMembers (新字段),兼容 totalReferrals
totalReferrals: Number(d.teamMembers || d.team_members || d.totalReferrals || d.total_referrals || 0),
// 直推人数:优先使用 directReferrals (新字段)
directReferrals: Number(d.directReferrals || d.direct_referrals || 0),
totalContribution: Number(d.totalContribution || d.total_contribution || 0).toFixed(2) totalContribution: Number(d.totalContribution || d.total_contribution || 0).toFixed(2)
}, },
cardTitle: currentRoleText cardTitle: currentRoleText
@ -92,12 +95,17 @@ Page({
// Flexible data extraction // Flexible data extraction
let rawList = []; let rawList = [];
let totalDirects = 0; // Initialize total count
if (Array.isArray(body.data)) { if (Array.isArray(body.data)) {
rawList = body.data; rawList = body.data;
totalDirects = rawList.length;
} else if (body.data && Array.isArray(body.data.list)) { } else if (body.data && Array.isArray(body.data.list)) {
rawList = body.data.list; rawList = body.data.list;
totalDirects = body.data.total || rawList.length; // Use total from API if available
} else if (body.list && Array.isArray(body.list)) { } else if (body.list && Array.isArray(body.list)) {
rawList = body.list; rawList = body.list;
totalDirects = body.total || rawList.length;
} }
console.log('[团队页面] rawList:', JSON.stringify(rawList.slice(0, 2), null, 2)); console.log('[团队页面] rawList:', JSON.stringify(rawList.slice(0, 2), null, 2));
@ -111,6 +119,7 @@ Page({
}; };
const list = rawList.map((x) => { const list = rawList.map((x) => {
// ... (mapping logic) ...
const user = x.user || {}; const user = x.user || {};
// Map fields robustly // Map fields robustly
let avatar = x.avatarUrl || x.avatar_url || x.userAvatar || user.avatarUrl || user.avatar_url || ''; let avatar = x.avatarUrl || x.avatar_url || x.userAvatar || user.avatarUrl || user.avatar_url || '';
@ -140,7 +149,17 @@ Page({
}; };
}); });
this.setData({ list }); // 优先使用 stats API 返回的 directReferrals
// 如果 stats API 未返回有效值 (<=0),则回退使用列表接口的 total 或长度
const currentStatsDirects = this.data.stats.directReferrals;
const finalDirects = (currentStatsDirects && currentStatsDirects > 0)
? currentStatsDirects
: totalDirects;
this.setData({
list,
'stats.directReferrals': finalDirects
});
} catch (err) { } catch (err) {
console.log('API failed, using mock data', err); console.log('API failed, using mock data', err);
this.setData({ this.setData({
@ -176,11 +195,11 @@ Page({
getCardTitle(type) { getCardTitle(type) {
const map = { const map = {
'guardian_card': '守护会员', 'guardian_card': '守护会员',
'companion_card': '伴会员', 'companion_card': '伴会员',
'soulmate_card': '心伴会员', 'soulmate_card': '心伴会员',
'listener_card': '倾听会员', 'listener_card': '倾听会员',
'guardian': '守护会员', 'guardian': '守护会员',
'companion': '伴会员', 'companion': '伴会员',
'soulmate': '心伴会员', 'soulmate': '心伴会员',
'listener': '倾听会员', 'listener': '倾听会员',
'identity_card': '身份会员', 'identity_card': '身份会员',

View File

@ -22,7 +22,7 @@
<view class="stats-row"> <view class="stats-row">
<view class="stat-col"> <view class="stat-col">
<text class="stat-label">直推人数</text> <text class="stat-label">直推人数</text>
<text class="stat-num">{{list.length}}</text> <text class="stat-num">{{stats.directReferrals || list.length}}</text>
</view> </view>
<view class="stat-col"> <view class="stat-col">
<text class="stat-label">团队总计</text> <text class="stat-label">团队总计</text>

View File

@ -279,7 +279,29 @@ Page({
} }
} catch (err) { } catch (err) {
console.error('点赞失败', err) console.error('点赞失败', err)
wx.showToast({ title: '操作失败', icon: 'none' }) wx.showToast({ title: '保存失败', icon: 'none' })
}
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '高端定制 - 专属主题旅行线路',
path: `/pages/theme-travel/theme-travel?referralCode=${referralCode}`
}
},
/**
* 分享到朋友圈
*/
onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
return {
title: '高端定制 - 专属主题旅行线路',
query: `referralCode=${referralCode}`
} }
}, },