智慧教务系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

749 lines
16 KiB

<!--学员消息对话详情页面-->
<template>
<view class="main_box">
<!-- 自定义导航栏 -->
<view class="navbar_section">
<view class="navbar_back" @click="goBack">
<text class="back_icon"></text>
</view>
<view class="navbar_title">{{ fromName || '对话详情' }}</view>
<view class="navbar_action"></view>
</view>
<!-- 消息列表 -->
<view class="conversation_section">
<scroll-view
class="message_scroll"
:scroll-y="true"
:scroll-into-view="scrollToView"
:scroll-with-animation="true"
>
<view v-if="loading" class="loading_section">
<view class="loading_text">加载中...</view>
</view>
<view v-else-if="messagesList.length === 0" class="empty_section">
<view class="empty_icon">💬</view>
<view class="empty_text">暂无对话记录</view>
</view>
<view v-else class="messages_container">
<view
v-for="message in messagesList"
:key="message.id"
:id="`msg_${message.id}`"
:class="['message_bubble', message.is_sent_by_student ? 'sent' : 'received']"
>
<view class="message_info">
<view class="sender_name">{{ message.from_name }}</view>
<view class="message_time">{{ formatTime(message.create_time) }}</view>
</view>
<view class="message_content">
<!-- 文本消息 -->
<view v-if="message.message_type === 'text' || !message.message_type" class="content_text">{{ message.content }}</view>
<!-- 图片消息 -->
<view v-if="message.message_type === 'img'" class="content_image" @click="previewImage(message.content)">
<image class="chat_image" :src="message.content" mode="aspectFill"></image>
</view>
</view>
<view class="message_status" v-if="message.is_sent_by_student">
<text class="status_text" v-if="message.is_read">已读</text>
<text class="status_text unread" v-else>未读</text>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 回复输入框 -->
<view class="reply_section">
<view class="reply_input_box">
<textarea
class="reply_textarea"
v-model="replyContent"
placeholder="输入回复内容..."
:maxlength="500"
:auto-height="true"
:show-count="false"
></textarea>
<view class="reply_actions">
<view class="left_actions">
<view class="attachment_button" @click="openImagePicker">
<text class="attachment_icon">📷</text>
</view>
<text class="char_count">{{ replyContent.length }}/500</text>
</view>
<view
class="send_button"
:class="{ disabled: !canSend }"
@click="sendReply"
>
<text class="send_text">发送</text>
</view>
</view>
</view>
</view>
<!-- 图片选择弹窗 -->
<fui-bottom-popup :show="showImagePicker" @close="closeImagePicker">
<view class="image_picker_section">
<view class="picker_title">选择图片</view>
<view class="picker_options">
<view class="picker_option" @click="chooseImage">
<view class="option_icon">📷</view>
<view class="option_text">相册</view>
<AQUplodeImage
:uploadUrl="uploadUrl"
:extraData="{ input_name: 'img_upload', formData:{} }"
@uplodeImageRes="handleImageUpload"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0;"
>
</AQUplodeImage>
</view>
</view>
</view>
</fui-bottom-popup>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
import AQUplodeImage from '@/components/AQ/AQUplodeImage'
import {
Api_url
} from "@/common/config.js"
export default {
components: {
AQUplodeImage
},
data() {
return {
studentId: 0,
fromType: '',
fromId: 0,
fromName: '',
messagesList: [],
loading: false,
replyContent: '',
sending: false,
scrollToView: '',
currentPage: 1,
hasMore: true,
showImagePicker: false,
uploadUrl: `${Api_url}/memberUploadImage` // 学员端上传接口
}
},
computed: {
canSend() {
return this.replyContent.trim().length > 0 && !this.sending
}
},
onLoad(options) {
// 获取页面参数
this.studentId = parseInt(options.student_id) || 0
this.fromType = options.from_type || ''
this.fromId = parseInt(options.from_id) || 0
this.fromName = decodeURIComponent(options.from_name || '')
// 验证必要参数
if (!this.studentId || !this.fromType || !this.fromId) {
uni.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
return
}
this.initPage()
},
methods: {
goBack() {
uni.navigateBack()
},
async initPage() {
await this.loadMessages()
this.scrollToBottom()
},
async loadMessages() {
this.loading = true
try {
console.log('加载对话消息:', {
student_id: this.studentId,
from_type: this.fromType,
from_id: this.fromId
})
const response = await apiRoute.getConversationMessages({
student_id: this.studentId,
from_type: this.fromType,
from_id: this.fromId,
page: this.currentPage,
limit: 20
})
if (response && response.code === 1 && response.data) {
const apiData = response.data
const newList = apiData.list || []
if (this.currentPage === 1) {
this.messagesList = newList
} else {
this.messagesList = [...this.messagesList, ...newList]
}
this.hasMore = apiData.has_more || false
console.log('对话消息加载成功:', this.messagesList)
} else {
console.warn('API返回失败:', response?.msg)
uni.showToast({
title: '加载消息失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取对话消息失败:', error)
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
this.loading = false
}
},
async sendReply() {
if (!this.canSend) return
const content = this.replyContent.trim()
if (!content) {
uni.showToast({
title: '请输入回复内容',
icon: 'none'
})
return
}
this.sending = true
try {
console.log('发送回复:', {
student_id: this.studentId,
to_type: this.fromType,
to_id: this.fromId,
content: content
})
const response = await apiRoute.replyMessage({
student_id: this.studentId,
to_type: this.fromType,
to_id: this.fromId,
content: content,
message_type: 'text'
})
if (response && response.code === 1) {
// 清空输入框
this.replyContent = ''
// 添加新消息到列表
const newMessage = {
id: response.data.data?.id || Date.now(),
content: content,
from_name: '我',
from_type: 'student',
from_id: this.studentId,
is_sent_by_student: true,
is_read: 0,
create_time: Math.floor(Date.now() / 1000)
}
this.messagesList.push(newMessage)
// 滚动到底部
setTimeout(() => {
this.scrollToBottom()
}, 100)
uni.showToast({
title: '发送成功',
icon: 'success'
})
console.log('回复发送成功')
} else {
uni.showToast({
title: response?.msg || '发送失败',
icon: 'none'
})
}
} catch (error) {
console.error('发送回复失败:', error)
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
this.sending = false
}
},
scrollToBottom() {
if (this.messagesList.length > 0) {
const lastMessage = this.messagesList[this.messagesList.length - 1]
this.scrollToView = `msg_${lastMessage.id}`
}
},
formatTime(timestamp) {
const date = new Date(timestamp * 1000)
const now = new Date()
const diffHours = (now - date) / (1000 * 60 * 60)
if (diffHours < 1) {
return '刚刚'
} else if (diffHours < 24) {
return Math.floor(diffHours) + '小时前'
} else if (diffHours < 48) {
return '昨天 ' + date.toTimeString().slice(0, 5)
} else {
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const time = date.toTimeString().slice(0, 5)
return `${month}-${day} ${time}`
}
},
// 打开图片选择器
openImagePicker() {
this.showImagePicker = true
},
// 关闭图片选择器
closeImagePicker() {
this.showImagePicker = false
},
// 选择图片(备用方法)
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log('选择图片成功:', res)
// 这里可以添加直接上传逻辑,或者让AQUplodeImage组件处理
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none'
})
}
})
},
// 处理图片上传回调
handleImageUpload(resData, extraData) {
console.log('图片上传成功:', resData, extraData)
if (extraData.input_name === 'img_upload') {
// 关闭图片选择器
this.closeImagePicker()
// 发送图片消息
this.sendImageMessage(resData.url)
}
},
// 发送图片消息
async sendImageMessage(imageUrl) {
if (!imageUrl) {
uni.showToast({
title: '图片上传失败',
icon: 'none'
})
return
}
this.sending = true
try {
console.log('发送图片消息:', {
student_id: this.studentId,
to_type: this.fromType,
to_id: this.fromId,
content: imageUrl,
message_type: 'img'
})
const response = await apiRoute.replyMessage({
student_id: this.studentId,
to_type: this.fromType,
to_id: this.fromId,
content: imageUrl,
message_type: 'img'
})
if (response && response.code === 1) {
// 添加新图片消息到列表
const newMessage = {
id: response.data.data?.id || Date.now(),
content: imageUrl,
message_type: 'img',
from_name: '我',
from_type: 'student',
from_id: this.studentId,
is_sent_by_student: true,
is_read: 0,
create_time: Math.floor(Date.now() / 1000)
}
this.messagesList.push(newMessage)
// 滚动到底部
setTimeout(() => {
this.scrollToBottom()
}, 100)
uni.showToast({
title: '图片发送成功',
icon: 'success'
})
console.log('图片消息发送成功')
} else {
uni.showToast({
title: response?.msg || '图片发送失败',
icon: 'none'
})
}
} catch (error) {
console.error('发送图片消息失败:', error)
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
this.sending = false
}
},
// 预览图片
previewImage(imageUrl) {
if (!imageUrl) return
uni.previewImage({
current: imageUrl,
urls: [imageUrl]
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
height: 100vh;
display: flex;
flex-direction: column;
}
// 自定义导航栏
.navbar_section {
display: flex;
justify-content: space-between;
align-items: center;
background: #29D3B4;
padding: 40rpx 32rpx 20rpx;
// 小程序端适配状态栏
// #ifdef MP-WEIXIN
padding-top: 80rpx;
// #endif
.navbar_back {
width: 60rpx;
.back_icon {
color: #fff;
font-size: 40rpx;
font-weight: 600;
}
}
.navbar_title {
color: #fff;
font-size: 32rpx;
font-weight: 600;
}
.navbar_action {
width: 60rpx;
}
}
// 对话区域
.conversation_section {
flex: 1;
overflow: hidden;
.message_scroll {
height: 100%;
padding: 20rpx;
}
.loading_section, .empty_section {
padding: 80rpx 32rpx;
text-align: center;
.loading_text, .empty_text {
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
}
.empty_icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
}
.messages_container {
padding-bottom: 40rpx;
.message_bubble {
margin-bottom: 32rpx;
&.sent {
display: flex;
flex-direction: column;
align-items: flex-end;
.message_info {
align-items: flex-end;
margin-bottom: 8rpx;
.sender_name {
color: #29D3B4;
font-weight: 600;
}
}
.message_content {
background: #29D3B4;
color: #fff;
max-width: 70%;
border-radius: 20rpx 20rpx 8rpx 20rpx;
}
.message_status {
margin-top: 8rpx;
.status_text {
font-size: 22rpx;
color: #999;
&.unread {
color: #f39c12;
}
}
}
}
&.received {
display: flex;
flex-direction: column;
align-items: flex-start;
.message_info {
align-items: flex-start;
margin-bottom: 8rpx;
.sender_name {
color: #333;
font-weight: 600;
}
}
.message_content {
background: #fff;
color: #333;
max-width: 70%;
border-radius: 20rpx 20rpx 20rpx 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
}
.message_info {
display: flex;
align-items: center;
gap: 12rpx;
.sender_name {
font-size: 24rpx;
}
.message_time {
font-size: 20rpx;
color: #999;
}
}
.message_content {
padding: 24rpx;
.content_text {
font-size: 28rpx;
line-height: 1.6;
word-wrap: break-word;
}
}
}
}
}
// 回复输入区域
.reply_section {
background: #fff;
border-top: 1px solid #f0f0f0;
padding: 20rpx 24rpx;
// #ifdef MP-WEIXIN
padding-bottom: 40rpx; // 小程序底部安全区域
// #endif
.reply_input_box {
background: #f8f9fa;
border-radius: 16rpx;
padding: 16rpx;
.reply_textarea {
width: 100%;
min-height: 80rpx;
max-height: 200rpx;
font-size: 28rpx;
line-height: 1.4;
background: transparent;
border: none;
outline: none;
resize: none;
&::placeholder {
color: #999;
}
}
.reply_actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
.left_actions {
display: flex;
align-items: center;
gap: 16rpx;
.attachment_button {
width: 60rpx;
height: 60rpx;
background: #29D3B4;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
.attachment_icon {
font-size: 32rpx;
color: #fff;
}
}
.char_count {
font-size: 22rpx;
color: #999;
}
}
.send_button {
background: #29D3B4;
color: #fff;
padding: 12rpx 32rpx;
border-radius: 20rpx;
font-size: 26rpx;
&.disabled {
background: #ccc;
color: #999;
}
.send_text {
font-weight: 600;
}
}
}
}
}
// 图片选择弹窗样式
.image_picker_section {
background: #fff;
border-radius: 16rpx 16rpx 0 0;
padding: 40rpx 32rpx;
min-height: 200rpx;
.picker_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 40rpx;
}
.picker_options {
display: flex;
justify-content: center;
.picker_option {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
background: #f8f9fa;
min-width: 160rpx;
.option_icon {
font-size: 48rpx;
margin-bottom: 12rpx;
}
.option_text {
font-size: 24rpx;
color: #666;
}
}
}
}
// 消息中的图片样式
.content_image {
margin: 8rpx 0;
.chat_image {
max-width: 200rpx;
max-height: 200rpx;
border-radius: 12rpx;
object-fit: cover;
}
}
</style>