增加技师端---用户端 聊天界面
This commit is contained in:
11671
staff_uniapp/package-lock.json
generated
11671
staff_uniapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -86,4 +86,4 @@
|
||||
"vite": "4.1.4",
|
||||
"weapp-tailwindcss-webpack-plugin": "1.12.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
102
staff_uniapp/src/api/chat.ts
Normal file
102
staff_uniapp/src/api/chat.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取会话信息
|
||||
* @param {number} conversationId 会话ID(数据库自增ID)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiGetConversationInfo = (conversationId) => {
|
||||
return request.get({
|
||||
url: '/chat/conversation_info',
|
||||
data: { conversation_id: conversationId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天历史
|
||||
* @param {Object} params
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiGetChatHistory = (params) => {
|
||||
return request.get({
|
||||
url: '/chat/history',
|
||||
data: {
|
||||
conversation_id: params.conversation_id,
|
||||
page: params.page,
|
||||
page_size: params.page_size
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param {Object} data
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiSendMessage = (data) => {
|
||||
return request.post({
|
||||
url: '/chat/send',
|
||||
data: {
|
||||
conversation_id: data.conversation_id,
|
||||
sender_id: data.sender_id,
|
||||
sender_type: data.sender_type,
|
||||
content: data.content
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
* @param {Object} data
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiMarkMessagesAsRead = (data) => {
|
||||
return request.post({
|
||||
url: '/chat/mark_as_read',
|
||||
data: {
|
||||
conversation_id: data.conversation_id,
|
||||
user_id: data.user_id
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取会话列表(技师端)
|
||||
* @param {number} techId 技师ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiGetConversations = (techId) => {
|
||||
return request.get({
|
||||
url: '/chat/conversations',
|
||||
data: { tech_id: techId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param {number} userId 用户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiGetUserInfo = (userId) => {
|
||||
return request.get({
|
||||
url: '/user/info',
|
||||
data: { id: userId }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取未读消息数量
|
||||
* @returns
|
||||
*/
|
||||
export const apiGetUnreadMessageCount = () => request.get({ url: '/chat/unreadCount' })
|
||||
|
||||
/**
|
||||
* 获取最后一条消息
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const apiGetLastMessage = (params: {
|
||||
tech_id: number
|
||||
order_id: number
|
||||
}) => request.get({ url: '/chat/lastMessage', data: params })
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isDevMode } from '@/utils/env'
|
||||
const envBaseUrl = import.meta.env.VITE_APP_BASE_URL || ''
|
||||
const envBaseUrl = import.meta.env.VITE_APP_BASE_URL || 'http://anmo.com'
|
||||
|
||||
let baseUrl = `${envBaseUrl}/`
|
||||
|
||||
|
||||
@@ -171,6 +171,21 @@
|
||||
"navigationBarTitleText": "头像裁剪",
|
||||
"navigationBarBackgroundColor": "#000000"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path" : "pages/chat/index",
|
||||
"style" :
|
||||
{
|
||||
"navigationBarTitleText" : ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path" : "pages/chat/list",
|
||||
"style" :
|
||||
{
|
||||
"navigationBarTitleText" : ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
|
||||
461
staff_uniapp/src/pages/chat/index.vue
Normal file
461
staff_uniapp/src/pages/chat/index.vue
Normal file
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<view class="chat-container">
|
||||
<!-- 导航栏 -->
|
||||
<u-navbar
|
||||
:title="chatTitle"
|
||||
:is-back="true"
|
||||
:border-bottom="false"
|
||||
title-color="#000"
|
||||
back-icon-color="#000"
|
||||
>
|
||||
<template #right>
|
||||
<u-icon
|
||||
name="more-dot-fill"
|
||||
size="22"
|
||||
color="#000"
|
||||
@click="showActions = true"
|
||||
></u-icon>
|
||||
</template>
|
||||
</u-navbar>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<u-action-sheet
|
||||
:show="showActions"
|
||||
:actions="actions"
|
||||
@close="showActions = false"
|
||||
@select="handleAction"
|
||||
></u-action-sheet>
|
||||
|
||||
<!-- 聊天区域 -->
|
||||
<scroll-view
|
||||
scroll-y="true"
|
||||
class="chat-messages"
|
||||
:scroll-top="scrollTop"
|
||||
@scrolltolower="loadHistory"
|
||||
>
|
||||
<view v-for="(msg, index) in messages" :key="index" class="message-item">
|
||||
<!-- 对方消息(用户) -->
|
||||
<view v-if="msg.sender_type === 1" class="message-other">
|
||||
<u-avatar :src="userInfo.avatar" size="40"></u-avatar>
|
||||
<view class="message-content">
|
||||
<view class="message-bubble">{{ msg.content }}</view>
|
||||
<view class="message-time">{{ formatTime(msg.create_time) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的消息(技师) -->
|
||||
<view v-else class="message-me">
|
||||
<view class="message-content">
|
||||
<view class="message-bubble">{{ msg.content }}</view>
|
||||
<view class="message-time">{{ formatTime(msg.create_time) }}</view>
|
||||
</view>
|
||||
<u-avatar :src="techInfo.avatar" size="40"></u-avatar>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view v-if="loadingHistory" class="loading-more">
|
||||
<u-loading-icon></u-loading-icon>
|
||||
<text class="ml-2">加载中...</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="input-area">
|
||||
<u-input
|
||||
v-model="inputMessage"
|
||||
placeholder="输入消息..."
|
||||
border="none"
|
||||
class="input-box"
|
||||
@confirm="sendMessage"
|
||||
></u-input>
|
||||
<u-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
:disabled="!inputMessage.trim()"
|
||||
@click="sendMessage"
|
||||
>
|
||||
发送
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'uniapp-router-next'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import {
|
||||
apiGetChatHistory,
|
||||
apiSendMessage,
|
||||
apiMarkMessagesAsRead,
|
||||
apiGetConversationInfo
|
||||
} from '@/api/chat'
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const messages = ref([])
|
||||
const inputMessage = ref('')
|
||||
const conversationId = ref(0) // 使用数据库自增ID
|
||||
const loadingHistory = ref(false)
|
||||
const scrollTop = ref(0)
|
||||
const ws = ref(null)
|
||||
const userInfo = ref({
|
||||
avatar: '/static/default-avatar.png',
|
||||
nickname: '用户'
|
||||
})
|
||||
const techInfo = ref({
|
||||
avatar: userStore.userInfo.avatar,
|
||||
nickname: userStore.userInfo.nickname
|
||||
})
|
||||
const chatTitle = ref('在线聊天')
|
||||
const showActions = ref(false)
|
||||
const actions = ref([
|
||||
{ name: '查看用户信息' },
|
||||
{ name: '清除聊天记录' },
|
||||
{ name: '投诉用户' }
|
||||
])
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 加载会话信息
|
||||
const loadConversationInfo = async () => {
|
||||
try {
|
||||
const res = await apiGetConversationInfo(conversationId.value)
|
||||
if (res.code === 200) {
|
||||
userInfo.value = res.data.user_info
|
||||
techInfo.value = res.data.tech_info
|
||||
chatTitle.value = userInfo.value.nickname
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载会话信息失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载聊天历史
|
||||
const loadHistory = async () => {
|
||||
loadingHistory.value = true
|
||||
try {
|
||||
const res = await apiGetChatHistory({
|
||||
conversation_id: conversationId.value,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
})
|
||||
messages.value = res.list
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('加载聊天历史失败', error)
|
||||
} finally {
|
||||
loadingHistory.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.value.trim()) return
|
||||
|
||||
const content = inputMessage.value.trim()
|
||||
|
||||
try {
|
||||
// 通过WebSocket发送消息
|
||||
sendMessageViaWebSocket(content)
|
||||
|
||||
// 创建本地消息对象(临时)
|
||||
const newMsg = {
|
||||
id: Date.now(), // 临时ID
|
||||
conversation_id: conversationId.value,
|
||||
sender_id: userStore.userInfo.id,
|
||||
sender_type: 2, // 技师
|
||||
receiver_id: userInfo.value.id,
|
||||
receiver_type: 1, // 用户
|
||||
content: content,
|
||||
message_type: 1, // 文本
|
||||
read_status: 0, // 未读
|
||||
create_time: new Date().toISOString(),
|
||||
user: { // 技师信息
|
||||
avatar: techInfo.value.avatar
|
||||
},
|
||||
isTemp: true // 标记为临时消息
|
||||
}
|
||||
|
||||
// 添加到消息列表
|
||||
messages.value.push(newMsg)
|
||||
inputMessage.value = ''
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('发送消息失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 标记消息为已读
|
||||
const markAsRead = async () => {
|
||||
if (!conversationId.value) return
|
||||
try {
|
||||
await apiMarkMessagesAsRead({
|
||||
conversation_id: conversationId.value,
|
||||
user_id: userStore.userInfo.id // 技师ID
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('标记已读失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
// 使用更可靠的滚动方式
|
||||
scrollTop.value = scrollTop.value + 1
|
||||
setTimeout(() => {
|
||||
scrollTop.value = 999999
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化WebSocket
|
||||
const initWebSocket = () => {
|
||||
const token = userStore.token
|
||||
const userId = userStore.userInfo.id
|
||||
const userType = 2 // 技师
|
||||
|
||||
ws.value = new WebSocket(`ws://anmo.com:9501?token=${token}&type=${userType}`)
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('WebSocket连接成功')
|
||||
}
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.action === 'new') {
|
||||
handleNewMessage(data.data)
|
||||
}
|
||||
}
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('WebSocket错误', error)
|
||||
}
|
||||
|
||||
ws.value.onclose = () => {
|
||||
console.log('WebSocket连接关闭')
|
||||
// 尝试重新连接
|
||||
setTimeout(initWebSocket, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理新消息
|
||||
const handleNewMessage = (msg) => {
|
||||
// 确保消息属于当前会话
|
||||
if (msg.conversation_id === conversationId.value) {
|
||||
// 如果是自己发送的消息(替换临时消息)
|
||||
if (msg.sender_type === 2) {
|
||||
// 找到对应的临时消息(通过内容匹配)
|
||||
const tempIndex = messages.value.findIndex(m =>
|
||||
m.isTemp && m.content === msg.content
|
||||
);
|
||||
if (tempIndex !== -1) {
|
||||
// 替换临时消息为服务器返回的消息
|
||||
messages.value.splice(tempIndex, 1, {
|
||||
...msg,
|
||||
user: {
|
||||
avatar: techInfo.value.avatar
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 如果没有找到临时消息,直接添加
|
||||
messages.value.push({
|
||||
...msg,
|
||||
user: {
|
||||
avatar: techInfo.value.avatar
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// 如果是对方(用户)的消息
|
||||
else {
|
||||
messages.value.push({
|
||||
...msg,
|
||||
user: {
|
||||
avatar: userInfo.value.avatar
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息通过WebSocket
|
||||
const sendMessageViaWebSocket = (content) => {
|
||||
if (!ws.value || ws.value.readyState !== WebSocket.OPEN) return
|
||||
|
||||
const message = {
|
||||
action: 'send',
|
||||
conversation_id: conversationId.value,
|
||||
sender_id: userStore.userInfo.id,
|
||||
sender_type: 2, // 技师
|
||||
receiver_id: userInfo.value.id,
|
||||
receiver_type: 1, // 用户
|
||||
content: content
|
||||
}
|
||||
|
||||
ws.value.send(JSON.stringify(message))
|
||||
}
|
||||
|
||||
// 处理操作菜单选择
|
||||
const handleAction = (item) => {
|
||||
showActions.value = false
|
||||
switch (item.name) {
|
||||
case '查看用户信息':
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/detail?id=${userInfo.value.id}`
|
||||
})
|
||||
break;
|
||||
case '清除聊天记录':
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清除聊天记录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
})
|
||||
break;
|
||||
case '投诉用户':
|
||||
uni.navigateTo({
|
||||
url: '/pages/complaint/create?target_id=' + userInfo.value.id
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 从路由参数获取会话ID
|
||||
conversationId.value = parseInt(route.query.conversation_id)
|
||||
|
||||
if (!conversationId.value) {
|
||||
uni.showToast({
|
||||
title: '会话ID无效',
|
||||
icon: 'error'
|
||||
})
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
|
||||
// 加载会话信息
|
||||
await loadConversationInfo()
|
||||
|
||||
// 加载聊天历史
|
||||
await loadHistory()
|
||||
|
||||
// 标记为已读
|
||||
await markAsRead()
|
||||
|
||||
// 初始化WebSocket
|
||||
initWebSocket()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
ws.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.message-other, .message-me {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.message-other {
|
||||
.message-content {
|
||||
margin-left: 20rpx;
|
||||
max-width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.message-me {
|
||||
justify-content: flex-end;
|
||||
|
||||
.message-content {
|
||||
margin-right: 20rpx;
|
||||
max-width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 15rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-other .message-bubble {
|
||||
background-color: #ffffff;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
}
|
||||
|
||||
.message-me .message-bubble {
|
||||
background-color: #95ec69;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background-color: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
flex: 1;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 50rpx;
|
||||
padding: 0 30rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
207
staff_uniapp/src/pages/chat/list.vue
Normal file
207
staff_uniapp/src/pages/chat/list.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<view class="chat-list-container">
|
||||
<!-- 导航栏 -->
|
||||
<u-navbar
|
||||
title="聊天列表"
|
||||
:is-back="true"
|
||||
:border-bottom="false"
|
||||
title-color="#000"
|
||||
back-icon-color="#000"
|
||||
></u-navbar>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-box">
|
||||
<u-search
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索用户"
|
||||
:show-action="false"
|
||||
shape="square"
|
||||
bg-color="#f5f5f5"
|
||||
></u-search>
|
||||
</view>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<scroll-view scroll-y="true" class="conversation-list">
|
||||
<view
|
||||
v-for="(conversation, index) in filteredConversations"
|
||||
:key="index"
|
||||
class="conversation-item"
|
||||
@click="goToChat(conversation)"
|
||||
>
|
||||
<u-avatar
|
||||
:src="conversation.user_avatar"
|
||||
size="60"
|
||||
shape="circle"
|
||||
></u-avatar>
|
||||
|
||||
<view class="conversation-info">
|
||||
<view class="user-name">{{ conversation.user_name }}</view>
|
||||
<view class="last-message">{{ conversation.last_message }}</view>
|
||||
</view>
|
||||
|
||||
<view class="conversation-meta">
|
||||
<view class="message-time">{{ formatTime(conversation.last_message_time) }}</view>
|
||||
<view v-if="conversation.unread_count > 0" class="unread-count">
|
||||
{{ conversation.unread_count }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="filteredConversations.length === 0" class="empty-state">
|
||||
<u-empty
|
||||
mode="list"
|
||||
icon="http://cdn.uviewui.com/uview/empty/list.png"
|
||||
>
|
||||
</u-empty>
|
||||
<text class="empty-text">暂无聊天记录</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { apiGetConversations } from '@/api/chat'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const searchKeyword = ref('')
|
||||
const conversations = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 过滤会话列表
|
||||
const filteredConversations = computed(() => {
|
||||
if (!searchKeyword.value) return conversations.value
|
||||
|
||||
return conversations.value.filter(convo =>
|
||||
convo.user.nickname.includes(searchKeyword.value) ||
|
||||
convo.last_message.content.includes(searchKeyword.value)
|
||||
)
|
||||
})
|
||||
|
||||
// 跳转到聊天页面
|
||||
const goToChat = (conversation) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/index?conversation_id=${conversation.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载会话列表
|
||||
const loadConversations = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await apiGetConversations({
|
||||
tech_id: userStore.userInfo.id
|
||||
})
|
||||
console.log(res)
|
||||
conversations.value = res
|
||||
} catch (error) {
|
||||
console.error('加载会话列表失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConversations()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 20rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
flex: 1;
|
||||
padding: 0 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.conversation-info {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.user-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.last-message {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
.message-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #f56c6c;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15019
uniapp/package-lock.json
generated
15019
uniapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
86
uniapp/src/api/chat.ts
Normal file
86
uniapp/src/api/chat.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import request from '@/utils/request'
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param {number} userId 用户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiGetUserInfo = (userId) => {
|
||||
return request.get({
|
||||
url: '/user/info',
|
||||
data: { id: userId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取技师信息
|
||||
* @param {number} techId 技师ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiGetTechInfo = (techId) => {
|
||||
return request.get({
|
||||
url: '/chat/info',
|
||||
data: { id: techId }
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取会话ID
|
||||
* @param {Object} params
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const apiGetConversationId = (params) => {
|
||||
return request.get({
|
||||
url: '/chat/conversation_id',
|
||||
data: {
|
||||
user_id: params.user_id,
|
||||
tech_id: params.tech_id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天历史记录
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const apiGetChatHistory = (params: {
|
||||
tech_id: number
|
||||
order_id: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) => request.get({ url: '/chat/history', data: params })
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const apiSendMessage = (params: {
|
||||
tech_id: number
|
||||
order_id: number
|
||||
content: string
|
||||
}) => request.post({ url: '/chat/send', data: params })
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const apiMarkMessagesAsRead = (params: {
|
||||
conversation_id: string
|
||||
}) => request.post({ url: '/chat/markAsRead', data: params })
|
||||
|
||||
/**
|
||||
* 获取未读消息数量
|
||||
* @returns
|
||||
*/
|
||||
export const apiGetUnreadMessageCount = () => request.get({ url: '/chat/unreadCount' })
|
||||
|
||||
/**
|
||||
* 获取最后一条消息
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const apiGetLastMessage = (params: {
|
||||
tech_id: number
|
||||
order_id: number
|
||||
}) => request.get({ url: '/chat/lastMessage', data: params })
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isDevMode } from "@/utils/env";
|
||||
const envBaseUrl = import.meta.env.VITE_APP_BASE_URL || "";
|
||||
const envBaseUrl = import.meta.env.VITE_APP_BASE_URL || "http://anmo.com";
|
||||
|
||||
let baseUrl = `${envBaseUrl}/`;
|
||||
|
||||
|
||||
@@ -144,6 +144,13 @@
|
||||
"navigationBarTitleText": "商家列表",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path" : "pages/chat/index",
|
||||
"style" :
|
||||
{
|
||||
"navigationBarTitleText" : "在线聊天"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
|
||||
553
uniapp/src/pages/chat/index.vue
Normal file
553
uniapp/src/pages/chat/index.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<view class="chat-container">
|
||||
<!-- 导航栏 -->
|
||||
<u-navbar
|
||||
:title="chatTitle"
|
||||
:is-back="true"
|
||||
:border-bottom="false"
|
||||
title-color="#000"
|
||||
back-icon-color="#000"
|
||||
>
|
||||
<template #right>
|
||||
<u-icon
|
||||
name="more-dot-fill"
|
||||
size="22"
|
||||
color="#000"
|
||||
@click="showActions = true"
|
||||
></u-icon>
|
||||
</template>
|
||||
</u-navbar>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<u-action-sheet
|
||||
:show="showActions"
|
||||
:actions="actions"
|
||||
@close="showActions = false"
|
||||
@select="handleAction"
|
||||
></u-action-sheet>
|
||||
|
||||
<!-- 加载会话提示 -->
|
||||
<view v-if="loadingConversation" class="loading-conversation">
|
||||
<u-loading-icon></u-loading-icon>
|
||||
<text class="ml-2">加载会话中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 聊天区域 -->
|
||||
<scroll-view
|
||||
v-else
|
||||
scroll-y="true"
|
||||
class="chat-messages"
|
||||
:scroll-top="scrollTop"
|
||||
@scrolltolower="loadHistory"
|
||||
>
|
||||
<view v-for="(msg, index) in messages" :key="index" class="message-item">
|
||||
<!-- 我的消息(用户) -->
|
||||
<view v-if="msg.sender_type === 2" class="message-other">
|
||||
<u-avatar :src="userInfo.avatar" size="40"></u-avatar>
|
||||
<view class="message-content">
|
||||
<view class="message-bubble">{{ msg.content }}</view>
|
||||
<view class="message-time">{{ formatTime(msg.create_time) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 对方消息(技师) -->
|
||||
<view v-else class="message-me">
|
||||
<view class="message-content">
|
||||
<view class="message-bubble">{{ msg.content }}</view>
|
||||
<view class="message-time">{{ formatTime(msg.create_time) }}</view>
|
||||
</view>
|
||||
<u-avatar :src="techInfo.avatar" size="40"></u-avatar>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view v-if="loadingHistory" class="loading-more">
|
||||
<u-loading-icon></u-loading-icon>
|
||||
<text class="ml-2">加载中...</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="input-area">
|
||||
<u-input
|
||||
v-model="inputMessage"
|
||||
placeholder="输入消息..."
|
||||
border="none"
|
||||
class="input-box"
|
||||
:disabled="!conversationId || loadingConversation"
|
||||
@confirm="sendMessage"
|
||||
></u-input>
|
||||
<u-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
:disabled="!inputMessage.trim() || !conversationId || loadingConversation"
|
||||
@click="sendMessage"
|
||||
>
|
||||
发送
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'uniapp-router-next'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import {
|
||||
apiGetChatHistory,
|
||||
apiSendMessage,
|
||||
apiMarkMessagesAsRead,
|
||||
apiGetUnreadMessageCount,
|
||||
apiGetLastMessage,
|
||||
apiGetConversationId,
|
||||
apiGetUserInfo,
|
||||
apiGetTechInfo
|
||||
} from '@/api/chat'
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const messages = ref([])
|
||||
const inputMessage = ref('')
|
||||
const unreadCount = ref(0)
|
||||
const lastMessage = ref(null)
|
||||
const conversationId = ref('')
|
||||
const techId = ref(0)
|
||||
const orderId = ref(0)
|
||||
const loadingHistory = ref(false)
|
||||
const loadingConversation = ref(false)
|
||||
const scrollTop = ref(0)
|
||||
const ws = ref(null)
|
||||
const userInfo = ref({
|
||||
avatar: '/static/default-avatar.png',
|
||||
nickname: '用户'
|
||||
})
|
||||
const techInfo = ref({
|
||||
avatar: userStore.userInfo.avatar,
|
||||
nickname: userStore.userInfo.nickname
|
||||
})
|
||||
const chatTitle = ref('在线聊天')
|
||||
const showActions = ref(false)
|
||||
const actions = ref([
|
||||
{ name: '查看用户信息' },
|
||||
{ name: '清除聊天记录' },
|
||||
{ name: '投诉用户' }
|
||||
])
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 加载聊天历史
|
||||
const loadHistory = async () => {
|
||||
if (!conversationId.value) return
|
||||
|
||||
loadingHistory.value = true
|
||||
try {
|
||||
const res = await apiGetChatHistory({
|
||||
conversation_id: conversationId.value,
|
||||
page: 1,
|
||||
page_size: 99999
|
||||
})
|
||||
messages.value = res.list.filter(msg => msg != null)
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('加载聊天历史失败', error)
|
||||
} finally {
|
||||
loadingHistory.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.value.trim() || !conversationId.value) return
|
||||
|
||||
const content = inputMessage.value.trim()
|
||||
|
||||
try {
|
||||
// 通过WebSocket发送消息
|
||||
sendMessageViaWebSocket(content)
|
||||
|
||||
// 创建本地消息对象(临时)
|
||||
const newMsg = {
|
||||
id: Date.now(), // 临时ID
|
||||
conversation_id: conversationId.value,
|
||||
sender_id: userStore.userInfo.id,
|
||||
sender_type: 1, // 用户
|
||||
receiver_id: techId.value,
|
||||
receiver_type: 2, // 技师
|
||||
content: content,
|
||||
message_type: 1, // 文本
|
||||
read_status: 0, // 未读
|
||||
create_time: new Date().toISOString(),
|
||||
order_id: orderId.value,
|
||||
user: { // 添加用户信息
|
||||
avatar: userStore.userInfo.avatar
|
||||
},
|
||||
isTemp: true // 标记为临时消息
|
||||
}
|
||||
|
||||
// 添加到消息列表
|
||||
messages.value.push(newMsg)
|
||||
inputMessage.value = ''
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('发送消息失败', error)
|
||||
uni.showToast({
|
||||
title: '发送消息失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 标记消息为已读
|
||||
const markAsRead = async () => {
|
||||
if (!conversationId.value) return
|
||||
try {
|
||||
await apiMarkMessagesAsRead({
|
||||
conversation_id: conversationId.value,
|
||||
user_id: userStore.userInfo.id
|
||||
})
|
||||
unreadCount.value = 0
|
||||
} catch (error) {
|
||||
console.error('标记已读失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未读消息数量
|
||||
const getUnreadCount = async () => {
|
||||
try {
|
||||
const res = await apiGetUnreadMessageCount()
|
||||
unreadCount.value = res.count
|
||||
} catch (error) {
|
||||
console.error('获取未读消息失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最后一条消息
|
||||
const getLastMessage = async () => {
|
||||
try {
|
||||
const res = await apiGetLastMessage({
|
||||
conversation_id: conversationId.value
|
||||
})
|
||||
lastMessage.value = res.data
|
||||
} catch (error) {
|
||||
console.error('获取最后一条消息失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
const res = await apiGetUserInfo(techId.value)
|
||||
if (res.code === 200) {
|
||||
userInfo.value = res.data
|
||||
chatTitle.value = userInfo.value.nickname
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取技师信息
|
||||
const getTechInfo = async () => {
|
||||
try {
|
||||
const res = await apiGetTechInfo(techId.value)
|
||||
if (res.code === 200) {
|
||||
techInfo.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取技师信息失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
// 使用更可靠的滚动方式
|
||||
scrollTop.value = scrollTop.value + 1
|
||||
setTimeout(() => {
|
||||
scrollTop.value = 999999
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化WebSocket
|
||||
const initWebSocket = () => {
|
||||
const token = userStore.token
|
||||
const userId = userStore.userInfo.id
|
||||
const userType = 1 // 用户
|
||||
|
||||
ws.value = new WebSocket(`ws://anmo.com:9501?token=${token}&type=${userType}`)
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('WebSocket连接成功')
|
||||
}
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.action === 'new') {
|
||||
handleNewMessage(data.data)
|
||||
}
|
||||
}
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('WebSocket错误', error)
|
||||
}
|
||||
|
||||
ws.value.onclose = () => {
|
||||
console.log('WebSocket连接关闭')
|
||||
// 尝试重新连接
|
||||
setTimeout(initWebSocket, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理新消息
|
||||
const handleNewMessage = (msg) => {
|
||||
|
||||
|
||||
// 确保消息属于当前会话
|
||||
if (msg.conversation_id === conversationId.value) {
|
||||
// 注意:这里直接使用 msg,而不是 msg.data
|
||||
|
||||
// 如果是自己发送的消息(替换临时消息)
|
||||
if (msg.sender_type === 1) { // 技师发送的消息
|
||||
// 找到对应的临时消息(通过内容匹配)
|
||||
const tempIndex = messages.value.findIndex(m =>
|
||||
m.isTemp && m.content === msg.content
|
||||
);
|
||||
if (tempIndex !== -1) {
|
||||
// 替换临时消息为服务器返回的消息
|
||||
messages.value.splice(tempIndex, 1, msg);
|
||||
} else {
|
||||
// 如果没有找到临时消息,直接添加
|
||||
messages.value.push(msg);
|
||||
}
|
||||
}
|
||||
// 如果是对方(用户)的消息
|
||||
else if (msg.sender_type === 2) {
|
||||
messages.value.push(msg);
|
||||
}
|
||||
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 发送消息通过WebSocket
|
||||
const sendMessageViaWebSocket = (content) => {
|
||||
if (!ws.value || ws.value.readyState !== WebSocket.OPEN) return
|
||||
|
||||
const message = {
|
||||
action: 'send',
|
||||
conversation_id: conversationId.value,
|
||||
sender_id: userStore.userInfo.id,
|
||||
sender_type: 1, // 用户
|
||||
receiver_id: techId.value,
|
||||
receiver_type: 2, // 技师
|
||||
content: content,
|
||||
order_id: orderId.value
|
||||
}
|
||||
|
||||
ws.value.send(JSON.stringify(message))
|
||||
}
|
||||
|
||||
// 处理操作菜单选择
|
||||
const handleAction = (item) => {
|
||||
showActions.value = false
|
||||
switch (item.name) {
|
||||
case '查看用户信息':
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/detail?id=${techId.value}`
|
||||
})
|
||||
break;
|
||||
case '清除聊天记录':
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清除聊天记录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
})
|
||||
break;
|
||||
case '投诉用户':
|
||||
uni.navigateTo({
|
||||
url: '/pages/complaint/create?target_id=' + techId.value
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载会话ID
|
||||
const loadConversationId = async () => {
|
||||
loadingConversation.value = true
|
||||
try {
|
||||
const res = await apiGetConversationId({
|
||||
tech_id: techId.value
|
||||
})
|
||||
|
||||
conversationId.value = res.conversation_id
|
||||
console.log('获取会话ID成功:', conversationId.value)
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取会话ID失败', error)
|
||||
uni.showToast({
|
||||
title: '获取会话失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
loadingConversation.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 从路由参数获取信息
|
||||
techId.value = parseInt(route.query.tech_id)
|
||||
orderId.value = parseInt(route.query.order_id || 0)
|
||||
|
||||
// 验证参数
|
||||
if (isNaN(techId.value)) {
|
||||
console.error('无效的技师ID:', route.query.tech_id)
|
||||
uni.showToast({
|
||||
title: '参数错误',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 加载会话ID
|
||||
await loadConversationId()
|
||||
if (!conversationId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
await getUserInfo()
|
||||
|
||||
// 获取技师信息
|
||||
await getTechInfo()
|
||||
|
||||
// 加载数据
|
||||
await loadHistory()
|
||||
await getUnreadCount()
|
||||
await getLastMessage()
|
||||
|
||||
// 标记为已读
|
||||
await markAsRead()
|
||||
|
||||
// 初始化WebSocket
|
||||
initWebSocket()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
ws.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.loading-conversation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.message-other, .message-me {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.message-other {
|
||||
.message-content {
|
||||
margin-left: 20rpx;
|
||||
max-width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.message-me {
|
||||
justify-content: flex-end;
|
||||
|
||||
.message-content {
|
||||
margin-right: 20rpx;
|
||||
max-width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 15rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-other .message-bubble {
|
||||
background-color: #ffffff;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
}
|
||||
|
||||
.message-me .message-bubble {
|
||||
background-color: #95ec69;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background-color: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
flex: 1;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 50rpx;
|
||||
padding: 0 30rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -121,12 +121,23 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex-none">
|
||||
<u-button size="medium" @click="call(orderData.coach_info.mobile)">
|
||||
<u-icon name="phone" color="#333" size="24rpx"></u-icon>
|
||||
<text class="ml-1">联系技师</text>
|
||||
</u-button>
|
||||
</view>
|
||||
<view class="flex-none flex space-x-2">
|
||||
<!-- 在线聊天按钮 -->
|
||||
<u-button
|
||||
size="medium"
|
||||
type="primary"
|
||||
@click="startChat(orderData.coach_id)"
|
||||
>
|
||||
<u-icon name="chat" color="#fff" size="24rpx"></u-icon>
|
||||
<text class="ml-1 text-white">在线聊天</text>
|
||||
</u-button>
|
||||
|
||||
<!-- 联系技师按钮 -->
|
||||
<u-button size="medium" @click="call(orderData.coach_info.mobile)">
|
||||
<u-icon name="phone" color="#333" size="24rpx"></u-icon>
|
||||
<text class="ml-1">联系技师</text>
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品卡片 -->
|
||||
@@ -401,7 +412,12 @@ const showAppend = ref<boolean>(false)
|
||||
|
||||
const scrollTop = ref<number>(0)
|
||||
const percent = ref<number>(0)
|
||||
|
||||
// 跳转到聊天页面
|
||||
const startChat = (techId: number) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/index?tech_id=${techId}&order_id=${orderId.value}&tech_info=${encodeURIComponent(JSON.stringify(orderData.value.coach_info))}`
|
||||
})
|
||||
}
|
||||
const currentIcon = computed(() => orderBgMap[orderData.value.order_status])
|
||||
|
||||
const handleCommand = (row: { command: string; order_id: number }) => {
|
||||
|
||||
Reference in New Issue
Block a user