chore:
This commit is contained in:
@@ -116,6 +116,28 @@ function getUserId() {
|
|||||||
return guestId;
|
return guestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归在 info 响应中查找配置 uuid (媒体上传必填)。
|
||||||
|
* 后端字段名/层级不确定, 深度 4 内匹配 config_uuid / config_uid / uuid 任意字段。
|
||||||
|
*/
|
||||||
|
function findConfigUuid(obj, depth) {
|
||||||
|
if (!obj || typeof obj !== 'object' || (depth || 0) > 4) return '';
|
||||||
|
var keys;
|
||||||
|
try { keys = Object.keys(obj); } catch (e) { return ''; }
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
var k = keys[i];
|
||||||
|
if (/^(config_uuid|config_uid|uuid)$/i.test(k) && obj[k]) return String(obj[k]);
|
||||||
|
}
|
||||||
|
for (var j = 0; j < keys.length; j++) {
|
||||||
|
var v = obj[keys[j]];
|
||||||
|
if (v && typeof v === 'object') {
|
||||||
|
var found = findConfigUuid(v, (depth || 0) + 1);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前 AI Agent。
|
* 获取当前 AI Agent。
|
||||||
*/
|
*/
|
||||||
@@ -783,13 +805,14 @@ export default {
|
|||||||
async: false,
|
async: false,
|
||||||
});
|
});
|
||||||
console.log('[AI Service] getServiceConfig response:', JSON.stringify(response));
|
console.log('[AI Service] getServiceConfig response:', JSON.stringify(response));
|
||||||
// 媒体上传需要 config_uuid (后端校验 config_uid/config_uuid 不能为空)。
|
// 媒体上传需要 config_uuid (后端校验不能为空)。递归查找并持久化。
|
||||||
// 此前从未写入过该缓存, 导致上传表单里 config_uuid 恒为空。
|
|
||||||
// 这里从 info 响应中提取并持久化, 兼容多种字段名。
|
|
||||||
const cfg = response && response.data;
|
const cfg = response && response.data;
|
||||||
if (cfg && typeof cfg === 'object') {
|
const uuid = findConfigUuid(cfg, 0);
|
||||||
const uuid = cfg.config_uuid || cfg.config_uid || cfg.uuid || (cfg.info && (cfg.info.config_uuid || cfg.info.uuid)) || '';
|
if (uuid) {
|
||||||
if (uuid) uni.setStorageSync('current_config_uuid', uuid);
|
uni.setStorageSync('current_config_uuid', uuid);
|
||||||
|
console.log('[AI Service] 已缓存 config_uuid:', uuid);
|
||||||
|
} else {
|
||||||
|
console.warn('[AI Service] info 响应中未找到 config_uuid/config_uid/uuid 字段, 上传将缺少该参数。响应结构:', JSON.stringify(cfg));
|
||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1038,9 +1061,10 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前 config_uuid (媒体上传路由用)
|
* 获取当前 config_uuid (媒体上传路由用)
|
||||||
|
* 优先级: info 接口缓存 > 本地配置 (.local.config.js aikefuConfigUuid)
|
||||||
*/
|
*/
|
||||||
getCurrentConfigUuid() {
|
getCurrentConfigUuid() {
|
||||||
return uni.getStorageSync('current_config_uuid') || '';
|
return uni.getStorageSync('current_config_uuid') || Config.aikefuConfigUuid || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ if (process?.env?.NODE_ENV === 'development') {
|
|||||||
}
|
}
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
const { uniacid = 0, domain = defaultDomain } = localDevConfig ?? { uniacid: 0, domain: defaultDomain }
|
const { uniacid = 0, domain = defaultDomain, aikefuConfigUuid = '' } = localDevConfig ?? { uniacid: 0, domain: defaultDomain }
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -80,6 +80,10 @@ export default {
|
|||||||
//api请求地址
|
//api请求地址
|
||||||
baseUrl: domain,
|
baseUrl: domain,
|
||||||
|
|
||||||
|
// aikefu 智能客服配置 uuid (媒体上传必填; 后端 /api/kefu/info 目前不返回该值,
|
||||||
|
// 需在 .local.config.js 对应环境配置里手动填写 aikefuConfigUuid: 'xxx')
|
||||||
|
aikefuConfigUuid: aikefuConfigUuid,
|
||||||
|
|
||||||
// 图片域名
|
// 图片域名
|
||||||
imgDomain: domain,
|
imgDomain: domain,
|
||||||
|
|
||||||
|
|||||||
@@ -99,8 +99,10 @@ async function uploadViaPhp(filePath, kind, addon, agentId) {
|
|||||||
name: 'file',
|
name: 'file',
|
||||||
formData: {
|
formData: {
|
||||||
kind,
|
kind,
|
||||||
agent_id: addon === 'aiagent' ? agentId : undefined,
|
// aikefu 后端必填 config_uuid; 同时兼容 config_uid 命名并附 agent_id 兜底
|
||||||
|
agent_id: agentId || undefined,
|
||||||
config_uuid: addon === 'aikefu' ? (aiService.getCurrentConfigUuid?.() || '') : undefined,
|
config_uuid: addon === 'aikefu' ? (aiService.getCurrentConfigUuid?.() || '') : undefined,
|
||||||
|
config_uid: addon === 'aikefu' ? (aiService.getCurrentConfigUuid?.() || '') : undefined,
|
||||||
uniacid: aiService.getUniacid(),
|
uniacid: aiService.getUniacid(),
|
||||||
user_id: aiService.getUserId?.() || '',
|
user_id: aiService.getUserId?.() || '',
|
||||||
},
|
},
|
||||||
@@ -135,7 +137,13 @@ async function uploadViaPhp(filePath, kind, addon, agentId) {
|
|||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
form.append('kind', kind)
|
form.append('kind', kind)
|
||||||
if (addon === 'aiagent') form.append('agent_id', agentId)
|
if (addon === 'aiagent') form.append('agent_id', agentId)
|
||||||
else form.append('config_uuid', aiService.getCurrentConfigUuid?.() || '')
|
else {
|
||||||
|
// aikefu 后端必填 config_uuid; 同时兼容 config_uid 命名并附 agent_id 兜底
|
||||||
|
const uuid = aiService.getCurrentConfigUuid?.() || ''
|
||||||
|
form.append('config_uuid', uuid)
|
||||||
|
form.append('config_uid', uuid)
|
||||||
|
form.append('agent_id', agentId)
|
||||||
|
}
|
||||||
form.append('uniacid', aiService.getUniacid())
|
form.append('uniacid', aiService.getUniacid())
|
||||||
form.append('user_id', aiService.getUserId?.() || '')
|
form.append('user_id', aiService.getUserId?.() || '')
|
||||||
const res = await fetch(Config.baseUrl + url, {
|
const res = await fetch(Config.baseUrl + url, {
|
||||||
@@ -184,8 +192,9 @@ async function uploadViaSts(filePath, kind, addon, agentId, config) {
|
|||||||
'token': store.state.token || ''
|
'token': store.state.token || ''
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
agent_id: addon === 'aiagent' ? agentId : undefined,
|
agent_id: agentId || undefined,
|
||||||
config_uuid: addon === 'aikefu' ? (aiService.getCurrentConfigUuid?.() || '') : undefined,
|
config_uuid: addon === 'aikefu' ? (aiService.getCurrentConfigUuid?.() || '') : undefined,
|
||||||
|
config_uid: addon === 'aikefu' ? (aiService.getCurrentConfigUuid?.() || '') : undefined,
|
||||||
uniacid: aiService.getUniacid(),
|
uniacid: aiService.getUniacid(),
|
||||||
user_id: aiService.getUserId?.() || '',
|
user_id: aiService.getUserId?.() || '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,10 +53,16 @@
|
|||||||
<!-- 用户消息 (WorkBuddy 风格: 右对齐 + 浅灰气泡 + 头像在右) -->
|
<!-- 用户消息 (WorkBuddy 风格: 右对齐 + 浅灰气泡 + 头像在右) -->
|
||||||
<view v-if="message.role === 'user'" class="user-message">
|
<view v-if="message.role === 'user'" class="user-message">
|
||||||
<view class="message-content">
|
<view class="message-content">
|
||||||
<view v-if="message.type === 'image'" class="message-bubble user-image-bubble">
|
<view v-if="message.type === 'image'">
|
||||||
<image :src="message.imageUrl" mode="aspectFill" class="user-image" />
|
<view class="message-bubble user-image-bubble">
|
||||||
<view v-if="message.uploading" class="upload-mask"><text class="upload-tip">上传中...</text></view>
|
<image :src="message.imageUrl" mode="aspectFill" class="user-image" />
|
||||||
<view v-if="message.uploadError" class="upload-mask upload-error"><text class="upload-tip">{{ message.uploadError }}</text></view>
|
<view v-if="message.uploading" class="upload-mask"><text class="upload-tip">上传中...</text></view>
|
||||||
|
<view v-if="message.uploadError" class="upload-mask upload-error"><text class="upload-tip">{{ message.uploadError }}</text></view>
|
||||||
|
</view>
|
||||||
|
<!-- 图片配文: 图文一起发出时, 文字显示在图片下方 -->
|
||||||
|
<view v-if="message.content" class="message-bubble image-caption-bubble">
|
||||||
|
<text class="message-text" selectable>{{ message.content }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 用户文件气泡
|
<!-- PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 用户文件气泡
|
||||||
tools-panel _uploadOneFile 选 kind='file' (audio/video 当前未走
|
tools-panel _uploadOneFile 选 kind='file' (audio/video 当前未走
|
||||||
@@ -315,6 +321,13 @@
|
|||||||
<!-- 输入区域 (新版: 顶部胶囊形输入条 + 底部 5 个图标) -->
|
<!-- 输入区域 (新版: 顶部胶囊形输入条 + 底部 5 个图标) -->
|
||||||
<view class="input-area" :style="inputAreaStyle">
|
<view class="input-area" :style="inputAreaStyle">
|
||||||
<view class="input-card">
|
<view class="input-card">
|
||||||
|
<!-- 待发送图片附件预览: 挂在输入条内部顶端(文字行上方), 出现时输入框自动向上撑高, 删除后恢复原高度 -->
|
||||||
|
<view v-if="pendingImage" class="pending-attachment">
|
||||||
|
<image :src="pendingImage.uploaded ? pendingImage.uploaded.url : pendingImage.tempPath" mode="aspectFill" class="pending-image" />
|
||||||
|
<view v-if="pendingImage.uploading" class="pending-mask"><text>上传中</text></view>
|
||||||
|
<view v-else-if="pendingImage.uploadError" class="pending-mask error"><text>失败</text></view>
|
||||||
|
<view class="pending-remove" @tap="removePendingImage">×</view>
|
||||||
|
</view>
|
||||||
<!-- 顶部输入行: 语音图标 + 输入区 + 加号 + 发送 -->
|
<!-- 顶部输入行: 语音图标 + 输入区 + 加号 + 发送 -->
|
||||||
<view class="input-row-top">
|
<view class="input-row-top">
|
||||||
<!-- 无文字时左侧显示语音图标,点击才打开语音面板 -->
|
<!-- 无文字时左侧显示语音图标,点击才打开语音面板 -->
|
||||||
@@ -353,8 +366,8 @@
|
|||||||
<text class="plus-icon">+</text>
|
<text class="plus-icon">+</text>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 有文字时显示发送按钮 -->
|
<!-- 有文字或图片附件上传完成时显示发送按钮 -->
|
||||||
<button v-if="hasInputText" class="top-send-btn" @click="sendMessage">
|
<button v-if="hasInputText || canSendPendingImage" class="top-send-btn" @click="sendMessage">
|
||||||
<!-- H5/小程序都兼容:旋转箭头模拟向上的纸飞机 -->
|
<!-- H5/小程序都兼容:旋转箭头模拟向上的纸飞机 -->
|
||||||
<text class="send-plane-text">➤</text>
|
<text class="send-plane-text">➤</text>
|
||||||
</button>
|
</button>
|
||||||
@@ -390,7 +403,7 @@
|
|||||||
<!-- 更多工具面板 -->
|
<!-- 更多工具面板 -->
|
||||||
<ai-chat-tools-panel
|
<ai-chat-tools-panel
|
||||||
:visible.sync="showToolsPanel"
|
:visible.sync="showToolsPanel"
|
||||||
@image-selected="onImageSelected"
|
@image-picked="onImagePicked"
|
||||||
@file-selected="onFileSelected"
|
@file-selected="onFileSelected"
|
||||||
@voice-start="showVoicePanel"
|
@voice-start="showVoicePanel"
|
||||||
@location-selected="onLocationSelected"
|
@location-selected="onLocationSelected"
|
||||||
@@ -454,6 +467,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import aiService, { CONVERSATION_KEY } from '@/common/js/ai-service.js'
|
import aiService, { CONVERSATION_KEY } from '@/common/js/ai-service.js'
|
||||||
|
import { uploadImage } from '@/common/js/media-upload.js'
|
||||||
import IMAGE_CONFIG from '@/common/js/image-config.js'
|
import IMAGE_CONFIG from '@/common/js/image-config.js'
|
||||||
import { isClipboardPrivacyError } from '@/common/js/uniapp.utils.js'
|
import { isClipboardPrivacyError } from '@/common/js/uniapp.utils.js'
|
||||||
import AiChatSearch from './components/ai-chat-search.vue'
|
import AiChatSearch from './components/ai-chat-search.vue'
|
||||||
@@ -519,6 +533,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
messages: [],
|
messages: [],
|
||||||
inputText: '',
|
inputText: '',
|
||||||
|
// 输入框待发送图片附件 (选图后先预览并后台上传, 发送时随文字一起发出)
|
||||||
|
pendingImage: null,
|
||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
scrollIntoViewId: null,
|
scrollIntoViewId: null,
|
||||||
hasMoreHistory: false,
|
hasMoreHistory: false,
|
||||||
@@ -609,6 +625,10 @@ export default {
|
|||||||
const hasEmoji = this.containsEmoji(this.inputText)
|
const hasEmoji = this.containsEmoji(this.inputText)
|
||||||
return hasContent || hasEmoji
|
return hasContent || hasEmoji
|
||||||
},
|
},
|
||||||
|
// 待发送图片已上传完成, 可发送
|
||||||
|
canSendPendingImage() {
|
||||||
|
return !!(this.pendingImage && !this.pendingImage.uploading && !this.pendingImage.uploadError && this.pendingImage.uploaded)
|
||||||
|
},
|
||||||
hasInputText() {
|
hasInputText() {
|
||||||
return this.inputText && this.inputText.trim() !== ''
|
return this.inputText && this.inputText.trim() !== ''
|
||||||
}
|
}
|
||||||
@@ -735,9 +755,27 @@ export default {
|
|||||||
this.$emit('nickname-changed', newNickname)
|
this.$emit('nickname-changed', newNickname)
|
||||||
},
|
},
|
||||||
|
|
||||||
onImageSelected(imageMessage) {
|
// 选图后: 不直接进聊天区, 作为输入框上方附件预览并立即开始上传
|
||||||
this.messages.push(imageMessage)
|
onImagePicked(paths) {
|
||||||
this.$emit('image-selected', imageMessage)
|
const list = Array.isArray(paths) ? paths : (paths ? [paths] : [])
|
||||||
|
if (!list.length) return
|
||||||
|
const path = list[0]
|
||||||
|
this.pendingImage = { tempPath: path, uploading: true, uploaded: null, uploadError: null }
|
||||||
|
uploadImage(path).then((uploaded) => {
|
||||||
|
if (!this.pendingImage) return
|
||||||
|
this.$set(this.pendingImage, 'uploading', false)
|
||||||
|
this.$set(this.pendingImage, 'uploaded', uploaded)
|
||||||
|
}).catch((e) => {
|
||||||
|
if (!this.pendingImage) return
|
||||||
|
this.$set(this.pendingImage, 'uploading', false)
|
||||||
|
this.$set(this.pendingImage, 'uploadError', (e && e.message) || '上传失败')
|
||||||
|
uni.showToast({ title: '图片上传失败: ' + ((e && e.message) || ''), icon: 'none' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除待发送图片附件
|
||||||
|
removePendingImage() {
|
||||||
|
this.pendingImage = null
|
||||||
},
|
},
|
||||||
|
|
||||||
onFileSelected(fileMessage) {
|
onFileSelected(fileMessage) {
|
||||||
@@ -983,11 +1021,24 @@ async loadChatHistoryIfExist() {
|
|||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
async sendMessage() {
|
async sendMessage() {
|
||||||
// 确保有内容或表情
|
// 确保有内容、表情或待发送图片
|
||||||
const hasContent = this.inputText && this.inputText.trim() !== ''
|
const hasContent = this.inputText && this.inputText.trim() !== ''
|
||||||
const hasEmoji = this.containsEmoji(this.inputText)
|
const hasEmoji = this.containsEmoji(this.inputText)
|
||||||
|
const pending = this.pendingImage
|
||||||
|
|
||||||
if (!hasContent && !hasEmoji) return
|
// 附件图片必须上传完成才能发送
|
||||||
|
if (pending) {
|
||||||
|
if (pending.uploading) {
|
||||||
|
uni.showToast({ title: '图片上传中,请稍候', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pending.uploadError || !pending.uploaded) {
|
||||||
|
uni.showToast({ title: '图片上传失败,请删除后重试', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasContent && !hasEmoji && !pending) return
|
||||||
|
|
||||||
// 收集已上传、尚未随消息发送的附件 (用户图片/文件消息)
|
// 收集已上传、尚未随消息发送的附件 (用户图片/文件消息)
|
||||||
const readyAttachments = this.messages
|
const readyAttachments = this.messages
|
||||||
@@ -1006,22 +1057,44 @@ async loadChatHistoryIfExist() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 输入框待发送图片: 图文一起发出
|
||||||
|
if (pending && pending.uploaded) {
|
||||||
|
readyAttachments.push({
|
||||||
|
type: pending.uploaded.kind || 'image',
|
||||||
|
url: pending.uploaded.url,
|
||||||
|
mime: pending.uploaded.mime,
|
||||||
|
name: pending.uploaded.name,
|
||||||
|
size: pending.uploaded.size
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 记录用户发出消息的时间戳, AI 回复完成时算 elapsed
|
// 记录用户发出消息的时间戳, AI 回复完成时算 elapsed
|
||||||
this._aiRequestStart = Date.now()
|
this._aiRequestStart = Date.now()
|
||||||
|
|
||||||
const queryText = this.inputText && this.inputText.trim()
|
const queryText = hasContent
|
||||||
? this.inputText.trim()
|
? this.inputText.trim()
|
||||||
: (readyAttachments.length ? '[附件]' : '')
|
: (readyAttachments.length ? '[附件]' : '')
|
||||||
const userMessage = {
|
// 带图片时消息类型为 image, 图片 + 配文一起显示; 纯文字时仍为 text
|
||||||
id: ++this.messageId,
|
const userMessage = pending
|
||||||
role: 'user',
|
? {
|
||||||
type: 'text',
|
id: ++this.messageId,
|
||||||
content: queryText,
|
role: 'user',
|
||||||
timestamp: Date.now()
|
type: 'image',
|
||||||
}
|
imageUrl: pending.uploaded.url || pending.tempPath,
|
||||||
|
content: hasContent ? this.inputText.trim() : '',
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: ++this.messageId,
|
||||||
|
role: 'user',
|
||||||
|
type: 'text',
|
||||||
|
content: queryText,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
this.shouldScrollToBottom = true
|
this.shouldScrollToBottom = true
|
||||||
this.messages.push(userMessage)
|
this.messages.push(userMessage)
|
||||||
this.inputText = ''
|
this.inputText = ''
|
||||||
|
this.pendingImage = null
|
||||||
|
|
||||||
// 显示AI正在处理 - 带状态轮换提示
|
// 显示AI正在处理 - 带状态轮换提示
|
||||||
const loadingMessage = {
|
const loadingMessage = {
|
||||||
@@ -1046,7 +1119,8 @@ async loadChatHistoryIfExist() {
|
|||||||
try {
|
try {
|
||||||
// 直接发送消息(跳过健康检查,避免健康检查接口格式不匹配导致误判)
|
// 直接发送消息(跳过健康检查,避免健康检查接口格式不匹配导致误判)
|
||||||
// 如果服务不可用,实际发送时会抛出异常,由 catch 块处理
|
// 如果服务不可用,实际发送时会抛出异常,由 catch 块处理
|
||||||
const enhancedMessage = this.enhanceEmojiMessage(userMessage.content)
|
// 纯图片无文字时, 给 AI 的 query 用占位文案, 避免空 query
|
||||||
|
const enhancedMessage = this.enhanceEmojiMessage(userMessage.content || (readyAttachments.length ? '[图片]' : ''))
|
||||||
if (this.enableStreaming) {
|
if (this.enableStreaming) {
|
||||||
await this.sendStreamMessage(enhancedMessage, readyAttachments)
|
await this.sendStreamMessage(enhancedMessage, readyAttachments)
|
||||||
} else {
|
} else {
|
||||||
@@ -4578,4 +4652,54 @@ _handleStreamError(msg) {
|
|||||||
&.upload-error .upload-tip { color: #ffb3b3; }
|
&.upload-error .upload-tip { color: #ffb3b3; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 图片消息配文气泡 (图文一起发出时, 显示在图片下方) */
|
||||||
|
.image-caption-bubble {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 14rpx 22rpx;
|
||||||
|
max-width: 460rpx;
|
||||||
|
.message-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框内待发送图片缩略图: 位于文字输入行上方, 出现时输入框自动向上撑高 */
|
||||||
|
.pending-attachment {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin: 6rpx 14rpx 10rpx;
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
.pending-image { width: 100%; height: 100%; display: block; }
|
||||||
|
.pending-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text { color: #fff; font-size: 20rpx; }
|
||||||
|
&.error text { color: #ffb3b3; }
|
||||||
|
}
|
||||||
|
.pending-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; right: 0;
|
||||||
|
width: 36rpx; height: 36rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 26rpx;
|
||||||
|
border-radius: 0 0 0 14rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -80,32 +80,11 @@ export default {
|
|||||||
count: 9,
|
count: 9,
|
||||||
sizeType: ['compressed'],
|
sizeType: ['compressed'],
|
||||||
sourceType: ['album', 'camera'],
|
sourceType: ['album', 'camera'],
|
||||||
success: async (res) => {
|
success: (res) => {
|
||||||
that.hideToolsPanel()
|
that.hideToolsPanel()
|
||||||
for (const p of res.tempFilePaths) {
|
// 新交互: 选图后不直接进聊天区, 交给父组件作为输入框附件预览,
|
||||||
const pending = {
|
// 用户可继续输入文字, 点发送时图文一起发出
|
||||||
id: ++that.messageId,
|
that.$emit('image-picked', res.tempFilePaths || [])
|
||||||
role: 'user',
|
|
||||||
type: 'image',
|
|
||||||
content: '',
|
|
||||||
imageUrl: p,
|
|
||||||
uploading: true,
|
|
||||||
uploadError: null,
|
|
||||||
uploaded: null,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}
|
|
||||||
that.$emit('image-selected', pending)
|
|
||||||
try {
|
|
||||||
const uploaded = await uploadImage(p)
|
|
||||||
that.$set(pending, 'uploading', false)
|
|
||||||
that.$set(pending, 'uploaded', uploaded)
|
|
||||||
that.$set(pending, 'imageUrl', uploaded.url)
|
|
||||||
} catch (e) {
|
|
||||||
that.$set(pending, 'uploading', false)
|
|
||||||
that.$set(pending, 'uploadError', e.message || '上传失败')
|
|
||||||
uni.showToast({ title: '图片上传失败: ' + (e.message || ''), icon: 'none' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
if (err.errMsg && !err.errMsg.includes('cancel')) {
|
if (err.errMsg && !err.errMsg.includes('cancel')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user