chore:
This commit is contained in:
@@ -53,10 +53,16 @@
|
||||
<!-- 用户消息 (WorkBuddy 风格: 右对齐 + 浅灰气泡 + 头像在右) -->
|
||||
<view v-if="message.role === 'user'" class="user-message">
|
||||
<view class="message-content">
|
||||
<view v-if="message.type === 'image'" class="message-bubble user-image-bubble">
|
||||
<image :src="message.imageUrl" mode="aspectFill" class="user-image" />
|
||||
<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 v-if="message.type === 'image'">
|
||||
<view class="message-bubble user-image-bubble">
|
||||
<image :src="message.imageUrl" mode="aspectFill" class="user-image" />
|
||||
<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>
|
||||
<!-- PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 用户文件气泡
|
||||
tools-panel _uploadOneFile 选 kind='file' (audio/video 当前未走
|
||||
@@ -315,6 +321,13 @@
|
||||
<!-- 输入区域 (新版: 顶部胶囊形输入条 + 底部 5 个图标) -->
|
||||
<view class="input-area" :style="inputAreaStyle">
|
||||
<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">
|
||||
<!-- 无文字时左侧显示语音图标,点击才打开语音面板 -->
|
||||
@@ -353,8 +366,8 @@
|
||||
<text class="plus-icon">+</text>
|
||||
</button>
|
||||
|
||||
<!-- 有文字时显示发送按钮 -->
|
||||
<button v-if="hasInputText" class="top-send-btn" @click="sendMessage">
|
||||
<!-- 有文字或图片附件上传完成时显示发送按钮 -->
|
||||
<button v-if="hasInputText || canSendPendingImage" class="top-send-btn" @click="sendMessage">
|
||||
<!-- H5/小程序都兼容:旋转箭头模拟向上的纸飞机 -->
|
||||
<text class="send-plane-text">➤</text>
|
||||
</button>
|
||||
@@ -390,7 +403,7 @@
|
||||
<!-- 更多工具面板 -->
|
||||
<ai-chat-tools-panel
|
||||
:visible.sync="showToolsPanel"
|
||||
@image-selected="onImageSelected"
|
||||
@image-picked="onImagePicked"
|
||||
@file-selected="onFileSelected"
|
||||
@voice-start="showVoicePanel"
|
||||
@location-selected="onLocationSelected"
|
||||
@@ -454,6 +467,7 @@
|
||||
</template>
|
||||
<script>
|
||||
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 { isClipboardPrivacyError } from '@/common/js/uniapp.utils.js'
|
||||
import AiChatSearch from './components/ai-chat-search.vue'
|
||||
@@ -519,6 +533,8 @@ export default {
|
||||
return {
|
||||
messages: [],
|
||||
inputText: '',
|
||||
// 输入框待发送图片附件 (选图后先预览并后台上传, 发送时随文字一起发出)
|
||||
pendingImage: null,
|
||||
scrollTop: 0,
|
||||
scrollIntoViewId: null,
|
||||
hasMoreHistory: false,
|
||||
@@ -609,6 +625,10 @@ export default {
|
||||
const hasEmoji = this.containsEmoji(this.inputText)
|
||||
return hasContent || hasEmoji
|
||||
},
|
||||
// 待发送图片已上传完成, 可发送
|
||||
canSendPendingImage() {
|
||||
return !!(this.pendingImage && !this.pendingImage.uploading && !this.pendingImage.uploadError && this.pendingImage.uploaded)
|
||||
},
|
||||
hasInputText() {
|
||||
return this.inputText && this.inputText.trim() !== ''
|
||||
}
|
||||
@@ -735,9 +755,27 @@ export default {
|
||||
this.$emit('nickname-changed', newNickname)
|
||||
},
|
||||
|
||||
onImageSelected(imageMessage) {
|
||||
this.messages.push(imageMessage)
|
||||
this.$emit('image-selected', imageMessage)
|
||||
// 选图后: 不直接进聊天区, 作为输入框上方附件预览并立即开始上传
|
||||
onImagePicked(paths) {
|
||||
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) {
|
||||
@@ -983,11 +1021,24 @@ async loadChatHistoryIfExist() {
|
||||
|
||||
// 发送消息
|
||||
async sendMessage() {
|
||||
// 确保有内容或表情
|
||||
// 确保有内容、表情或待发送图片
|
||||
const hasContent = this.inputText && this.inputText.trim() !== ''
|
||||
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
|
||||
@@ -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
|
||||
this._aiRequestStart = Date.now()
|
||||
|
||||
const queryText = this.inputText && this.inputText.trim()
|
||||
const queryText = hasContent
|
||||
? this.inputText.trim()
|
||||
: (readyAttachments.length ? '[附件]' : '')
|
||||
const userMessage = {
|
||||
id: ++this.messageId,
|
||||
role: 'user',
|
||||
type: 'text',
|
||||
content: queryText,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
// 带图片时消息类型为 image, 图片 + 配文一起显示; 纯文字时仍为 text
|
||||
const userMessage = pending
|
||||
? {
|
||||
id: ++this.messageId,
|
||||
role: 'user',
|
||||
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.messages.push(userMessage)
|
||||
this.inputText = ''
|
||||
this.pendingImage = null
|
||||
|
||||
// 显示AI正在处理 - 带状态轮换提示
|
||||
const loadingMessage = {
|
||||
@@ -1046,7 +1119,8 @@ async loadChatHistoryIfExist() {
|
||||
try {
|
||||
// 直接发送消息(跳过健康检查,避免健康检查接口格式不匹配导致误判)
|
||||
// 如果服务不可用,实际发送时会抛出异常,由 catch 块处理
|
||||
const enhancedMessage = this.enhanceEmojiMessage(userMessage.content)
|
||||
// 纯图片无文字时, 给 AI 的 query 用占位文案, 避免空 query
|
||||
const enhancedMessage = this.enhanceEmojiMessage(userMessage.content || (readyAttachments.length ? '[图片]' : ''))
|
||||
if (this.enableStreaming) {
|
||||
await this.sendStreamMessage(enhancedMessage, readyAttachments)
|
||||
} else {
|
||||
@@ -4578,4 +4652,54 @@ _handleStreamError(msg) {
|
||||
&.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>
|
||||
|
||||
@@ -80,32 +80,11 @@ export default {
|
||||
count: 9,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
that.hideToolsPanel()
|
||||
for (const p of res.tempFilePaths) {
|
||||
const pending = {
|
||||
id: ++that.messageId,
|
||||
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' })
|
||||
}
|
||||
}
|
||||
// 新交互: 选图后不直接进聊天区, 交给父组件作为输入框附件预览,
|
||||
// 用户可继续输入文字, 点发送时图文一起发出
|
||||
that.$emit('image-picked', res.tempFilePaths || [])
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg && !err.errMsg.includes('cancel')) {
|
||||
|
||||
Reference in New Issue
Block a user