From 9b2cdecc16c395d11e71d047b16b8457df0d75d3 Mon Sep 17 00:00:00 2001 From: FengHui Date: Thu, 9 Apr 2026 21:14:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=8A=9F?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packageA/pages/chat/components/ai-paging.vue | 768 +++++++++++++++++-- stores/userChatGroupStore.js | 204 ++--- utils/request.js | 2 +- 3 files changed, 801 insertions(+), 173 deletions(-) diff --git a/packageA/pages/chat/components/ai-paging.vue b/packageA/pages/chat/components/ai-paging.vue index cadd2b2..37766db 100644 --- a/packageA/pages/chat/components/ai-paging.vue +++ b/packageA/pages/chat/components/ai-paging.vue @@ -101,8 +101,19 @@ v-for="(file, vInex) in msg.files" :key="vInex" @click="jumpUrl(file)" + :class="{ 'msg-image-file': isImage(file.type, file.name) }" > - + + {{ file.name || '附件' }} @@ -283,27 +294,48 @@ - - - - - {{ file.name }} - - - - {{ file.size }} - + + + + + + + + 加载中... + + + + 加载失败 + 点击重试 + + + + {{ filesList.filter(f => isImage(f.type, f.name)).indexOf(file) + 1 }}/{{ filesList.filter(f => isImage(f.type, f.name)).length }} + + + + {{ file.name }} + + + + {{ file.size }} + + + 图片 + + {{ file.size }} - @@ -652,8 +684,93 @@ function getGuess() { }); } -function isImage(type) { - return new RegExp('image').test(type); +// 从文件路径提取文件名 +function getFileNameFromPath(filePath) { + if (!filePath) return ''; + // 处理反斜杠和正斜杠 + const path = filePath.replace(/\\/g, '/'); + const lastSlashIndex = path.lastIndexOf('/'); + if (lastSlashIndex !== -1) { + return path.substring(lastSlashIndex + 1); + } + return path; +} + +// 从文件名推断文件类型 +function getFileTypeFromName(fileName) { + if (!fileName) return ''; + const lowerName = fileName.toLowerCase(); + if (lowerName.endsWith('.jpg') || lowerName.endsWith('.jpeg')) return 'image/jpeg'; + if (lowerName.endsWith('.png')) return 'image/png'; + if (lowerName.endsWith('.gif')) return 'image/gif'; + if (lowerName.endsWith('.bmp')) return 'image/bmp'; + if (lowerName.endsWith('.webp')) return 'image/webp'; + if (lowerName.endsWith('.svg')) return 'image/svg+xml'; + if (lowerName.endsWith('.pdf')) return 'application/pdf'; + if (lowerName.endsWith('.doc')) return 'application/msword'; + if (lowerName.endsWith('.docx')) return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + if (lowerName.endsWith('.ppt')) return 'application/vnd.ms-powerpoint'; + if (lowerName.endsWith('.pptx')) return 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; + if (lowerName.endsWith('.xls')) return 'application/vnd.ms-excel'; + if (lowerName.endsWith('.xlsx')) return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + if (lowerName.endsWith('.txt')) return 'text/plain'; + if (lowerName.endsWith('.md')) return 'text/markdown'; + if (lowerName.endsWith('.html') || lowerName.endsWith('.htm')) return 'text/html'; + if (lowerName.endsWith('.csv')) return 'text/csv'; + return ''; +} + +function isImage(type, name) { + if (!type && !name) return false; + + const debug = false; + if (debug) console.log('isImage检查:', { type, name }); + + if (type) { + const lowerType = type.toLowerCase(); + + if (lowerType.includes('image/')) { + if (debug) console.log('通过MIME类型识别为图片:', lowerType); + return true; + } + + if (lowerType.includes('image')) { + if (debug) console.log('通过"image"关键词识别为图片:', lowerType); + return true; + } + + const imageExtensionsInType = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'heic', 'heif', 'tiff', 'tif']; + for (const ext of imageExtensionsInType) { + if (lowerType.includes(ext)) { + if (debug) console.log('通过类型中的扩展名识别为图片:', ext); + return true; + } + } + } + + if (name) { + const lowerName = name.toLowerCase(); + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.heic', '.heif', '.tiff', '.tif', '.jfif', '.pjpeg', '.pjp']; + for (const ext of imageExtensions) { + if (lowerName.endsWith(ext)) { + if (debug) console.log('通过文件扩展名识别为图片:', ext); + return true; + } + } + + const imageExtensionsWithoutDot = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'heic', 'heif', 'tiff', 'tif', 'jfif', 'pjpeg', 'pjp']; + for (const ext of imageExtensionsWithoutDot) { + if (lowerName.endsWith('.' + ext)) { + if (debug) console.log('通过带点的扩展名识别为图片:', ext); + return true; + } + } + + if (debug) console.log('未识别为图片:', lowerName); + } + + if (debug) console.log('不是图片文件'); + return false; } function isFile(type) { @@ -665,71 +782,504 @@ function isFile(type) { } function jumpUrl(file) { - if (file.url) { - window.open(file.url); - } else { + if (!file.url && !file.tempPath) { $api.msg('文件地址丢失'); + return; + } + + if (isImage(file.type, file.name)) { + const imageUrl = file.tempPath || file.url; + uni.previewImage({ urls: [imageUrl], current: 0 }); + } else { + window.open(file.url); } } -function VerifyNumberFiles(num) { - if (filesList.value.length >= config.allowedFileNumber) { - $api.msg(`最大上传文件数量 ${config.allowedFileNumber} 个`); +function VerifyNumberFiles(additionalCount = 1) { + const currentCount = filesList.value.length; + const maxCount = config.allowedFileNumber || 9; + + if (currentCount + additionalCount > maxCount) { + $api.msg(`最多只能上传${maxCount}个文件,当前已有${currentCount}个`); return true; } else { return false; } } +function onImageLoad(file) { + const index = filesList.value.findIndex(f => f.url === file.url); + if (index !== -1) { + filesList.value[index].loading = false; + filesList.value[index].error = false; + } +} + +function onImageError(file) { + const index = filesList.value.findIndex(f => f.url === file.url); + if (index !== -1) { + filesList.value[index].loading = false; + filesList.value[index].error = true; + } +} + +function retryImageLoad(file) { + const index = filesList.value.findIndex(f => f.url === file.url); + if (index !== -1) { + filesList.value[index].loading = true; + filesList.value[index].error = false; + // 触发重新加载 + setTimeout(() => { + if (filesList.value[index]) { + filesList.value[index].loading = false; + } + }, 100); + } +} + function uploadCamera(type = 'camera') { - if (VerifyNumberFiles()) return; - uni.chooseImage({ - count: 1, //默认9 - sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 - sourceType: [type], //从相册选择 - success: function (res) { - const tempFilePaths = res.tempFilePaths; - const file = res.tempFiles[0]; - // 继续上传 - $api.uploadFile(tempFilePaths[0], true).then((resData) => { - resData = JSON.parse(resData); - if (isImage(file.type)) { - filesList.value.push({ - url: resData.msg, - type: file.type, - name: file.name, + // 在微信小程序中检查权限 + // #ifdef MP-WEIXIN + const scope = type === 'camera' ? 'scope.camera' : 'scope.writePhotosAlbum'; + + uni.getSetting({ + success: (res) => { + if (!res.authSetting[scope]) { + // 未授权,发起授权请求 + uni.authorize({ + scope: scope, + success: () => { + // 授权成功,执行选择图片 + chooseImageAfterAuth(); + }, + fail: (err) => { + console.error('授权失败:', err); + if (err.errMsg && err.errMsg.includes('auth deny')) { + uni.showModal({ + title: '权限提示', + content: `需要${type === 'camera' ? '相机' : '相册'}权限才能上传图片,请在设置中开启权限`, + showCancel: false, + confirmText: '确定' + }); + } else { + $api.msg('授权失败,请重试'); + } + } + }); + } else { + // 已授权,直接执行选择图片 + chooseImageAfterAuth(); + } + }, + fail: (err) => { + console.error('获取设置失败:', err); + chooseImageAfterAuth(); // 失败时也尝试选择图片 + } + }); + // #endif + + // #ifndef MP-WEIXIN + // 非微信小程序环境直接选择图片 + chooseImageAfterAuth(); + // #endif + + function chooseImageAfterAuth() { + uni.chooseImage({ + count: 9, //支持多选,最多9张 + sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 + // sourceType: [type], // 移除 sourceType 参数,解决微信 "unknown scene" 错误 + success: function (res) { + const tempFilePaths = res.tempFilePaths; + const files = res.tempFiles; + + // 检查文件数量限制 + if (VerifyNumberFiles(files.length)) return; + + // 显示上传提示 + $api.msg(`开始上传${files.length}张图片...`); + + // 上传所有图片 + const uploadPromises = tempFilePaths.map((filePath, index) => { + return $api.uploadFile(filePath, true).then((resData) => { + resData = JSON.parse(resData); + if (resData.code === 200 && resData.filePath) { + // 从文件对象获取文件信息 + const fileInfo = files[index] || {}; + const fileName = getFileNameFromPath(fileInfo.name || filePath); + const fileType = fileInfo.type || getFileTypeFromName(fileName); + const fileSize = fileInfo.size || 0; + + // 使用统一的文件路径处理函数 + const processedFile = processFilePath( + resData.filePath, // 原始服务器路径 + fileName, + fileType, + fileSize, + tempFilePaths[index] // 临时路径用于预览 + ); + + // 添加图片加载状态字段 + processedFile.loading = false; + processedFile.error = false; + + return processedFile; + } else { + throw new Error(resData.msg || '上传失败'); + } + }); + }); + + Promise.all(uploadPromises).then((uploadedFiles) => { + // 将所有上传成功的文件添加到filesList,并设置图片加载状态 + uploadedFiles.forEach(file => { + if (isImage(file.type, file.name)) { + file.loading = true; // 图片开始加载 + } + filesList.value.push(file); }); textInput.value = state.uploadFileTips; + showfile.value = false; // 关闭上传面板 + $api.msg(`成功上传${uploadedFiles.length}张图片`); + }).catch((error) => { + $api.msg(error.message || '图片上传失败'); + }); + }, + fail: function (err) { + console.error('选择图片失败:', err); + // 专门处理微信 "unknown scene" 错误 + if (err.errMsg && err.errMsg.includes('unknown scene')) { + $api.msg('选择图片失败,请尝试重新选择或检查权限设置'); + } else if (err.errMsg && err.errMsg.includes('auth deny')) { + $api.msg('没有相机或相册权限,请在设置中开启'); + } else { + $api.msg('选择图片失败: ' + (err.errMsg || '未知错误')); } - }); - }, - }); + } + }); + } } function getUploadFile(type = 'camera') { - if (VerifyNumberFiles()) return; - uni.chooseFile({ - count: 1, - success: (res) => { - const tempFilePaths = res.tempFilePaths; - const file = res.tempFiles[0]; - const allowedTypes = config.allowedFileTypes || []; - const size = $api.formatFileSize(file.size); - if (!allowedTypes.includes(file.type)) { - return $api.msg('仅支持 txt md html word pdf ppt csv excel 格式类型'); + // 检测是否是微信小程序环境 + const isWeixinMiniProgram = typeof wx !== 'undefined' && wx.chooseMessageFile; + + console.log('当前环境检测:', { + isWeixinMiniProgram, + wxExists: typeof wx !== 'undefined', + hasChooseMessageFile: typeof wx !== 'undefined' && wx.chooseMessageFile + }); + + if (isWeixinMiniProgram) { + console.log('微信小程序环境,使用uni.chooseMessageFile'); + // 微信小程序环境:使用uni.chooseMessageFile + uni.chooseMessageFile({ + count: 9, + type: 'file', + success: (res) => { + console.log('uni.chooseMessageFile返回的res:', res); + // uni.chooseMessageFile返回的数据结构不同 + // tempFiles数组中每个对象有path、name、size等属性 + const tempFiles = res.tempFiles || []; + const tempFilePaths = tempFiles.map(file => file.path); + const files = tempFiles; + + console.log('tempFilePaths:', tempFilePaths); + console.log('tempFiles:', tempFiles); + if (files && files.length > 0) { + console.log('第一个文件详情:', files[0]); + console.log('第一个文件的所有属性:', Object.keys(files[0])); + // 详细记录每个文件的所有属性 + files.forEach((file, index) => { + console.log(`文件 ${index} 完整对象:`, JSON.stringify(file)); + console.log(`文件 ${index} 关键属性:`, { + name: file.name, + type: file.type, + path: file.path, + size: file.size, + hasType: 'type' in file, + typeIsString: typeof file.type === 'string', + typeLength: file.type ? file.type.length : 0 + }); + }); + } + + // 调用公共的文件处理函数 + handleSelectedFiles(tempFilePaths, files); + }, + fail: (err) => { + console.error('uni.chooseMessageFile失败:', err); + // 微信小程序特定的错误处理 + if (err.errMsg && err.errMsg.includes('cancel')) { + // 用户取消选择,不提示错误 + console.log('用户取消选择文件'); + } else { + $api.msg('选择文件失败,请重试'); + } } - // 继续上传 - $api.uploadFile(tempFilePaths[0], true).then((resData) => { - resData = JSON.parse(resData); - filesList.value.push({ - url: resData.msg, - type: file.type, - name: file.name, - size: size, - }); - textInput.value = state.uploadFileTips; + }); + } else { + console.log('非微信小程序环境,使用uni.chooseFile'); + // 其他平台:使用uni.chooseFile + uni.chooseFile({ + count: 9, + success: (res) => { + console.log('uni.chooseFile返回的res:', res); + const tempFilePaths = res.tempFilePaths; + const files = res.tempFiles; + + console.log('tempFilePaths:', tempFilePaths); + console.log('tempFiles:', files); + if (files && files.length > 0) { + console.log('第一个文件详情:', files[0]); + console.log('第一个文件的所有属性:', Object.keys(files[0])); + // 详细记录每个文件的所有属性 + files.forEach((file, index) => { + console.log(`文件 ${index} 完整对象:`, JSON.stringify(file)); + console.log(`文件 ${index} 关键属性:`, { + name: file.name, + type: file.type, + path: file.path, + size: file.size, + hasType: 'type' in file, + typeIsString: typeof file.type === 'string', + typeLength: file.type ? file.type.length : 0 + }); + }); + } + + // 调用公共的文件处理函数 + handleSelectedFiles(tempFilePaths, files); + }, + fail: (err) => { + console.error('uni.chooseFile失败:', err); + if (err.errMsg && !err.errMsg.includes('cancel')) { + $api.msg('选择文件失败,请重试'); + } + } + }); + } +} + +// 处理服务器返回的文件路径,生成正确的显示URL和服务器路径 +function processFilePath(originalFilePath, fileName, fileType, fileSize, tempPath) { + let displayUrl = originalFilePath; // 用于缩略图显示的URL(外网域名) + const serverPath = originalFilePath; // 原始服务器路径(内网地址,用于对话接口) + + console.log('processFilePath原始fileUrl:', originalFilePath); + + // 如果filePath包含IP地址,替换为配置的域名(用于显示) + if (displayUrl && displayUrl.includes('://')) { + // 检查是否是本地临时路径(如http://tmp/),如果是则保持原样 + if (displayUrl.startsWith('http://tmp/')) { + // 本地临时路径,保持原样 + console.log('本地临时路径,保持原样:', displayUrl); + } else { + // 方法1:直接替换域名部分(保持路径结构)- 使用兼容的方法 + // 例如:http://10.98.80.146/file/xxx.jpg -> https://www.xjksly.cn/file/xxx.jpg + const originalUrl = displayUrl; + + // 提取协议、域名和路径部分 + let protocol = ''; + let domain = ''; + let path = ''; + + // 分离协议和剩余部分 + const protocolIndex = originalUrl.indexOf('://'); + if (protocolIndex !== -1) { + protocol = originalUrl.substring(0, protocolIndex + 3); + const rest = originalUrl.substring(protocolIndex + 3); + const slashIndex = rest.indexOf('/'); + if (slashIndex !== -1) { + domain = rest.substring(0, slashIndex); + path = rest.substring(slashIndex); + } else { + domain = rest; + path = '/'; + } + } + + console.log('解析结果:', { protocol, domain, path }); + + // 使用config.baseUrl或config.imgBaseUrl作为新域名 + let baseUrl = config.baseUrl || config.imgBaseUrl; + // 如果baseUrl以/api/ks结尾,去掉这个部分 + if (baseUrl.endsWith('/api/ks')) { + baseUrl = baseUrl.substring(0, baseUrl.length - 7); + } + + // 构建新URL:使用新域名 + 原路径 + // 确保baseUrl不以/结尾,path以/开头 + let cleanBaseUrl = baseUrl; + if (cleanBaseUrl.endsWith('/')) { + cleanBaseUrl = cleanBaseUrl.substring(0, cleanBaseUrl.length - 1); + } + if (!path.startsWith('/')) { + path = '/' + path; + } + + displayUrl = cleanBaseUrl + path; + console.log('替换域名后的displayUrl(用于显示):', displayUrl); + } + } else if (displayUrl && !displayUrl.startsWith('http')) { + // 如果filePath不是完整的URL,直接添加图片基础URL前缀 + displayUrl = config.imgBaseUrl + displayUrl; + console.log('非完整URL,添加imgBaseUrl前缀:', displayUrl); + } + + return { + url: displayUrl, // 使用处理后的完整URL(用于缩略图显示) + serverPath: serverPath, // 原始服务器路径(内网地址,用于对话接口) + type: fileType, + name: fileName, + size: fileSize > 0 ? $api.formatFileSize(fileSize) : '', + tempPath: tempPath // 保存临时路径用于预览 + }; +} + +// 处理选择的文件(公共逻辑) +function handleSelectedFiles(tempFilePaths, files) { + // 检查文件数量限制 + if (VerifyNumberFiles(files.length)) return; + + const allowedTypes = config.allowedFileTypes || []; + + // 调试:在文件类型检查之前记录详细信息 + console.log('=== handleSelectedFiles 文件类型检查开始 ==='); + console.log('allowedTypes配置:', allowedTypes); + console.log('files数组长度:', files.length); + files.forEach((file, index) => { + console.log(`文件 ${index} 检查前详情:`, { + name: file.name, + type: file.type, + hasType: 'type' in file, + typeValue: file.type, + typeIsString: typeof file.type === 'string', + typeLength: file.type ? file.type.length : 0, + size: file.size, + path: file.path + }); + // 尝试从文件名推断类型 + const inferredType = getFileTypeFromName(file.name); + console.log(`文件 ${index} 从文件名推断的类型:`, inferredType); + // 检查是否在allowedTypes中 + const isAllowedByType = allowedTypes.includes(file.type); + const isAllowedByInferred = allowedTypes.includes(inferredType); + const isImageFile = isImage(file.type, file.name); + console.log(`文件 ${index} 检查结果:`, { + isAllowedByType, + isAllowedByInferred, + isImageFile, + finalAllowed: isAllowedByType || isAllowedByInferred || isImageFile + }); + }); + console.log('=== handleSelectedFiles 文件类型检查结束 ==='); + + // 检查所有文件类型 - 增强版:优先使用file.type,回退到文件名推断 + const invalidFiles = files.filter(file => { + // 如果文件类型存在且在允许的类型列表中,则通过 + if (file.type && allowedTypes.includes(file.type)) { + return false; + } + + // 尝试从文件名推断类型 + const inferredType = getFileTypeFromName(file.name); + if (inferredType && allowedTypes.includes(inferredType)) { + return false; + } + + // 如果是图片,也通过 + if (isImage(file.type, file.name)) { + return false; + } + + // 否则,文件无效 + return true; + }); + + if (invalidFiles.length > 0) { + // 调试:查看不支持的文件类型 + console.log('不支持的文件类型:', invalidFiles.map(f => ({name: f.name, type: f.type}))); + // 更详细地显示为什么这些文件不被支持 + invalidFiles.forEach((file, index) => { + const inferredType = getFileTypeFromName(file.name); + console.log(`无效文件 ${index} 分析:`, { + name: file.name, + type: file.type, + inferredType, + isImage: isImage(file.type, file.name), + isInAllowedTypes: allowedTypes.includes(file.type), + isInferredInAllowedTypes: allowedTypes.includes(inferredType) }); - }, + }); + return $api.msg('仅支持 txt md html word doc docx pdf ppt pptx csv excel xlsx 和图片格式'); + } + + // 显示上传提示 + const imageCount = files.filter(file => isImage(file.type, file.name)).length; + const otherFileCount = files.length - imageCount; + let tip = '开始上传'; + if (imageCount > 0) tip += `${imageCount}张图片`; + if (otherFileCount > 0) { + if (imageCount > 0) tip += '和'; + tip += `${otherFileCount}个文件`; + } + $api.msg(tip + '...'); + + // 上传所有文件 + const uploadPromises = tempFilePaths.map((filePath, index) => { + return $api.uploadFile(filePath, true).then((resData) => { + resData = JSON.parse(resData); + console.log('服务器返回的resData:', resData); + if (resData.code === 200 && resData.filePath) { + // 从文件对象或文件路径获取文件信息 + const fileInfo = files[index] || {}; + const fileName = fileInfo.name || getFileNameFromPath(tempFilePaths[index]); + const fileType = fileInfo.type || getFileTypeFromName(fileName); + const fileSize = fileInfo.size || 0; + + // 使用统一的文件路径处理函数 + const processedFile = processFilePath( + resData.filePath, // 原始服务器路径 + fileName, + fileType, + fileSize, + tempFilePaths[index] // 临时路径用于预览 + ); + + // 调试:记录文件信息 + console.log('上传文件信息:', { + fileInfo: fileInfo, + type: fileType, + name: fileName, + size: fileSize, + isImage: isImage(fileType, fileName), + originalFilePath: resData.filePath, + displayUrl: processedFile.url, + serverPath: processedFile.serverPath, + imgBaseUrl: config.imgBaseUrl + }); + + return processedFile; + } else { + throw new Error(resData.msg || '上传失败'); + } + }); + }); + + Promise.all(uploadPromises).then((uploadedFiles) => { + // 将所有上传成功的文件添加到filesList + uploadedFiles.forEach(file => { + if (isImage(file.type, file.name)) { + file.loading = true; // 图片开始加载 + file.error = false; + } + filesList.value.push(file); + }); + textInput.value = state.uploadFileTips; + showfile.value = false; // 关闭上传面板 + $api.msg(`成功上传${uploadedFiles.length}个文件`); + }).catch((error) => { + $api.msg(error.message || '文件上传失败'); }); } @@ -1095,6 +1645,25 @@ defineExpose({ scrollToBottom, closeGuess, closeFile, changeQueries, handleTouch font-size: 24rpx .msg-files:active background: #e9e9e9 + +// 消息中的图片文件样式 +.msg-image-file + height: 120rpx + width: 120rpx + padding: 4rpx + flex-direction: column + justify-content: center + .msg-image-thumbnail + width: 100% + height: 80rpx + border-radius: 8rpx + margin-bottom: 8rpx + .msg-file-text + font-size: 20rpx + text-align: center + white-space: nowrap + overflow: hidden + text-overflow: ellipsis .guess padding: 5rpx 0 10rpx 0 .guess-list @@ -1422,12 +1991,13 @@ image-margin-top = 40rpx .area-uploadfiles position: absolute - top: -180rpx + bottom: 100% width: calc(100% - 30rpx) background: #FFFFFF left: 0 padding: 10rpx 0 10rpx 30rpx box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06); + z-index: 1003 .uploadfiles-scroll height: 100% .uploadfiles-list @@ -1453,11 +2023,17 @@ image-margin-top = 40rpx height: 100% font-size: 24rpx position: relative - min-width: 460rpx; - height: 160rpx; - border-radius: 12rpx 12rpx 12rpx 12rpx; + min-width: 200rpx; + max-width: 200rpx; + height: 200rpx; + border-radius: 12rpx; border: 2rpx solid #E2E2E2; overflow: hidden + display: flex + flex-direction: column + &.file-border + border: 2rpx solid #4A90E2; + background: #F5F9FF; .file-del position: absolute right: 25rpx @@ -1489,9 +2065,61 @@ image-margin-top = 40rpx white-space: nowrap color: #7B7B7B; max-width: 100% + .file-thumbnail + flex: 1 + display: flex + align-items: center + justify-content: center + overflow: hidden + border-radius: 10rpx 10rpx 0 0 + background: #F8F9FA .file-iconImg height: 100% width: 100% + object-fit: cover + .file-info + padding: 12rpx 16rpx + background: white + border-top: 1rpx solid #F0F0F0 + .file-type-tag + font-size: 20rpx + color: #4A90E2 + background: #F0F7FF + padding: 4rpx 8rpx + border-radius: 4rpx + .image-loading, .image-error + position: absolute + top: 0 + left: 0 + right: 0 + bottom: 0 + display: flex + flex-direction: column + align-items: center + justify-content: center + background: rgba(255, 255, 255, 0.9) + .loading-text, .error-text + font-size: 24rpx + margin-top: 10rpx + .loading-text + color: #999 + .error-text + color: #ff4444 + .retry-text + font-size: 22rpx + color: #4A90E2 + margin-top: 5rpx + text-decoration: underline + .image-count-badge + position: absolute + top: 10rpx + right: 10rpx + background: rgba(0, 0, 0, 0.6) + color: white + font-size: 20rpx + padding: 4rpx 8rpx + border-radius: 12rpx + z-index: 2 .filerow display: flex align-items: center diff --git a/stores/userChatGroupStore.js b/stores/userChatGroupStore.js index 62aabb9..edeab52 100644 --- a/stores/userChatGroupStore.js +++ b/stores/userChatGroupStore.js @@ -107,108 +107,108 @@ const useChatGroupDBStore = defineStore("messageGroup", () => { return await baseDB.db.add(massageName.value, payload); } - async function getStearm(text, fileUrls = [], progress, options = {}) { - - return new Promise((resolve, reject) => { - try { - toggleTyping(true); - const customDataID = 'message_' + UUID.generate() - - // 对话历史管理:只保留最近的N条消息,防止token超限 - // 计算消息数量,只保留最近的10条消息(可根据实际情况调整) - const MAX_HISTORY_MESSAGES = 10; - const historyMessages = messages.value.slice(-MAX_HISTORY_MESSAGES); - - const params = { - data: text, - sessionId: chatSessionID.value, - dataId: customDataID - }; - if (fileUrls && fileUrls.length) { - params['fileUrl'] = fileUrls.map((item) => item.url); - } - // ------> - const MsgData = { - text: text, - self: true, - displayText: text, - files: fileUrls - }; - addMessage(MsgData); // 添加message数据 - // <------ - const newMsg = { - text: '', // 存储原始结构化内容 - self: false, - displayText: '', // 用于流式渲染展示 - dataId: customDataID - }; - const index = messages.value.length; - messages.value.push(newMsg); - - const rawParts = Array.isArray(text) ? text : [text]; // 统一处理 - - // 用于追加每个部分的流式数据 - let partIndex = 0; - - function handleUnload() { - newMsg.parentGroupId = chatSessionID.value; - baseDB.db.add(massageName.value, newMsg).then((id) => { - messages.value[index] = { - ...newMsg, - id - }; - }); - } - // #ifdef H5 - if (typeof window !== 'undefined') { - window.addEventListener("unload", handleUnload); - } - // #endif - - function onDataReceived(data) { - // 支持追加多个部分 - newMsg.text += data; - newMsg.displayText += data; - messages.value[index] = { - ...newMsg - }; - progress && progress(); - - // 调用外部传入的onDataReceived回调 - if (options.onDataReceived) { - options.onDataReceived(data, newMsg, index); - } - } - - function onError(error) { - msg('服务响应异常'); - reject(error); - } - - function onComplete() { - messages.value[index] = { - ...newMsg - }; - toggleTyping(false); - // #ifdef H5 - if (typeof window !== 'undefined') { - window.removeEventListener("unload", handleUnload); - } - // #endif - handleUnload(); - // 调用外部传入的onComplete回调 - if (options.onComplete) { - options.onComplete(); - } - resolve(); - } - - $api.streamRequest('/chat', params, onDataReceived, onError, onComplete); - } catch (err) { - console.log(err); - reject(err); - } - }); + async function getStearm(text, fileUrls = [], progress, options = {}) { + + return new Promise((resolve, reject) => { + try { + toggleTyping(true); + const customDataID = 'message_' + UUID.generate() + + // 对话历史管理:只保留最近的N条消息,防止token超限 + // 计算消息数量,只保留最近的10条消息(可根据实际情况调整) + const MAX_HISTORY_MESSAGES = 10; + const historyMessages = messages.value.slice(-MAX_HISTORY_MESSAGES); + + const params = { + data: text, + sessionId: chatSessionID.value, + dataId: customDataID + }; + if (fileUrls && fileUrls.length) { + params['fileUrl'] = fileUrls.map((item) => item.serverPath || item.url); + } + // ------> + const MsgData = { + text: text, + self: true, + displayText: text, + files: fileUrls + }; + addMessage(MsgData); // 添加message数据 + // <------ + const newMsg = { + text: '', // 存储原始结构化内容 + self: false, + displayText: '', // 用于流式渲染展示 + dataId: customDataID + }; + const index = messages.value.length; + messages.value.push(newMsg); + + const rawParts = Array.isArray(text) ? text : [text]; // 统一处理 + + // 用于追加每个部分的流式数据 + let partIndex = 0; + + function handleUnload() { + newMsg.parentGroupId = chatSessionID.value; + baseDB.db.add(massageName.value, newMsg).then((id) => { + messages.value[index] = { + ...newMsg, + id + }; + }); + } + // #ifdef H5 + if (typeof window !== 'undefined') { + window.addEventListener("unload", handleUnload); + } + // #endif + + function onDataReceived(data) { + // 支持追加多个部分 + newMsg.text += data; + newMsg.displayText += data; + messages.value[index] = { + ...newMsg + }; + progress && progress(); + + // 调用外部传入的onDataReceived回调 + if (options.onDataReceived) { + options.onDataReceived(data, newMsg, index); + } + } + + function onError(error) { + msg('服务响应异常'); + reject(error); + } + + function onComplete() { + messages.value[index] = { + ...newMsg + }; + toggleTyping(false); + // #ifdef H5 + if (typeof window !== 'undefined') { + window.removeEventListener("unload", handleUnload); + } + // #endif + handleUnload(); + // 调用外部传入的onComplete回调 + if (options.onComplete) { + options.onComplete(); + } + resolve(); + } + + $api.streamRequest('/chat', params, onDataReceived, onError, onComplete); + } catch (err) { + console.log(err); + reject(err); + } + }); } // 状态控制 diff --git a/utils/request.js b/utils/request.js index 2bf8535..648f0fc 100644 --- a/utils/request.js +++ b/utils/request.js @@ -155,7 +155,7 @@ export function uploadFile(tempFilePaths, loading = false) { header["Authorization"] = encodeURIComponent(Authorization); return new Promise((resolve, reject) => { uni.uploadFile({ - url: config.baseUrl + '/app/file/upload', + url: config.baseUrl + '/app/file/uploadFile', filePath: tempFilePaths, name: 'file', header,