chore:滑动图片是水平滑动滚动条根据后台显示,AI智能点赞svg

This commit is contained in:
2026-09-18 09:58:15 +08:00
parent 8b2670bf31
commit 1b1704f7c6
3 changed files with 332 additions and 59 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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;