AI 对话web、wx优化
This commit is contained in:
@@ -261,6 +261,15 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
// 消息内容包装器(包含气泡和文件列表)
|
||||
.message-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.bot-message-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -426,18 +435,121 @@
|
||||
.chat-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
// 已上传文件预览区
|
||||
.uploaded-files {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px 8px 4px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.uploaded-file-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 80px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-preview-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-preview-doc {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.doc-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.remove-file-btn {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: #ff4d4f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// 输入行(上传按钮+输入框+发送按钮)
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.upload-btn.uploading {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
@@ -458,6 +570,16 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.send-btn.active {
|
||||
background: linear-gradient(135deg, #5b9eff 0%, #4b87ff 100%);
|
||||
border-color: #4b87ff;
|
||||
}
|
||||
|
||||
.send-btn.active .send-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.send-icon {
|
||||
@@ -465,6 +587,65 @@
|
||||
color: #4b87ff;
|
||||
}
|
||||
|
||||
// 消息中的文件列表
|
||||
.message-files {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-file-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.file-thumb {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-thumb.image {
|
||||
border: 1px solid #e5e5e5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.file-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-thumb.doc {
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.file-name-small {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
// 打字指示器动画
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
|
||||
@@ -52,8 +52,23 @@
|
||||
:class="item.type === 'user' ? 'user-message' : 'bot-message'">
|
||||
<!-- 用户消息(右侧) -->
|
||||
<view class="user-message-content" v-if="item.type === 'user'">
|
||||
<view class="message-bubble user-bubble">
|
||||
<text class="message-text">{{item.content}}</text>
|
||||
<view class="message-content-wrapper">
|
||||
<!-- 文字气泡 -->
|
||||
<view class="message-bubble user-bubble" v-if="item.content">
|
||||
<text class="message-text">{{item.content}}</text>
|
||||
</view>
|
||||
<!-- 用户消息的文件列表(在气泡外面) -->
|
||||
<view v-if="item.files && item.files.length > 0" class="message-files">
|
||||
<view v-for="fileId in item.files" :key="fileId" class="message-file-item" @tap="previewFile(fileId)">
|
||||
<view v-if="isImageFileById(fileId)" class="file-thumb image">
|
||||
<image :src="getFileDownloadUrl(fileId)" mode="aspectFill" class="file-img" />
|
||||
</view>
|
||||
<view v-else class="file-thumb doc">
|
||||
<text class="file-icon">📄</text>
|
||||
<text class="file-name-small">{{getFileName(fileId)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="avatar user-avatar">
|
||||
<text class="avatar-text">我</text>
|
||||
@@ -103,9 +118,31 @@
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="chat-input-wrap">
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
|
||||
<view class="send-btn" @tap="sendMessage">
|
||||
<text class="send-icon">➤</text>
|
||||
<!-- 已上传文件预览 -->
|
||||
<view v-if="uploadedFiles.length > 0" class="uploaded-files">
|
||||
<view v-for="(file, index) in uploadedFiles" :key="file.id || index" class="uploaded-file-item">
|
||||
<view v-if="isImageFile(file)" class="file-preview-image">
|
||||
<image :src="getFilePreviewUrl(file)" mode="aspectFill" class="preview-img" />
|
||||
</view>
|
||||
<view v-else class="file-preview-doc">
|
||||
<text class="doc-icon">📄</text>
|
||||
</view>
|
||||
<text class="file-name">{{file.name || '文件'}}</text>
|
||||
<view class="remove-file-btn" @tap="removeUploadedFile(index)">
|
||||
<text class="remove-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 输入框 -->
|
||||
<view class="input-row">
|
||||
<view class="upload-btn" :class="{ uploading: isUploading }" @tap="showUploadOptions">
|
||||
<text v-if="isUploading" class="upload-icon">⏳</text>
|
||||
<text v-else class="upload-icon">📎</text>
|
||||
</view>
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
|
||||
<view class="send-btn" :class="{ active: inputText.trim() || uploadedFiles.length > 0 }" @tap="sendMessage">
|
||||
<text class="send-icon">➤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -117,13 +154,15 @@
|
||||
import { ref, nextTick, onMounted } from 'vue'
|
||||
import { guestAPI, aiChatAPI, workcaseChatAPI } from '@/api'
|
||||
import type { TbWorkcaseDTO } from '@/types'
|
||||
import { AGENT_ID } from '@/config'
|
||||
import type { DifyFileInfo } from '@/types/ai/aiChat'
|
||||
import { AGENT_ID, FILE_DOWNLOAD_URL } from '@/config'
|
||||
// 前端消息展示类型
|
||||
interface ChatMessageItem {
|
||||
type: 'user' | 'bot'
|
||||
content: string
|
||||
time: string
|
||||
actions?: string[] | null
|
||||
files?: string[] // 文件ID数组
|
||||
}
|
||||
const agentId = AGENT_ID
|
||||
// 响应式数据
|
||||
@@ -136,6 +175,12 @@
|
||||
const headerPaddingTop = ref<number>(44) // header顶部padding,默认44px
|
||||
const headerTotalHeight = ref<number>(76) // header总高度,默认76px
|
||||
|
||||
// 文件上传相关
|
||||
const uploadedFiles = ref<DifyFileInfo[]>([])
|
||||
const isUploading = ref<boolean>(false)
|
||||
// 文件信息缓存 (fileId -> DifyFileInfo)
|
||||
const fileInfoCache = ref<Map<string, DifyFileInfo>>(new Map())
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref({
|
||||
wechatId: '',
|
||||
@@ -257,18 +302,38 @@
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
const text = inputText.value.trim()
|
||||
if (!text || isTyping.value) return
|
||||
// 允许只有文件或只有文本
|
||||
if ((!text && uploadedFiles.value.length === 0) || isTyping.value) return
|
||||
|
||||
// 添加用户消息
|
||||
addMessage('user', text)
|
||||
const query = text || '[文件]'
|
||||
const currentFiles = [...uploadedFiles.value] // 保存当前文件列表副本
|
||||
|
||||
// 将文件信息缓存起来,用于立即渲染
|
||||
currentFiles.forEach(f => {
|
||||
if (f.sys_file_id) {
|
||||
fileInfoCache.value.set(f.sys_file_id, f)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加用户消息(包含文件)
|
||||
const userMessage: ChatMessageItem = {
|
||||
type: 'user',
|
||||
content: text,
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
files: currentFiles.length > 0 ? currentFiles.map(item => item.sys_file_id || '') : undefined
|
||||
}
|
||||
messages.value.push(userMessage)
|
||||
inputText.value = ''
|
||||
|
||||
// 调用AI聊天接口
|
||||
await callAIChat(text)
|
||||
// 清空已上传的文件
|
||||
uploadedFiles.value = []
|
||||
|
||||
// 调用AI聊天接口(携带文件)
|
||||
await callAIChat(query, currentFiles)
|
||||
}
|
||||
|
||||
// 调用AI聊天接口
|
||||
async function callAIChat(query : string) {
|
||||
async function callAIChat(query : string, files : DifyFileInfo[] = []) {
|
||||
isTyping.value = true
|
||||
|
||||
try {
|
||||
@@ -288,14 +353,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 准备流式对话
|
||||
const prepareRes = await aiChatAPI.prepareChatMessageSession({
|
||||
// 准备流式对话(包含文件)
|
||||
const prepareData = {
|
||||
chatId: chatId.value,
|
||||
query: query,
|
||||
agentId: agentId,
|
||||
userType: userType.value,
|
||||
userId: userInfo.value.userId
|
||||
})
|
||||
userId: userInfo.value.userId,
|
||||
files: files.length > 0 ? files : undefined
|
||||
}
|
||||
console.log('准备流式对话参数:', JSON.stringify(prepareData))
|
||||
|
||||
const prepareRes = await aiChatAPI.prepareChatMessageSession(prepareData)
|
||||
if (!prepareRes.success || !prepareRes.data) {
|
||||
throw new Error(prepareRes.message || '准备对话失败')
|
||||
}
|
||||
@@ -594,10 +663,9 @@
|
||||
count: 1,
|
||||
sourceType: ['camera'],
|
||||
success: (res) => {
|
||||
// 处理图片上传逻辑
|
||||
console.log('选择的图片:', res.tempFilePaths)
|
||||
addMessage('user', '[图片]')
|
||||
simulateAIResponse('收到您发送的图片')
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
uploadSingleFile(res.tempFilePaths[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -605,24 +673,177 @@
|
||||
// 从相册选择
|
||||
function chooseImageFromAlbum() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
count: 9,
|
||||
sourceType: ['album'],
|
||||
success: (res) => {
|
||||
// 处理图片上传逻辑
|
||||
console.log('选择的图片:', res.tempFilePaths)
|
||||
addMessage('user', '[图片]')
|
||||
simulateAIResponse('收到您发送的图片')
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
res.tempFilePaths.forEach((filePath: string) => {
|
||||
uploadSingleFile(filePath)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 选择文件
|
||||
function chooseFile() {
|
||||
// 这里可以扩展文件选择功能
|
||||
uni.showToast({
|
||||
title: '文件选择功能开发中',
|
||||
icon: 'none'
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序使用 chooseMessageFile
|
||||
uni.chooseMessageFile({
|
||||
count: 5,
|
||||
type: 'file',
|
||||
extension: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'],
|
||||
success: (res: any) => {
|
||||
console.log('选择文件成功:', res)
|
||||
if (res.tempFiles && res.tempFiles.length > 0) {
|
||||
res.tempFiles.forEach((file: any) => {
|
||||
uploadSingleFile(file.path)
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('选择文件失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择文件失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 非微信小程序环境
|
||||
// @ts-ignore
|
||||
if (typeof uni.chooseFile === 'function') {
|
||||
// @ts-ignore
|
||||
uni.chooseFile({
|
||||
count: 5,
|
||||
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'],
|
||||
success: (res: any) => {
|
||||
console.log('选择文件成功:', res)
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
res.tempFilePaths.forEach((filePath: string) => {
|
||||
uploadSingleFile(filePath)
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('选择文件失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择文件失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '当前环境不支持文件选择',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 上传单个文件
|
||||
async function uploadSingleFile(filePath: string) {
|
||||
console.log('开始上传文件:', filePath)
|
||||
|
||||
if (!agentId) {
|
||||
uni.showToast({ title: '智能体未配置', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
isUploading.value = true
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
|
||||
try {
|
||||
const result = await aiChatAPI.uploadFileForChat(filePath, agentId)
|
||||
console.log('上传结果:', result)
|
||||
|
||||
if (result.success && result.data) {
|
||||
uploadedFiles.value.push(result.data)
|
||||
uni.showToast({ title: '上传成功', icon: 'success', duration: 1000 })
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('文件上传失败:', error)
|
||||
uni.showToast({ title: '上传失败: ' + (error.message || '未知错误'), icon: 'none' })
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 移除已上传的文件
|
||||
function removeUploadedFile(index: number) {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 判断是否为图片文件
|
||||
function isImageFile(file: DifyFileInfo): boolean {
|
||||
return file.type === 'image' || file.mime_type?.startsWith('image/') || false
|
||||
}
|
||||
|
||||
// 获取文件预览URL
|
||||
function getFilePreviewUrl(file: DifyFileInfo): string {
|
||||
return file.preview_url || file.source_url || file.url || ''
|
||||
}
|
||||
|
||||
// 获取文件下载URL(通过文件ID)
|
||||
function getFileDownloadUrl(fileId: string): string {
|
||||
return `${FILE_DOWNLOAD_URL}${fileId}`
|
||||
}
|
||||
|
||||
// 判断文件ID对应的文件是否为图片
|
||||
function isImageFileById(fileId: string): boolean {
|
||||
// 从缓存中查找文件信息
|
||||
const file = fileInfoCache.value.get(fileId)
|
||||
if (file) {
|
||||
return isImageFile(file)
|
||||
}
|
||||
// 如果缓存中没有,尝试从uploadedFiles中查找
|
||||
const uploadedFile = uploadedFiles.value.find(f => f.sys_file_id === fileId)
|
||||
if (uploadedFile) {
|
||||
return isImageFile(uploadedFile)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取文件名(从缓存)
|
||||
function getFileName(fileId: string): string {
|
||||
const file = fileInfoCache.value.get(fileId)
|
||||
return file?.name || fileId.substring(0, 8) + '...'
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
function previewFile(fileId: string) {
|
||||
const url = getFileDownloadUrl(fileId)
|
||||
// 如果是图片,使用图片预览
|
||||
if (isImageFileById(fileId)) {
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
current: url
|
||||
})
|
||||
} else {
|
||||
// 其他文件,提示下载或打开
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '是否下载该文件?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.downloadFile({
|
||||
url: url,
|
||||
success: (downloadRes) => {
|
||||
if (downloadRes.statusCode === 200) {
|
||||
uni.showToast({ title: '下载成功', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user