Compare commits
4 Commits
b2feeabc48
...
638f13df1f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
638f13df1f | ||
|
|
d32801cb5b | ||
|
|
3ad56d0250 | ||
|
|
4146309ff2 |
|
|
@ -71,6 +71,7 @@ Page({
|
||||||
// 滚动控制
|
// 滚动控制
|
||||||
scrollIntoView: '',
|
scrollIntoView: '',
|
||||||
scrollTop: 0, // 当前滚动位置
|
scrollTop: 0, // 当前滚动位置
|
||||||
|
scrollAnimation: false, // 控制滚动动画
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|
@ -127,7 +128,10 @@ Page({
|
||||||
|
|
||||||
// 免费畅聊相关
|
// 免费畅聊相关
|
||||||
freeTime: null,
|
freeTime: null,
|
||||||
countdownText: ''
|
countdownText: '',
|
||||||
|
|
||||||
|
// 聊天模式 (ai | human)
|
||||||
|
chatMode: 'ai'
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
|
|
@ -140,6 +144,10 @@ Page({
|
||||||
this.messageTimer = null
|
this.messageTimer = null
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
|
|
||||||
|
// 获取当前用户ID
|
||||||
|
const userId = app.globalData.userId || wx.getStorageSync(config.STORAGE_KEYS.USER_ID)
|
||||||
|
this.setData({ userId })
|
||||||
|
|
||||||
// 获取参数
|
// 获取参数
|
||||||
const characterId = options.id || ''
|
const characterId = options.id || ''
|
||||||
const conversationId = options.conversationId || ''
|
const conversationId = options.conversationId || ''
|
||||||
|
|
@ -177,10 +185,20 @@ Page({
|
||||||
if (!this.data.loading) {
|
if (!this.data.loading) {
|
||||||
this.loadQuotaStatus()
|
this.loadQuotaStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 开启消息轮询
|
||||||
|
this.startPolling()
|
||||||
|
},
|
||||||
|
|
||||||
|
onHide() {
|
||||||
|
// 页面隐藏时停止轮询
|
||||||
|
this.stopPolling()
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnload() {
|
onUnload() {
|
||||||
// 页面卸载时清理
|
// 页面卸载时清理
|
||||||
|
this.stopPolling()
|
||||||
|
|
||||||
// 清除消息处理定时器
|
// 清除消息处理定时器
|
||||||
if (this.messageTimer) {
|
if (this.messageTimer) {
|
||||||
clearTimeout(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条)
|
* 加载聊天历史(首次加载最近20条)
|
||||||
|
* 优化策略:优先从本地缓存加载,实现"无感"体验,防止后端切换模式导致消息丢失
|
||||||
*/
|
*/
|
||||||
async loadChatHistory() {
|
async loadChatHistory() {
|
||||||
const { characterId, pageSize } = this.data
|
const { characterId, pageSize } = this.data
|
||||||
|
|
||||||
if (!characterId) {
|
if (!characterId) {
|
||||||
const welcomeMsg = {
|
this.showWelcomeMessage()
|
||||||
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
|
|
||||||
})
|
|
||||||
return
|
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 {
|
try {
|
||||||
console.log('[chat-detail] 开始加载聊天历史, characterId:', characterId)
|
console.log('[chat-detail] 开始从API同步聊天历史, characterId:', characterId)
|
||||||
|
|
||||||
// 首次只加载最近20条消息
|
// 首次只加载最近20条消息
|
||||||
const res = await api.chat.getChatHistoryByCharacter(characterId, {
|
const res = await api.chat.getChatHistoryByCharacter(characterId, {
|
||||||
limit: pageSize,
|
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) {
|
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))
|
||||||
|
|
||||||
this.setData({
|
// 合并策略:以API数据为准更新,但保留API未返回的本地旧数据(如果有)
|
||||||
messages,
|
// 简单起见,如果本地为空,直接用API;如果本地有值,做去重合并
|
||||||
hasMore: res.data.length >= pageSize,
|
|
||||||
page: 1,
|
|
||||||
isFirstLoad: false
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('[chat-detail] 消息已设置, 当前数量:', this.data.messages.length)
|
let finalMessages = []
|
||||||
console.log('[chat-detail] 首次加载完成,不自动滚动到底部')
|
if (this.data.messages.length === 0) {
|
||||||
} else {
|
finalMessages = apiMessages
|
||||||
console.log('[chat-detail] 没有历史记录,显示欢迎消息')
|
} else {
|
||||||
const welcomeMsg = {
|
// 合并逻辑:将API返回的新消息合并到现有列表中
|
||||||
id: 'welcome',
|
// 1. 创建现有消息的ID映射
|
||||||
text: `你好!我是${this.data.character.name},很高兴认识你~`,
|
const existingIds = new Set(this.data.messages.map(m => m.id))
|
||||||
isMe: false,
|
|
||||||
time: util.formatTime(new Date(), 'HH:mm'),
|
// 2. 找出API返回中本地没有的消息
|
||||||
type: 'text'
|
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: finalMessages,
|
||||||
|
hasMore: res.data.length >= pageSize,
|
||||||
|
page: 1,
|
||||||
|
isFirstLoad: false
|
||||||
|
}, () => {
|
||||||
|
// API更新时也使用无动画滚动,确保位置稳定
|
||||||
|
this.scrollToBottom(false)
|
||||||
|
// 更新缓存
|
||||||
|
this.saveMessagesToCache(finalMessages)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.log('加载聊天历史失败:', err)
|
console.log('加载聊天历史失败:', err)
|
||||||
|
if (this.data.messages.length === 0) {
|
||||||
|
this.showWelcomeMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示欢迎消息
|
||||||
|
*/
|
||||||
|
showWelcomeMessage() {
|
||||||
const welcomeMsg = {
|
const welcomeMsg = {
|
||||||
id: 'welcome',
|
id: 'welcome',
|
||||||
text: `你好!我是${this.data.character.name},很高兴认识你~`,
|
text: `你好!我是${this.data.character.name},很高兴认识你~`,
|
||||||
|
|
@ -524,7 +727,16 @@ Page({
|
||||||
isFirstLoad: false,
|
isFirstLoad: false,
|
||||||
hasMore: 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) {
|
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 = {
|
const baseMessage = {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
text: msg.content,
|
text: msg.content,
|
||||||
isMe: msg.role === 'user',
|
isMe: isMe,
|
||||||
time: util.formatTime(msg.created_at || msg.timestamp, 'HH:mm'),
|
time: util.formatTime(msg.created_at || msg.timestamp, 'HH:mm'),
|
||||||
type: msg.message_type || 'text'
|
type: msg.message_type || 'text'
|
||||||
}
|
}
|
||||||
|
|
@ -743,6 +980,9 @@ Page({
|
||||||
|
|
||||||
// 只检查输入是否为空
|
// 只检查输入是否为空
|
||||||
if (!inputText.trim()) return
|
if (!inputText.trim()) return
|
||||||
|
|
||||||
|
// 触发高速轮询模式,确保及时收到回复
|
||||||
|
this.triggerFastPolling()
|
||||||
|
|
||||||
// 检查登录
|
// 检查登录
|
||||||
if (app.checkNeedLogin()) return
|
if (app.checkNeedLogin()) return
|
||||||
|
|
@ -842,8 +1082,10 @@ Page({
|
||||||
console.log('[chat-detail] 合并处理消息:', messagesToProcess.length, '条')
|
console.log('[chat-detail] 合并处理消息:', messagesToProcess.length, '条')
|
||||||
console.log('[chat-detail] 合并后内容:', combinedMessage)
|
console.log('[chat-detail] 合并后内容:', combinedMessage)
|
||||||
|
|
||||||
// 显示AI正在输入
|
// 显示AI正在输入(仅在AI模式下)
|
||||||
this.setData({ isTyping: true })
|
if (this.data.chatMode === 'ai') {
|
||||||
|
this.setData({ isTyping: true })
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 构建对话历史(最近10条消息,只包含文字消息)
|
// 构建对话历史(最近10条消息,只包含文字消息)
|
||||||
|
|
@ -918,22 +1160,29 @@ Page({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加AI回复
|
// 只有当有回复内容时才添加AI消息
|
||||||
const aiMessage = {
|
// 如果是人工模式(human),后端可能只返回成功但不返回content,或者content为空
|
||||||
id: res.data.id || util.generateId(),
|
const content = res.data.content || res.data.message
|
||||||
text: res.data.content || res.data.message,
|
if (content) {
|
||||||
isMe: false,
|
// 添加AI回复
|
||||||
time: util.formatTime(new Date(), 'HH:mm'),
|
const aiMessage = {
|
||||||
audioUrl: res.data.audio_url,
|
id: res.data.id || util.generateId(),
|
||||||
type: 'text' // 标记为文字消息
|
text: content,
|
||||||
|
isMe: false,
|
||||||
|
time: util.formatTime(new Date(), 'HH:mm'),
|
||||||
|
audioUrl: res.data.audio_url,
|
||||||
|
type: 'text' // 标记为文字消息
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setData({
|
||||||
|
messages: [...this.data.messages, aiMessage]
|
||||||
|
}, () => {
|
||||||
|
// AI回复后滚动到底部
|
||||||
|
this.scrollToBottom()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('[chat-detail] 收到空回复,不添加消息气泡 (可能是Human模式)')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setData({
|
|
||||||
messages: [...this.data.messages, aiMessage]
|
|
||||||
}, () => {
|
|
||||||
// AI回复后滚动到底部
|
|
||||||
this.scrollToBottom()
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.error || res.message || '发送失败')
|
throw new Error(res.error || res.message || '发送失败')
|
||||||
}
|
}
|
||||||
|
|
@ -1014,13 +1263,15 @@ Page({
|
||||||
/**
|
/**
|
||||||
* 滚动到底部(仅在发送/接收新消息时调用)
|
* 滚动到底部(仅在发送/接收新消息时调用)
|
||||||
* 使用 scroll-into-view 属性,自动滚动到最后一条消息
|
* 使用 scroll-into-view 属性,自动滚动到最后一条消息
|
||||||
|
* @param {boolean} animated - 是否使用动画滚动,默认true
|
||||||
*/
|
*/
|
||||||
scrollToBottom() {
|
scrollToBottom(animated = true) {
|
||||||
const messages = this.data.messages
|
const messages = this.data.messages
|
||||||
if (messages && messages.length > 0) {
|
if (messages && messages.length > 0) {
|
||||||
// 使用 setTimeout 确保 DOM 已更新
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setData({
|
this.setData({
|
||||||
|
scrollAnimation: animated,
|
||||||
scrollIntoView: `msg-${messages.length - 1}`
|
scrollIntoView: `msg-${messages.length - 1}`
|
||||||
})
|
})
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
scroll-y
|
scroll-y
|
||||||
class="chat-scroll"
|
class="chat-scroll"
|
||||||
scroll-into-view="{{scrollIntoView}}"
|
scroll-into-view="{{scrollIntoView}}"
|
||||||
scroll-with-animation="{{true}}"
|
scroll-with-animation="{{scrollAnimation}}"
|
||||||
enhanced="{{true}}"
|
enhanced="{{true}}"
|
||||||
show-scrollbar="{{false}}"
|
show-scrollbar="{{false}}"
|
||||||
bindscroll="onScroll"
|
bindscroll="onScroll"
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 点击活动卡片
|
* 点击活动卡片
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,9 @@
|
||||||
<image src="{{item.image}}" class="activity-image" mode="aspectFill"></image>
|
<image src="{{item.image}}" class="activity-image" mode="aspectFill"></image>
|
||||||
<!-- 位置徽章 -->
|
<!-- 位置徽章 -->
|
||||||
<view class="location-badge">
|
<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>
|
<text>{{item.venue}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 活动信息 -->
|
<!-- 活动信息 -->
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -121,50 +121,32 @@ Page({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载功能入口图标
|
* 加载功能入口图标
|
||||||
* 从后台素材管理API加载 (group=entries)
|
* 从后台素材管理API加载
|
||||||
*/
|
*/
|
||||||
async loadEntries() {
|
async loadEntries() {
|
||||||
// 暂时禁用API加载,使用本地配置的图标
|
|
||||||
console.log('使用本地配置的功能入口图标')
|
|
||||||
return;
|
|
||||||
/*
|
|
||||||
try {
|
try {
|
||||||
const res = await api.pageAssets.getAssets('entries')
|
const res = await api.pageAssets.getEntertainmentCategories()
|
||||||
console.log('功能入口 API响应:', res)
|
console.log('娱乐页分类图标 API响应:', res)
|
||||||
|
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data && res.data.length > 0) {
|
||||||
const icons = res.data
|
// 映射API数据到前端格式
|
||||||
const { categoryList } = this.data
|
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)
|
this.setData({ categoryList })
|
||||||
const idMap = {
|
console.log(`加载了 ${categoryList.length} 个娱乐页分类图标`)
|
||||||
1: 'entry_1', // 兴趣搭子
|
} else {
|
||||||
2: 'entry_2', // 同城活动
|
console.log('娱乐页分类图标API返回为空,使用本地默认配置')
|
||||||
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('已更新娱乐页功能入口图标')
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载功能入口失败', err)
|
console.error('加载功能入口图标失败', err)
|
||||||
|
// 失败时保持使用 data 初始化时的本地配置,无需额外操作
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -523,7 +505,13 @@ Page({
|
||||||
onCategoryTap(e) {
|
onCategoryTap(e) {
|
||||||
const { id, name, url } = e.currentTarget.dataset
|
const { id, name, url } = e.currentTarget.dataset
|
||||||
|
|
||||||
// 从版本配置中获取分类信息
|
// 优先跳转配置的 URL
|
||||||
|
if (url) {
|
||||||
|
wx.navigateTo({ url: url })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从版本配置中获取分类信息(作为兜底)
|
||||||
const categoryList = versionConfig.getCategoryList()
|
const categoryList = versionConfig.getCategoryList()
|
||||||
const category = categoryList.find(item => item.id === id)
|
const category = categoryList.find(item => item.id === id)
|
||||||
|
|
||||||
|
|
@ -825,5 +813,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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ Page({
|
||||||
async loadBanner() {
|
async loadBanner() {
|
||||||
try {
|
try {
|
||||||
// 尝试获取 interest_partner 分组的素材
|
// 尝试获取 interest_partner 分组的素材
|
||||||
|
// 后端已修复:请求 group=interest_partner 时,会自动返回 interest_partner_banners 的数据,并封装成 { banners: [] } 格式
|
||||||
const res = await api.pageAssets.getAssets('interest_partner')
|
const res = await api.pageAssets.getAssets('interest_partner')
|
||||||
console.log('[兴趣搭子] Banner API响应:', res)
|
console.log('[兴趣搭子] Banner API响应:', res)
|
||||||
|
|
||||||
|
|
@ -66,12 +67,14 @@ Page({
|
||||||
|
|
||||||
// 如果 banners 为空,尝试读取单图字段
|
// 如果 banners 为空,尝试读取单图字段
|
||||||
if (!banners || banners.length === 0) {
|
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 (singleBanner) {
|
||||||
// 支持逗号分隔的多图字符串
|
// 支持逗号分隔的多图字符串
|
||||||
if (typeof singleBanner === 'string' && singleBanner.includes(',')) {
|
if (typeof singleBanner === 'string' && singleBanner.includes(',')) {
|
||||||
banners = singleBanner.split(',').map(s => s.trim()).filter(s => s)
|
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]
|
banners = [singleBanner]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,8 +82,16 @@ Page({
|
||||||
|
|
||||||
if (banners && banners.length > 0) {
|
if (banners && banners.length > 0) {
|
||||||
// 处理图片URL
|
// 处理图片URL
|
||||||
const bannerList = banners.map(url => {
|
const bannerList = banners.map(item => {
|
||||||
if (typeof url !== 'string') return ''
|
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
|
let fullUrl = url
|
||||||
if (fullUrl.startsWith('/')) {
|
if (fullUrl.startsWith('/')) {
|
||||||
fullUrl = config.API_BASE_URL.replace('/api', '') + fullUrl
|
fullUrl = config.API_BASE_URL.replace('/api', '') + fullUrl
|
||||||
|
|
@ -138,10 +149,21 @@ Page({
|
||||||
|
|
||||||
console.log('[兴趣搭子] API原始响应:', JSON.stringify(res).substring(0, 500))
|
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) {
|
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)
|
console.log('[兴趣搭子] 解析后的列表数量:', partnerList.length)
|
||||||
if (partnerList.length > 0) {
|
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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,6 @@
|
||||||
<image src="{{item}}" class="banner-image" mode="aspectFill"></image>
|
<image src="{{item}}" class="banner-image" mode="aspectFill"></image>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
|
|
||||||
<!-- 遮罩层 -->
|
|
||||||
<view class="hero-overlay"></view>
|
|
||||||
|
|
||||||
<!-- 文字内容 -->
|
|
||||||
<view class="hero-content">
|
|
||||||
<view class="hero-title">寻找志同道合的伙伴</view>
|
|
||||||
<view class="hero-subtitle">加入感兴趣的社群,开启精彩退休生活</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 兴趣分类列表 -->
|
<!-- 兴趣分类列表 -->
|
||||||
|
|
@ -72,22 +63,7 @@
|
||||||
</view>
|
</view>
|
||||||
</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 style="height: 60rpx;"></view>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ page {
|
||||||
/* 顶部Banner */
|
/* 顶部Banner */
|
||||||
.hero-banner {
|
.hero-banner {
|
||||||
margin: 32rpx 32rpx 48rpx;
|
margin: 32rpx 32rpx 48rpx;
|
||||||
height: 240rpx;
|
height: 400rpx;
|
||||||
border-radius: 32rpx;
|
border-radius: 32rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -164,13 +164,31 @@ Page({
|
||||||
// 优先使用API返回的中文等级名称
|
// 优先使用API返回的中文等级名称
|
||||||
const userLevel = record.fromUserRoleName || record.userRoleName || roleMap[record.fromUserRole] || roleMap[record.userRole] || record.levelText || '普通用户';
|
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 {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
orderNo: record.orderNo || record.order_no || record.id || '---',
|
orderNo: record.orderNo || record.order_no || record.id || '---',
|
||||||
userName: record.fromUserName || record.userName || '匿名用户',
|
userName: record.fromUserName || record.userName || '匿名用户',
|
||||||
userAvatar: avatar || this.data.defaultAvatar,
|
userAvatar: avatar || this.data.defaultAvatar,
|
||||||
productName: this.getOrderTypeText(record.orderType || record.type),
|
productName: productName,
|
||||||
userLevel: userLevel,
|
userLevel: userLevel,
|
||||||
|
levelClass: levelClass,
|
||||||
orderAmount: record.orderAmount ? Number(record.orderAmount).toFixed(2) : (record.amount ? Number(record.amount).toFixed(2) : '0.00'),
|
orderAmount: record.orderAmount ? Number(record.orderAmount).toFixed(2) : (record.amount ? Number(record.amount).toFixed(2) : '0.00'),
|
||||||
time: fmtTime
|
time: fmtTime
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,10 @@
|
||||||
<image class="user-avatar" src="{{item.userAvatar || defaultAvatar}}" mode="aspectFill" binderror="onAvatarError" data-index="{{index}}" />
|
<image class="user-avatar" src="{{item.userAvatar || defaultAvatar}}" mode="aspectFill" binderror="onAvatarError" data-index="{{index}}" />
|
||||||
<view class="order-info">
|
<view class="order-info">
|
||||||
<view class="user-name">{{item.userName}}</view>
|
<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-time">时间: {{item.time}}</view>
|
||||||
<view class="order-no">单号: {{item.orderNo}}</view>
|
<view class="order-no">单号: {{item.orderNo}}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,43 @@
|
||||||
color: #B06AB3;
|
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 {
|
.bottom-tip {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -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 })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 || '';
|
||||||
|
|
@ -128,6 +137,15 @@ Page({
|
||||||
roleMap[x.distributorRole] || roleMap[user.distributorRole] ||
|
roleMap[x.distributorRole] || roleMap[user.distributorRole] ||
|
||||||
roleMap[x.role] || roleMap[user.role] ||
|
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 {
|
return {
|
||||||
...x,
|
...x,
|
||||||
|
|
@ -135,12 +153,23 @@ Page({
|
||||||
userAvatar: avatar || this.data.defaultAvatar,
|
userAvatar: avatar || this.data.defaultAvatar,
|
||||||
userName: name,
|
userName: name,
|
||||||
levelText: levelText,
|
levelText: levelText,
|
||||||
|
levelClass: levelClass,
|
||||||
totalContribution: contribution,
|
totalContribution: contribution,
|
||||||
boundAtText: this.formatDate(new Date(dateStr))
|
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) {
|
} catch (err) {
|
||||||
console.log('API failed, using mock data', err);
|
console.log('API failed, using mock data', err);
|
||||||
this.setData({
|
this.setData({
|
||||||
|
|
@ -176,11 +205,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': '身份会员',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
<view class="member-info">
|
<view class="member-info">
|
||||||
<view class="name-row">
|
<view class="name-row">
|
||||||
<text class="member-name">{{item.userName}}</text>
|
<text class="member-name">{{item.userName}}</text>
|
||||||
<view class="tag-badge">{{item.levelText}}</view>
|
<view class="tag-badge {{item.levelClass}}">{{item.levelText}}</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="member-meta">{{item.boundAtText}}</text>
|
<text class="member-meta">{{item.boundAtText}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
|
||||||
|
|
@ -150,14 +150,39 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-badge {
|
.tag-badge {
|
||||||
background: #B06AB3;
|
background: #E5E7EB;
|
||||||
color: #ffffff;
|
color: #374151;
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 4rpx 12rpx;
|
padding: 4rpx 12rpx;
|
||||||
border-radius: 999rpx;
|
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 {
|
.member-meta {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #6B7280;
|
color: #6B7280;
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
11
utils/api.js
11
utils/api.js
|
|
@ -402,6 +402,12 @@ const chat = {
|
||||||
*/
|
*/
|
||||||
getConversations: () => request('/conversations', { silent: true }),
|
getConversations: () => request('/conversations', { silent: true }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话详情
|
||||||
|
* @param {string} id - 会话ID
|
||||||
|
*/
|
||||||
|
getConversationDetail: (id) => request(`/conversations/${id}`, { silent: true }),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
* @param {string} conversationId - 会话ID
|
* @param {string} conversationId - 会话ID
|
||||||
|
|
@ -910,6 +916,11 @@ const pageAssets = {
|
||||||
*/
|
*/
|
||||||
getEntertainmentBanners: () => request('/page-assets/entertainment-banners'),
|
getEntertainmentBanners: () => request('/page-assets/entertainment-banners'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取娱乐页分类图标列表
|
||||||
|
*/
|
||||||
|
getEntertainmentCategories: () => request('/page-assets/entertainment-categories'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取合作入驻页在线Banner列表
|
* 获取合作入驻页在线Banner列表
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user