Compare commits

...

4 Commits

Author SHA1 Message Date
zhiyun
638f13df1f Update: Merge remote changes and preserve local features 2026-02-05 01:11:49 +08:00
zhiyun
d32801cb5b Apply stashed changes and resolve conflicts 2026-02-05 01:07:28 +08:00
zhiyun
3ad56d0250 Merge updates from origin/master 2026-02-05 01:04:19 +08:00
zhiyun
4146309ff2 Merge remote-tracking branch 'origin/master' into master 2026-02-05 01:02:05 +08:00
34 changed files with 1740 additions and 309 deletions

View File

@ -71,6 +71,7 @@ Page({
// 滚动控制
scrollIntoView: '',
scrollTop: 0, // 当前滚动位置
scrollAnimation: false, // 控制滚动动画
// 加载状态
loading: true,
@ -127,7 +128,10 @@ Page({
// 免费畅聊相关
freeTime: null,
countdownText: ''
countdownText: '',
// 聊天模式 (ai | human)
chatMode: 'ai'
},
onLoad(options) {
@ -140,6 +144,10 @@ Page({
this.messageTimer = null
this.isProcessing = false
// 获取当前用户ID
const userId = app.globalData.userId || wx.getStorageSync(config.STORAGE_KEYS.USER_ID)
this.setData({ userId })
// 获取参数
const characterId = options.id || ''
const conversationId = options.conversationId || ''
@ -177,10 +185,20 @@ Page({
if (!this.data.loading) {
this.loadQuotaStatus()
}
// 开启消息轮询
this.startPolling()
},
onHide() {
// 页面隐藏时停止轮询
this.stopPolling()
},
onUnload() {
// 页面卸载时清理
this.stopPolling()
// 清除消息处理定时器
if (this.messageTimer) {
clearTimeout(this.messageTimer)
@ -201,6 +219,156 @@ Page({
}
},
/**
* 开启消息轮询 (智能自适应模式)
* 策略
* 1. 默认状态低频轮询 (每4秒)降低性能损耗
* 2. 交互状态当用户发送消息后进入"高速模式" (每1.5)持续60秒确保及时收到回复
*/
startPolling() {
this.stopPolling()
// 如果处于高速模式,使用短间隔
const interval = this.isFastPolling ? 1500 : 4000
console.log(`[chat-detail] 开启消息轮询 (模式: ${this.isFastPolling ? '高速' : '省流'}, 间隔: ${interval}ms)`)
this.pollingTimer = setTimeout(() => {
this.checkNewMessages().finally(() => {
// 递归调用,确保上一次请求结束后才开始下一次计时
// 页面未卸载且未停止轮询时继续
if (this.data.conversationId) {
this.startPolling()
}
})
}, interval)
},
/**
* 触发高速轮询模式
* 在用户发送消息后调用
*/
triggerFastPolling() {
console.log('[chat-detail] 激活高速轮询模式')
this.isFastPolling = true
this.startPolling() // 立即重启轮询以应用新间隔
// 清除旧的定时器
if (this.fastPollingTimeout) clearTimeout(this.fastPollingTimeout)
// 60秒后自动恢复普通模式
this.fastPollingTimeout = setTimeout(() => {
console.log('[chat-detail] 高速模式结束,恢复省流模式')
this.isFastPolling = false
// 不需要立即重启,下次轮询会自动使用新间隔
}, 60000)
},
/**
* 停止消息轮询
*/
stopPolling() {
if (this.pollingTimer) {
clearTimeout(this.pollingTimer)
this.pollingTimer = null
}
},
/**
* 检查新消息
*/
async checkNewMessages() {
// 如果正在加载中、正在发送消息或页面不可见,跳过
if (this.data.loading || this.data.loadingMore || this.data.isSending) return
const { characterId, messages, conversationId } = this.data
if (!characterId) return
try {
// 1. 获取最新的一页消息
// 添加时间戳防止缓存
const res = await api.chat.getChatHistoryByCharacter(characterId, {
limit: 20,
page: 1,
_t: Date.now()
})
if (res.success && res.data && res.data.length > 0) {
// 转换消息格式
const latestMessages = res.data.map(msg => this.transformMessage(msg))
// 筛选出本地不存在的新消息
// 重点:只筛选对方发来的消息(isMe: false),避免本地已存在的用户消息重复显示
// 用户自己的消息(isMe: true)由本地乐观更新处理,轮询时不重复添加
const newMessages = latestMessages.filter(msg => {
// 2. 本地必须不存在该ID
const exists = messages.some(m => m.id === msg.id)
return !exists
})
if (newMessages.length > 0) {
console.log('[chat-detail] 轮询发现新消息:', newMessages.length, '条')
// 将新消息追加到列表末尾
// 按时间排序确保顺序正确
newMessages.sort((a, b) => new Date(a.time) - new Date(b.time))
const updatedMessages = [...messages, ...newMessages]
this.setData({
messages: updatedMessages
}, () => {
this.scrollToBottom()
// 如果有新消息,标记会话已读
if (this.data.conversationId) {
this.markConversationAsRead(this.data.conversationId)
}
// 收到新消息时,如果是对方发的,也可以触发一下高速模式,以便快速接收连续回复
this.triggerFastPolling()
// 更新本地缓存
this.saveMessagesToCache(updatedMessages)
})
}
}
// 2. 检查会话模式状态
if (conversationId) {
// 降低频率只有在高速模式或者每5次轮询检查一次会话状态
// 简单起见,这里每次轮询都检查(因为是 silent 请求,且频率不高)
this.checkConversationStatus(conversationId)
}
} catch (err) {
// 静默失败,不打印过多日志
}
},
/**
* 检查会话状态 (AI/Human 模式)
*/
async checkConversationStatus(conversationId) {
try {
const res = await api.chat.getConversationDetail(conversationId)
if (res && res.success && res.data) {
const mode = res.data.currentMode || res.data.mode || 'ai'
// 如果模式发生变化,或者首次获取
if (mode !== this.data.chatMode) {
console.log('[chat-detail] 会话模式变更为:', mode)
this.setData({ chatMode: mode })
// 移除人工模式的显式提示,保持沉浸感
// 让用户感觉不到是真人在接管
}
}
} catch (err) {
// 静默失败
}
},
/**
* 标记会话已读
* 进入聊天详情页时调用清除未读数
@ -450,68 +618,103 @@ Page({
/**
* 加载聊天历史首次加载最近20条
* 优化策略优先从本地缓存加载实现"无感"体验防止后端切换模式导致消息丢失
*/
async loadChatHistory() {
const { characterId, pageSize } = this.data
if (!characterId) {
const welcomeMsg = {
id: 'welcome',
text: `你好!我是${this.data.character.name},很高兴认识你~`,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text'
}
this.setData({
messages: [welcomeMsg],
isFirstLoad: false,
hasMore: false
})
this.showWelcomeMessage()
return
}
// 1. 先尝试从本地缓存加载,确保秒开且不丢失历史
const cachedMessages = wx.getStorageSync(`chat_history_${characterId}`) || []
if (cachedMessages.length > 0) {
console.log(`[chat-detail] 从本地缓存加载了 ${cachedMessages.length} 条消息`)
this.setData({
messages: cachedMessages,
isFirstLoad: false,
hasMore: true // 假设还有更多,允许下拉
}, () => {
// 首次加载(缓存)使用无动画滚动,避免视觉跳动
this.scrollToBottom(false)
})
}
try {
console.log('[chat-detail] 开始加载聊天历史, characterId:', characterId)
console.log('[chat-detail] 开始从API同步聊天历史, characterId:', characterId)
// 首次只加载最近20条消息
const res = await api.chat.getChatHistoryByCharacter(characterId, {
limit: pageSize,
page: 1
page: 1,
_t: Date.now() // 防缓存
})
console.log('[chat-detail] API响应:', JSON.stringify(res).substring(0, 200))
if (res.success && res.data && res.data.length > 0) {
console.log('[chat-detail] 收到历史消息数量:', res.data.length)
console.log('[chat-detail] API返回消息数量:', res.data.length)
const messages = res.data.map(msg => this.transformMessage(msg))
const apiMessages = res.data.map(msg => this.transformMessage(msg))
// 合并策略以API数据为准更新但保留API未返回的本地旧数据(如果有)
// 简单起见如果本地为空直接用API如果本地有值做去重合并
let finalMessages = []
if (this.data.messages.length === 0) {
finalMessages = apiMessages
} else {
// 合并逻辑将API返回的新消息合并到现有列表中
// 1. 创建现有消息的ID映射
const existingIds = new Set(this.data.messages.map(m => m.id))
// 2. 找出API返回中本地没有的消息
const newApiMessages = apiMessages.filter(m => !existingIds.has(m.id))
// 3. 将新消息追加进来 (注意顺序)
// API通常返回按时间排序好的或者我们需要重排
finalMessages = [...this.data.messages, ...newApiMessages]
// 4. 按时间重新排序确保正确
finalMessages.sort((a, b) => new Date(a.time) - new Date(b.time))
}
// 只有当消息列表真的发生变化时才更新,避免闪烁
if (finalMessages.length !== this.data.messages.length) {
this.setData({
messages,
messages: finalMessages,
hasMore: res.data.length >= pageSize,
page: 1,
isFirstLoad: false
}, () => {
// API更新时也使用无动画滚动确保位置稳定
this.scrollToBottom(false)
// 更新缓存
this.saveMessagesToCache(finalMessages)
})
console.log('[chat-detail] 消息已设置, 当前数量:', this.data.messages.length)
console.log('[chat-detail] 首次加载完成,不自动滚动到底部')
} else {
console.log('[chat-detail] 没有历史记录,显示欢迎消息')
const welcomeMsg = {
id: 'welcome',
text: `你好!我是${this.data.character.name},很高兴认识你~`,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text'
console.log('[chat-detail] 消息列表无变化,跳过更新')
this.setData({ isFirstLoad: false })
}
} else {
console.log('[chat-detail] API未返回历史记录')
if (this.data.messages.length === 0) {
this.showWelcomeMessage()
}
this.setData({
messages: [welcomeMsg],
isFirstLoad: false,
hasMore: false
})
}
} catch (err) {
console.log('加载聊天历史失败:', err)
if (this.data.messages.length === 0) {
this.showWelcomeMessage()
}
}
},
/**
* 显示欢迎消息
*/
showWelcomeMessage() {
const welcomeMsg = {
id: 'welcome',
text: `你好!我是${this.data.character.name},很高兴认识你~`,
@ -524,7 +727,16 @@ Page({
isFirstLoad: false,
hasMore: false
})
}
},
/**
* 将消息保存到本地缓存
*/
saveMessagesToCache(messages) {
if (!this.data.characterId || !messages || messages.length === 0) return
// 只缓存最近100条避免Storage爆满
const messagesToSave = messages.slice(-100)
wx.setStorageSync(`chat_history_${this.data.characterId}`, messagesToSave)
},
/**
@ -591,10 +803,35 @@ Page({
* 转换消息格式
*/
transformMessage(msg) {
// 调试日志:打印原始消息结构,帮助排查 role/sender_id 问题
console.log('[chat-detail] transformMessage raw:', msg)
let isMe = false
const currentUserId = this.data.userId || app.globalData.userId || wx.getStorageSync(config.STORAGE_KEYS.USER_ID)
// 1. 优先使用 sender_id 判断 (最准确)
if (msg.sender_id && currentUserId) {
isMe = String(msg.sender_id) === String(currentUserId)
}
// 2. 其次使用 role 判断
else if (msg.role) {
// 只有明确是 user 且不是 assistant/system 时才认为是自己
isMe = msg.role === 'user'
}
// 3. 最后尝试 sender_type
else if (msg.sender_type) {
isMe = msg.sender_type === 'user'
}
// 4. 强制修正:如果 role 是 assistant 或 system绝对不是我
if (msg.role === 'assistant' || msg.role === 'system') {
isMe = false
}
const baseMessage = {
id: msg.id,
text: msg.content,
isMe: msg.role === 'user',
isMe: isMe,
time: util.formatTime(msg.created_at || msg.timestamp, 'HH:mm'),
type: msg.message_type || 'text'
}
@ -744,6 +981,9 @@ Page({
// 只检查输入是否为空
if (!inputText.trim()) return
// 触发高速轮询模式,确保及时收到回复
this.triggerFastPolling()
// 检查登录
if (app.checkNeedLogin()) return
@ -842,8 +1082,10 @@ Page({
console.log('[chat-detail] 合并处理消息:', messagesToProcess.length, '条')
console.log('[chat-detail] 合并后内容:', combinedMessage)
// 显示AI正在输入
// 显示AI正在输入仅在AI模式下
if (this.data.chatMode === 'ai') {
this.setData({ isTyping: true })
}
try {
// 构建对话历史最近10条消息只包含文字消息
@ -918,10 +1160,14 @@ Page({
})
}
// 只有当有回复内容时才添加AI消息
// 如果是人工模式(human)后端可能只返回成功但不返回content或者content为空
const content = res.data.content || res.data.message
if (content) {
// 添加AI回复
const aiMessage = {
id: res.data.id || util.generateId(),
text: res.data.content || res.data.message,
text: content,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
audioUrl: res.data.audio_url,
@ -934,6 +1180,9 @@ Page({
// AI回复后滚动到底部
this.scrollToBottom()
})
} else {
console.log('[chat-detail] 收到空回复,不添加消息气泡 (可能是Human模式)')
}
} else {
throw new Error(res.error || res.message || '发送失败')
}
@ -1014,13 +1263,15 @@ Page({
/**
* 滚动到底部仅在发送/接收新消息时调用
* 使用 scroll-into-view 属性自动滚动到最后一条消息
* @param {boolean} animated - 是否使用动画滚动默认true
*/
scrollToBottom() {
scrollToBottom(animated = true) {
const messages = this.data.messages
if (messages && messages.length > 0) {
// 使用 setTimeout 确保 DOM 已更新
setTimeout(() => {
this.setData({
scrollAnimation: animated,
scrollIntoView: `msg-${messages.length - 1}`
})
}, 100)

View File

@ -37,7 +37,7 @@
scroll-y
class="chat-scroll"
scroll-into-view="{{scrollIntoView}}"
scroll-with-animation="{{true}}"
scroll-with-animation="{{scrollAnimation}}"
enhanced="{{true}}"
show-scrollbar="{{false}}"
bindscroll="onScroll"

View File

@ -233,14 +233,35 @@ Page({
},
fail: () => {
wx.hideLoading()
wx.showToast({
title: '下载失败',
icon: 'none'
})
wx.showToast({ 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

@ -49,7 +49,7 @@
<image src="{{item.image}}" class="activity-image" mode="aspectFill"></image>
<!-- 位置徽章 -->
<view class="location-badge">
<image src="/images/icon-location-white.png" class="location-icon" mode="aspectFit"></image>
<image src="/images/icon-location.png" class="location-icon" mode="aspectFit"></image>
<text>{{item.venue}}</text>
</view>
</view>

View File

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

View File

@ -42,5 +42,27 @@ Page({
} finally {
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

@ -121,50 +121,32 @@ Page({
/**
* 加载功能入口图标
* 从后台素材管理API加载 (group=entries)
* 从后台素材管理API加载
*/
async loadEntries() {
// 暂时禁用API加载使用本地配置的图标
console.log('使用本地配置的功能入口图标')
return;
/*
try {
const res = await api.pageAssets.getAssets('entries')
console.log('功能入口 API响应:', res)
const res = await api.pageAssets.getEntertainmentCategories()
console.log('娱乐页分类图标 API响应:', res)
if (res.success && res.data) {
const icons = res.data
const { categoryList } = this.data
if (res.success && res.data && res.data.length > 0) {
// 映射API数据到前端格式
const categoryList = res.data.map(item => ({
id: item.id,
name: item.name,
icon: this.processImageUrl(item.iconUrl || item.icon),
url: item.pagePath || item.url || '',
sort: item.sort || 0
})).sort((a, b) => a.sort - b.sort) // 按sort字段排序
// 映射图标:搭子(id=1), 同城(id=2), 户外(id=3), 定制(id=4), 学堂(id=5), 传递(id=6)
const idMap = {
1: 'entry_1', // 兴趣搭子
2: 'entry_2', // 同城活动
3: 'entry_3', // 户外郊游
4: 'entry_4', // 定制主题
5: 'entry_5', // 快乐学堂
6: 'entry_6' // 爱心传递
}
const updatedCategoryList = categoryList.map(item => {
const assetKey = idMap[item.id]
const iconUrl = icons[assetKey]
if (iconUrl) {
return {
...item,
icon: this.processImageUrl(iconUrl)
}
}
return item
})
this.setData({ categoryList: updatedCategoryList })
console.log('已更新娱乐页功能入口图标')
this.setData({ categoryList })
console.log(`加载了 ${categoryList.length} 个娱乐页分类图标`)
} else {
console.log('娱乐页分类图标API返回为空使用本地默认配置')
}
} catch (err) {
console.error('加载功能入口失败', err)
console.error('加载功能入口图标失败', err)
// 失败时保持使用 data 初始化时的本地配置,无需额外操作
}
*/
},
/**
@ -523,7 +505,13 @@ Page({
onCategoryTap(e) {
const { id, name, url } = e.currentTarget.dataset
// 从版本配置中获取分类信息
// 优先跳转配置的 URL
if (url) {
wx.navigateTo({ url: url })
return
}
// 从版本配置中获取分类信息(作为兜底)
const categoryList = versionConfig.getCategoryList()
const category = categoryList.find(item => item.id === id)
@ -825,5 +813,27 @@ Page({
}
}
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: '提示',
content: '请联系在线客服',
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' })
}
}
},
/**
* 用户点击右上角分享
*/
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 {
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.loadHeartBalance()
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

@ -57,6 +57,7 @@ Page({
async loadBanner() {
try {
// 尝试获取 interest_partner 分组的素材
// 后端已修复:请求 group=interest_partner 时,会自动返回 interest_partner_banners 的数据,并封装成 { banners: [] } 格式
const res = await api.pageAssets.getAssets('interest_partner')
console.log('[兴趣搭子] Banner API响应:', res)
@ -66,12 +67,14 @@ Page({
// 如果 banners 为空,尝试读取单图字段
if (!banners || banners.length === 0) {
const singleBanner = res.data.banner || res.data.interest_banner || res.data.top_banner
const singleBanner = res.data.banner || res.data.interest_banner || res.data.top_banner || (Array.isArray(res.data) ? res.data : [])
if (singleBanner) {
// 支持逗号分隔的多图字符串
if (typeof singleBanner === 'string' && singleBanner.includes(',')) {
banners = singleBanner.split(',').map(s => s.trim()).filter(s => s)
} else {
} else if (Array.isArray(singleBanner)) {
banners = singleBanner
} else if (typeof singleBanner === 'string') {
banners = [singleBanner]
}
}
@ -79,8 +82,16 @@ Page({
if (banners && banners.length > 0) {
// 处理图片URL
const bannerList = banners.map(url => {
if (typeof url !== 'string') return ''
const bannerList = banners.map(item => {
let url = ''
if (typeof item === 'string') {
url = item
} else if (item && typeof item === 'object') {
url = item.asset_url || item.url || item.imageUrl || ''
}
if (!url) return ''
let fullUrl = url
if (fullUrl.startsWith('/')) {
fullUrl = config.API_BASE_URL.replace('/api', '') + fullUrl
@ -138,10 +149,21 @@ Page({
console.log('[兴趣搭子] API原始响应:', JSON.stringify(res).substring(0, 500))
// 线上API返回格式{ success: true, data: [...] } 或 { success: true, data: { list: [...] } }
// 线上API返回格式{ success: true, data: [...] } 或 { success: true, data: { list: [...] } } 或 { success: true, data: { data: [...] } }
if (res.success && res.data) {
// 兼容两种返回格式
let partnerList = Array.isArray(res.data) ? res.data : (res.data.list || [])
// 兼容多种返回格式
// 1. res.data 是数组
// 2. res.data.data 是数组 (Laravel分页或标准包装)
// 3. res.data.list 是数组 (自定义列表包装)
let partnerList = []
if (Array.isArray(res.data)) {
partnerList = res.data
} else if (res.data.data && Array.isArray(res.data.data)) {
partnerList = res.data.data
} else if (res.data.list && Array.isArray(res.data.list)) {
partnerList = res.data.list
}
console.log('[兴趣搭子] 解析后的列表数量:', partnerList.length)
if (partnerList.length > 0) {
@ -330,5 +352,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

@ -28,15 +28,6 @@
<image src="{{item}}" class="banner-image" mode="aspectFill"></image>
</swiper-item>
</swiper>
<!-- 遮罩层 -->
<view class="hero-overlay"></view>
<!-- 文字内容 -->
<view class="hero-content">
<view class="hero-title">寻找志同道合的伙伴</view>
<view class="hero-subtitle">加入感兴趣的社群,开启精彩退休生活</view>
</view>
</view>
<!-- 兴趣分类列表 -->
@ -72,22 +63,7 @@
</view>
</view>
<!-- 如何加入说明 -->
<view class="how-to-join">
<view class="join-icon-wrapper">
<view class="join-icon">
<view class="dot dot-1"></view>
<view class="dot dot-2"></view>
<view class="dot dot-3"></view>
<view class="dot dot-4"></view>
<view class="line"></view>
</view>
</view>
<view class="join-content">
<view class="join-title">如何加入</view>
<view class="join-desc">点击上方感兴趣的分类,保存二维码图片或长按扫码识别,一键申请即可。</view>
</view>
</view>
<!-- 底部占位 -->
<view style="height: 60rpx;"></view>

View File

@ -19,7 +19,7 @@ page {
/* 顶部Banner */
.hero-banner {
margin: 32rpx 32rpx 48rpx;
height: 240rpx;
height: 400rpx;
border-radius: 32rpx;
position: relative;
overflow: hidden;

View File

@ -311,5 +311,29 @@ Page({
} finally {
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) => ({
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),
status: this.formatStatus(o.status),
// Use statusText from API, fallback to local formatting
status: o.statusText || this.formatStatus(o.status),
statusClass: `status-${o.status}`,
// Handle createdAt
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 });

View File

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

View File

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

@ -164,13 +164,31 @@ Page({
// 优先使用API返回的中文等级名称
const userLevel = record.fromUserRoleName || record.userRoleName || roleMap[record.fromUserRole] || roleMap[record.userRole] || record.levelText || '普通用户';
const type = record.orderType || record.type;
let productName = this.getOrderTypeText(type);
// 后端已把年卡会员改成分销商等级,不显示 VIP会员
if (type === 'vip') {
productName = '';
}
// Determine level class
const rawRole = record.fromUserRole || record.userRole || record.distributorRole || '';
let levelClass = '';
if (rawRole.includes('soulmate')) levelClass = 'tag-soulmate';
else if (rawRole.includes('guardian')) levelClass = 'tag-guardian';
else if (rawRole.includes('companion')) levelClass = 'tag-companion';
else if (rawRole.includes('listener')) levelClass = 'tag-listener';
else if (rawRole.includes('partner')) levelClass = 'tag-partner';
return {
id: record.id,
orderNo: record.orderNo || record.order_no || record.id || '---',
userName: record.fromUserName || record.userName || '匿名用户',
userAvatar: avatar || this.data.defaultAvatar,
productName: this.getOrderTypeText(record.orderType || record.type),
productName: productName,
userLevel: userLevel,
levelClass: levelClass,
orderAmount: record.orderAmount ? Number(record.orderAmount).toFixed(2) : (record.amount ? Number(record.amount).toFixed(2) : '0.00'),
time: fmtTime
}

View File

@ -53,7 +53,10 @@
<image class="user-avatar" src="{{item.userAvatar || defaultAvatar}}" mode="aspectFill" binderror="onAvatarError" data-index="{{index}}" />
<view class="order-info">
<view class="user-name">{{item.userName}}</view>
<view class="product-info">{{item.productName}} · {{item.userLevel}}</view>
<view class="product-info">
{{item.productName ? item.productName + ' · ' : ''}}
<text class="tag-badge {{item.levelClass}}">{{item.userLevel}}</text>
</view>
<view class="order-time">时间: {{item.time}}</view>
<view class="order-no">单号: {{item.orderNo}}</view>
</view>

View File

@ -191,6 +191,43 @@
color: #B06AB3;
}
/* Tag Styles */
.tag-badge {
display: inline-block;
font-size: 20rpx;
font-weight: 700;
padding: 2rpx 10rpx;
border-radius: 999rpx;
margin-left: 8rpx;
background: #E5E7EB;
color: #374151;
}
.tag-soulmate {
background: #7C3AED;
color: #ffffff;
}
.tag-guardian {
background: #3B82F6;
color: #ffffff;
}
.tag-companion {
background: #10B981;
color: #ffffff;
}
.tag-listener {
background: #F59E0B;
color: #ffffff;
}
.tag-partner {
background: #EF4444;
color: #ffffff;
}
/* 底部提示 */
.bottom-tip {
text-align: center;

View File

@ -52,6 +52,14 @@ Page({
auditStatus: app.globalData.auditStatus
});
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();
},
@ -62,15 +70,27 @@ Page({
wx.showNavigationBarLoading();
try {
if (isLoggedIn) {
// Stage 1: Critical user info and unread counts (Visual priority)
await Promise.all([
this.loadMe(),
this.loadBalance(),
this.loadCommission(),
this.loadCounts(),
this.loadUnreadCount()
]);
// 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 {
this.setData({
me: { nickname: '未登录', avatar: this.data.defaultAvatar },
@ -598,7 +618,13 @@ Page({
try {
const res = await api.commission.getStats()
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) {
console.error('加载推荐码失败:', err)

View File

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

View File

@ -289,5 +289,27 @@ Page({
}
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 {
const { activeTab } = this.data
const params = {
category: 'city', // 单身聚会通常属于同城活动
category: 'singles-party',
limit: 100
}
@ -276,5 +276,27 @@ Page({
console.error('保存失败', err)
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 imageUrl = require('../../utils/imageUrl')
// 常用表情
const EMOJIS = [
"😊", "😀", "😁", "😃", "😂", "🤣", "😅", "😆", "😉", "😋", "😎", "😍", "😘", "🥰", "😗", "😙",
"🙂", "🤗", "🤩", "🤔", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮", "😯", "😪", "😫", "😴",
"😌", "😛", "😜", "😝", "😒", "😓", "😔", "😕", "🙃", "😲", "😖", "😞", "😟", "😤", "😢", "😭",
"😨", "😩", "😬", "😰", "😱", "😳", "😵", "😡", "😠", "😷", "🤒", "🤕", "😇", "🥳", "🥺",
"👋", "👌", "✌️", "🤞", "👍", "👎", "👏", "🙌", "🤝", "🙏", "💪", "❤️", "🧡", "💛", "💚", "💙",
"💜", "🖤", "💔", "💕", "💖", "💗", "💘", "💝", "🌹", "🌺", "🌻", "🌼", "🌷", "🎉", "🎊", "🎁"
]
Page({
data: {
statusBarHeight: 44,
@ -16,7 +26,17 @@ Page({
ticketId: '',
scrollIntoView: '',
scrollTop: 0,
pollingTimer: null
pollingTimer: null,
// 新增状态
isVoiceMode: false,
isRecording: false,
showEmoji: false,
showMorePanel: false,
voiceCancelHint: false,
recordingDuration: 0,
emojis: EMOJIS,
playingVoiceId: null
},
onLoad() {
@ -74,7 +94,6 @@ Page({
this.setData({ ticketId: latestTicket.id })
await this.loadMessages(latestTicket.id)
} else {
// 如果没有工单,可以在首次发送消息时创建
console.log('[support] No existing tickets found.')
}
} catch (err) {
@ -95,7 +114,11 @@ Page({
const messages = res.data.messages.map(msg => ({
id: msg.id,
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'),
senderName: msg.senderName
}))
@ -119,7 +142,25 @@ Page({
const content = this.data.inputText.trim()
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
const tempId = util.generateId()
const now = new Date()
@ -127,39 +168,59 @@ Page({
const userMsg = {
id: tempId,
isMe: true,
text: content,
time: util.formatTime(now, 'HH:mm')
text: type === 'text' ? content : (type === 'image' ? '[图片]' : '[语音]'),
type: type,
imageUrl: type === 'image' ? content : '',
audioUrl: type === 'voice' ? content : '',
duration: duration,
time: util.formatTime(now, 'HH:mm'),
uploading: true
}
this.setData({
messages: [...this.data.messages, userMsg],
inputText: '',
inputFocus: true
inputFocus: type === 'text' // 仅文本发送后聚焦
}, () => {
this.scrollToBottom()
})
try {
const payload = {
content: content,
type: type,
duration: duration,
userName: app.globalData.userInfo?.nickname || '访客'
}
if (this.data.ticketId) {
// 回复已有工单
await api.customerService.reply({
ticketId: this.data.ticketId,
content: content,
userName: app.globalData.userInfo?.nickname || '访客'
...payload
})
} else {
// 创建新工单
const guestId = wx.getStorageSync('guestId')
const res = await api.customerService.create({
category: 'other',
content: content,
userName: app.globalData.userInfo?.nickname || '访客',
guestId: guestId
guestId: guestId,
...payload
})
if (res.success && res.data) {
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) {
await this.loadMessages(this.data.ticketId)
@ -167,6 +228,15 @@ Page({
} catch (err) {
console.error('[support] send message error:', err)
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 {
this.isSending = false
}
@ -203,12 +273,222 @@ Page({
},
onTapChatArea() {
this.setData({ inputFocus: false })
this.setData({
inputFocus: false,
showEmoji: false,
showMorePanel: false
})
},
scrollToBottom() {
this.setData({
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>
</view>
<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>
</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">
<text class="message-time">{{item.time}}</text>
</view>
@ -70,9 +84,23 @@
<!-- 用户消息(右侧) -->
<block wx:else>
<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>
</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>
</view>
<view class="avatar-wrap user-avatar">
@ -101,10 +129,31 @@
</scroll-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="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
class="figma-text-input"
placeholder="请输入您要咨询的问题..."
@ -113,14 +162,80 @@
confirm-type="send"
bindconfirm="onSend"
focus="{{inputFocus}}"
adjust-position="{{!showEmoji}}"
hold-keyboard="{{true}}"
/>
</view>
<view class="figma-send-btn {{inputText.length > 0 ? 'active' : ''}}" bindtap="onSend">
<!-- 表情按钮 -->
<view class="figma-emoji-btn {{showEmoji ? 'active' : ''}}" bindtap="onEmojiToggle">
<image src="{{showEmoji ? '/images/icon-keyboard.png' : '/images/chat-input-emoji.png'}}" class="figma-btn-icon" mode="aspectFit"></image>
</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>
<text class="send-text">发送</text>
</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 File

@ -1,4 +1,5 @@
/* pages/support/support.wxss */
/* 样式复用自 AI聊天详情页 (pages/chat-detail/chat-detail.wxss) 以保持一致体验 */
/* 页面容器 */
.page-container {
@ -9,7 +10,7 @@
position: relative;
}
/* 聊天区域包装器 */
/* 聊天区域包装器 - 使用固定定位确保正确布局 */
.chat-area-wrapper {
position: fixed;
left: 0;
@ -20,6 +21,17 @@
overflow: hidden;
}
/* 面板打开时的透明遮罩层 */
.panel-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
z-index: 98;
}
/* 状态栏区域 */
.status-bar-area {
position: fixed;
@ -267,21 +279,46 @@
padding-bottom: env(safe-area-inset-bottom);
}
.figma-input-container {
.input-container {
display: flex;
align-items: center;
gap: 16rpx;
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 {
flex: 1;
background: #F9FAFB;
border: 2rpx solid #F3F4F6;
border-radius: 40rpx;
border-radius: 32rpx;
padding: 0 32rpx;
height: 120rpx;
height: 96rpx;
display: flex;
align-items: center;
}
@ -289,43 +326,337 @@
.figma-text-input {
width: 100%;
height: 100%;
font-size: 38rpx;
font-size: 36rpx;
color: #101828;
font-family: Arial, sans-serif;
}
.figma-send-btn {
width: 180rpx;
height: 88rpx;
background: #F3F4F6;
border-radius: 44rpx;
.figma-emoji-btn {
width: 80rpx;
height: 80rpx;
background: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
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;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.figma-btn-icon {
width: 36rpx;
height: 36rpx;
filter: grayscale(1) opacity(0.5);
.figma-send-btn .figma-btn-icon {
width: 44rpx;
height: 44rpx;
}
.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);
}
.send-text {
font-size: 28rpx;
font-weight: 700;
color: #9CA3AF;
.voice-wave {
display: flex;
align-items: center;
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;
}
.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({
stats: {
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)
},
cardTitle: currentRoleText
@ -92,12 +95,17 @@ Page({
// Flexible data extraction
let rawList = [];
let totalDirects = 0; // Initialize total count
if (Array.isArray(body.data)) {
rawList = body.data;
totalDirects = rawList.length;
} else if (body.data && Array.isArray(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)) {
rawList = body.list;
totalDirects = body.total || rawList.length;
}
console.log('[团队页面] rawList:', JSON.stringify(rawList.slice(0, 2), null, 2));
@ -111,6 +119,7 @@ Page({
};
const list = rawList.map((x) => {
// ... (mapping logic) ...
const user = x.user || {};
// Map fields robustly
let avatar = x.avatarUrl || x.avatar_url || x.userAvatar || user.avatarUrl || user.avatar_url || '';
@ -129,18 +138,38 @@ Page({
roleMap[x.role] || roleMap[user.role] ||
'普通用户';
// Determine level class
const rawRole = x.userRole || user.userRole || x.distributorRole || user.distributorRole || x.role || user.role || '';
let levelClass = '';
if (rawRole.includes('soulmate')) levelClass = 'tag-soulmate';
else if (rawRole.includes('guardian')) levelClass = 'tag-guardian';
else if (rawRole.includes('companion')) levelClass = 'tag-companion';
else if (rawRole.includes('listener')) levelClass = 'tag-listener';
else if (rawRole.includes('partner')) levelClass = 'tag-partner';
return {
...x,
userId: x.userId || x.id,
userAvatar: avatar || this.data.defaultAvatar,
userName: name,
levelText: levelText,
levelClass: levelClass,
totalContribution: contribution,
boundAtText: this.formatDate(new Date(dateStr))
};
});
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) {
console.log('API failed, using mock data', err);
this.setData({
@ -176,11 +205,11 @@ Page({
getCardTitle(type) {
const map = {
'guardian_card': '守护会员',
'companion_card': '伴会员',
'companion_card': '伴会员',
'soulmate_card': '心伴会员',
'listener_card': '倾听会员',
'guardian': '守护会员',
'companion': '伴会员',
'companion': '伴会员',
'soulmate': '心伴会员',
'listener': '倾听会员',
'identity_card': '身份会员',

View File

@ -22,7 +22,7 @@
<view class="stats-row">
<view class="stat-col">
<text class="stat-label">直推人数</text>
<text class="stat-num">{{list.length}}</text>
<text class="stat-num">{{stats.directReferrals || list.length}}</text>
</view>
<view class="stat-col">
<text class="stat-label">团队总计</text>
@ -49,7 +49,7 @@
<view class="member-info">
<view class="name-row">
<text class="member-name">{{item.userName}}</text>
<view class="tag-badge">{{item.levelText}}</view>
<view class="tag-badge {{item.levelClass}}">{{item.levelText}}</view>
</view>
<text class="member-meta">{{item.boundAtText}}</text>
</view>

View File

@ -150,14 +150,39 @@
}
.tag-badge {
background: #B06AB3;
color: #ffffff;
background: #E5E7EB;
color: #374151;
font-size: 20rpx;
font-weight: 700;
padding: 4rpx 12rpx;
border-radius: 999rpx;
}
.tag-soulmate {
background: #7C3AED;
color: #ffffff;
}
.tag-guardian {
background: #3B82F6;
color: #ffffff;
}
.tag-companion {
background: #10B981;
color: #ffffff;
}
.tag-listener {
background: #F59E0B;
color: #ffffff;
}
.tag-partner {
background: #EF4444;
color: #ffffff;
}
.member-meta {
font-size: 24rpx;
color: #6B7280;

View File

@ -279,7 +279,29 @@ Page({
}
} catch (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}`
}
},

View File

@ -402,6 +402,12 @@ const chat = {
*/
getConversations: () => request('/conversations', { silent: true }),
/**
* 获取会话详情
* @param {string} id - 会话ID
*/
getConversationDetail: (id) => request(`/conversations/${id}`, { silent: true }),
/**
* 删除会话
* @param {string} conversationId - 会话ID
@ -910,6 +916,11 @@ const pageAssets = {
*/
getEntertainmentBanners: () => request('/page-assets/entertainment-banners'),
/**
* 获取娱乐页分类图标列表
*/
getEntertainmentCategories: () => request('/page-assets/entertainment-categories'),
/**
* 获取合作入驻页在线Banner列表
*/