feat(ai-chat): 增强位置消息处理,支持透传位置、时间、页面上下文,优化用户体验
This commit is contained in:
@@ -523,6 +523,9 @@ export default {
|
||||
user: getUserId(),
|
||||
conversation_id: this.getConversationId() || '',
|
||||
attachments: (options && options.attachments) || [],
|
||||
// PR-BASIC-CONTEXT-TO-WB (2026-09-28): 透传位置/时间/页面上下文
|
||||
// 给 WbAgentOsClient 提供 timestamp/timezone/location/page/metadata 等基础元素
|
||||
context: (options && options.context) || null,
|
||||
...getDomainParams()
|
||||
},
|
||||
header: {
|
||||
@@ -588,6 +591,8 @@ export default {
|
||||
user_id: getUserId(),
|
||||
conversation_id: this.getConversationId() || '',
|
||||
attachments: opts.attachments || [],
|
||||
// PR-BASIC-CONTEXT-TO-WB (2026-09-28): 透传位置/时间/页面上下文
|
||||
context: (opts && opts.context) || null,
|
||||
...getDomainParams()
|
||||
})
|
||||
});
|
||||
@@ -608,7 +613,11 @@ export default {
|
||||
*/
|
||||
async _fallbackToHttp(message, onEvent, onComplete, opts = {}) {
|
||||
try {
|
||||
const result = await this.sendMessage(message, { attachments: (opts && opts.attachments) || [] });
|
||||
const result = await this.sendMessage(message, {
|
||||
attachments: (opts && opts.attachments) || [],
|
||||
// PR-BASIC-CONTEXT-TO-WB (2026-09-28): MP HTTP 降级路径同步传递 context
|
||||
context: (opts && opts.context) || null
|
||||
});
|
||||
const content = result && result.content ? result.content : '';
|
||||
// 阻塞式降级也尝试把后端返回的思考过程/用量透出来, 让小程序在 WS 不可用时仍能显示"思考过程 (N 步)"
|
||||
const fallbackPayload = {
|
||||
@@ -666,6 +675,8 @@ export default {
|
||||
user_id: getUserId(),
|
||||
conversation_id: this.getConversationId() || '',
|
||||
attachments: (opts && opts.attachments) || [],
|
||||
// PR-BASIC-CONTEXT-TO-WB (2026-09-28): 透传位置/时间/页面上下文
|
||||
context: (opts && opts.context) || null,
|
||||
...getDomainParams()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -42,7 +42,9 @@
|
||||
<!-- PR-USER-MULTI-VOICE-FILTER (2026-09-28): 补 voiceList / videoList / attachments 字段检查
|
||||
原过滤只识别顶层 fileName / audioUrl / imageUrl / videoUrl,
|
||||
多条语音/视频消息顶层无这些字段 (在 voiceList/videoList 里嵌套), 会被误过滤掉不渲染
|
||||
修复: 显式判断 voiceList / videoList 非空 + attachments 非空 -->
|
||||
修复: 显式判断 voiceList / videoList 非空 + attachments 非空
|
||||
PR-LOCATION-FILTER (2026-09-28): 位置消息顶层无上述字段, 但有 latitude/longitude,
|
||||
单独判断以确保位置气泡不被过滤掉 -->
|
||||
<view
|
||||
v-for="(message, index) in messages.filter(msg =>
|
||||
msg.type === 'loading'
|
||||
@@ -53,6 +55,7 @@
|
||||
|| (msg.voiceList && msg.voiceList.length)
|
||||
|| (msg.videoList && msg.videoList.length)
|
||||
|| (msg.attachments && msg.attachments.length)
|
||||
|| (msg.latitude !== undefined && msg.longitude !== undefined)
|
||||
|| (msg.content && msg.content.trim() !== '')
|
||||
)"
|
||||
:key="getMessageKey(message, index)"
|
||||
@@ -129,6 +132,22 @@
|
||||
<text class="message-text" selectable>{{ message.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- PR-LOCATION-PENDING (2026-09-28): 用户位置气泡
|
||||
显示在用户消息列表, 同时通过 context.location 字段透传给 WB.
|
||||
📍 emoji + 名称 + 地址 + 坐标 (备选) -->
|
||||
<view v-else-if="message.type === 'location'" class="message-bubble user-location-bubble">
|
||||
<view class="location-item">
|
||||
<text class="location-icon">📍</text>
|
||||
<view class="location-info">
|
||||
<text class="location-name">{{ message.name || $lang('ai-chat.index.location') || '位置' }}</text>
|
||||
<text class="location-address" v-if="message.address">{{ message.address }}</text>
|
||||
<text class="location-coord" v-else>{{ message.latitude }}, {{ message.longitude }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="message.content" class="voice-bubble-text">
|
||||
<text class="message-text" selectable>{{ message.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- PR-USER-VOICE-BUBBLE (2026-09-28): 用户语音消息气泡
|
||||
模仿微信/Telegram 的语音消息样式:
|
||||
[▶播放] [语音 + 进度条] [时长]
|
||||
@@ -477,6 +496,13 @@
|
||||
</view>
|
||||
<text class="pending-voice-duration">{{ formatDuration((f.duration || 0) / 1000) }}</text>
|
||||
</view>
|
||||
<view v-else-if="f.type === 'location'" class="pending-location-thumb">
|
||||
<text class="pending-location-icon">📍</text>
|
||||
<view class="pending-location-info">
|
||||
<text class="pending-location-name">{{ f.name || $lang('ai-chat.index.location') || '位置' }}</text>
|
||||
<text class="pending-location-addr" v-if="f.address">{{ f.address }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="pending-file-thumb">
|
||||
<text class="pending-file-icon">{{ f.type === 'video' ? '🎬' : (f.type === 'audio' ? '🎵' : '📄') }}</text>
|
||||
<text class="pending-file-name">{{ f.fileName }}</text>
|
||||
@@ -1217,8 +1243,138 @@ export default {
|
||||
this.pauseAudio(playing)
|
||||
},
|
||||
|
||||
// PR-BASIC-CONTEXT-TO-WB (2026-09-28): 构建发给 WbAgentOsClient 的 context
|
||||
// 4 类基础元素:
|
||||
// 1. timestamp - 客户端时间 (ms), AI 可识别"几分钟前"类问题
|
||||
// 2. timezone - 客户端时区 (e.g. "Asia/Shanghai"), 避免时区歧义
|
||||
// 3. location - 用户位置 (可选, 仅当 pending 含位置时填, 否则 null)
|
||||
// 4. page - 当前所在页面 (route/title), 让 AI 知道用户在干什么
|
||||
// 5. metadata - 会话级设备/平台信息 (platform/screen/locale), 一次性建立
|
||||
// 跨平台兼容: H5 用 Intl/navigator, MP/鸿蒙用 uni.getSystemInfoSync 兜底
|
||||
_buildSendContext({ locationFiles }) {
|
||||
const ctx = {
|
||||
timestamp: Date.now(),
|
||||
timezone: this._detectTimezone(),
|
||||
location: null,
|
||||
page: this._getCurrentPageRoute(),
|
||||
metadata: this._getSessionMetadata()
|
||||
}
|
||||
// 位置: 只取第一个, 多位置场景后续再加
|
||||
if (locationFiles && locationFiles.length) {
|
||||
const loc = locationFiles[0]
|
||||
ctx.location = {
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
address: loc.address || '',
|
||||
name: loc.name || ''
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
},
|
||||
|
||||
// 检测客户端时区 (H5 优先用 Intl, MP/鸿蒙走 fallback)
|
||||
_detectTimezone() {
|
||||
try {
|
||||
// H5 / 鸿蒙 WebView 一般有 Intl
|
||||
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
||||
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
if (tz) return tz
|
||||
}
|
||||
} catch (e) { /* fallback */ }
|
||||
// MP / 鸿蒙 fallback: 直接读本机偏移 (粗略, 单位小时)
|
||||
try {
|
||||
const offset = new Date().getTimezoneOffset()
|
||||
const sign = offset > 0 ? '-' : '+'
|
||||
const abs = Math.abs(offset)
|
||||
const h = Math.floor(abs / 60)
|
||||
const m = abs % 60
|
||||
return 'UTC' + sign + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m)
|
||||
} catch (e) {
|
||||
return 'UTC'
|
||||
}
|
||||
},
|
||||
|
||||
// 当前页面路由 (H5 用 window.location, MP/鸿蒙用 getCurrentPages)
|
||||
_getCurrentPageRoute() {
|
||||
try {
|
||||
// #ifdef H5
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
return {
|
||||
route: window.location.pathname || '',
|
||||
hash: window.location.hash || '',
|
||||
query: window.location.search || ''
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || MP-HARMONY
|
||||
const pages = getCurrentPages && getCurrentPages()
|
||||
if (pages && pages.length) {
|
||||
const last = pages[pages.length - 1]
|
||||
return {
|
||||
route: last && last.route ? last.route : '',
|
||||
options: last && last.options ? last.options : {}
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
} catch (e) { /* ignore */ }
|
||||
return { route: '', options: {} }
|
||||
},
|
||||
|
||||
// 会话级 metadata: 平台/屏幕/locale, 第一次取后缓存 (会话期间不变)
|
||||
_getSessionMetadata() {
|
||||
if (this._sessionMetaCache) return this._sessionMetaCache
|
||||
let platform = 'h5'
|
||||
// #ifdef MP-WEIXIN
|
||||
platform = 'mp-weixin'
|
||||
// #endif
|
||||
// #ifdef MP-HARMONY
|
||||
platform = 'mp-harmony'
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
platform = 'app'
|
||||
// #endif
|
||||
let screen = { width: 0, height: 0, dpr: 1 }
|
||||
let locale = 'zh-cn'
|
||||
let ua = ''
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && uni.getSystemInfoSync) {
|
||||
const sys = uni.getSystemInfoSync()
|
||||
if (sys) {
|
||||
screen = {
|
||||
width: sys.screenWidth || sys.windowWidth || 0,
|
||||
height: sys.screenHeight || sys.windowHeight || 0,
|
||||
dpr: sys.pixelRatio || 1
|
||||
}
|
||||
locale = sys.language || sys.locale || 'zh-cn'
|
||||
if (sys.uniPlatform) platform = sys.uniPlatform
|
||||
}
|
||||
}
|
||||
// H5 平台补充 UA (uni.getSystemInfoSync 也提供)
|
||||
if (typeof navigator !== 'undefined') {
|
||||
ua = navigator.userAgent || ''
|
||||
if (!screen.width && window.screen) {
|
||||
screen = {
|
||||
width: window.screen.width || 0,
|
||||
height: window.screen.height || 0,
|
||||
dpr: window.devicePixelRatio || 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
this._sessionMetaCache = { platform, screen, locale, userAgent: ua }
|
||||
return this._sessionMetaCache
|
||||
},
|
||||
|
||||
onLocationSelected(locationMessage) {
|
||||
this.messages.push(locationMessage)
|
||||
// PR-LOCATION-PENDING (2026-09-28): 位置消息走 pending 流程
|
||||
// 原实现直接 push 到 messages, 用户点发送后位置根本不会传给 WB
|
||||
// 改为挂到 pendingFiles, 等用户点发送时:
|
||||
// 1) 进 messages 显示位置气泡 (用户视觉)
|
||||
// 2) 坐标/地址塞到 context.location 透传给 WB (AI 知晓)
|
||||
// 位置不算附件, 不参与 _uploadOneFile 上传链路, type='location' 标识
|
||||
const localId = ++this.pendingIdSeq
|
||||
this.$set(locationMessage, 'localId', localId)
|
||||
this.pendingFiles.push(locationMessage)
|
||||
this.$emit('location-selected', locationMessage)
|
||||
},
|
||||
|
||||
@@ -1779,13 +1935,18 @@ async loadChatHistoryIfExist() {
|
||||
// 把 pendingFilesList 里的音频拆出来 → voiceList 数组, 每条独立可播放.
|
||||
// PR-USER-MEDIA-TYPE-BUBBLE (2026-09-28): 修复视频/文件被错误归到 type='text'
|
||||
// 渲染成空文字气泡 + 🎬/📎 卡片的 bug. 现在每种 kind 都有专属气泡.
|
||||
// 优先级: 图片 > 视频 > 语音 > 文件 > 文字 (高优先级类型独占气泡, 其余挂到 attachments)
|
||||
// 优先级: 图片 > 视频 > 语音 > 文件 > 位置 > 文字 (高优先级类型独占气泡, 其余挂到 attachments)
|
||||
// 位置 (location) 单独走 context 字段透传给 WB, 不进 attachments.
|
||||
// PR-LOCATION-PENDING (2026-09-28): 位置从 pendingFiles 里挑出来,
|
||||
// type='location' 标识 (不进 upload 链路, uploaded=null)
|
||||
const voiceFiles = pendingFilesList.filter(p => p.uploaded && p.uploaded.kind === 'audio')
|
||||
const videoFiles = pendingFilesList.filter(p => p.uploaded && p.uploaded.kind === 'video')
|
||||
const fileFiles = pendingFilesList.filter(p => p.uploaded && p.uploaded.kind === 'file')
|
||||
const locationFiles = pendingFilesList.filter(p => p.type === 'location')
|
||||
const hasVoice = voiceFiles.length > 0
|
||||
const hasVideo = videoFiles.length > 0
|
||||
const hasFile = fileFiles.length > 0
|
||||
const hasLocation = locationFiles.length > 0
|
||||
|
||||
const userMessage = firstPendingImage
|
||||
? {
|
||||
@@ -1879,17 +2040,38 @@ async loadChatHistoryIfExist() {
|
||||
timestamp: Date.now()
|
||||
}
|
||||
})()
|
||||
: {
|
||||
id: ++this.messageId,
|
||||
role: 'user',
|
||||
type: 'text',
|
||||
// PR-FRONTEND-VOICE-INPUT (2026-09-24): 用户气泡只显示用户自己输入的文字
|
||||
// 给 AI 的指令 hint 在 queryText 里, 这里不要污染用户气泡
|
||||
content: userText,
|
||||
// 纯文字(无任何附件)时 attachments 留空
|
||||
attachments: [],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
: hasLocation
|
||||
? (() => {
|
||||
// PR-LOCATION-PENDING (2026-09-28): 位置气泡, 同时作为消息体展示 + WB 上下文
|
||||
// 取首个位置为主; 用户文本挂到 content 与位置一起显示
|
||||
// 坐标/地址不重复进 attachments (附件是文件流媒体), 走 context.location 字段
|
||||
const loc = locationFiles[0]
|
||||
return {
|
||||
id: ++this.messageId,
|
||||
role: 'user',
|
||||
type: 'location',
|
||||
content: userText,
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
address: loc.address || '',
|
||||
name: loc.name || '',
|
||||
// 位置不挂 attachments (附件是文件类, 位置是 metadata-like)
|
||||
// 若用户位置 + 文件混发, 文件归到对应分支优先
|
||||
attachments: [],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
})()
|
||||
: {
|
||||
id: ++this.messageId,
|
||||
role: 'user',
|
||||
type: 'text',
|
||||
// PR-FRONTEND-VOICE-INPUT (2026-09-24): 用户气泡只显示用户自己输入的文字
|
||||
// 给 AI 的指令 hint 在 queryText 里, 这里不要污染用户气泡
|
||||
content: userText,
|
||||
// 纯文字(无任何附件)时 attachments 留空
|
||||
attachments: [],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
this.shouldScrollToBottom = true
|
||||
this.messages.push(userMessage)
|
||||
this.inputText = ''
|
||||
@@ -1926,10 +2108,15 @@ async loadChatHistoryIfExist() {
|
||||
// PR-FRONTEND-VOICE-INPUT (2026-09-25): content 只发用户文本 (有文本时),
|
||||
// 纯附件时 fallback 到 _buildAttachmentQuery 的类型标识, 不再 append 指令 hint.
|
||||
const enhancedMessage = this.enhanceEmojiMessage(queryText)
|
||||
// PR-BASIC-CONTEXT-TO-WB (2026-09-28): 透传位置/时间/页面/会话 metadata 给 WbAgentOsClient
|
||||
// 让 AI 知道用户在哪、什么时候发的、当前在哪个页面、设备/平台信息
|
||||
// context 字段在 ai-service.js 三个发送路径(sendMessage/sendStreamMessage/sendHttpStream)
|
||||
// 都已添加, 缺失则后端拿到 null, 不影响旧版后端兼容
|
||||
const context = this._buildSendContext({ locationFiles })
|
||||
if (this.enableStreaming) {
|
||||
await this.sendStreamMessage(enhancedMessage, readyAttachments)
|
||||
await this.sendStreamMessage(enhancedMessage, readyAttachments, context)
|
||||
} else {
|
||||
await this.sendNormalMessage(enhancedMessage, readyAttachments)
|
||||
await this.sendNormalMessage(enhancedMessage, readyAttachments, context)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -1959,11 +2146,13 @@ async loadChatHistoryIfExist() {
|
||||
},
|
||||
|
||||
// 发送普通消息
|
||||
async sendNormalMessage(userMessage, attachments = []) {
|
||||
async sendNormalMessage(userMessage, attachments = [], context = null) {
|
||||
const response = await aiService.sendMessage(userMessage, {
|
||||
conversationId: this.currentConversationId,
|
||||
stream: false,
|
||||
attachments
|
||||
attachments,
|
||||
// PR-BASIC-CONTEXT-TO-WB (2026-09-28): 透传 context 给 WB
|
||||
context
|
||||
})
|
||||
|
||||
// 移除加载状态
|
||||
@@ -1999,7 +2188,7 @@ async loadChatHistoryIfExist() {
|
||||
this.$emit('ai-response', aiMessage)
|
||||
},
|
||||
// 发送流式消息(自动适配 H5 / 微信小程序), 支持思考流 / 选项 / 工具调用 / 附件
|
||||
async sendStreamMessage(userMessage, attachments = []) {
|
||||
async sendStreamMessage(userMessage, attachments = [], context = null) {
|
||||
// 标记进入流式接收, 禁止重复发送, 同时让思考面板处于"思考中"展开状态
|
||||
this.isStreaming = true
|
||||
// 找到已有的 loading 消息,复用它在 DOM 中的位置,实现无缝过渡
|
||||
@@ -2152,10 +2341,10 @@ async sendStreamMessage(userMessage, attachments = []) {
|
||||
|
||||
try {
|
||||
// #ifdef H5
|
||||
await aiService.sendHttpStream(userMessage, onEvent, onComplete, { attachments })
|
||||
await aiService.sendHttpStream(userMessage, onEvent, onComplete, { attachments, context })
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
await aiService.sendStreamMessage(userMessage, onEvent, onComplete, { attachments })
|
||||
await aiService.sendStreamMessage(userMessage, onEvent, onComplete, { attachments, context })
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.error('流式请求失败:', error)
|
||||
@@ -3458,6 +3647,9 @@ _handleStreamError(msg) {
|
||||
// 记录用户发出消息的时间戳, AI 回复完成时算 elapsed
|
||||
this._aiRequestStart = Date.now()
|
||||
|
||||
// PR-BASIC-CONTEXT-TO-WB (2026-09-28): 重发/商品页也透传 context (无位置)
|
||||
const context = this._buildSendContext({ locationFiles: [] })
|
||||
|
||||
const userMessage = {
|
||||
id: ++this.messageId,
|
||||
role: 'user',
|
||||
@@ -3489,10 +3681,10 @@ _handleStreamError(msg) {
|
||||
// 使用AI服务获取回复
|
||||
if (this.enableStreaming) {
|
||||
// 流式响应
|
||||
await this.sendStreamMessage(userMessage.content, attachments)
|
||||
await this.sendStreamMessage(userMessage.content, attachments, context)
|
||||
} else {
|
||||
// 普通响应
|
||||
await this.sendNormalMessage(userMessage.content, attachments)
|
||||
await this.sendNormalMessage(userMessage.content, attachments, context)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -6607,6 +6799,97 @@ _handleStreamError(msg) {
|
||||
}
|
||||
}
|
||||
|
||||
/* PR-LOCATION-PENDING (2026-09-28): 用户位置气泡
|
||||
复用主色背景 + 📍 emoji + 名称/地址, 跨平台一致 */
|
||||
.user-location-bubble {
|
||||
padding: 18rpx 24rpx !important;
|
||||
min-width: 280rpx;
|
||||
max-width: 480rpx;
|
||||
background: $color-primary !important;
|
||||
border-radius: 24rpx !important;
|
||||
color: #fff !important;
|
||||
|
||||
.location-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
.location-icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.location-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.location-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
line-height: 1.2;
|
||||
/* 跨平台防溢出 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 360rpx;
|
||||
}
|
||||
.location-address,
|
||||
.location-coord {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 360rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* PR-LOCATION-PENDING (2026-09-28): 输入区待发送位置预览
|
||||
与 pending-voice-thumb 风格一致, 紧凑卡片 + 📍 + 名称 + 地址 */
|
||||
.pending-location-thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background: #f5f7fa;
|
||||
border-radius: 12rpx;
|
||||
min-width: 200rpx;
|
||||
max-width: 360rpx;
|
||||
|
||||
.pending-location-icon {
|
||||
font-size: 32rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pending-location-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.pending-location-name {
|
||||
font-size: 24rpx;
|
||||
color: #1f1f1f;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pending-location-addr {
|
||||
font-size: 20rpx;
|
||||
color: #8a9fb8;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 图片消息配文气泡 (图文一起发出时, 独立气泡显示在图片下方)
|
||||
PR-FIX-CAPTION-BUBBLE-SIZE (2026-09-24): 这里原先单独定义了
|
||||
background/border-radius/padding/max-width/字号, 使配文气泡比普通文字气泡小一圈
|
||||
|
||||
Reference in New Issue
Block a user