-
4app.json
-
BINpages/images/kzt.png
-
16pages/personal/personal.js
-
BINpagesA/images/add.png
-
BINpagesA/images/cuo.png
-
BINpagesA/images/duig.png
-
BINpagesA/images/ps.png
-
BINpagesA/images/zp.png
-
27pagesA/pages/advisory/advisory.js
-
1pagesA/pages/askingSyDetails/askingSyDetails.js
-
534pagesA/pages/expertChat/expertChat.js
-
4pagesA/pages/expertChat/expertChat.json
-
280pagesA/pages/expertChat/expertChat.wxml
-
685pagesA/pages/expertChat/expertChat.wxss
-
127pagesA/pages/serviceEvaluation/serviceEvaluation.js
-
4pagesA/pages/serviceEvaluation/serviceEvaluation.json
-
53pagesA/pages/serviceEvaluation/serviceEvaluation.wxml
-
146pagesA/pages/serviceEvaluation/serviceEvaluation.wxss
-
20pagesB/pages/publishAdd/publishAdd.js
-
2project.private.config.json
-
12utils/api.js
|
After Width: 320 | Height: 320 | Size: 8.5 KiB |
|
After Width: 200 | Height: 200 | Size: 4.4 KiB |
|
After Width: 200 | Height: 200 | Size: 4.6 KiB |
|
After Width: 200 | Height: 200 | Size: 2.5 KiB |
|
After Width: 200 | Height: 200 | Size: 3.3 KiB |
|
After Width: 200 | Height: 200 | Size: 2.7 KiB |
@ -0,0 +1,534 @@ |
|||||
|
// 专家端聊天页面 - 纯模拟数据,无真实接口
|
||||
|
Page({ |
||||
|
data: { |
||||
|
// 专家信息
|
||||
|
expertInfo: { |
||||
|
id: 'expert_001', |
||||
|
name: '张医生', |
||||
|
avatar: '/pages/images/tx.png' |
||||
|
}, |
||||
|
expertAvatar: '/pages/images/tx.png', |
||||
|
|
||||
|
// 对话信息
|
||||
|
conversation: { |
||||
|
userId: 'user_001', |
||||
|
userName: '李华', |
||||
|
userAvatar: '/pages/images/tx.png' |
||||
|
}, |
||||
|
|
||||
|
// 在线状态
|
||||
|
onlineStatus: true, |
||||
|
|
||||
|
// 消息列表
|
||||
|
messageList: [], |
||||
|
scrollTop: 0, |
||||
|
scrollAnimate: true, |
||||
|
|
||||
|
// 输入相关
|
||||
|
inputValue: '', |
||||
|
inputFocus: false, |
||||
|
|
||||
|
// 多媒体面板
|
||||
|
showMediaSheet: false, |
||||
|
|
||||
|
|
||||
|
// 页面状态
|
||||
|
loadingMore: false, |
||||
|
hasMore: true, |
||||
|
page: 1, |
||||
|
|
||||
|
// 模拟数据
|
||||
|
mockMessages: [ |
||||
|
{ |
||||
|
id: 'msg_1', |
||||
|
isMe: false, |
||||
|
type: 'text', |
||||
|
content: '医生您好,我想咨询一下皮肤问题', |
||||
|
timestamp: Date.now() - 3600000, |
||||
|
status: 'success', |
||||
|
avatar: '/pages/images/tx.png' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'msg_2', |
||||
|
isMe: true, |
||||
|
type: 'text', |
||||
|
content: '您好,请描述一下您的具体症状', |
||||
|
timestamp: Date.now() - 3500000, |
||||
|
status: 'success', |
||||
|
avatar: '/pages/images/tx.png' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'msg_3', |
||||
|
isMe: false, |
||||
|
type: 'text', |
||||
|
content: '脸上起了很多小红点,有点痒', |
||||
|
timestamp: Date.now() - 3400000, |
||||
|
status: 'success', |
||||
|
avatar: '/pages/images/tx.png' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'msg_4', |
||||
|
isMe: false, |
||||
|
type: 'image', |
||||
|
content: 'https://picsum.photos/200/200?random=1', |
||||
|
timestamp: Date.now() - 3300000, |
||||
|
status: 'success', |
||||
|
avatar: '/pages/images/tx.png' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'msg_5', |
||||
|
isMe: true, |
||||
|
type: 'text', |
||||
|
content: '看起来像是过敏反应,最近有接触什么新的东西吗?', |
||||
|
timestamp: Date.now() - 3200000, |
||||
|
status: 'success', |
||||
|
avatar: '/pages/images/tx.png' |
||||
|
} |
||||
|
], |
||||
|
|
||||
|
// 定时器
|
||||
|
typingTimer: null, |
||||
|
mockUserTimer: null |
||||
|
}, |
||||
|
|
||||
|
onLoad: function(options) { |
||||
|
console.log('专家端聊天页面加载', options); |
||||
|
|
||||
|
// 获取传递的参数
|
||||
|
if (options.userId) { |
||||
|
this.setData({ |
||||
|
'conversation.userId': options.userId, |
||||
|
'conversation.userName': options.userName || '用户' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 加载模拟消息
|
||||
|
this.loadMockMessages(); |
||||
|
|
||||
|
// 模拟用户在线状态变化
|
||||
|
this.simulateOnlineStatus(); |
||||
|
|
||||
|
// 模拟用户发送消息
|
||||
|
this.startMockUserTyping(); |
||||
|
}, |
||||
|
|
||||
|
onShow: function() { |
||||
|
// 滚动到底部
|
||||
|
setTimeout(() => { |
||||
|
this.scrollToBottom(false); |
||||
|
}, 200); |
||||
|
}, |
||||
|
|
||||
|
onUnload: function() { |
||||
|
// 清理定时器
|
||||
|
if (this.data.mockUserTimer) { |
||||
|
clearInterval(this.data.mockUserTimer); |
||||
|
} |
||||
|
if (this.data.typingTimer) { |
||||
|
clearTimeout(this.data.typingTimer); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 返回上一页
|
||||
|
goBack: function() { |
||||
|
wx.navigateBack({ |
||||
|
delta: 1 |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
|
||||
|
|
||||
|
// 加载模拟消息
|
||||
|
loadMockMessages: function() { |
||||
|
// 处理消息时间显示
|
||||
|
const messages = this.processMessageTimes(this.data.mockMessages); |
||||
|
|
||||
|
this.setData({ |
||||
|
messageList: messages, |
||||
|
hasMore: false |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 加载更多消息(模拟)
|
||||
|
loadMoreMessages: function() { |
||||
|
if (this.data.loadingMore || !this.data.hasMore) return; |
||||
|
|
||||
|
this.setData({ loadingMore: true }); |
||||
|
|
||||
|
// 模拟网络延迟
|
||||
|
setTimeout(() => { |
||||
|
// 模拟更早的消息
|
||||
|
const olderMessages = [ |
||||
|
{ |
||||
|
id: 'old_' + Date.now(), |
||||
|
isMe: false, |
||||
|
type: 'text', |
||||
|
content: '之前也出现过类似情况', |
||||
|
timestamp: Date.now() - 86400000, |
||||
|
status: 'success', |
||||
|
avatar: '/pages/images/tx.png' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'old_' + (Date.now() + 1), |
||||
|
isMe: true, |
||||
|
type: 'text', |
||||
|
content: '那当时是怎么处理的呢?', |
||||
|
timestamp: Date.now() - 86000000, |
||||
|
status: 'success', |
||||
|
avatar: '/pages/images/tx.png' |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
const processedOld = this.processMessageTimes(olderMessages); |
||||
|
const newList = [...processedOld, ...this.data.messageList]; |
||||
|
|
||||
|
this.setData({ |
||||
|
messageList: newList, |
||||
|
loadingMore: false, |
||||
|
hasMore: false // 模拟没有更多了
|
||||
|
}); |
||||
|
|
||||
|
// 调整滚动位置
|
||||
|
setTimeout(() => { |
||||
|
this.setData({ |
||||
|
scrollAnimate: false, |
||||
|
scrollTop: 300 |
||||
|
}, () => { |
||||
|
setTimeout(() => { |
||||
|
this.setData({ scrollAnimate: true }); |
||||
|
}, 100); |
||||
|
}); |
||||
|
}, 50); |
||||
|
}, 800); |
||||
|
}, |
||||
|
|
||||
|
// 模拟用户在线状态变化
|
||||
|
simulateOnlineStatus: function() { |
||||
|
setInterval(() => { |
||||
|
// 随机改变在线状态(模拟)
|
||||
|
const random = Math.random(); |
||||
|
this.setData({ |
||||
|
onlineStatus: random > 0.3 // 70%时间在线
|
||||
|
}); |
||||
|
}, 30000); |
||||
|
}, |
||||
|
|
||||
|
// 模拟用户正在输入并发送消息
|
||||
|
startMockUserTyping: function() { |
||||
|
// 每45-90秒模拟用户发送一条消息
|
||||
|
const timer = setInterval(() => { |
||||
|
// 只有在线时才发送
|
||||
|
if (this.data.onlineStatus) { |
||||
|
this.simulateUserMessage(); |
||||
|
} |
||||
|
}, Math.random() * 45000 + 45000); // 45-90秒
|
||||
|
|
||||
|
this.setData({ mockUserTimer: timer }); |
||||
|
}, |
||||
|
|
||||
|
// 模拟用户发送消息
|
||||
|
simulateUserMessage: function() { |
||||
|
const messages = [ |
||||
|
'好的,我明白了', |
||||
|
'谢谢医生!', |
||||
|
'需要用什么药吗?', |
||||
|
'大概多久能好?', |
||||
|
'有没有什么需要注意的?', |
||||
|
'好的,我试试看', |
||||
|
'明白了,非常感谢!' |
||||
|
]; |
||||
|
|
||||
|
const randomMsg = messages[Math.floor(Math.random() * messages.length)]; |
||||
|
|
||||
|
const newMsg = { |
||||
|
id: 'user_' + Date.now() + Math.random(), |
||||
|
isMe: false, |
||||
|
type: 'text', |
||||
|
content: randomMsg, |
||||
|
timestamp: Date.now(), |
||||
|
status: 'success', |
||||
|
avatar: this.data.conversation.userAvatar || '/pages/images/tx.png' |
||||
|
}; |
||||
|
|
||||
|
this.addMessageToList(newMsg); |
||||
|
|
||||
|
// 震动提示(可选)
|
||||
|
wx.vibrateShort({ type: 'light' }); |
||||
|
}, |
||||
|
|
||||
|
// 发送文本消息
|
||||
|
sendTextMessage: function() { |
||||
|
const content = this.data.inputValue.trim(); |
||||
|
if (!content) return; |
||||
|
|
||||
|
const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); |
||||
|
|
||||
|
// 创建本地消息
|
||||
|
const newMessage = { |
||||
|
id: messageId, |
||||
|
isMe: true, |
||||
|
type: 'text', |
||||
|
content: content, |
||||
|
timestamp: Date.now(), |
||||
|
status: 'sending', |
||||
|
avatar: this.data.expertAvatar |
||||
|
}; |
||||
|
|
||||
|
// 添加到列表
|
||||
|
this.addMessageToList(newMessage); |
||||
|
|
||||
|
// 清空输入框
|
||||
|
this.setData({ inputValue: '' }); |
||||
|
|
||||
|
// 模拟发送延迟
|
||||
|
setTimeout(() => { |
||||
|
this.updateMessageStatus(messageId, 'success'); |
||||
|
}, 500); |
||||
|
}, |
||||
|
|
||||
|
// 添加消息到列表
|
||||
|
addMessageToList: function(message) { |
||||
|
const { messageList } = this.data; |
||||
|
|
||||
|
// 新消息添加到末尾
|
||||
|
messageList.push(message); |
||||
|
|
||||
|
// 重新处理时间显示
|
||||
|
const processedMessages = this.processMessageTimes(messageList); |
||||
|
|
||||
|
this.setData({ |
||||
|
messageList: processedMessages |
||||
|
}, () => { |
||||
|
this.scrollToBottom(true); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 更新消息状态
|
||||
|
updateMessageStatus: function(messageId, status) { |
||||
|
const { messageList } = this.data; |
||||
|
const index = messageList.findIndex(msg => msg.id === messageId); |
||||
|
|
||||
|
if (index !== -1) { |
||||
|
messageList[index].status = status; |
||||
|
this.setData({ messageList }); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 重试发送
|
||||
|
retrySend: function(e) { |
||||
|
const messageId = e.currentTarget.dataset.id; |
||||
|
const { messageList } = this.data; |
||||
|
const msg = messageList.find(m => m.id === messageId); |
||||
|
|
||||
|
if (msg) { |
||||
|
msg.status = 'sending'; |
||||
|
this.setData({ messageList }); |
||||
|
|
||||
|
setTimeout(() => { |
||||
|
msg.status = 'success'; |
||||
|
this.setData({ messageList }); |
||||
|
}, 500); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 处理消息时间显示
|
||||
|
processMessageTimes: function(messages) { |
||||
|
if (!messages || messages.length === 0) return []; |
||||
|
|
||||
|
const timeInterval = 5; // 5分钟间隔显示时间
|
||||
|
|
||||
|
return messages.map((msg, index) => { |
||||
|
const showTime = index === 0 || |
||||
|
(msg.timestamp - messages[index - 1].timestamp) > timeInterval * 60 * 1000; |
||||
|
|
||||
|
return { |
||||
|
...msg, |
||||
|
showTime |
||||
|
}; |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 格式化时间
|
||||
|
formatTime: function(timestamp) { |
||||
|
if (!timestamp) return ''; |
||||
|
|
||||
|
const date = new Date(Number(timestamp)); |
||||
|
const hours = date.getHours().toString().padStart(2, '0'); |
||||
|
const minutes = date.getMinutes().toString().padStart(2, '0'); |
||||
|
return `${hours}:${minutes}`; |
||||
|
}, |
||||
|
|
||||
|
// 格式化文件大小
|
||||
|
formatFileSize: function(bytes) { |
||||
|
if (!bytes || bytes === 0) return '0B'; |
||||
|
|
||||
|
const units = ['B', 'KB', 'MB', 'GB']; |
||||
|
let size = bytes; |
||||
|
let unitIndex = 0; |
||||
|
|
||||
|
while (size >= 1024 && unitIndex < units.length - 1) { |
||||
|
size /= 1024; |
||||
|
unitIndex++; |
||||
|
} |
||||
|
|
||||
|
return size.toFixed(1) + units[unitIndex]; |
||||
|
}, |
||||
|
|
||||
|
// 输入处理
|
||||
|
onInput: function(e) { |
||||
|
this.setData({ inputValue: e.detail.value }); |
||||
|
}, |
||||
|
|
||||
|
// 输入框获得焦点
|
||||
|
onInputFocus: function() { |
||||
|
this.setData({ inputFocus: true }, () => { |
||||
|
setTimeout(() => { |
||||
|
this.scrollToBottom(true); |
||||
|
}, 200); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 输入框失去焦点
|
||||
|
onInputBlur: function() { |
||||
|
this.setData({ inputFocus: false }); |
||||
|
}, |
||||
|
|
||||
|
// 清除输入
|
||||
|
clearInput: function() { |
||||
|
this.setData({ |
||||
|
inputValue: '', |
||||
|
inputFocus: true |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 滚动事件
|
||||
|
onScroll: function(e) { |
||||
|
const scrollTop = e.detail.scrollTop; |
||||
|
this.setData({ lastScrollTop: scrollTop }); |
||||
|
|
||||
|
// 滚动到顶部加载更多
|
||||
|
if (scrollTop <= 30 && !this.data.loadingMore && this.data.hasMore) { |
||||
|
this.loadMoreMessages(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 滚动到底部
|
||||
|
scrollToBottom: function(animate = true) { |
||||
|
this.setData({ |
||||
|
scrollAnimate: animate |
||||
|
}, () => { |
||||
|
setTimeout(() => { |
||||
|
this.setData({ scrollTop: 999999 }); |
||||
|
}, 50); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 显示多媒体选择面板
|
||||
|
showMediaActionSheet: function() { |
||||
|
this.setData({ |
||||
|
showMediaSheet: true, |
||||
|
inputFocus: false |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 隐藏多媒体选择面板
|
||||
|
hideMediaActionSheet: function() { |
||||
|
this.setData({ showMediaSheet: false }); |
||||
|
}, |
||||
|
|
||||
|
// 选择图片(模拟)
|
||||
|
chooseImage: function() { |
||||
|
this.hideMediaActionSheet(); |
||||
|
|
||||
|
// 模拟选择图片
|
||||
|
const mockImages = [ |
||||
|
'https://picsum.photos/200/200?random=2', |
||||
|
'https://picsum.photos/200/200?random=3', |
||||
|
'https://picsum.photos/200/200?random=4' |
||||
|
]; |
||||
|
|
||||
|
const randomImage = mockImages[Math.floor(Math.random() * mockImages.length)]; |
||||
|
|
||||
|
const messageId = 'img_' + Date.now(); |
||||
|
|
||||
|
const newMessage = { |
||||
|
id: messageId, |
||||
|
isMe: true, |
||||
|
type: 'image', |
||||
|
content: randomImage, |
||||
|
timestamp: Date.now(), |
||||
|
status: 'uploading', |
||||
|
progress: 0, |
||||
|
avatar: this.data.expertAvatar |
||||
|
}; |
||||
|
|
||||
|
this.addMessageToList(newMessage); |
||||
|
|
||||
|
// 模拟上传进度
|
||||
|
let progress = 0; |
||||
|
const interval = setInterval(() => { |
||||
|
progress += 20; |
||||
|
|
||||
|
if (progress >= 100) { |
||||
|
clearInterval(interval); |
||||
|
this.updateMessageStatus(messageId, 'success'); |
||||
|
} else { |
||||
|
const msgIndex = this.data.messageList.findIndex(m => m.id === messageId); |
||||
|
if (msgIndex !== -1) { |
||||
|
this.data.messageList[msgIndex].progress = progress; |
||||
|
this.setData({ messageList: this.data.messageList }); |
||||
|
} |
||||
|
} |
||||
|
}, 200); |
||||
|
}, |
||||
|
|
||||
|
// 选择视频(模拟)
|
||||
|
chooseVideo: function() { |
||||
|
this.hideMediaActionSheet(); |
||||
|
|
||||
|
const messageId = 'video_' + Date.now(); |
||||
|
|
||||
|
const newMessage = { |
||||
|
id: messageId, |
||||
|
isMe: true, |
||||
|
type: 'video', |
||||
|
content: 'https://example.com/video.mp4', |
||||
|
thumb: 'https://picsum.photos/200/200?random=5', |
||||
|
timestamp: Date.now(), |
||||
|
status: 'uploading', |
||||
|
progress: 0, |
||||
|
avatar: this.data.expertAvatar |
||||
|
}; |
||||
|
|
||||
|
this.addMessageToList(newMessage); |
||||
|
|
||||
|
// 模拟上传进度
|
||||
|
let progress = 0; |
||||
|
const interval = setInterval(() => { |
||||
|
progress += 25; |
||||
|
|
||||
|
if (progress >= 100) { |
||||
|
clearInterval(interval); |
||||
|
this.updateMessageStatus(messageId, 'success'); |
||||
|
} else { |
||||
|
const msgIndex = this.data.messageList.findIndex(m => m.id === messageId); |
||||
|
if (msgIndex !== -1) { |
||||
|
this.data.messageList[msgIndex].progress = progress; |
||||
|
this.setData({ messageList: this.data.messageList }); |
||||
|
} |
||||
|
} |
||||
|
}, 300); |
||||
|
}, |
||||
|
|
||||
|
// 预览图片
|
||||
|
previewImage: function(e) { |
||||
|
const url = e.currentTarget.dataset.url; |
||||
|
wx.previewImage({ |
||||
|
current: url, |
||||
|
urls: [url] |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 阻止事件冒泡
|
||||
|
stopPropagation: function() {} |
||||
|
}); |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"navigationBarTitleText":"咨询", |
||||
|
"usingComponents": {} |
||||
|
} |
||||
@ -0,0 +1,280 @@ |
|||||
|
<!-- 专家端聊天页面 --> |
||||
|
<view class="consult-page"> |
||||
|
<!-- 头部专家信息 --> |
||||
|
<view class="consult-header"> |
||||
|
<view class="header-content"> |
||||
|
<view class="back-btn" bindtap="goBack"> |
||||
|
<image src="/pagesA/images/back.png" class="back-icon"></image> |
||||
|
</view> |
||||
|
<view class="header-center"> |
||||
|
<view class="expert-info"> |
||||
|
<text class="expert-name">{{conversation.userName || '用户'}}</text> |
||||
|
<view class="expert-status"> |
||||
|
<view class="status-dot {{onlineStatus ? 'online' : 'offline'}}"></view> |
||||
|
<text class="status-text">{{onlineStatus ? '在线' : '离线'}}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 聊天内容区域 --> |
||||
|
<scroll-view |
||||
|
id="chatScroll" |
||||
|
class="chat-container" |
||||
|
scroll-y |
||||
|
scroll-top="{{scrollTop}}" |
||||
|
scroll-with-animation="{{scrollAnimate}}" |
||||
|
enable-back-to-top="true" |
||||
|
show-scrollbar="{{false}}" |
||||
|
bindscroll="onScroll" |
||||
|
> |
||||
|
<!-- 加载更多提示 --> |
||||
|
<view class="load-more-tip" wx:if="{{loadingMore}}"> |
||||
|
<text>加载中...</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 消息列表 --> |
||||
|
<block wx:for="{{messageList}}" wx:key="id"> |
||||
|
<!-- 对方消息(用户) --> |
||||
|
<view class="message-item message-left" wx:if="{{!item.isMe}}"> |
||||
|
<view class="message-avatar"> |
||||
|
<image src="{{item.avatar || '/pages/images/tx.png'}}" class="avatar-img"></image> |
||||
|
</view> |
||||
|
|
||||
|
<view class="messages-content-wrapper"> |
||||
|
<view class="message-arrow arrow-left"></view> |
||||
|
|
||||
|
<!-- 文本消息 --> |
||||
|
<view class="message-bubble bubble-left" wx:if="{{item.type === 'text'}}"> |
||||
|
<text class="message-text">{{item.content}}</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 图片消息 --> |
||||
|
<view class="media-bubble" wx:elif="{{item.type === 'image'}}"> |
||||
|
<image |
||||
|
src="{{item.content}}" |
||||
|
class="message-image" |
||||
|
mode="aspectFill" |
||||
|
bindtap="previewImage" |
||||
|
data-url="{{item.content}}" |
||||
|
></image> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 视频消息 --> |
||||
|
<view class="media-bubble" wx:elif="{{item.type === 'video'}}"> |
||||
|
<video |
||||
|
src="{{item.content}}" |
||||
|
class="message-video" |
||||
|
controls |
||||
|
show-center-play-btn |
||||
|
poster="{{item.thumb}}" |
||||
|
></video> |
||||
|
<view class="video-play-overlay"> |
||||
|
<image src="/images/icons/play.png" class="play-icon"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 文件消息 --> |
||||
|
<view class="message-bubble bubble-left message-file" wx:elif="{{item.type === 'file'}}"> |
||||
|
<view class="file-icon-box"> |
||||
|
<image src="/images/icons/file_icon.png" class="file-icon"></image> |
||||
|
</view> |
||||
|
<view class="file-info"> |
||||
|
<text class="file-name">{{item.fileName}}</text> |
||||
|
<text class="file-size">{{formatFileSize(item.fileSize)}}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 消息时间(如果显示) --> |
||||
|
<text class="message-time" wx:if="{{item.showTime}}">{{formatTime(item.timestamp)}}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 我的消息(专家) --> |
||||
|
<view class="message-item message-right" wx:else> |
||||
|
<view class="message-content-wrapper"> |
||||
|
<view class="message-arrow arrow-right"></view> |
||||
|
|
||||
|
<!-- 文本消息 --> |
||||
|
<view class="message-bubble bubble-right" wx:if="{{item.type === 'text'}}"> |
||||
|
<text class="message-text">{{item.content}}</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 图片消息 --> |
||||
|
<view class="media-bubble" wx:elif="{{item.type === 'image'}}"> |
||||
|
<image |
||||
|
src="{{item.content}}" |
||||
|
class="message-image" |
||||
|
mode="aspectFill" |
||||
|
bindtap="previewImage" |
||||
|
data-url="{{item.content}}" |
||||
|
></image> |
||||
|
<!-- 上传进度 --> |
||||
|
<view class="upload-progress" wx:if="{{item.status === 'uploading'}}"> |
||||
|
<view class="progress-circle"> |
||||
|
<text class="progress-text">{{item.progress}}%</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 视频消息 --> |
||||
|
<view class="media-bubble" wx:elif="{{item.type === 'video'}}"> |
||||
|
<video |
||||
|
src="{{item.content}}" |
||||
|
class="message-video" |
||||
|
controls |
||||
|
show-center-play-btn |
||||
|
poster="{{item.thumb}}" |
||||
|
></video> |
||||
|
<view class="video-play-overlay"> |
||||
|
<image src="/images/icons/play.png" class="play-icon"></image> |
||||
|
</view> |
||||
|
<view class="upload-progress" wx:if="{{item.status === 'uploading'}}"> |
||||
|
<view class="progress-circle"> |
||||
|
<text class="progress-text">{{item.progress}}%</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 文件消息 --> |
||||
|
<view class="message-bubble bubble-right message-file" wx:elif="{{item.type === 'file'}}"> |
||||
|
<view class="file-icon-box"> |
||||
|
<image src="/images/icons/file_icon.png" class="file-icon"></image> |
||||
|
</view> |
||||
|
<view class="file-info"> |
||||
|
<text class="file-name">{{item.fileName}}</text> |
||||
|
<text class="file-size">{{formatFileSize(item.fileSize)}}</text> |
||||
|
</view> |
||||
|
<view class="upload-progress" wx:if="{{item.status === 'uploading'}}"> |
||||
|
<view class="progress-circle"> |
||||
|
<text class="progress-text">{{item.progress}}%</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 消息状态 --> |
||||
|
<view class="message-status" wx:if="{{item.status === 'sending'}}"> |
||||
|
<text class="status-text">发送中</text> |
||||
|
</view> |
||||
|
<view class="message-status" wx:elif="{{item.status === 'fail'}}"> |
||||
|
<text class="status-text fail">发送失败</text> |
||||
|
<text class="retry-text" bindtap="retrySend" data-id="{{item.id}}">重试</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 消息时间 --> |
||||
|
<text class="message-time" wx:if="{{item.showTime}}">{{formatTime(item.timestamp)}}</text> |
||||
|
</view> |
||||
|
|
||||
|
<view class="message-avatar"> |
||||
|
<image src="{{expertAvatar || '/pages/images/tx.png'}}" class="avatar-img"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
</block> |
||||
|
|
||||
|
<!-- 底部空间 --> |
||||
|
<view class="chat-bottom-space"></view> |
||||
|
|
||||
|
<!-- 空状态 --> |
||||
|
<view class="empty-tip" wx:if="{{messageList.length === 0}}"> |
||||
|
<text class="empty-text">暂无聊天记录,开始咨询吧</text> |
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
|
||||
|
<!-- 输入区域 --> |
||||
|
<view class="input-section" id="inputSection"> |
||||
|
<view class="text-input-panel"> |
||||
|
<!-- 添加按钮 --> |
||||
|
<view class="add-btn" bindtap="showMediaActionSheet"> |
||||
|
<image src="/pagesA/images/add.png" class="add-icon"></image> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 输入框包装器 --> |
||||
|
<view class="input-wrapper"> |
||||
|
<textarea |
||||
|
auto-height |
||||
|
maxlength="500" |
||||
|
class="chat-textarea" |
||||
|
placeholder="请输入消息..." |
||||
|
placeholder-class="input-placeholder" |
||||
|
value="{{inputValue}}" |
||||
|
bindinput="onInput" |
||||
|
confirm-type="send" |
||||
|
focus="{{inputFocus}}" |
||||
|
adjust-position="{{false}}" |
||||
|
cursor-spacing="20" |
||||
|
bindfocus="onInputFocus" |
||||
|
bindblur="onInputBlur" |
||||
|
show-confirm-bar="{{false}}" |
||||
|
disable-default-padding="{{true}}" |
||||
|
></textarea> |
||||
|
|
||||
|
<!-- 清空按钮 --> |
||||
|
<view class="input-actions" wx:if="{{inputValue}}"> |
||||
|
<button class="clear-btn" bindtap="clearInput"> |
||||
|
<image src="/pagesA/images/ch.png" class="clear-icon"></image> |
||||
|
</button> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 发送按钮 --> |
||||
|
<button |
||||
|
class="send-btn" |
||||
|
bindtap="sendTextMessage" |
||||
|
wx:if="{{inputValue}}" |
||||
|
> |
||||
|
<text class="send-text">发送</text> |
||||
|
</button> |
||||
|
|
||||
|
<!-- 占位 --> |
||||
|
<view class="send-placeholder" wx:else></view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 多媒体选择面板 --> |
||||
|
<view class="media-action-sheet" wx:if="{{showMediaSheet}}" catchtap="hideMediaActionSheet"> |
||||
|
<view class="media-sheet-content" catchtap="stopPropagation"> |
||||
|
<view class="media-sheet-header"> |
||||
|
<text class="sheet-title">发送内容</text> |
||||
|
<view class="close-sheet-btn" bindtap="hideMediaActionSheet"> |
||||
|
<image src="/pagesA/images/cuo.png"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<view class="media-options-grid"> |
||||
|
<view class="media-option" bindtap="chooseImage"> |
||||
|
<view class="option-icon-box"> |
||||
|
<image src="/pagesA/images/zp.png"></image> |
||||
|
</view> |
||||
|
<text class="option-text">照片</text> |
||||
|
</view> |
||||
|
|
||||
|
<view class="media-option" bindtap="chooseVideo"> |
||||
|
<view class="option-icon-box"> |
||||
|
<image src="/pagesA/images/ps.png"></image> |
||||
|
</view> |
||||
|
<text class="option-text">视频</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<view class="sheet-bottom"> |
||||
|
<text class="bottom-tip">最多可选择9张照片</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 更多菜单 --> |
||||
|
<view class="more-menu" wx:if="{{showMoreMenu}}" catchtap="hideMoreMenu"> |
||||
|
<view class="menu-content"> |
||||
|
<view class="menu-item" bindtap="clearHistory"> |
||||
|
<text>清空聊天记录</text> |
||||
|
</view> |
||||
|
<view class="menu-item" bindtap="reportUser"> |
||||
|
<text>举报用户</text> |
||||
|
</view> |
||||
|
<view class="menu-item" bindtap="blockUser"> |
||||
|
<text>拉黑用户</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
@ -0,0 +1,685 @@ |
|||||
|
/* 专家端聊天样式 */ |
||||
|
.consult-page { |
||||
|
width: 100vw; |
||||
|
height: 100vh; |
||||
|
background: #f5f5f5; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
/* 头部样式 */ |
||||
|
.consult-header { |
||||
|
background: #ffffff; |
||||
|
border-bottom: 1rpx solid #e5e5e5; |
||||
|
position: relative; |
||||
|
z-index: 1000; |
||||
|
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.05); |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.header-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 12rpx 24rpx; |
||||
|
height: 96rpx; |
||||
|
} |
||||
|
|
||||
|
.back-btn, .header-right { |
||||
|
width: 48rpx; |
||||
|
height: 48rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.back-icon, .more-icon { |
||||
|
width: 40rpx; |
||||
|
height: 40rpx; |
||||
|
} |
||||
|
|
||||
|
.header-center { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.expert-info { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.expert-name { |
||||
|
font-size: 32rpx; |
||||
|
font-weight: 600; |
||||
|
color: #000000; |
||||
|
line-height: 44rpx; |
||||
|
margin-bottom: 4rpx; |
||||
|
} |
||||
|
|
||||
|
.expert-status { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.status-dot { |
||||
|
width: 16rpx; |
||||
|
height: 16rpx; |
||||
|
border-radius: 50%; |
||||
|
margin-right: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.status-dot.online { |
||||
|
background: #07c160; |
||||
|
animation: pulse 2s infinite; |
||||
|
} |
||||
|
|
||||
|
.status-dot.offline { |
||||
|
background: #cccccc; |
||||
|
} |
||||
|
|
||||
|
@keyframes pulse { |
||||
|
0% { box-shadow: 0 0 0 0 rgba(7, 193, 96, 0.4); } |
||||
|
70% { box-shadow: 0 0 0 8rpx rgba(7, 193, 96, 0); } |
||||
|
100% { box-shadow: 0 0 0 0 rgba(7, 193, 96, 0); } |
||||
|
} |
||||
|
|
||||
|
.status-text { |
||||
|
font-size: 24rpx; |
||||
|
color: #666666; |
||||
|
} |
||||
|
|
||||
|
/* 聊天容器 */ |
||||
|
.chat-container { |
||||
|
flex: 1; |
||||
|
padding: 20rpx 0; |
||||
|
background: #f5f5f5; |
||||
|
overflow-y: auto; |
||||
|
position: relative; |
||||
|
height: 0; |
||||
|
} |
||||
|
|
||||
|
/* 消息项 */ |
||||
|
.message-item { |
||||
|
display: flex; |
||||
|
margin-bottom: 24rpx; |
||||
|
padding: 0 30rpx; |
||||
|
opacity: 0; |
||||
|
animation: fadeIn 0.3s ease forwards; |
||||
|
align-items: flex-start; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
@keyframes fadeIn { |
||||
|
from { opacity: 0; transform: translateY(10rpx); } |
||||
|
to { opacity: 1; transform: translateY(0); } |
||||
|
} |
||||
|
|
||||
|
.message-left { justify-content: flex-start; } |
||||
|
.message-right { justify-content: flex-end; } |
||||
|
|
||||
|
/* 头像 */ |
||||
|
.message-avatar { |
||||
|
width: 80rpx; |
||||
|
height: 80rpx; |
||||
|
border-radius: 8rpx; |
||||
|
overflow: hidden; |
||||
|
flex-shrink: 0; |
||||
|
background: #ffffff; |
||||
|
border: 1rpx solid #f0f0f0; |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
.message-left .message-avatar { margin-right: 16rpx; } |
||||
|
.message-right .message-avatar { margin-left: 16rpx; } |
||||
|
|
||||
|
.avatar-img { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
|
||||
|
/* 消息内容包装器 */ |
||||
|
.message-content-wrapper { |
||||
|
max-width: 480rpx; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
.message-left .message-content-wrapper { align-items: flex-start; } |
||||
|
.message-right .message-content-wrapper { align-items: flex-end; } |
||||
|
|
||||
|
/* 气泡箭头 */ |
||||
|
.message-arrow { |
||||
|
position: absolute; |
||||
|
width: 0; |
||||
|
height: 0; |
||||
|
border-style: solid; |
||||
|
border-width: 12rpx; |
||||
|
top: 30rpx; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
.arrow-left { |
||||
|
left: -24rpx; |
||||
|
border-color: transparent #ffffff transparent transparent; |
||||
|
} |
||||
|
|
||||
|
.arrow-right { |
||||
|
right: -24rpx; |
||||
|
border-color: transparent transparent transparent #95ec69; |
||||
|
} |
||||
|
|
||||
|
/* 消息气泡 */ |
||||
|
.message-bubble { |
||||
|
position: relative; |
||||
|
padding: 16rpx 20rpx; |
||||
|
word-break: break-word; |
||||
|
box-sizing: border-box; |
||||
|
min-height: 60rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.bubble-left { |
||||
|
background: #ffffff; |
||||
|
border-radius: 10rpx; |
||||
|
border-top-left-radius: 4rpx; |
||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); |
||||
|
} |
||||
|
|
||||
|
.bubble-right { |
||||
|
background: #95ec69; |
||||
|
border-radius: 10rpx; |
||||
|
border-top-right-radius: 4rpx; |
||||
|
box-shadow: 0 2rpx 8rpx rgba(149, 236, 105, 0.2); |
||||
|
} |
||||
|
|
||||
|
/* 文本消息 */ |
||||
|
.message-text { |
||||
|
font-size: 32rpx; |
||||
|
color: #000000; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
|
||||
|
.bubble-right .message-text { color: #000000; } |
||||
|
|
||||
|
/* 媒体消息 */ |
||||
|
.media-bubble { |
||||
|
position: relative; |
||||
|
border-radius: 10rpx; |
||||
|
overflow: hidden; |
||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); |
||||
|
background: #ffffff; |
||||
|
min-height: 60rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.message-left .media-bubble { border-top-left-radius: 4rpx; } |
||||
|
.message-right .media-bubble { border-top-right-radius: 4rpx; } |
||||
|
|
||||
|
.message-image { |
||||
|
width: 280rpx; |
||||
|
height: 280rpx; |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.message-video { |
||||
|
width: 280rpx; |
||||
|
height: 280rpx; |
||||
|
background: #000000; |
||||
|
} |
||||
|
|
||||
|
.video-play-overlay { |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
left: 50%; |
||||
|
transform: translate(-50%, -50%); |
||||
|
width: 80rpx; |
||||
|
height: 80rpx; |
||||
|
border-radius: 50%; |
||||
|
background: rgba(0, 0, 0, 0.6); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.play-icon { |
||||
|
width: 40rpx; |
||||
|
height: 40rpx; |
||||
|
margin-left: 4rpx; |
||||
|
} |
||||
|
|
||||
|
/* 文件消息 */ |
||||
|
.message-file { |
||||
|
min-width: 280rpx; |
||||
|
padding: 20rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
min-height: 60rpx; |
||||
|
} |
||||
|
|
||||
|
.file-icon-box { |
||||
|
width: 56rpx; |
||||
|
height: 72rpx; |
||||
|
margin-right: 16rpx; |
||||
|
position: relative; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.file-icon { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.file-info { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.file-name { |
||||
|
font-size: 28rpx; |
||||
|
font-weight: 500; |
||||
|
color: #000000; |
||||
|
margin-bottom: 6rpx; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.file-size { |
||||
|
font-size: 24rpx; |
||||
|
color: #666666; |
||||
|
} |
||||
|
|
||||
|
/* 消息时间 */ |
||||
|
.message-time { |
||||
|
font-size: 22rpx; |
||||
|
color: #999999; |
||||
|
margin-top: 8rpx; |
||||
|
align-self: flex-start; |
||||
|
} |
||||
|
|
||||
|
.message-right .message-time { |
||||
|
align-self: flex-end; |
||||
|
} |
||||
|
|
||||
|
/* 消息状态 */ |
||||
|
.message-status { |
||||
|
font-size: 22rpx; |
||||
|
color: #999999; |
||||
|
margin-top: 4rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.message-status .fail { |
||||
|
color: #ff4d4f; |
||||
|
} |
||||
|
|
||||
|
.retry-text { |
||||
|
color: #1890ff; |
||||
|
text-decoration: underline; |
||||
|
margin-left: 8rpx; |
||||
|
} |
||||
|
|
||||
|
/* 上传进度 */ |
||||
|
.upload-progress { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: rgba(0, 0, 0, 0.5); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
border-radius: inherit; |
||||
|
} |
||||
|
|
||||
|
.progress-circle { |
||||
|
width: 80rpx; |
||||
|
height: 80rpx; |
||||
|
border-radius: 50%; |
||||
|
border: 6rpx solid rgba(255, 255, 255, 0.3); |
||||
|
border-top-color: #ffffff; |
||||
|
animation: spin 1s linear infinite; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
0% { transform: rotate(0deg); } |
||||
|
100% { transform: rotate(360deg); } |
||||
|
} |
||||
|
|
||||
|
.progress-text { |
||||
|
font-size: 20rpx; |
||||
|
color: #ffffff; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
/* 底部留白 */ |
||||
|
.chat-bottom-space { height: 40rpx; } |
||||
|
|
||||
|
/* 加载更多 */ |
||||
|
.load-more-tip { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
padding: 30rpx 0; |
||||
|
color: #999999; |
||||
|
font-size: 24rpx; |
||||
|
} |
||||
|
|
||||
|
/* 空状态 */ |
||||
|
.empty-tip { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 100rpx 0; |
||||
|
color: #999999; |
||||
|
} |
||||
|
|
||||
|
.empty-text { |
||||
|
font-size: 28rpx; |
||||
|
color: #999999; |
||||
|
} |
||||
|
|
||||
|
/* 输入区域 */ |
||||
|
.input-section { |
||||
|
background: #ffffff; |
||||
|
border-top: 1rpx solid #e5e5e5; |
||||
|
padding: 16rpx 30rpx; |
||||
|
position: relative; |
||||
|
z-index: 100; |
||||
|
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05); |
||||
|
flex-shrink: 0; |
||||
|
width: 100%; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.text-input-panel { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 16rpx; |
||||
|
min-height: 72rpx; |
||||
|
} |
||||
|
|
||||
|
.add-btn { |
||||
|
width: 72rpx; |
||||
|
height: 72rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.add-icon { |
||||
|
width: 60rpx; |
||||
|
height: 60rpx; |
||||
|
} |
||||
|
|
||||
|
.input-wrapper { |
||||
|
flex: 1; |
||||
|
position: relative; |
||||
|
background: #f5f5f5; |
||||
|
border-radius: 10rpx; |
||||
|
min-height: 72rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 0 24rpx; |
||||
|
transition: all 0.2s; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.input-wrapper:active { |
||||
|
background: #e8e8e8; |
||||
|
} |
||||
|
|
||||
|
.chat-textarea { |
||||
|
flex: 1; |
||||
|
width: 100%; |
||||
|
font-size: 30rpx; |
||||
|
color: #333333; |
||||
|
line-height: 1.4; |
||||
|
min-height: 48rpx; |
||||
|
max-height: 160rpx; |
||||
|
padding: 12rpx 0; |
||||
|
margin: 0; |
||||
|
background: transparent; |
||||
|
border: none; |
||||
|
box-sizing: border-box; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.input-placeholder { |
||||
|
color: #999999; |
||||
|
font-size: 28rpx; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
|
||||
|
.input-actions { |
||||
|
position: absolute; |
||||
|
right: 16rpx; |
||||
|
top: 50%; |
||||
|
transform: translateY(-50%); |
||||
|
z-index: 2; |
||||
|
flex-shrink: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.clear-btn { |
||||
|
width: 36rpx; |
||||
|
height: 36rpx; |
||||
|
border: none; |
||||
|
background: transparent; |
||||
|
padding: 0; |
||||
|
margin: 0; |
||||
|
line-height: 1; |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.clear-btn::after { |
||||
|
border: none; |
||||
|
} |
||||
|
|
||||
|
.clear-btn:active { |
||||
|
background: rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.clear-icon { |
||||
|
width: 28rpx; |
||||
|
height: 28rpx; |
||||
|
} |
||||
|
|
||||
|
.send-btn { |
||||
|
background: linear-gradient(135deg, #07c160 0%, #06ae56 100%); |
||||
|
width: 112rpx; |
||||
|
height: 66rpx; |
||||
|
border-radius: 10rpx; |
||||
|
border: none; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
transition: all 0.3s ease; |
||||
|
padding: 0; |
||||
|
margin: 0; |
||||
|
line-height: 1; |
||||
|
flex-shrink: 0; |
||||
|
box-shadow: 0 4rpx 8rpx rgba(7, 193, 96, 0.2); |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.send-btn:active { |
||||
|
background: linear-gradient(135deg, #06ae56 0%, #059c4c 100%); |
||||
|
transform: scale(0.96); |
||||
|
box-shadow: 0 2rpx 4rpx rgba(7, 193, 96, 0.3); |
||||
|
} |
||||
|
|
||||
|
.send-text { |
||||
|
font-size: 28rpx; |
||||
|
color: #ffffff; |
||||
|
font-weight: 600; |
||||
|
letter-spacing: 2rpx; |
||||
|
} |
||||
|
|
||||
|
.send-placeholder { |
||||
|
width: 112rpx; |
||||
|
height: 72rpx; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
/* 多媒体选择面板 */ |
||||
|
.media-action-sheet { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: rgba(0, 0, 0, 0.5); |
||||
|
display: flex; |
||||
|
align-items: flex-end; |
||||
|
justify-content: center; |
||||
|
z-index: 2000; |
||||
|
animation: fadeIn 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.media-sheet-content { |
||||
|
width: 100%; |
||||
|
background: #F7F7F7; |
||||
|
border-radius: 40rpx 40rpx 0 0; |
||||
|
padding: 40rpx 30rpx calc(10rpx + env(safe-area-inset-bottom)); |
||||
|
animation: slideUp 0.3s ease; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
@keyframes slideUp { |
||||
|
from { transform: translateY(100%); } |
||||
|
to { transform: translateY(0); } |
||||
|
} |
||||
|
|
||||
|
.media-sheet-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
margin-bottom: 40rpx; |
||||
|
padding: 0 10rpx 20rpx; |
||||
|
border-bottom: 1px solid #E4E4E4; |
||||
|
} |
||||
|
|
||||
|
.sheet-title { |
||||
|
font-size: 32rpx; |
||||
|
font-weight: 600; |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
.close-sheet-btn image { |
||||
|
width: 60rpx; |
||||
|
height: 60rpx; |
||||
|
} |
||||
|
|
||||
|
.media-options-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(4, 1fr); |
||||
|
gap: 40rpx 30rpx; |
||||
|
margin-bottom: 40rpx; |
||||
|
} |
||||
|
|
||||
|
.media-option { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.option-icon-box { |
||||
|
width: 120rpx; |
||||
|
height: 120rpx; |
||||
|
border-radius: 30rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
margin-bottom: 16rpx; |
||||
|
transition: transform 0.2s; |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
|
||||
|
.media-option:active .option-icon-box { |
||||
|
transform: scale(0.95); |
||||
|
} |
||||
|
|
||||
|
.option-icon-box image { |
||||
|
width: 60rpx; |
||||
|
height: 60rpx; |
||||
|
} |
||||
|
|
||||
|
.option-text { |
||||
|
font-size: 26rpx; |
||||
|
color: #666666; |
||||
|
} |
||||
|
|
||||
|
.sheet-bottom { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.bottom-tip { |
||||
|
font-size: 24rpx; |
||||
|
color: #999999; |
||||
|
} |
||||
|
|
||||
|
/* 更多菜单 */ |
||||
|
.more-menu { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: rgba(0, 0, 0, 0.3); |
||||
|
z-index: 2000; |
||||
|
} |
||||
|
|
||||
|
.menu-content { |
||||
|
position: absolute; |
||||
|
top: 96rpx; |
||||
|
right: 24rpx; |
||||
|
background: #ffffff; |
||||
|
border-radius: 12rpx; |
||||
|
padding: 16rpx 0; |
||||
|
min-width: 200rpx; |
||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
.menu-item { |
||||
|
padding: 24rpx 32rpx; |
||||
|
font-size: 28rpx; |
||||
|
color: #333333; |
||||
|
border-bottom: 1rpx solid #f0f0f0; |
||||
|
} |
||||
|
|
||||
|
.menu-item:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.menu-item:active { |
||||
|
background: #f5f5f5; |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
import http from '../../../utils/api' |
||||
|
const baseUrl = require('../../../utils/baseUrl') |
||||
|
|
||||
|
Page({ |
||||
|
data: { |
||||
|
baseUrl: baseUrl, |
||||
|
loadMoreText: '上拉加载更多', |
||||
|
evaluations: [], |
||||
|
pageNo: 1, |
||||
|
pageSize: 10, |
||||
|
total: 0, |
||||
|
isLoading: false, |
||||
|
hasMore: true |
||||
|
}, |
||||
|
|
||||
|
onLoad() { |
||||
|
this.getfeedback(true) |
||||
|
}, |
||||
|
|
||||
|
// 服务评价列表
|
||||
|
getfeedback(isRefresh = false) { |
||||
|
// 防止重复请求
|
||||
|
if (this.data.isLoading) return |
||||
|
|
||||
|
// 如果没有更多数据且不是刷新,直接返回
|
||||
|
if (!this.data.hasMore && !isRefresh) { |
||||
|
this.setData({ |
||||
|
loadMoreText: '—— 没有更多了 ——' |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.setData({ isLoading: true }) |
||||
|
|
||||
|
// 如果是刷新,重置为第一页
|
||||
|
if (isRefresh) { |
||||
|
this.setData({ |
||||
|
pageNo: 1, |
||||
|
hasMore: true |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 显示加载提示
|
||||
|
if (this.data.pageNo > 1) { |
||||
|
this.setData({ loadMoreText: '加载中...' }) |
||||
|
} |
||||
|
|
||||
|
http.feedback({ |
||||
|
data: { |
||||
|
type: '服务评价', |
||||
|
pageNo: this.data.pageNo, |
||||
|
pageSize: this.data.pageSize |
||||
|
}, |
||||
|
success: res => { |
||||
|
console.log('评价列表响应:', res) |
||||
|
|
||||
|
if (res && res.rows) { |
||||
|
const list = res.rows || [] |
||||
|
const total = res.total || 0 |
||||
|
|
||||
|
// 判断是否还有更多数据
|
||||
|
const hasMore = this.data.pageNo * this.data.pageSize < total |
||||
|
|
||||
|
this.setData({ |
||||
|
evaluations: isRefresh ? list : this.data.evaluations.concat(list), |
||||
|
total: total, |
||||
|
hasMore: hasMore, |
||||
|
loadMoreText: hasMore ? '上拉加载更多' : '—— 没有更多了 ——', |
||||
|
pageNo: this.data.pageNo + 1 |
||||
|
}) |
||||
|
} else { |
||||
|
// 处理异常情况
|
||||
|
if (isRefresh) { |
||||
|
this.setData({ |
||||
|
evaluations: [], |
||||
|
hasMore: false, |
||||
|
loadMoreText: '—— 没有更多了 ——' |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
fail: err => { |
||||
|
console.error('获取评价列表失败:', err) |
||||
|
wx.showToast({ |
||||
|
title: '加载失败', |
||||
|
icon: 'none' |
||||
|
}) |
||||
|
|
||||
|
// 加载失败时,恢复加载提示
|
||||
|
this.setData({ |
||||
|
loadMoreText: this.data.hasMore ? '点击重试' : '—— 没有更多了 ——' |
||||
|
}) |
||||
|
}, |
||||
|
complete: () => { |
||||
|
this.setData({ isLoading: false }) |
||||
|
wx.hideNavigationBarLoading() |
||||
|
wx.stopPullDownRefresh() |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 下拉刷新
|
||||
|
onPullDownRefresh() { |
||||
|
wx.showNavigationBarLoading() |
||||
|
this.getfeedback(true) |
||||
|
}, |
||||
|
|
||||
|
// 上拉加载更多
|
||||
|
onReachBottom() { |
||||
|
if (this.data.hasMore && !this.data.isLoading) { |
||||
|
this.getfeedback() |
||||
|
} else if (!this.data.hasMore) { |
||||
|
this.setData({ |
||||
|
loadMoreText: '—— 没有更多了 ——' |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 重新加载(可用于点击重试)
|
||||
|
retryLoad() { |
||||
|
if (this.data.evaluations.length === 0) { |
||||
|
this.getfeedback(true) |
||||
|
} else if (this.data.hasMore) { |
||||
|
this.getfeedback() |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"navigationBarTitleText":"服务评价", |
||||
|
"usingComponents": {} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
<!-- 纯净评价列表页 - 无tab,只有列表 --> |
||||
|
<view class="page"> |
||||
|
|
||||
|
<!-- 自定义列表 --> |
||||
|
<view class="list-container"> |
||||
|
|
||||
|
<!-- 评价项列表 --> |
||||
|
<block wx:for="{{evaluations}}" wx:key="id" wx:for-index="idx"> |
||||
|
|
||||
|
<!-- 评价卡片 --> |
||||
|
<view class="evaluation-card" > |
||||
|
|
||||
|
<!-- 用户信息行:头像 + 昵称 + 时间 --> |
||||
|
<view class="user-row"> |
||||
|
<!-- 头像区域(支持图片和文字占位) --> |
||||
|
<view class="avatar-wrapper"> |
||||
|
<image class="avatar" src="{{item.avatar?baseUrl+item.avatar:'/pages/images/tx.png'}}" mode="aspectFill"></image> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 用户信息和时间 --> |
||||
|
<view class="user-info"> |
||||
|
<view class="name-row"> |
||||
|
<text class="user-name">{{item.nickName?item.nickName:'用户'+idx}}</text> |
||||
|
<text class="time">{{item.createdAt}}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 评价内容 --> |
||||
|
<view class="content-section"> |
||||
|
<rich-text class="content-text" space="emsp" nodes="{{item.content}}"></rich-text> |
||||
|
</view> |
||||
|
|
||||
|
|
||||
|
<!-- 底部装饰线(极简分隔) --> |
||||
|
<view class="card-footer" wx:if="{{idx < evaluations.length - 1}}"> |
||||
|
<view class="divider"></view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</block> |
||||
|
|
||||
|
<!-- 空状态 --> |
||||
|
<view class="empty-state" wx:if="{{evaluations.length === 0}}"> |
||||
|
<image class="empty-icon" src="/pages/images/kzt.png" mode="aspectFit"></image> |
||||
|
<text class="empty-text">暂无评价</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 加载更多提示 --> |
||||
|
<view class="load-more" wx:if="{{evaluations.length > 0}}"> |
||||
|
<text class="load-more-text">{{loadMoreText}}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
@ -0,0 +1,146 @@ |
|||||
|
.page { |
||||
|
min-height: 100vh; |
||||
|
background-color: #ffffff; |
||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; |
||||
|
} |
||||
|
|
||||
|
/* 列表容器 - 直接贴边 */ |
||||
|
.list-container { |
||||
|
padding: 0 16px; |
||||
|
} |
||||
|
|
||||
|
/* 评价卡片 - 极简风格 */ |
||||
|
.evaluation-card { |
||||
|
background-color: #ffffff; |
||||
|
padding: 20px 0; |
||||
|
position: relative; |
||||
|
transition: background-color 0.2s ease; |
||||
|
} |
||||
|
|
||||
|
.evaluation-card:active { |
||||
|
background-color: #f8f9fc; |
||||
|
} |
||||
|
|
||||
|
/* 用户信息行 */ |
||||
|
.user-row { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
|
||||
|
/* 头像区域 */ |
||||
|
.avatar-wrapper { |
||||
|
margin-right: 12px; |
||||
|
} |
||||
|
|
||||
|
.avatar { |
||||
|
width: 48px; |
||||
|
height: 48px; |
||||
|
border-radius: 50%; |
||||
|
background-color: #f0f2f6; |
||||
|
overflow: hidden; |
||||
|
flex-shrink: 0; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); |
||||
|
} |
||||
|
|
||||
|
.avatar-placeholder { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.avatar-text { |
||||
|
font-size: 20px; |
||||
|
font-weight: 500; |
||||
|
color: #ffffff; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
/* 用户信息区 */ |
||||
|
.user-info { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.name-row { |
||||
|
display: flex; |
||||
|
align-items: baseline; |
||||
|
justify-content: space-between; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
|
||||
|
.user-name { |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
color: #1e2a3a; |
||||
|
letter-spacing: -0.2px; |
||||
|
} |
||||
|
|
||||
|
.time { |
||||
|
font-size: 12px; |
||||
|
color: #98a2b3; |
||||
|
font-weight: 400; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/* 评价内容 */ |
||||
|
.content-section { |
||||
|
margin-bottom: 12px; |
||||
|
padding-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.content-text { |
||||
|
font-size: 15px; |
||||
|
line-height: 1.5; |
||||
|
color: #1e2a3a; |
||||
|
word-break: break-word; |
||||
|
font-weight: 400; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/* 卡片底部分隔线 */ |
||||
|
.card-footer { |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.divider { |
||||
|
height: 1px; |
||||
|
background-color: #eef1f4; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
/* 空状态 */ |
||||
|
.empty-state { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 80px 20px; |
||||
|
background-color: #ffffff; |
||||
|
} |
||||
|
|
||||
|
.empty-icon { |
||||
|
width: 140px; |
||||
|
height: 140px; |
||||
|
margin-bottom: 20px; |
||||
|
opacity: 0.6; |
||||
|
} |
||||
|
|
||||
|
.empty-text { |
||||
|
font-size: 15px; |
||||
|
color: #9aa6b8; |
||||
|
font-weight: 400; |
||||
|
} |
||||
|
|
||||
|
/* 加载更多 */ |
||||
|
.load-more { |
||||
|
text-align: center; |
||||
|
padding: 24px 0 30px; |
||||
|
} |
||||
|
|
||||
|
.load-more-text { |
||||
|
font-size: 12px; |
||||
|
color: #b8c0cc; |
||||
|
font-weight: 400; |
||||
|
} |
||||