chore:滑动图片是水平滑动滚动条根据后台显示,AI智能点赞svg
This commit is contained in:
@@ -218,7 +218,7 @@ const localDevConfig = ({
|
||||
uniacid: 317,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
'5g-312': { // 电磁兼容,模板显示不全商品
|
||||
'5g-312': { // 天然植物功效活性物
|
||||
uniacid: 312,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
@@ -243,7 +243,7 @@ const localDevConfig = ({
|
||||
// })['local-11']; // 选择要使用的环境配置
|
||||
// })['2811']; // 本地
|
||||
//})['5g-317']; // 电磁兼容
|
||||
})['192.168.8.118-1']; //
|
||||
})['5g-312']; //
|
||||
// })['local-2']; // 本地
|
||||
// })['local-11']; // 本地
|
||||
// })['1-test']; // test
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<view :style="componentStyle" data-component-name="diy-image-nav">
|
||||
<scroll-view :class="['image-nav', value.showStyle == 'fixed' ? 'fixed-layout' : value.showStyle]"
|
||||
:scroll-x="value.showStyle == 'singleSlide'">
|
||||
<!-- PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): 水平滚动条显隐三态(后台配置)
|
||||
slide=滑动时显示(默认) / always=始终显示 / never=永不显示
|
||||
小程序端: show-scrollbar 仅在 enhanced=true 时生效, 所以两者同步绑定 -->
|
||||
<scroll-view :class="['image-nav', value.showStyle == 'fixed' ? 'fixed-layout' : value.showStyle, scrollBarClass]"
|
||||
:scroll-x="value.showStyle == 'singleSlide'"
|
||||
:show-scrollbar="scrollBarMode != 'never'"
|
||||
:enhanced="scrollBarMode != 'never'">
|
||||
<view class="uni-scroll-view-content">
|
||||
<view v-for="(item, index) in value.list" :key="index" :class="['image-nav-item', value.mode]"
|
||||
style="margin-right: 28rpx;">
|
||||
@@ -60,7 +65,10 @@ export default {
|
||||
slideDidDrag: false, // 5px 阈值后置 true,用于判断 mouseup 后是否拦截 click
|
||||
slideStartX: 0,
|
||||
slideStartScrollLeft: 0,
|
||||
_suppressNextClick: false // 拖拽结束后拦截一次 click,防止误触发跳转
|
||||
_suppressNextClick: false, // 拖拽结束后拦截一次 click,防止误触发跳转
|
||||
// PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): "滑动时显示"模式专用
|
||||
// 滚动中为 true → 给 scroll-view 加 is-scrolling 类展示滚动条, 停止 800ms 后隐藏
|
||||
slideScrolling: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -75,12 +83,14 @@ export default {
|
||||
var self = this;
|
||||
this.$nextTick(function() {
|
||||
self.bindSlideMouseEvents();
|
||||
self.bindSlideScrollbar();
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
beforeDestroy() {
|
||||
// #ifdef H5
|
||||
this.unbindSlideMouseEvents();
|
||||
this.unbindSlideScrollbar();
|
||||
// #endif
|
||||
},
|
||||
watch: {
|
||||
@@ -123,9 +133,111 @@ export default {
|
||||
isIndicatorDots() {
|
||||
return this.value.carousel.type != 'hide' &&
|
||||
1 != Math.ceil(this.value.list.length / (this.value.pageCount * this.value.rowCount));
|
||||
},
|
||||
|
||||
// ========== PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): 水平滚动条显隐三态 ==========
|
||||
// 后台"水平滚动条"配置: 滑动时显示 / 始终显示 / 永不显示 → 归一化为 slide / always / never
|
||||
// 字段名兼容: scrollBar / scrollbar / scrollBarType / horizontalScrollBar / horizontalScrollbar
|
||||
// (忽略大小写/下划线/中划线差异, 并向下探测 value.scrollBar / value.style 等嵌套容器)
|
||||
// 值兼容: 英文枚举(slide/auto/always/never/none...) / 中文枚举(滑动时显示/始终显示/永不显示)
|
||||
// / 数字(0=滑动时显示 1=始终显示 2=永不显示) / 布尔(true=始终显示 false=永不显示)
|
||||
// 兜底: 未下发或无法识别 → 'slide' (滑动时显示, 与历史行为一致)
|
||||
scrollBarMode() {
|
||||
const v = this.value || {};
|
||||
const norm = k => String(k).toLowerCase().replace(/[\s_-]/g, '');
|
||||
// 从配置对象中取"滚动条"字段的原始值
|
||||
const pickRaw = obj => {
|
||||
if (!obj || typeof obj != 'object') return undefined;
|
||||
for (const key in obj) {
|
||||
const k = norm(key);
|
||||
if (k.indexOf('scrollbar') != -1 || k.indexOf('scrolltype') != -1) {
|
||||
const val = obj[key];
|
||||
if (val && typeof val === 'object') {
|
||||
// 子配置对象形态: { scrollBar: { type: 'always' } }
|
||||
return val.type !== undefined ? val.type : (val.value !== undefined ? val.value : val.mode);
|
||||
}
|
||||
if (val !== undefined && val !== null && val !== '') return val;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
let raw = pickRaw(v);
|
||||
// 嵌套容器兜底
|
||||
if (raw === undefined) {
|
||||
const containers = ['scrollBar', 'scrollbar', 'style', 'config', 'setting', 'data'];
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
if (v[containers[i]] && typeof v[containers[i]] === 'object') {
|
||||
raw = pickRaw(v[containers[i]]);
|
||||
if (raw !== undefined) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (raw === undefined || raw === null || raw === '') return 'slide';
|
||||
if (typeof raw === 'boolean') return raw ? 'always' : 'never';
|
||||
if (raw === 0 || raw === '0') return 'slide';
|
||||
if (raw === 1 || raw === '1') return 'always';
|
||||
if (raw === 2 || raw === '2') return 'never';
|
||||
|
||||
const map = {
|
||||
slide: 'slide', scroll: 'slide', auto: 'slide', scrolling: 'slide', whilescrolling: 'slide', moveslide: 'slide',
|
||||
always: 'always', alwaysdisplay: 'always', show: 'always',
|
||||
never: 'never', none: 'never', hidden: 'never', hide: 'never',
|
||||
// 后台直出中文枚举时兜底
|
||||
'滑动时显示': 'slide', '滑动时': 'slide', '滚动时显示': 'slide',
|
||||
'始终显示': 'always', '一直显示': 'always', '永久显示': 'always',
|
||||
'永不显示': 'never', '从不显示': 'never', '不显示': 'never'
|
||||
};
|
||||
const hit = map[String(raw).toLowerCase().trim()] || map[norm(raw)];
|
||||
return hit || 'slide';
|
||||
},
|
||||
|
||||
// scroll-view 上的滚动条显隐 class: scrollbar-slide / scrollbar-always / scrollbar-never
|
||||
// "滑动时显示"在滚动中追加 is-scrolling (由 scroll 监听维护)
|
||||
scrollBarClass() {
|
||||
const cls = ['scrollbar-' + this.scrollBarMode];
|
||||
if (this.scrollBarMode == 'slide' && this.slideScrolling) cls.push('is-scrolling');
|
||||
return cls;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// ========== PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): H5 "滑动时显示"滚动条控制 ==========
|
||||
// 背景: H5 端 scroll-view 的横向滚动条默认被 CSS 隐藏; "滑动时显示"需要监听 scroll,
|
||||
// 滚动中给容器加 is-scrolling 类展示滚动条, 停止滚动 800ms 后移除类再隐藏。
|
||||
// "始终显示/永不显示"为纯 CSS 类控制(scrollbar-always/scrollbar-never), 无需 JS。
|
||||
bindSlideScrollbar() {
|
||||
// #ifdef H5
|
||||
if (this._slideScrollBound) return;
|
||||
var el = this.getSlideContainer();
|
||||
if (!el) return;
|
||||
this._slideScrollEl = el;
|
||||
this._slideScrollBound = true;
|
||||
var self = this;
|
||||
this._onSlideScroll = function() {
|
||||
if (self.scrollBarMode != 'slide') return;
|
||||
if (!self.slideScrolling) self.slideScrolling = true;
|
||||
clearTimeout(self._slideScrollTimer);
|
||||
self._slideScrollTimer = setTimeout(function() {
|
||||
self.slideScrolling = false;
|
||||
}, 800);
|
||||
};
|
||||
el.addEventListener('scroll', this._onSlideScroll, { passive: true });
|
||||
// #endif
|
||||
},
|
||||
unbindSlideScrollbar() {
|
||||
// #ifdef H5
|
||||
if (this._slideScrollEl && this._onSlideScroll) {
|
||||
this._slideScrollEl.removeEventListener('scroll', this._onSlideScroll);
|
||||
}
|
||||
clearTimeout(this._slideScrollTimer);
|
||||
this._slideScrollEl = null;
|
||||
this._onSlideScroll = null;
|
||||
this._slideScrollBound = false;
|
||||
this.slideScrolling = false;
|
||||
// #endif
|
||||
},
|
||||
|
||||
// 页面跳转
|
||||
redirectTo(link) {
|
||||
if (!link.wap_url || this.$util.getCurrRoute() != this.$util.MEMBER_PAGE_URL || this.storeToken) {
|
||||
@@ -327,17 +439,61 @@ export default {
|
||||
// #ifdef H5
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
// 隐藏滚动条,视觉上仍保持单页布局;用户用鼠标拖拽/滚轮即可滚动
|
||||
scrollbar-width: none; // Firefox
|
||||
-ms-overflow-style: none; // IE/Edge legacy
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
// 禁止鼠标选中文字(拖拽时选中会显得很别扭)
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
// 拖拽时关闭过渡,避免 Chrome 的惯性滚动与 JS 抢控制权
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
// ========== PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): 水平滚动条显隐三态 ==========
|
||||
// 默认态(滑动时显示的静止态 / 永不显示): 隐藏滚动条,视觉上仍保持单页布局;
|
||||
// 用户用鼠标拖拽/滚轮即可滚动(滚动能力不受滚动条显隐影响)
|
||||
scrollbar-width: none; // Firefox
|
||||
-ms-overflow-style: none; // IE/Edge legacy
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
// uni scroll-view 在 H5 编译后真滚动容器是内层 .uni-scroll-view, 同步隐藏
|
||||
::v-deep .uni-scroll-view {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
::v-deep .uni-scroll-view::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// "始终显示" 与 "滑动时显示"(滚动中, 由 is-scrolling 类驱动): 展示细滚动条
|
||||
&.scrollbar-always,
|
||||
&.scrollbar-slide.is-scrolling {
|
||||
scrollbar-width: thin; // Firefox
|
||||
scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
|
||||
&::-webkit-scrollbar {
|
||||
display: block;
|
||||
height: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 2px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
::v-deep .uni-scroll-view {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
|
||||
}
|
||||
::v-deep .uni-scroll-view::-webkit-scrollbar {
|
||||
display: block;
|
||||
height: 4px;
|
||||
}
|
||||
::v-deep .uni-scroll-view::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 2px;
|
||||
}
|
||||
::v-deep .uni-scroll-view::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,16 @@
|
||||
<rich-text v-if="searchKeyword" :nodes="highlightContent(message.content)" class="message-rich-text" />
|
||||
<text v-else class="message-text" selectable>{{ message.content }}</text>
|
||||
</view>
|
||||
<!-- 用户发送的附件(文件/视频/音频)预览: 与文字/图片同气泡下方展示。
|
||||
图片附件不在此重复渲染(messageImages 已在上方图片气泡展示), 这里只渲染非图片附件 -->
|
||||
<view v-if="message.attachments && message.attachments.length && message.role === 'user'" class="attachments user-attachments">
|
||||
<template v-for="att in message.attachments">
|
||||
<view v-if="!(att.type === 'image' || att.kind === 'image')" :key="att.url" class="attachment" @click="previewAttachment(att)">
|
||||
<text class="file-icon">{{ att.type === 'video' ? '🎬' : (att.type === 'audio' ? '🎵' : '📎') }}</text>
|
||||
<text class="file-name">{{ att.name || '文件' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<view class="avatar" @tap="showAvatarSelector = true">
|
||||
<image :src="localUserAvatar" mode="aspectFill" show-menu-by-longpress/>
|
||||
@@ -336,10 +346,11 @@
|
||||
<text class="iconfont icon-copy">⎘</text>
|
||||
</view>
|
||||
<view class="ai-action-btn" :class="{ active: message._liked }" @click="onAiActionLike(message)">
|
||||
<text class="iconfont icon-thumbsup">👍</text>
|
||||
<!-- PR-FRONTEND-ACTION-SVG-ICON (2026-09-18): 点赞/点踩改用线性 SVG 矢量图, 不再用彩色 emoji -->
|
||||
<view class="svg-icon icon-like"></view>
|
||||
</view>
|
||||
<view class="ai-action-btn" :class="{ active: message._disliked }" @click="onAiActionDislike(message)">
|
||||
<text class="iconfont icon-thumbsdown">👎</text>
|
||||
<view class="svg-icon icon-dislike"></view>
|
||||
</view>
|
||||
<view class="ai-action-btn more-btn" @click="onAiActionMore(message)">
|
||||
<text class="iconfont icon-more">···</text>
|
||||
@@ -356,9 +367,11 @@
|
||||
<!-- 输入区域 (新版: 顶部胶囊形输入条 + 底部 5 个图标) -->
|
||||
<view class="input-area" :style="inputAreaStyle">
|
||||
<view class="input-card" :class="{ 'is-voice-mode': voiceMode, 'is-text-mode': showTextMode }">
|
||||
<!-- 待发送图片附件预览: 挂在输入条内部顶端(文字行上方), 出现时输入框自动向上撑高, 删除后恢复原高度 -->
|
||||
<view v-if="pendingImages.length" class="pending-attachment">
|
||||
<view v-for="p in pendingImages" :key="p.localId" class="pending-item">
|
||||
<!-- 待发送附件预览(图片 / 文件 / 视频 / 音频): 挂在输入条内部顶端(文字行上方),
|
||||
出现时输入框自动向上撑高, 删除后恢复原高度。
|
||||
图片与文件走同一套 pending 逻辑: 选完先在这里预览, 等用户点发送再随文字一起发出 -->
|
||||
<view v-if="pendingImages.length || pendingFiles.length" class="pending-attachment">
|
||||
<view v-for="p in pendingImages" :key="'img-' + p.localId" class="pending-item">
|
||||
<image :src="p.uploaded ? p.uploaded.url : p.tempPath" mode="aspectFill" class="pending-image" />
|
||||
<view v-if="p.uploading" class="pending-mask"><text>上传中</text></view>
|
||||
<view v-else-if="p.uploadError" class="pending-mask error"><text>失败</text>
|
||||
@@ -367,6 +380,18 @@
|
||||
</view>
|
||||
<view class="pending-remove" @tap="removePendingImage(p.localId)">×</view>
|
||||
</view>
|
||||
<view v-for="f in pendingFiles" :key="'file-' + f.localId" class="pending-item">
|
||||
<image v-if="f.type === 'image' && (f.uploaded ? f.uploaded.url : f.imageUrl)" :src="f.uploaded ? f.uploaded.url : f.imageUrl" mode="aspectFill" class="pending-image" />
|
||||
<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>
|
||||
</view>
|
||||
<view v-if="f.uploading" class="pending-mask"><text>上传中</text></view>
|
||||
<view v-else-if="f.uploadError" class="pending-mask error"><text>失败</text>
|
||||
<view class="pending-retry-delete" @tap.stop="removePendingFile(f.localId)">删除</view>
|
||||
</view>
|
||||
<view class="pending-remove" @tap="removePendingFile(f.localId)">×</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- PR-FRONTEND-INPUT-3STATES (2026-09-17): 输入区三态
|
||||
① 默认(未输入/未聚焦): 单行胶囊 —— 语音图标 + 输入框 + 加号
|
||||
@@ -623,6 +648,9 @@ export default {
|
||||
// 必须用非下划线前缀. PR-FRONTEND-MEDIA-UPLOAD-FIX-HOTFIX (2026-09-16)
|
||||
pendingIdSeq: 0,
|
||||
pendingImages: [],
|
||||
// PR-FRONTEND-MEDIA-PENDING-FILE (2026-09-17): 文件/视频/音频与图片同一套 pending 逻辑
|
||||
// 选完先挂在这里预览, 等用户点发送再随文字一起发出 (不再选完直接进聊天区)
|
||||
pendingFiles: [],
|
||||
scrollTop: 0,
|
||||
scrollIntoViewId: null,
|
||||
hasMoreHistory: false,
|
||||
@@ -727,19 +755,24 @@ export default {
|
||||
if (!this.pendingImages.length) return false
|
||||
return this.pendingImages.every(p => !p.uploading && !p.uploadError && p.uploaded)
|
||||
},
|
||||
// PR-FRONTEND-MEDIA-PENDING-FILE (2026-09-17): 待发送文件附件是否均可发送
|
||||
canSendPendingFile() {
|
||||
if (!this.pendingFiles || !this.pendingFiles.length) return false
|
||||
return this.pendingFiles.every(p => !p.uploading && !p.uploadError && p.uploaded)
|
||||
},
|
||||
hasInputText() {
|
||||
return this.inputText && this.inputText.trim() !== ''
|
||||
},
|
||||
// PR-FRONTEND-INPUT-3STATES (2026-09-17): 文字输入态
|
||||
// 聚焦 或 已有文字时展开为"输入框一行 + 工具栏一行", 否则回到单行胶囊。
|
||||
// 用聚焦(而不是只判断文字)是为了: 删除最后一个字时布局不会突然塌回胶囊。
|
||||
// 另外: 有"上传完成的待发送图片"时也必须展开, 否则发送按钮(在工具栏行)不可见。
|
||||
// 另外: 有"上传完成的待发送图片/文件"时也必须展开, 否则发送按钮(在工具栏行)不可见。
|
||||
showTextMode() {
|
||||
return !this.voiceMode && (this.inputFocused || this.hasInputText || this.canSendPendingImage)
|
||||
return !this.voiceMode && (this.inputFocused || this.hasInputText || this.canSendPendingImage || this.canSendPendingFile)
|
||||
},
|
||||
// 发送按钮可用态 (无内容时按钮保留但置灰)
|
||||
canSend() {
|
||||
return this.hasInputText || this.canSendPendingImage
|
||||
return this.hasInputText || this.canSendPendingImage || this.canSendPendingFile
|
||||
}
|
||||
},
|
||||
// 新增:组件销毁时清除所有定时器
|
||||
@@ -914,10 +947,23 @@ export default {
|
||||
},
|
||||
|
||||
onFileSelected(fileMessage) {
|
||||
this.messages.push(fileMessage)
|
||||
// PR-FRONTEND-MEDIA-PENDING-FILE (2026-09-17): 文件与图片同一逻辑 ——
|
||||
// 选完不直接进聊天区, 挂到 pendingFiles 在输入框上方预览, 等点发送再随问题一起发出
|
||||
const localId = ++this.pendingIdSeq
|
||||
this.$set(fileMessage, 'localId', localId)
|
||||
this.pendingFiles.push(fileMessage)
|
||||
this.$emit('file-selected', fileMessage)
|
||||
},
|
||||
|
||||
// 移除待发送文件附件 (按 localId)
|
||||
removePendingFile(localId) {
|
||||
if (localId === undefined || localId === null) {
|
||||
this.pendingFiles = []
|
||||
return
|
||||
}
|
||||
this.pendingFiles = this.pendingFiles.filter(p => p.localId !== localId)
|
||||
},
|
||||
|
||||
onLocationSelected(locationMessage) {
|
||||
this.messages.push(locationMessage)
|
||||
this.$emit('location-selected', locationMessage)
|
||||
@@ -1199,47 +1245,46 @@ async loadChatHistoryIfExist() {
|
||||
// PR-FRONTEND-MEDIA-UPLOAD-FIX (2026-09-16): pendingImages 数组版
|
||||
// 原 pendingImage 单数, 改支持 1-9 张图一起发
|
||||
async sendMessage() {
|
||||
// 确保有内容、表情或待发送图片
|
||||
// 确保有内容、表情或待发送附件(图片/文件)
|
||||
const hasContent = this.inputText && this.inputText.trim() !== ''
|
||||
const hasEmoji = this.containsEmoji(this.inputText)
|
||||
const pendingList = this.pendingImages
|
||||
const pendingImagesList = this.pendingImages
|
||||
const pendingFilesList = this.pendingFiles
|
||||
const allPending = [...pendingImagesList, ...pendingFilesList]
|
||||
|
||||
// 附件图片必须上传完成才能发送 (数组版: 任何一张还在 uploading/出错都拦)
|
||||
if (pendingList.length) {
|
||||
if (pendingList.some(p => p.uploading)) {
|
||||
uni.showToast({ title: '图片上传中,请稍候', icon: 'none' })
|
||||
// 附件必须上传完成才能发送 (任何一张/个还在 uploading/出错都拦)
|
||||
if (allPending.length) {
|
||||
if (allPending.some(p => p.uploading)) {
|
||||
uni.showToast({ title: '附件上传中,请稍候', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (pendingList.some(p => p.uploadError || !p.uploaded)) {
|
||||
uni.showToast({ title: '图片上传失败,请删除后重试', icon: 'none' })
|
||||
if (allPending.some(p => p.uploadError || !p.uploaded)) {
|
||||
uni.showToast({ title: '附件上传失败,请删除后重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasContent && !hasEmoji && !pendingList.length) return
|
||||
if (!hasContent && !hasEmoji && !allPending.length) return
|
||||
|
||||
// 收集已上传、尚未随消息发送的附件 (用户图片/文件消息)
|
||||
const readyAttachments = this.messages
|
||||
.filter(m => m.role === 'user' && m.uploaded && !m.sendWithBatch)
|
||||
.map(m => {
|
||||
m.sendWithBatch = true
|
||||
return {
|
||||
// PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 不再把 audio/video 强转 file
|
||||
// m.uploaded.kind 是后端响应 (image/file/audio/video); m.type 是前端分类
|
||||
type: m.uploaded.kind || m.type || 'file',
|
||||
url: m.uploaded.url,
|
||||
mime: m.uploaded.mime,
|
||||
name: m.uploaded.name,
|
||||
size: m.uploaded.size
|
||||
}
|
||||
})
|
||||
// 待发送文件/视频/音频附件 (与图片一并带出给后端)
|
||||
const fileAttachments = pendingFilesList
|
||||
.filter(p => p.uploaded)
|
||||
.map(p => ({
|
||||
// PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 不再把 audio/video 强转 file
|
||||
// p.uploaded.kind 是后端响应 (image/file/audio/video); p.type 是前端分类
|
||||
type: p.uploaded.kind || p.type || 'file',
|
||||
url: p.uploaded.url,
|
||||
mime: p.uploaded.mime,
|
||||
name: p.uploaded.name,
|
||||
size: p.uploaded.size
|
||||
}))
|
||||
|
||||
// 输入框待发送图片: 全部 push, 第一张作为 userMessage.imageUrl 显示
|
||||
// PR-FRONTEND-MEDIA-INLINE-BASE64 (2026-09-16): 同时给附件补 data: <base64> 字段,
|
||||
// 避开 backend MediaGuard SSRF 闸门 (LAN URL 被识别为内网 IP 直接丢).
|
||||
// 注意: 必须 await 让 base64 生成后再 build userMessage / sendStreamMessage, 否则附件没 data 字段
|
||||
const enrichedPendingAttachments = await Promise.all(
|
||||
pendingList
|
||||
pendingImagesList
|
||||
.filter(p => p.uploaded)
|
||||
.map(async p => {
|
||||
const base = {
|
||||
@@ -1261,7 +1306,9 @@ async loadChatHistoryIfExist() {
|
||||
return base
|
||||
})
|
||||
)
|
||||
readyAttachments.push(...enrichedPendingAttachments)
|
||||
|
||||
// 实际发送给后端的内容 = 图片(含 base64) + 文件/视频/音频
|
||||
const readyAttachments = [...fileAttachments, ...enrichedPendingAttachments]
|
||||
|
||||
// 记录用户发出消息的时间戳, AI 回复完成时算 elapsed
|
||||
this._aiRequestStart = Date.now()
|
||||
@@ -1269,25 +1316,37 @@ async loadChatHistoryIfExist() {
|
||||
const queryText = hasContent
|
||||
? this.inputText.trim()
|
||||
: (readyAttachments.length ? '[附件]' : '')
|
||||
// 带图片时消息类型为 image, 图片 + 配文一起显示; 纯文字时仍为 text
|
||||
const firstPending = pendingList.length ? pendingList[0] : null
|
||||
const userMessage = firstPending
|
||||
// 带图片时消息类型为 image, 图片 + 配文一起显示; 纯文件/文字时仍为 text
|
||||
const firstPendingImage = pendingImagesList.length ? pendingImagesList[0] : null
|
||||
// 用户气泡内展示用的附件: 图片(供 messageImages 渲染多图) + 文件/视频/音频
|
||||
// 注: 图片附件由上方图片气泡渲染, 文件附件由用户消息 attachments 块渲染, 互不重复
|
||||
const displayAttachments = [
|
||||
...enrichedPendingAttachments.map(a => ({
|
||||
url: a.url,
|
||||
mime: a.mime,
|
||||
name: a.name,
|
||||
size: a.size,
|
||||
kind: a.type || 'image',
|
||||
...(a.data ? { data: a.data, encoding: a.encoding } : {}),
|
||||
})),
|
||||
...fileAttachments.map(a => ({
|
||||
url: a.url,
|
||||
mime: a.mime,
|
||||
name: a.name,
|
||||
size: a.size,
|
||||
kind: a.type || 'file',
|
||||
})),
|
||||
]
|
||||
const userMessage = firstPendingImage
|
||||
? {
|
||||
id: ++this.messageId,
|
||||
role: 'user',
|
||||
type: 'image',
|
||||
imageUrl: firstPending.uploaded.url || firstPending.tempPath,
|
||||
imageUrl: firstPendingImage.uploaded.url || firstPendingImage.tempPath,
|
||||
// PR-FRONTEND-MEDIA-UPLOAD-FIX: 多图时把其他图片 URL 也带在消息上, 模板侧可循环展示
|
||||
// PR-FRONTEND-MEDIA-INLINE-BASE64 (2026-09-16): 复用上面 enrichedPendingAttachments 的 data 字段
|
||||
// (同一份 base64 计算, 不要再读一次 file → 节省内存 + CPU)
|
||||
attachments: enrichedPendingAttachments.map(a => ({
|
||||
url: a.url,
|
||||
mime: a.mime,
|
||||
name: a.name,
|
||||
size: a.size,
|
||||
kind: a.type || 'image',
|
||||
...(a.data ? { data: a.data, encoding: a.encoding } : {}),
|
||||
})),
|
||||
attachments: displayAttachments,
|
||||
content: hasContent ? this.inputText.trim() : '',
|
||||
timestamp: Date.now()
|
||||
}
|
||||
@@ -1296,12 +1355,15 @@ async loadChatHistoryIfExist() {
|
||||
role: 'user',
|
||||
type: 'text',
|
||||
content: queryText,
|
||||
// 纯文件(无图)时把文件挂到 attachments, 用户消息气泡下方展示文件卡片
|
||||
attachments: displayAttachments,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
this.shouldScrollToBottom = true
|
||||
this.shouldScrollToBottom = true
|
||||
this.messages.push(userMessage)
|
||||
this.inputText = ''
|
||||
this.pendingImages = []
|
||||
this.pendingFiles = []
|
||||
|
||||
// 显示AI正在处理 - 带状态轮换提示
|
||||
const loadingMessage = {
|
||||
@@ -4031,12 +4093,39 @@ _handleStreamError(msg) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* PR-FRONTEND-ACTION-SVG-ICON (2026-09-18): 点赞/点踩线性 SVG 矢量图标
|
||||
微信小程序不支持内联 SVG, 沿用 voice-icon-bg 的 base64 背景图方案;
|
||||
默认灰色描边, active 态切换为蓝色描边版本 */
|
||||
.svg-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
|
||||
&.icon-like {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjY2NjY2IiBzdHJva2Utd2lkdGg9IjEuOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNyAxMXY4YTEgMSAwIDAgMSAtMSAxaC0yYTEgMSAwIDAgMSAtMSAtMXYtN2ExIDEgMCAwIDEgMSAtMWgzYTQgNCAwIDAgMCA0IC00di0xYTIgMiAwIDAgMSA0IDB2NWgzYTIgMiAwIDAgMSAyIDJsLTEgNWEyIDIgMCAwIDEgLTIgMmgtN2E0IDQgMCAwIDEgLTQgLTQiLz48L3N2Zz4=");
|
||||
}
|
||||
|
||||
&.icon-dislike {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjY2NjY2IiBzdHJva2Utd2lkdGg9IjEuOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNyAxMXY4YTEgMSAwIDAgMSAtMSAxaC0yYTEgMSAwIDAgMSAtMSAtMXYtN2ExIDEgMCAwIDEgMSAtMWgzYTQgNCAwIDAgMCA0IC00di0xYTIgMiAwIDAgMSA0IDB2NWgzYTIgMiAwIDAgMSAyIDJsLTEgNWEyIDIgMCAwIDEgLTIgMmgtN2E0IDQgMCAwIDEgLTQgLTQiIHRyYW5zZm9ybT0icm90YXRlKDE4MCAxMiAxMikiLz48L3N2Zz4=");
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e6f0ff;
|
||||
|
||||
.iconfont {
|
||||
color: #1f6cff;
|
||||
}
|
||||
|
||||
.svg-icon.icon-like {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWY2Y2ZmIiBzdHJva2Utd2lkdGg9IjEuOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNyAxMXY4YTEgMSAwIDAgMSAtMSAxaC0yYTEgMSAwIDAgMSAtMSAtMXYtN2ExIDEgMCAwIDEgMSAtMWgzYTQgNCAwIDAgMCA0IC00di0xYTIgMiAwIDAgMSA0IDB2NWgzYTIgMiAwIDAgMSAyIDJsLTEgNWEyIDIgMCAwIDEgLTIgMmgtN2E0IDQgMCAwIDEgLTQgLTQiLz48L3N2Zz4=");
|
||||
}
|
||||
|
||||
.svg-icon.icon-dislike {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWY2Y2ZmIiBzdHJva2Utd2lkdGg9IjEuOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNyAxMXY4YTEgMSAwIDAgMSAtMSAxaC0yYTEgMSAwIDAgMSAtMSAtMXYtN2ExIDEgMCAwIDEgMSAtMWgzYTQgNCAwIDAgMCA0IC00di0xYTIgMiAwIDAgMSA0IDB2NWgzYTIgMiAwIDAgMSAyIDJsLTEgNWEyIDIgMCAwIDEgLTIgMmgtN2E0IDQgMCAwIDEgLTQgLTQiIHRyYW5zZm9ybT0icm90YXRlKDE4MCAxMiAxMikiLz48L3N2Zz4=");
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
@@ -5210,6 +5299,34 @@ _handleStreamError(msg) {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.pending-image { width: 100%; height: 100%; display: block; }
|
||||
/* PR-FRONTEND-MEDIA-PENDING-FILE: 非图片附件(文件/视频/音频)缩略占位 */
|
||||
.pending-file-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #eef2f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6rpx 8rpx;
|
||||
box-sizing: border-box;
|
||||
.pending-file-icon { font-size: 44rpx; line-height: 1; }
|
||||
.pending-file-name {
|
||||
margin-top: 6rpx;
|
||||
max-width: 100%;
|
||||
font-size: 18rpx;
|
||||
color: #55657a;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
/* 用户消息里的文件附件卡片 */
|
||||
.user-attachments { margin-top: 8rpx; }
|
||||
.pending-mask {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
|
||||
Reference in New Issue
Block a user