Files

1130 lines
34 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="status-bar"></view>
<!-- ===== 新建对话类型选择弹窗 ===== -->
<view v-if="showNewChatModal" class="ncd-overlay" @click="closeNewChatModal">
<view class="ncd-card" @click.stop>
<view class="ncd-header">
<view class="ncd-title-eng"><text>NEW</text></view>
<text class="ncd-title-zh">创建对话</text>
<uni-icons type="closeempty" color="#ff0000" size="24" @click="closeNewChatModal"></uni-icons>
</view>
<text>选择对话类型开启全新会话体验</text>
<view class="ncd-options">
<view class="ncd-option ncd-normal">
<view class="ncd-opt-icon">
<uni-icons type="chat" color="#000000" size="30"></uni-icons>
</view>
<view class="ncd-opt-title" @click="selectNormalChat">
<view class="ncd-opt-title-1">普通会话</view>
<view class="ncd-opt-title-2">与AI自由对话,探索任何话题</view>
</view>
<uni-icons type="arrow-right" class="arrow-right-style"></uni-icons>
</view>
<view class="ncd-option ncd-intelligence">
<view class="ncd-opt-icon">
<uni-icons type="star" color="#ffffff" size="30"></uni-icons>
</view>
<view class="ncd-opt-title">
<view class="ncd-opt-title-1">智能体会话</view>
<view class="ncd-opt-title-2">选择专属智能体获得精准专业服务</view>
</view>
<uni-icons type="arrow-right" class="arrow-right-style"></uni-icons>
</view>
</view>
</view>
</view>
<!-- ===== ai正在思考弹窗 ===== -->
<view v-if="isThinking" class="ncd-overlay" @click="closeNewChatModal">
<view class="ncd-card ai-card" @click.stop>
<view class="thinking-content">
<view class="loading-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
<text class="thinking-text">AI 正在思考中...</text>
</view>
</view>
</view>
<view class="chat-page page-container">
<!-- 弹窗出来时的笼罩层 -->
<view v-if="isChatSidebar" class="mask" @click="handleChatSidebar"></view>
<!-- 抽屉侧边栏 -->
<view class="chat-sidebar" :class="{'sidebar-show' : isChatSidebar}">
<ChatSidebar :chatList="UserConversations" v-model:showNewChatModal="showNewChatModal"
v-model:currentSessionId="currentSessionId" v-model:chatType="ChatType"
@refresh-conversations="takeUserConversations">
</ChatSidebar>
</view>
<view class="chat-wrapper">
<view class="chat-hearder">
<view class="chat-btn-group">
<!-- 侧边栏显示按钮 -->
<view class="head-btn" @click="handleChatSidebar">
<view class="iconfont icon-caidan"></view>
</view>
<view class="head-btn" @click="goWorkSpace">
<view class="iconfont icon-wenjianjia"></view>
</view>
<view class="head-btn log-out" @click="logOut">
<view class="iconfont icon-tuichu"></view>
</view>
</view>
</view>
<view class="main-chat">
<scroll-view class="chat-messages" direction="vertical" scroll-y :scroll-into-view="scrollToView"
@scrolltoupper="loadMoreMessages" :upper-threshold="0" :scroll-with-animation="true"
@scroll="onScroll">
<!-- 与AI的对话内容展示 -->
<view v-if="ChatType === 0" class="chat-message" v-for="(message, index) in currentMessages"
:key="index" :id="'msg-' + index" :class="{'message-user':message.role==='user'}">
<view v-if="message.role ==='user'" class="chat-avatar"
:class="{'chat-avatar-user':message.role==='user'}">
<image v-if="message.role === 'user' && UserData?.avatar" :src="UserData.avatar"
class="friend-avatar" mode="aspectFill"></image>
<view v-else class="iconfont icon-yonghuziliao"></view>
<!-- <view v-if="message.role ==='assistant'" class="iconfont icon-Robot"></view> -->
<!-- <view v-if="message.role ==='user'" class="iconfont icon-yonghuziliao"></view> -->
</view>
<view class="chat-content" :class="{'chat-content-user':message.role==='user'}"
v-if="message.content && String(message.content).trim() !== ''">
<!-- <view><rich-text :nodes="pareseMarkdown(message.content)"></rich-text></view> -->
<view v-html="pareseMarkdown(message.content)"></view>
<!-- <mp-html :content="pareseMarkdown(message.content)" /> -->
</view>
</view>
<!-- 与好友的对话内容展示 -->
<view v-if="ChatType === 1" class="chat-message" v-for="(message, index) in currentMessages"
:key="index" :id="'msg-' + index" :class="{'message-user':message.sender === UserId}">
<view class="chat-avatar" :class="{'chat-avatar-user':message.sender === UserId}">
<!-- 自己的消息显示自己的头像 -->
<image v-if="message.sender === UserId && UserData?.avatar" :src="UserData.avatar"
class="friend-avatar" mode="aspectFill"></image>
<!-- 好友的消息显示好友的头像 -->
<image v-else-if="takeTalkUserAvatar(message.sender)"
:src="takeTalkUserAvatar(message.sender)" class="friend-avatar" mode="aspectFill">
</image>
<!-- 没有头像时显示默认图标 -->
<view v-else class="iconfont icon-yonghuziliao"></view>
</view>
<view class="chat-content" :class="{'chat-content-user':message.sender === UserId}">
<view v-if="message.content && String(message.content).trim() !== ''"
v-html="pareseMarkdown(message.content)"></view>
<!-- 2. 图片 + 文件 渲染核心新增 -->
<view v-if="message.contentJson" class="message-file-list">
<!-- 遍历文件/图片列表 -->
<view v-for="(file, idx) in JSON.parse(message.contentJson)" :key="idx"
class="file-item">
<!-- 图片jpg/png/gif/webp -->
<image
v-if="['jpg','jpeg','png','gif','webp','bmp'].includes(file.extendName.toLowerCase())"
:src="file.url" mode="widthFix" class="message-image"
@click="previewImage(file.url)"></image>
<!-- 普通文件doc/xlsx/zip/pdf -->
<view v-else class="message-file" @click="openFile(file.url)">
<view class="file-icon">📄</view>
<view class="file-name">{{ file.name }}</view>
<view class="file-size">{{ formatFileSize(file.fileSize) }}</view>
</view>
</view>
</view>
</view>
</view>
<!-- 与群聊的对话内容展示 -->
<view v-if="ChatType === 2" class="chat-message" v-for="(message, index) in currentMessages"
:key="index" :id="'msg-' + index" :class="{'message-user':message.sender === UserId}">
<view class="chat-avatar" :class="{'chat-avatar-user':message.sender === UserId}">
<!-- 自己的消息显示自己的头像 -->
<image v-if="message.sender === UserId && UserData?.avatar" :src="UserData.avatar"
class="friend-avatar" mode="aspectFill"></image>
<!-- 群成员的消息显示群成员的头像 -->
<image v-else-if="groupMemberList.length > 0 && getGroupMemberAvatarById(message.sender)"
:src="getGroupMemberAvatarById(message.sender)" class="friend-avatar" mode="aspectFill">
</image>
<!-- 没有头像时显示默认图标 -->
<view v-else class="iconfont icon-yonghuziliao"></view>
</view>
<view class="chat-content" :class="{'chat-content-user':message.sender === UserId}">
<view v-if="message.message && String(message.message).trim() !== ''"
v-html="pareseMarkdown(message.message)"></view>
<!-- 2. 图片 + 文件 渲染核心新增 -->
<view v-if="message.contentJson" class="message-file-list">
<!-- 遍历文件/图片列表 -->
<view v-for="(file, idx) in JSON.parse(message.contentJson)" :key="idx"
class="file-item">
<!-- 图片jpg/png/gif/webp -->
<image
v-if="['jpg','jpeg','png','gif','webp','bmp'].includes(file.extendName.toLowerCase())"
:src="file.url" mode="widthFix" class="message-image"
@click="previewImage(file.url)"></image>
<!-- 普通文件doc/xlsx/zip/pdf -->
<view v-else class="message-file" @click="openFile(file.url)">
<view class="file-icon">📄</view>
<view class="file-name">{{ file.name }}</view>
<view class="file-size">{{ formatFileSize(file.fileSize) }}</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="chat-interactive-container">
<view class="chat-interactive-group">
<view class="chat-interactive-btn" @click="uploadPhoto">拍照上传</view>
<view class="chat-interactive-btn" @click="uploadFile()">上传文件</view>
</view>
<view class="image-list" v-if="previewFileArray.length > 0">
<view class="image-title">已选择 ({{ previewFileArray.length }})</view>
<view class="image-grid">
<view class="image-item" v-for="(item, index) in previewFileArray" :key="index">
<view class="image-info">
<text class="image-name">{{ item.name }}</text>
<text class="delete-btn" @click.stop="deleteImage(index)">删除</text>
</view>
</view>
</view>
</view>
<view class="chat-input-container">
<textarea class="message-input" placeholder="输入消息..." @confirm="sendMessage" confirm-type="send"
v-model="textMessage" auto-height></textarea>
<view class="input-btn-group">
<view class="iconfont icon-tingzhi icon-btn"></view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
computed,
getCurrentInstance,
nextTick,
onMounted,
ref,
watch
} from 'vue';
import {
onShow
} from '@dcloudio/uni-app';
import ChatSidebar from '@/components/ChatSidebar.vue'
import {
getUserConversations,
getConversationMessages,
addMessageDict,
createWorkspace,
createConversation,
getUserInfo,
getUserAvatar
} from '@/utils/cloud-api.js'
import {
getToken,
getCurrentSessionId,
getTaskCallId,
getChatType,
getReceiverId
} from '@/utils/user-info.js'
import snarkdown from 'snarkdown'
// import mpHtml from '@/uni_modules/mp-html/components/mp-html/mp-html.vue'
import {
useSocketStore
} from '@/stores/socket.js';
import {
getChatFriend,
getGroup,
getFriendMessages,
getGroupMessages,
getGroupMemberList,
uploadFileToServer
} from '@/utils/friend-api.js'
import {
useFriendSocketStore
} from '@/stores/friend-socket.js'
import {
chooseFile
} from '@/uni_modules/lime-choose-file'
import socketManager from '../../utils/socket';
const friendSocketStore = useFriendSocketStore()
// 连接实时通讯eriendsocket
const handleFriendConnect = () => {
if (friendSocketStore.isConnected) return
if (!userToken.value || !UserId.value) {
console.warn('Token或UserId未准备好');
return
}
friendSocketStore.connect({
token: userToken.value,
UserId: UserId.value
})
}
//store
const socketStore = useSocketStore()
// 聊天类型ai好友群聊
const ChatType = ref(0)
// marked.setOptions({
// breaks: true, // 自动把换行符转成 <br> 换行
// gfm: true, // 开启 GitHub 标准 Markdown 语法
// })
// 将 Markdown 表格转为 HTML 表格snarkdown 不支持表格语法)
const convertMarkdownTable = (text) => {
const lines = text.split('\n')
let result = []
let inTable = false
let tableRows = []
let alignments = []
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
// 检测表格行:以 | 开头和结尾
const isTableLine = /^\|.+|$/.test(line) && line.includes('|')
if (isTableLine) {
// 分隔行(如 |---|---|
if (/^\|[\s\-:]+\|[\s\-:|]+\|$/.test(line)) {
alignments = line.split('|').filter(c => c.trim()).map(c => {
if (c.trim().startsWith(':') && c.trim().endsWith(':')) return 'center'
if (c.trim().endsWith(':')) return 'right'
return 'left'
})
continue
}
tableRows.push(line)
inTable = true
} else {
if (inTable && tableRows.length > 0) {
// 结束表格,渲染为 HTML
result.push(renderTable(tableRows, alignments))
tableRows = []
alignments = []
inTable = false
}
result.push(line)
}
}
// 处理文末的表格
if (inTable && tableRows.length > 0) {
result.push(renderTable(tableRows, alignments))
}
return result.join('\n')
}
// 渲染表格行
const renderTable = (rows, alignments) => {
let html = '<table border="1" style="border-collapse:collapse;width:100%;margin:8px 0">'
rows.forEach((row, index) => {
const tag = index === 0 ? 'th' : 'td'
const cells = row.split('|').filter(c => c.trim() !== '')
html += '<tr>'
cells.forEach((cell, ci) => {
const align = alignments[ci] ? ` style="text-align:${alignments[ci]}"` : ''
html += `<${tag}${align} style="padding:6px 10px;border:1px solid #ddd">${cell.trim()}</${tag}>`
})
html += '</tr>'
})
html += '</table>'
return html
}
// Markdown转HTML节点
const pareseMarkdown = (content) => {
if (!content) return ''
content = sanitizeContent(content)
try {
// 先转换表格,再用 snarkdown 处理其他 Markdown
content = convertMarkdownTable(content)
const html = snarkdown(content)
return html
// // 过滤掉可能导致问题的 Unicode 属性
// const safeHtml = html.replace(/\\p\{[LN]\}/g, '')
// return safeHtml
} catch (e) {
console.error('解析失败', e)
return content
}
}
// 完全移除 <script> 标签及其内容,并检测表单标签
const sanitizeContent = (str) => {
// 检测是否存在 script 标签或表单相关标签
const hasScript = /<script\b[^>]*>([\s\S]*?)<\/script>/i.test(str);
const hasFormTags = /<(form|input|select|textarea|button)\b[^>]*>/i.test(str);
// 先移除所有 script 标签
let cleanedStr = str.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, '');
// 3. 【新增】检测并转义 minimax:tool_call 标签(只转义这个,不影响其他内容)
cleanedStr = cleanedStr
// 转义 <minimax:tool_call> ... </minimax:tool_call>
.replace(/<\/?minimax:tool_call>/g, m => m.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
// 转义 <|FunctionCallEnd|> 这类AI标记
.replace(/<\|[\w]+?\|>/g, m => m.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
// 如果存在脚本标签或表单标签添加红色加粗的h1提示
if (hasScript || hasFormTags) {
const warningHtml = '<h2 style="color: red; font-weight: bold;">表单仅预览,不可操作!!!</h2>';
return warningHtml + cleanedStr;
}
return cleanedStr;
}
// 操作对话内容中的图片和文件
// 图片预览
const previewImage = (url) => {
uni.previewImage({
urls: [url]
})
}
// 打开文件
const openFile = (url) => {
uni.downloadFile({
url: url,
success: (res) => {
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true
})
}
})
}
// 文件大小格式化
const formatFileSize = (size) => {
if (!size) return '0KB'
if (size < 1) {
return (size * 1024).toFixed(0) + 'KB'
}
return size.toFixed(2) + 'MB'
}
// 新建会话相关
// 新建普通会话
const selectNormalChat = async () => {
try {
const chars = '0123456789abcdef'
let name = ''
for (let i = 0; i < 24; i++) {
name += chars[Math.floor(Math.random() * chars.length)]
}
const workspaceId = await createWorkspace(userToken.value, name)
if (!workspaceId) {
uni.showToast({
title: '工作区创建失败',
icon: 'none'
})
return
}
const loginInfo = uni.getStorageSync('yxd_login_info')
let userName = ''
if (loginInfo) {
userName = loginInfo.username || '';
}
const conversationId = await createConversation(userToken.value, workspaceId, '新会话', userName)
if (!conversationId) {
uni.showToast({
title: '新会话创建失败',
icon: 'none'
})
return
}
takeUserConversations();
} catch (error) {
// ❌ 统一捕获异步错误,防止崩溃
console.error('新建普通会话失败:', error)
uni.showToast({
title: '创建会话失败,请重试',
icon: 'none'
})
}
}
const showNewChatModal = ref(false)
const closeNewChatModal = () => {
showNewChatModal.value = false;
}
// 侧边栏相关
const isChatSidebar = ref(false)
const handleChatSidebar = () => {
isChatSidebar.value = !isChatSidebar.value
}
// 跳转到工作区
const goWorkSpace = () => {
// 保存当前会话id确保从工作区返回时能恢复
if (currentSessionId.value) {
uni.setStorageSync('currentSessionId', currentSessionId.value)
}
uni.navigateTo({
url: '/pages/WorkSpace/WorkSpace'
})
}
const logOut = () => {
uni.reLaunch({
url: '/pages/Login/Login'
})
}
const previewFileArray = ref([])
const uploadPhoto = () => {
console.log('点击了拍照上传');
uni.chooseImage({
count: 1,
sourceType: ['camera', 'album'],
success: (res) => {
const tempFiles = res.tempFiles;
const tempFilePaths = res.tempFilePaths;
tempFiles.forEach((file, index) => {
previewFileArray.value.push({
path: tempFilePaths[index], // 图片路径(用于显示)
name: file.name || `photo_${Date.now()}_${index}.jpg`, // 文件名
size: file.size, // 文件大小
type: 'image', // 文件类型标识
tempFile: file // 原始文件对象
});
});
},
fail: (err) => {
console.error('选择图片失败', err);
}
})
}
// 删除指定图片
const deleteImage = (index) => {
uni.showModal({
title: '提示',
content: '确定要删除这张图片吗?',
success: (res) => {
if (res.confirm) {
previewFileArray.value.splice(index, 1);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
};
const fileList = ref([])
const uploadFile = () => {
console.log('点击了上传文件');
chooseFile({
count: 5,
type: 'all',
success: (res) => {
console.log("成功了");
// 赋值文件列表
fileList.value = res.tempFiles;
// 把所有文件 push 到预览数组(展开运算符 ...
previewFileArray.value.push(...res.tempFiles);
uni.showToast({
title: `已选择 ${res.tempFiles.length} 个文件`,
icon: 'success'
})
},
fail: (err) => {
console.error('选择失败:', err)
uni.showToast({
title: '选择失败',
icon: 'error'
})
}
})
}
// 连接socket
const handleConnect = () => {
return new Promise((resolve, reject) => {
if (socketStore.isConnected) return resolve()
// 监听连接成功
const unwatch = watch(() => socketStore.isConnected, (connected) => {
if (connected) {
unwatch()
resolve()
}
})
// 监听连接错误
const unwatchError = watch(() => socketStore.connectionStatus, (status) => {
if (status === 'error') {
unwatch()
unwatchError()
reject(new Error('连接失败'))
}
})
socketStore.connect({
token: userToken.value,
conversationId: currentSessionId.value
})
})
}
// // 监听socket连接是否成功
// watch(()=>socketStore.isConnected,(newVal)=>{
// if(newVal&&)
// })
const isThinking = ref(false)
const isUploading = ref(false)
const uploadedFiles = ref([])
const sendMessage = async () => {
const message = textMessage.value.trim()
if (!message && previewFileArray.value.length === 0) return
// 有文件时先上传
if (previewFileArray.value.length > 0) {
isUploading.value = true
uni.showLoading({
title: '上传文件中...',
mask: true
})
try {
const results = []
for (const item of previewFileArray.value) {
const result = await uploadFileToServer(item.path)
results.push(result)
}
uploadedFiles.value = results
previewFileArray.value = []
} catch (err) {
uni.hideLoading()
console.error('文件上传失败:', err)
uni.showToast({
title: '文件上传失败,请重试',
icon: 'error'
})
isUploading.value = false
return
}
uni.hideLoading()
isUploading.value = false
}
if (ChatType.value === 0) sendAIMessage();
if (ChatType.value === 1) sendFriendMessage();
if (ChatType.value === 2) sendGroupMessage();
}
// 发送好友消息
const sendFriendMessage = async () => {
if (!friendSocketStore.isConnecting) await handleFriendConnect();
const receiverId = getReceiverId()
const hasFiles = uploadedFiles.value.length > 0
const body = {
"command": 1,
"sender": UserId.value,
"receiver": receiverId,
"avatar": "",
"sessionId": currentSessionId.value,
"message": textMessage.value,
"callBackMessage": false
}
if (hasFiles) {
body.contentType = 1
body.uploadVos = uploadedFiles.value
}
const data = {
"version": "1.1",
"body": body
}
friendSocketStore.send(data)
const newMessageData = {
"sender": UserId.value,
"receiver": receiverId,
"content": textMessage.value,
"taskId": null,
"avatar": null,
"createTime": "",
"messageType": "null",
"contentJson": hasFiles ? JSON.stringify(uploadedFiles.value) : 'null'
}
allmessages.value = [...allmessages.value, newMessageData]
textMessage.value = ''
uploadedFiles.value = []
setTimeout(() => {
takeFriendMessages()
}, 1000)
}
// 发送群聊消息
const sendGroupMessage = async () => {
if (!friendSocketStore.isConnecting) await handleFriendConnect();
const hasFiles = uploadedFiles.value.length > 0
const body = {
"command": 9,
"groupId": currentSessionId.value,
"message": textMessage.value || "",
"callBackMessage": hasFiles ? false : true,
"messageType": hasFiles ? 1 : 0,
"sender": UserId.value
}
if (hasFiles) {
body.uploadVos = uploadedFiles.value
}
const data = {
"version": "1.1",
"body": body
}
friendSocketStore.send(data)
const newMessageData = {
"id": allmessages.value.length + 1,
"message": textMessage.value,
"messageType": hasFiles ? 1 : 0,
"groupId": null,
"createTime": '',
"contentJson": hasFiles ? JSON.stringify(uploadedFiles.value) : '',
"sender": UserId.value
}
allmessages.value = [...allmessages.value, newMessageData]
textMessage.value = ''
uploadedFiles.value = []
setTimeout(() => {
takeGroupMessages()
}, 1000)
}
// // 发送AI消息
// const sendAIMessage = async () => {
// try {
// handleConnect();
// let messageContent = textMessage.value
// if (uploadedFiles.value.length) {
// const fileJson = JSON.stringify(uploadedFiles.value)
// messageContent = messageContent ?
// `${messageContent}\n[文件信息:${fileJson}]` :
// `[文件信息:${fileJson}]`
// }
// const message_id = await addMessageDict(userToken.value, 'user', currentSessionId.value,
// messageContent)
// if (message_id) {
// textMessage.value = ''
// uploadedFiles.value = []
// isThinking.value = true
// }
// await waitForAIResponse();
// } catch (error) {
// isThinking.value = false
// uni.showToast({
// title: `用户发送消息失败${error}`,
// icon: 'error'
// })
// }
// }
// AI 回复超时定时器
let aiResponseTimer = null
// 发送AI消息
const sendAIMessage = async () => {
try {
await handleConnect();
let messageContent = textMessage.value
if (uploadedFiles.value.length) {
const fileJson = JSON.stringify(uploadedFiles.value)
messageContent = messageContent ?
`${messageContent}\n[文件信息:${fileJson}]` :
`[文件信息:${fileJson}]`
}
const data = {
"type": "chat",
"conversation_id": currentSessionId.value,
"content": messageContent
}
socketStore.send(data)
isThinking.value = true
// 3分钟超时
aiResponseTimer = setTimeout(() => {
uni.showToast({
title: 'AI回复超时请重试',
icon: 'none'
})
isThinking.value = false
textMessage.value = ''
uploadedFiles.value = []
}, 180000)
} catch (error) {
isThinking.value = false
uni.showToast({
title: `用户发送消息失败${error}`,
icon: 'error'
})
}
}
// 全局监听 socketStore.isThinkingtrue → false 时表示 AI 回复结束
watch(() => socketStore.isThinking, (newVal, oldVal) => {
console.log("isThinking 变化:", oldVal, "→", newVal);
// AI 回复结束:从 true 变为 false且已有回复内容
if (oldVal && !newVal && socketStore.messageString) {
clearTimeout(aiResponseTimer)
console.log("ai返回的消息", socketStore.messageString);
console.log('AI回复结束刷新消息列表');
takeConversationMessages()
isThinking.value = false
textMessage.value = ''
uploadedFiles.value = []
}
})
const textMessage = ref('')
// 获取用户历史会话列表
const UserConversations = ref([])
// 当前会话id
const currentSessionId = ref('')
const userToken = ref('')
const UserId = ref('')
const UserAvatar = ref('')
const UserData = ref(null)
// 获取当前用户信息(提前加载,用于显示头像等)
const takeUserInfo = async () => {
try {
userToken.value = getToken();
UserData.value = await getUserInfo(userToken.value)
UserId.value = UserData.value._id;
UserAvatar.value = UserData.value.avatar || ''
console.log("用户信息已加载:", UserId.value, UserAvatar.value);
} catch (error) {
console.error("获取用户信息失败:", error);
}
}
// 获取对话列表
const takeUserConversations = async () => {
try {
userToken.value = getToken();
console.log("token:", userToken.value);
UserConversations.value = await getUserConversations(userToken.value) || [];
console.log("UserConversations:", UserConversations.value);
// 优先恢复已保存的会话id避免刷新到列表第一个
const savedSessionId = getCurrentSessionId();
if (savedSessionId && UserConversations.value.some(c => c._id === savedSessionId)) {
currentSessionId.value = savedSessionId;
} else if (UserConversations.value.length > 0) {
currentSessionId.value = UserConversations.value[0]._id;
} else {
currentSessionId.value = '';
}
console.log('保存会话id', currentSessionId.value);
uni.setStorageSync('currentSessionId', currentSessionId.value)
} catch (error) {
uni.showToast({
title: `获取会话列表失败${error}`,
icon: 'error'
})
}
}
// 好友列表
// 好友消息列表(不包括头像)
const FriendInfoList = ref([])
// 获取用户好友列表
const takeFriendList = async () => {
try {
console.log("开始获取好友列表");
// UserId 已在 takeUserInfo 中获取
const friendList = await getChatFriend(UserId.value)
console.log("friendList:", friendList);
if (friendList && friendList.length) {
FriendInfoList.value = await takeUserAvatar(friendList)
} else {
FriendInfoList.value = []
console.log("好友列表为空");
}
} catch (error) {
console.error("获取好友列表失败:", error);
FriendInfoList.value = []
} finally {
UserConversations.value = FriendInfoList.value
}
}
// 从好友列表中获取单个用户头像
const takeTalkUserAvatar = (userId) => {
if (!userId) return null
const user = FriendInfoList.value.find(item => item.receiver === userId)
return user?.avatar || null
}
// 获取好友头像
const takeUserAvatar = async (friendList) => {
if (!friendList?.length) return []
try {
const friendIds = friendList.map(item => item.receiver)
const friendAvatarList = await getUserAvatar(userToken.value, friendIds)
// 创建map,用于快速查找
const userMap = new Map(friendAvatarList.map(user => [user.user_id, user]) || [])
// 合并数据
return friendList.map(friend => {
const userInfo = userMap.get(friend.receiver)
return {
...friend,
avatar: userInfo?.avatar || null
}
})
} catch (err) {
console.error('获取好友头像失败', err);
return friendList
}
}
// 群聊列表
const GroupList = ref([])
// 获取用户群聊列表
const takeGroupList = async () => {
try {
console.log("开始获取群聊列表");
// UserId 已在 takeUserInfo 中获取
GroupList.value = await getGroup(UserId.value)
// console.log("GroupList1:", JSON.stringify(GroupList.value));
} catch (error) {
console.error("获取群聊列表失败:", error);
GroupList.value = []
} finally {
UserConversations.value = GroupList.value
}
}
// 群成员列表(包含头像)
const groupMemberList = ref([])
// 获取群成员头像
const takeGroupMemberAvatar = async (memberList) => {
if (!memberList?.length) return []
try {
const memberIds = memberList.map(item => item.groupContactId)
console.log("请求头像的ID列表:", memberIds)
const memberAvatarList = await getUserAvatar(userToken.value, memberIds)
console.log("头像接口返回数据:", memberAvatarList)
// 创建map,用于快速查找
const userMap = new Map(memberAvatarList.map(user => [user.user_id, user]) || [])
console.log("userMap的keys:", Array.from(userMap.keys()))
// 合并数据
return memberList.map(member => {
const memberId = member.groupContactId
const userInfo = userMap.get(memberId)
console.log(`查找 ${memberId} 的头像:`, userInfo)
return {
...member,
avatar: userInfo?.avatar || null
}
})
} catch (err) {
console.error('获取群成员头像失败', err);
return memberList
}
}
// 从群成员列表中获取单个用户头像
const getGroupMemberAvatarById = (userId) => {
if (!userId) return null
const member = groupMemberList.value.find(item => item.groupContactId === userId)
return member?.avatar || null
}
// 当前会话的消息
// const currentMessages = ref([])
const allmessages = ref([])
const pageInfoNumber = 100;
const scrollToView = ref('')
// 加载状态和分页相关变量
const isLoadingMore = ref(false) // 是否正在加载更多
const currentPage = ref(1) // 当前页码(如果后端支持分页)
// 计算属性:自动根据 allmessages 和 currentPage 计算显示消息
const currentMessages = computed(() => {
return allmessages.value.slice(-pageInfoNumber);
})
// 获取对话消息
const takeConversationMessages = async () => {
try {
allmessages.value = await getConversationMessages(userToken.value, currentSessionId.value) || [];
// console.log("AI消息", allmessages.value);
// currentMessages.value = allmessages.value.slice(-pageInfoNumber);
// 数据获取后执行滚动
scrollToBottom();
} catch (error) {
uni.showToast({
title: `获取ai会话内容失败${error}`,
icon: 'none'
})
}
}
const takeFriendMessages = async () => {
try {
allmessages.value = await getFriendMessages(currentSessionId.value) || [];
console.log("好友消息:", JSON.stringify(allmessages.value));
// 数据获取后执行滚动
scrollToBottom();
} catch (error) {
uni.showToast({
title: `获取好友会话消息失败${error}`,
icon: 'none'
})
}
}
const takeGroupMessages = async () => {
try {
allmessages.value = await getGroupMessages(currentSessionId.value) || [];
console.log("群聊消息:", allmessages.value);
// 获取群成员列表并获取头像
const memberList = await getGroupMemberList(currentSessionId.value)
console.log("获取到的群成员列表:", memberList);
if (memberList && memberList.length) {
groupMemberList.value = await takeGroupMemberAvatar(memberList)
console.log("群成员列表(带头像):", groupMemberList.value);
} else {
groupMemberList.value = []
}
// 数据获取后执行滚动
scrollToBottom();
} catch (error) {
uni.showToast({
title: `获取群聊会话失败${error}`,
icon: 'none'
})
}
}
// 加载更多消息
const loadMoreMessages = async () => {
console.log('加载更多信息');
if (allmessages.value.length > pageInfoNumber * currentPage.value) {
currentPage.value += 1;
const newMessages = allmessages.value.slice(-pageInfoNumber * currentPage.value);
const addMessage = newMessages.length - currentMessages.value.length
currentMessages.value = newMessages
// 等待DOM更新
await nextTick()
scrollToView.value = 'msg-' + (addMessage + 1);
}
// currentMessages.value = allmessages.value.slice(-pageInfoNumber);
}
// 监听滚动
const onScroll = (e) => {}
// 滑动到底部
const scrollToBottom = async () => {
await nextTick();
if (currentMessages.value.length > 0) {
scrollToView.value = 'msg-' + (currentMessages.value.length - 1);
}
}
// 监听好友消息
watch(() => friendSocketStore.MessageReceived, (newId) => {
console.log("收到了好友消息");
if (newId) {
console.log("ChatType:", ChatType.value);
switch (ChatType.value) {
case 0:
takeConversationMessages();
friendSocketStore.MessageReceived = false
break;
case 1:
takeFriendMessages();
friendSocketStore.MessageReceived = false
break;
case 2:
takeGroupMessages();
friendSocketStore.MessageReceived = false
break;
// 可选:默认兜底
default:
console.log("default 分支");
friendSocketStore.MessageReceived = false
break;
}
}
}, {
immediate: true
})
watch(currentSessionId, (newId) => {
if (newId) {
uni.setStorageSync('currentSessionId', currentSessionId.value)
// takeConversationMessages();
switch (ChatType.value) {
case 0:
takeConversationMessages();
break;
case 1:
takeFriendMessages();
break;
case 2:
takeGroupMessages();
break;
// 可选:默认兜底
default:
takeConversationMessages();
break;
}
}
}, {
immediate: true
})
onMounted(() => {
// currentSessionId.value = getCurrentSessionId()
})
onShow(async () => {
// 优先获取用户信息(用于显示头像等)
await takeUserInfo();
const chatType = getChatType()
ChatType.value = chatType
const sessionId = getCurrentSessionId()
// 只有在会话ID存在时才获取消息
if (sessionId) {
currentSessionId.value = sessionId
}
if (chatType === 0) {
await takeUserConversations();
// 只有在会话ID存在时才获取消息
if (currentSessionId.value) {
takeConversationMessages();
}
} else if (chatType === 1) {
await takeFriendList()
takeFriendMessages();
handleFriendConnect()
} else if (chatType === 2) {
await takeGroupList()
await handleFriendConnect()
await takeGroupMessages()
}
})
</script>
<style lang="scss" scoped>
@import "./chat.css";
</style>