1135 lines
34 KiB
Vue
1135 lines
34 KiB
Vue
<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, '<').replace(/>/g, '>'))
|
||
// 转义 <|FunctionCallEnd|> 这类AI标记
|
||
.replace(/<\|[\w]+?\|>/g, m => m.replace(/</g, '<').replace(/>/g, '>'));
|
||
|
||
// 如果存在脚本标签或表单标签,添加红色加粗的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 = () => {
|
||
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消息
|
||
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
|
||
await waitForAIResponse();
|
||
} catch (error) {
|
||
isThinking.value = false
|
||
uni.showToast({
|
||
title: `用户发送消息失败${error}`,
|
||
icon: 'error'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 等待ai回复信息并保存到数据库
|
||
const waitForAIResponse = () => {
|
||
return new Promise((resolve, reject) => {
|
||
// 设置超时
|
||
const timeout = setTimeout(() => {
|
||
unwatch()
|
||
reject(new Error('等待AI回复超时'));
|
||
}, 180000); // 3分钟超时
|
||
|
||
const finish = async () => {
|
||
try {
|
||
console.log("ai返回的消息:", socketStore.messageString);
|
||
console.log('AI回复结束,刷新消息列表');
|
||
await takeConversationMessages();
|
||
resolve();
|
||
} catch (error) {
|
||
console.error('保存AI回复失败:', error);
|
||
reject(error);
|
||
} finally {
|
||
isThinking.value = false
|
||
textMessage.value = ''
|
||
uploadedFiles.value = []
|
||
unwatch();
|
||
clearTimeout(timeout);
|
||
}
|
||
}
|
||
|
||
// 监听 isThinking:false → true 时不做处理,true → false 时表示 AI 回复结束
|
||
const unwatch = watch(() => socketStore.isThinking, (newVal, oldVal) => {
|
||
console.log("isThinking 变化:", oldVal, "→", newVal);
|
||
// AI 回复结束:从 true 变为 false,且已有回复内容
|
||
if (oldVal && !newVal && socketStore.messageString) {
|
||
finish()
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
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);
|
||
// 获取列表的第一个会话
|
||
if (UserConversations.value.length > 0) {
|
||
currentSessionId.value = UserConversations.value[0]._id; // 假设会话对象有 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> |