完成短信(苹果,华为,小米,vivo)各个机型发送图片功能,预览图片页面及图片信息列表展示样式

This commit is contained in:
tangxinyue 2026-06-04 09:55:08 +08:00
parent 1cd2226f2b
commit 6569f821e8
11 changed files with 1581 additions and 1050 deletions

View File

@ -57,8 +57,9 @@
<slot name="bottom"> <slot name="bottom">
<view class="bottom-container flex-align-center" v-show="!sortMode"> <view class="bottom-container flex-align-center" v-show="!sortMode">
<image v-if="phone != 'huawei' && phone != 'vivo'" class="add-img shrink-0" <image v-if="phone != 'huawei' && phone != 'vivo'" class="add-img shrink-0"
:src="`/static/image/phone-message/${phone}/chat-left.png`" :src="`/static/image/phone-message/${phone}/chat-left.png`" @click="chooseImage()"></image>
@click="phone === 'oppo' ? chooseImage() : null"></image> <image v-if="phone == 'vivo'" class="add-img shrink-0"
src="/static/image/phone-message/vivo/add.png" @click="chooseImage()"></image>
<image v-if="phone == 'huawei'" class="add-img shrink-0" <image v-if="phone == 'huawei'" class="add-img shrink-0"
:src="`/static/image/phone-message/huawei/emoji.png`"></image> :src="`/static/image/phone-message/huawei/emoji.png`"></image>
<view class="search-box flex-1 flex-align-center"> <view class="search-box flex-1 flex-align-center">
@ -91,7 +92,7 @@
@click="sendMessage"> @click="sendMessage">
</image> </image>
<image v-if="phone == 'huawei'" class="right-icon" <image v-if="phone == 'huawei'" class="right-icon"
src="/static/image/phone-message/huawei/chat-add.png"> src="/static/image/phone-message/huawei/chat-add.png" @click="chooseImage()">
</image> </image>
<image v-if="phone == 'huawei'" class="right-icon m-l-34" <image v-if="phone == 'huawei'" class="right-icon m-l-34"
:src="`/static/image/phone-message/huawei/${isSend ? 'send' : 'unsend'}.png`" :src="`/static/image/phone-message/huawei/${isSend ? 'send' : 'unsend'}.png`"
@ -1148,8 +1149,16 @@
.bottom-box { .bottom-box {
background-color: #fff; background-color: #fff;
.add-img {
width: 36rpx;
height: 36rpx;
margin-right: 40rpx;
}
.bottom-container { .bottom-container {
padding: 20rpx 48rpx 0; padding: 20rpx 48rpx 0 56rpx;
.search-box { .search-box {
min-height: 64rpx; min-height: 64rpx;
@ -1194,8 +1203,8 @@
} }
.bottom-placeholder { // .bottom-placeholder {
margin-top: env(safe-area-inset-bottom); // margin-top: env(safe-area-inset-bottom);
margin-top: constant(safe-area-inset-bottom); // margin-top: constant(safe-area-inset-bottom);
} // }
</style> </style>

View File

@ -34,24 +34,45 @@
</image> </image>
</view> </view>
<view class="chat-box" :id="'msg-' + index" :class="{ <view class="chat-box" :id="'msg-' + index" :class="{
'tail-right': shouldApplyTailRight(index) && !isImageMsg(message), 'tail-right': (shouldApplyTailRight(index) || isLastMeMessage(index)) && !isImageMsg(message),
'tail-left': shouldApplyTailLeft(index) && !isImageMsg(message), 'tail-left': shouldApplyTailLeft(index) && !isImageMsg(message),
'image-tail-left': shouldApplyTailLeft(index) && isImageMsg(message),
'delivered': isLastMeMessage(index) 'delivered': isLastMeMessage(index)
}" @longpress="!sortMode && onMessageLongPress(index, message)"> }" @longpress="!sortMode && onMessageLongPress(index, message)">
<text v-if="message.isMe && phone == 'mi'" class="send-text">送达</text> <text v-if="message.isMe && phone == 'mi'" class="send-text">送达</text>
<view class="chat-bubble" :class="{ 'image-bubble': isImageMsg(message) }"> <view class="chat-bubble" :class="{ 'image-bubble': isImageMsg(message) }">
<image v-if="isImageMsg(message)" :src="getImageSrc(message)" mode="aspectFill" <view v-if="isImageMsg(message)" class="image-wrap" :class="{
class="chat-image" :style="getImageStyle(message)" @tap.stop="handleImageClick(message)"> 'image-wrap-left': phone == 'iphone' && shouldApplyTailLeft(index) && isImageMsg(message),
'image-wrap-right': phone == 'iphone' && shouldApplyTailRight(index) && isImageMsg(message)
}">
<image :src="getImageSrc(message)" :mode="getImageMode(message, phone)"
class="chat-image" :class="{
'chat-image-left': phone == 'iphone' && shouldApplyTailLeft(index) && isImageMsg(message),
'chat-image-right': phone == 'iphone' && shouldApplyTailRight(index) && isImageMsg(message)
}" :style="phone == 'iphone' ? '' : getImageStyle(message)"
@tap.stop="handleImageClick(message)">
</image> </image>
<image v-if="phone == 'iphone'" :class="{
'mask-left-bottom': phone == 'iphone' && shouldApplyTailLeft(index) && isImageMsg(message),
'mask-right-bottom': phone == 'iphone' && shouldApplyTailRight(index) && isImageMsg(message)
}" style="width: 46rpx;height:10rpx;"
src="/static/image/phone-message/iphone/iphone-mask.png"></image>
</view>
<rich-text v-else :nodes="formatMessageContent(message.content, message.isMe)"></rich-text> <rich-text v-else :nodes="formatMessageContent(message.content, message.isMe)"></rich-text>
</view> </view>
<image v-if="shouldApplyTailLeft(index) && isImageMsg(message) && phone == 'iphone'"
style="width: 68rpx;height: 68rpx;" src="/static/image/phone-message/iphone/save.png">
</image>
<!-- <text v-if="message.isMe && phone == 'iphone' && isLastMeMessage(index)"
class="send-text">已送达</text> -->
</view> </view>
<view v-if="phone == 'huawei'" class="second-info"> <view v-if="phone == 'huawei'" class="second-info">
<text>{{ formatHuaweiBottomTime(message.time) }}</text> <text>{{ formatHuaweiBottomTime(message.time) }}</text>
<image :src="`/static/image/phone-message/huawei/chat-ka${message.simIndex}.png`"></image> <image :src="`/static/image/phone-message/huawei/chat-ka${message.simIndex}.png`"></image>
</view> </view>
<view v-if="(phone == 'oppo' || phone == 'vivo') && message.isMe" class="second-info"> <view v-if="(phone == 'oppo' || phone == 'vivo') && message.isMe" class="second-info">
<text v-if="message.isMe" class="delivered">已送达</text> <text v-if="message.isMe" class="delivered">{{ phone == 'vivo' ? '已发送' : '已送达' }}</text>
</view> </view>
</view> </view>
</view> </view>
@ -68,6 +89,7 @@ const handleImageClick = (message) => {
if (!src) return; if (!src) return;
const allImages = []; const allImages = [];
const allTimes = [];
let clickIndex = 0; let clickIndex = 0;
displayList.value.forEach(msg => { displayList.value.forEach(msg => {
@ -78,12 +100,13 @@ const handleImageClick = (message) => {
clickIndex = allImages.length; clickIndex = allImages.length;
} }
allImages.push(imgSrc); allImages.push(imgSrc);
allTimes.push(msg.time);
} }
} }
}); });
if (props.phone === 'oppo') { if (props.phone === 'oppo' || props.phone === 'mi') {
emit('previewImage', { images: allImages, index: clickIndex }); emit('previewImage', { images: allImages, index: clickIndex, times: allTimes });
} else { } else {
uni.previewImage({ urls: allImages, current: clickIndex }); uni.previewImage({ urls: allImages, current: clickIndex });
} }
@ -136,9 +159,9 @@ let sortItemRects = []
watch(() => props.sortMode, (val) => { watch(() => props.sortMode, (val) => {
if (val) { if (val) {
let list = props.messageList; let list = props.messageList;
if (props.phone !== 'oppo') { // if (props.phone !== 'oppo') {
list = list.filter(msg => !isImageMsg(msg)); // list = list.filter(msg => !isImageMsg(msg));
} // }
localSortList.value = list.map(item => ({ ...item })) localSortList.value = list.map(item => ({ ...item }))
} else { } else {
dragIndex.value = -1 dragIndex.value = -1
@ -154,9 +177,9 @@ const displayList = computed(() => {
if (props.sortMode) return localSortList.value if (props.sortMode) return localSortList.value
let list = props.messageList; let list = props.messageList;
if (props.phone !== 'oppo') { // if (props.phone !== 'oppo') {
list = list.filter(msg => !isImageMsg(msg)); // list = list.filter(msg => !isImageMsg(msg));
} // }
return list; return list;
}) })
@ -200,12 +223,8 @@ const shouldApplyTailRight = (index) => {
// c: isMe == false () // c: isMe == false ()
if (!nextMsg.isMe) return true; if (!nextMsg.isMe) return true;
// a: (180000 ) - 线 //
const currentMsgTime = new Date(currentMsg.time.replace(/-/g, '/')).getTime(); if (shouldShowTime(index + 1)) return true;
const nextMsgTime = new Date(nextMsg.time.replace(/-/g, '/')).getTime();
if (!isNaN(currentMsgTime) && !isNaN(nextMsgTime) && (nextMsgTime - currentMsgTime > 180000)) {
return true;
}
// //
return false; return false;
@ -224,21 +243,11 @@ const shouldApplyTailLeft = (index) => {
// c: isMe == true // c: isMe == true
if (nextMsg.isMe) return true; if (nextMsg.isMe) return true;
// a: (180000 ) //
const currentMsgTime = new Date(currentMsg.time.replace(/-/g, '/')).getTime(); if (shouldShowTime(index + 1)) return true;
const nextMsgTime = new Date(nextMsg.time.replace(/-/g, '/')).getTime();
if (!isNaN(currentMsgTime) && !isNaN(nextMsgTime) && (nextMsgTime - currentMsgTime > 180000)) {
return true;
}
// b: isMe == false //
for (let i = index + 1; i < displayList.value.length; i++) { return false;
if (!displayList.value[i].isMe) {
return false; // isMe==false
}
}
return true;
} }
// //
@ -392,9 +401,55 @@ const getImageSrc = (message) => {
return ''; return '';
} }
//
const getImageMode = (message, phone) => {
if (phone === 'iphone') return 'widthFix';
if (phone === 'huawei') {
const w = parseInt(message.imgWidth);
const h = parseInt(message.imgHeight);
if (!isNaN(w) && !isNaN(h)) {
if (h > w) return 'heightFix';
return 'widthFix';
}
return 'widthFix';
}
return 'aspectFill';
}
// //
const getImageStyle = (message) => { const getImageStyle = (message) => {
if (message.imgWidth && message.imgHeight) { if (message.imgWidth && message.imgHeight) {
if (props.phone === 'mi') {
const w = parseInt(message.imgWidth);
const h = parseInt(message.imgHeight);
if (!isNaN(w) && !isNaN(h)) {
if (h > w) {
return { width: '214rpx', height: '256rpx' };
} else {
return { width: '256rpx', height: '214rpx' };
}
}
} else if (props.phone === 'vivo') {
const w = parseInt(message.imgWidth);
const h = parseInt(message.imgHeight);
if (!isNaN(w) && !isNaN(h)) {
if (h > w) {
return { width: '284rpx', height: '400rpx' };
} else {
return { width: '400rpx', height: '284rpx' };
}
}
} else if (props.phone === 'huawei') {
const w = parseInt(message.imgWidth);
const h = parseInt(message.imgHeight);
if (!isNaN(w) && !isNaN(h)) {
if (h > w) {
return { height: '500rpx', 'max-height': '500rpx' };
} else {
return { width: '500rpx', 'max-width': '500rpx' };
}
}
}
return { return {
width: message.imgWidth, width: message.imgWidth,
height: message.imgHeight height: message.imgHeight
@ -550,7 +605,7 @@ const onSortTouchEnd = () => {
background-color: transparent !important; background-color: transparent !important;
padding: 0 !important; padding: 0 !important;
border: none !important; border: none !important;
border-radius: 16rpx !important; // border-radius: 16rpx !important;
overflow: hidden; overflow: hidden;
} }
@ -597,6 +652,85 @@ const onSortTouchEnd = () => {
margin: 4rpx 30rpx 0; margin: 4rpx 30rpx 0;
word-break: break-all; word-break: break-all;
} }
.image-bubble {
border-radius: 0 !important;
margin-left: 10rpx;
}
.image-wrap-left {
position: relative;
&::before {
content: "";
position: absolute;
z-index: 10;
top: 0;
left: 10px;
width: 17px;
height: 17px;
/* 使用径向渐变画出反向圆角中心点在右下角34rpx以内透明以外纯白 */
background: radial-gradient(circle at bottom right, transparent 16.5px, #FFFFFF 17px);
}
&::after {
content: "";
position: absolute;
z-index: 10;
height: 100%;
width: 10px;
background-color: #FFFFFF !important;
left: 0px;
top: 0;
bottom: 0;
border-radius: 0 0 10px 0;
}
}
.image-wrap-right {
position: relative;
&::before {
content: "";
position: absolute;
z-index: 10;
top: 0;
right: 10px;
width: 17px;
height: 17px;
/* 使用径向渐变画出反向圆角中心点在右下角34rpx以内透明以外纯白 */
background: radial-gradient(circle at bottom left, transparent 16.5px, #FFFFFF 17px);
}
&::after {
content: "";
position: absolute;
z-index: 10;
height: 100%;
width: 10px;
background-color: #FFFFFF !important;
right: 0px;
top: 0;
bottom: 0;
border-radius: 0 0 0 10px;
}
}
.mask-left-bottom {
position: absolute;
left: 7px;
bottom: 0px;
}
.mask-right-bottom {
position: absolute;
right: 7px;
bottom: 0px;
transform: scaleX(-1);
}
} }
@ -615,6 +749,14 @@ const onSortTouchEnd = () => {
position: relative; position: relative;
} }
.image-tail-left {
align-items: center;
::v-deep.image-bubble {
margin-right: 24rpx !important;
}
}
.tail-left::after { .tail-left::after {
position: absolute; position: absolute;
left: 18rpx; left: 18rpx;
@ -630,6 +772,23 @@ const onSortTouchEnd = () => {
position: relative; position: relative;
} }
.delivered {
display: flex;
flex-direction: column;
align-items: flex-end;
position: relative;
margin-bottom: 70rpx;
.send-text {
margin-right: 32rpx;
font-weight: 400;
font-size: 20rpx;
color: #8B8B8B;
line-height: 20rpx;
margin-top: 12rpx;
}
}
.delivered::before { .delivered::before {
position: absolute; position: absolute;
right: 18rpx; right: 18rpx;
@ -651,10 +810,28 @@ const onSortTouchEnd = () => {
width: 28rpx; width: 28rpx;
height: 36rpx; height: 36rpx;
} }
.chat-image {
width: 516rpx;
max-width: 516rpx !important;
}
.chat-image-left {
border-radius: 0 34rpx 34rpx 0;
}
.chat-image-right {
border-radius: 34rpx 0 0 34rpx;
}
} }
// //
.mi-style { .mi-style {
.chat-image {
border-radius: 40rpx;
}
.top-text { .top-text {
text-align: center; text-align: center;
font-size: 20rpx; font-size: 20rpx;
@ -888,6 +1065,10 @@ const onSortTouchEnd = () => {
margin-top: 24rpx !important; margin-top: 24rpx !important;
} }
.chat-image {
border-radius: 32rpx;
}
.time { .time {
color: #ACACAC; color: #ACACAC;
font-size: 24rpx; font-size: 24rpx;

View File

@ -1,5 +1,6 @@
<template> <template>
<view class="preview-container" v-if="show"> <view class="preview-container" :class="phone === 'mi' ? 'mi-bg' : 'oppo-bg'" v-if="show">
<template v-if="phone === 'oppo' || !phone || phone === 'iphone' || phone === 'huawei' || phone === 'vivo'">
<view class="header" @tap.stop :style="{ 'padding-top': statusBarHeight }"> <view class="header" @tap.stop :style="{ 'padding-top': statusBarHeight }">
<image class="icon-back" src="/static/image/phone-message/oppo/back-white.png" @tap="close"></image> <image class="icon-back" src="/static/image/phone-message/oppo/back-white.png" @tap="close"></image>
<image class="icon-download" src="/static/image/phone-message/oppo/save-white.png"></image> <image class="icon-download" src="/static/image/phone-message/oppo/save-white.png"></image>
@ -10,11 +11,41 @@
<image class="preview-img" :src="imgSrc" mode="aspectFit"></image> <image class="preview-img" :src="imgSrc" mode="aspectFit"></image>
</swiper-item> </swiper-item>
</swiper> </swiper>
</template>
<template v-else-if="phone === 'mi'">
<view class="mi-header" @tap.stop :style="{ 'padding-top': statusBarHeight }">
<!-- 复用现有返回图标并通过滤镜反色 -->
<image class="mi-icon-back" src="/static/image/phone-message/oppo/back-white.png" @tap="close"></image>
<view class="mi-header-center">
<view class="mi-date">{{ currentFormatDate }}</view>
<view class="mi-time">{{ currentFormatTime }}</view>
</view>
<view class="mi-icon-right"></view> <!-- 占位符以居中 -->
</view>
<swiper class="preview-swiper mi-swiper" :current="current" @change="onChange">
<swiper-item v-for="(imgSrc, index) in images" :key="index">
<view class="mi-img-container">
<image class="mi-preview-img" :src="imgSrc" mode="aspectFill"></image>
</view>
</swiper-item>
</swiper>
<view class="mi-footer">
<view class="mi-footer-item">
<image class="mi-footer-icon" src="/static/image/phone-message/mi/baocun.png"></image>
<view class="mi-footer-text">保存</view>
</view>
<view class="mi-footer-item">
<view class="mi-more-icon"></view>
<view class="mi-footer-text">更多</view>
</view>
</view>
</template>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'; import { ref, watch, computed } from 'vue';
const statusBarHeight = (uni.getSystemInfoSync().statusBarHeight || 44) + 'px'; const statusBarHeight = (uni.getSystemInfoSync().statusBarHeight || 44) + 'px';
@ -24,9 +55,17 @@ const props = defineProps({
type: Array, type: Array,
default: () => [] default: () => []
}, },
times: {
type: Array,
default: () => []
},
currentIndex: { currentIndex: {
type: Number, type: Number,
default: 0 default: 0
},
phone: {
type: String,
default: ''
} }
}); });
const emit = defineEmits(['update:show']); const emit = defineEmits(['update:show']);
@ -41,6 +80,53 @@ const onChange = (e) => {
current.value = e.detail.current; current.value = e.detail.current;
}; };
const currentFormatDate = computed(() => {
if (props.times && props.times.length > current.value) {
const timeVal = props.times[current.value];
if (timeVal) {
if (typeof timeVal === 'string' && timeVal.includes('-')) {
const parts = timeVal.split(' ');
if (parts.length > 0) {
const dateParts = parts[0].split('-');
if (dateParts.length === 3) {
return `${dateParts[0]}${parseInt(dateParts[1])}${parseInt(dateParts[2])}`;
}
}
}
// Fallback
const safeTimeStr = String(timeVal).replace(/-/g, '/');
const d = new Date(safeTimeStr);
if (!isNaN(d.getTime())) {
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}`;
}
}
}
return '';
});
const currentFormatTime = computed(() => {
if (props.times && props.times.length > current.value) {
const timeVal = props.times[current.value];
if (timeVal) {
if (typeof timeVal === 'string' && timeVal.includes(':')) {
const parts = timeVal.split(' ');
if (parts.length > 1) {
return parts[1];
}
}
// Fallback
const safeTimeStr = String(timeVal).replace(/-/g, '/');
const d = new Date(safeTimeStr);
if (!isNaN(d.getTime())) {
const h = d.getHours().toString().padStart(2, '0');
const m = d.getMinutes().toString().padStart(2, '0');
return `${h}:${m}`;
}
}
}
return '';
});
const close = () => { const close = () => {
emit('update:show', false); emit('update:show', false);
}; };
@ -75,12 +161,19 @@ const close = () => {
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-color: #000000;
z-index: 999999; z-index: 999999;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.oppo-bg {
background-color: #000000;
}
.mi-bg {
background-color: #FFFFFF;
}
.header { .header {
box-sizing: content-box; box-sizing: content-box;
width: 100%; width: 100%;
@ -122,4 +215,113 @@ const close = () => {
height: 100%; height: 100%;
display: block; display: block;
} }
/* ================= 小米样式 ================= */
.mi-header {
box-sizing: content-box;
width: 100%;
height: 98rpx;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0;
left: 0;
z-index: 2;
background-color: #FFFFFF;
}
.mi-icon-back {
width: 48rpx;
height: 48rpx;
margin: 0 34rpx;
filter: invert(1);
/* 白变黑 */
}
.mi-icon-right {
width: 48rpx;
height: 48rpx;
margin: 0 34rpx;
}
.mi-header-center {
display: flex;
flex-direction: column;
align-items: center;
}
.mi-date {
font-size: 32rpx;
color: #333333;
font-weight: 500;
}
.mi-time {
font-size: 24rpx;
color: #999999;
margin-top: 4rpx;
}
.mi-swiper {
z-index: 1;
display: flex;
align-items: center;
}
.mi-img-container {
width: 100vw;
height: 100vw;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.mi-preview-img {
width: 100%;
height: 100%;
display: block;
}
.mi-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 160rpx;
background-color: #FFFFFF;
display: flex;
justify-content: space-around;
align-items: flex-start;
padding-top: 20rpx;
z-index: 2;
}
.mi-footer-item {
display: flex;
flex-direction: column;
align-items: center;
}
.mi-footer-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 8rpx;
}
.mi-more-icon {
width: 48rpx;
height: 48rpx;
line-height: 38rpx;
font-size: 40rpx;
text-align: center;
color: #000;
margin-bottom: 8rpx;
font-weight: bold;
}
.mi-footer-text {
font-size: 24rpx;
color: #666666;
}
</style> </style>

View File

@ -26,9 +26,20 @@
<view class="content"> <view class="content">
<view v-if="isImageMsg(getLastMessage(item.chatList))" <view v-if="isImageMsg(getLastMessage(item.chatList))"
class="flex flex-align-center"> class="flex flex-align-center">
<template v-if="phone === 'iphone'">
<text>附件: 1张照片</text>
</template>
<template v-else-if="phone === 'huawei'">
<text>您的好友给您发了一个图片</text>
</template>
<template v-else-if="phone == 'oppo'">
<image style="width: 24rpx;height: 24rpx;margin-right: 16rpx;" <image style="width: 24rpx;height: 24rpx;margin-right: 16rpx;"
src="/static/image/phone-message/oppo/link.png"></image> src="/static/image/phone-message/oppo/link.png"></image>
<text>[图片]</text> <text>[图片]</text>
</template>
<template v-else>
<text>[图片]</text>
</template>
</view> </view>
<rich-text v-else :nodes="getLastMessage(item.chatList)?.content || ''"></rich-text> <rich-text v-else :nodes="getLastMessage(item.chatList)?.content || ''"></rich-text>
</view> </view>
@ -37,7 +48,8 @@
<image v-if="phone == 'iphone'" src="/static/image/phone-message/iphone/right.png"> <image v-if="phone == 'iphone'" src="/static/image/phone-message/iphone/right.png">
</image> </image>
<image v-if="item.noNotice && phone == 'iphone'" class="m-t-8" <image v-if="item.noNotice && phone == 'iphone'" class="m-t-8"
src="/static/image/phone-message/iphone/notice.png"></image> src="/static/image/phone-message/iphone/notice.png">
</image>
</view> </view>
</view> </view>
</view> </view>
@ -133,21 +145,12 @@ const isImageMsg = (message) => {
} }
/** /**
* 获取最新一条有效消息 oppo 屏蔽图片消息 * 获取最新一条有效消息
*/ */
const getLastMessage = (chatList) => { const getLastMessage = (chatList) => {
if (!chatList || chatList.length === 0) return null; if (!chatList || chatList.length === 0) return null;
if (props.phone === 'oppo') {
return chatList[chatList.length - 1]; return chatList[chatList.length - 1];
} }
// oppo
for (let i = chatList.length - 1; i >= 0; i--) {
if (!isImageMsg(chatList[i])) {
return chatList[i];
}
}
return null;
}
/** /**
* 点击列表元素 * 点击列表元素

View File

@ -1,4 +1,4 @@
<template> <template>
<!-- 水印 --> <!-- 水印 -->
<view v-if="$isVip()"> <view v-if="$isVip()">
<watermark dark="light" source="uni_alipay_other_message" /> <watermark dark="light" source="uni_alipay_other_message" />
@ -88,7 +88,14 @@
<view class="edit-row"> <view class="edit-row">
<text class="edit-label">内容</text> <text class="edit-label">内容</text>
</view> </view>
<editor id="editor" class="edit-textarea" placeholder="请输入消息内容..." @ready="onEditorReady"> <template v-if="editingMessage && editingMessage.type === 'image'">
<view class="edit-image-replace-box" @tap="replaceEditImage">
<image :src="editingNewImg || editingMessage.imgUrl" mode="aspectFit" class="replace-img">
</image>
<view class="replace-tip">点击替换图片</view>
</view>
</template>
<editor v-else id="editor" class="edit-textarea" placeholder="请输入消息内容..." @ready="onEditorReady">
</editor> </editor>
</view> </view>
<view class="edit-footer"> <view class="edit-footer">
@ -161,7 +168,8 @@
</view> </view>
</view> </view>
</view> </view>
<ImagePreview v-model:show="showPreview" :images="previewImages" :currentIndex="previewIndex" /> <ImagePreview v-model:show="showPreview" :images="previewImages" :times="previewTimes"
:currentIndex="previewIndex" :phone="data.phone" />
</view> </view>
</template> </template>
@ -199,13 +207,26 @@ const selectedMessage = ref(null)
const showEditPopup = ref(false) const showEditPopup = ref(false)
const editingMessage = ref(null) const editingMessage = ref(null)
const editingNewImg = ref("")
const replaceEditImage = () => {
uni.chooseImage({
count: 1,
success: (res) => {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
editingNewImg.value = res.tempFilePaths[0];
}
}
})
}
const showPreview = ref(false) const showPreview = ref(false)
const previewImages = ref([]) const previewImages = ref([])
const previewTimes = ref([])
const previewIndex = ref(0) const previewIndex = ref(0)
const handlePreviewImage = (data) => { const handlePreviewImage = (data) => {
previewImages.value = data.images previewImages.value = data.images
previewIndex.value = data.index previewIndex.value = data.index
previewTimes.value = data.times || []
showPreview.value = true showPreview.value = true
} }
@ -251,7 +272,7 @@ const saveChatList = () => {
const onEditorReady = () => { const onEditorReady = () => {
uni.createSelectorQuery().select('#editor').context((res) => { uni.createSelectorQuery().select('#editor').context((res) => {
editorCtx = res.context editorCtx = res.context
if (editingMessage.value && editingMessage.value.content) { if (editingMessage.value && editingMessage.value.content && editingMessage.value.type !== 'image') {
editorCtx.setContents({ editorCtx.setContents({
html: editingMessage.value.content html: editingMessage.value.content
}) })
@ -296,6 +317,7 @@ const closeActionPopup = () => {
*/ */
const handleEdit = () => { const handleEdit = () => {
editingMessage.value = selectedMessage.value; editingMessage.value = selectedMessage.value;
editingNewImg.value = "";
editingTime.value = selectedMessage.value.time || ""; editingTime.value = selectedMessage.value.time || "";
// //
const parts = (selectedMessage.value.time || "").split(' ') const parts = (selectedMessage.value.time || "").split(' ')
@ -308,7 +330,7 @@ const handleEdit = () => {
closeActionPopup(); closeActionPopup();
// //
if (editorCtx) { if (editorCtx && editingMessage.value.type !== 'image') {
setTimeout(() => { setTimeout(() => {
editorCtx.setContents({ editorCtx.setContents({
html: editingMessage.value.content html: editingMessage.value.content
@ -320,6 +342,7 @@ const handleEdit = () => {
const closeEditPopup = () => { const closeEditPopup = () => {
showEditPopup.value = false; showEditPopup.value = false;
editingMessage.value = null; editingMessage.value = null;
editingNewImg.value = "";
} }
/** /**
@ -346,12 +369,98 @@ const onSimKaChange = (value) => {
editingSimKa.value = value editingSimKa.value = value
} }
const confirmEdit = () => { const confirmEdit = async () => {
if (editingMessage.value && editorCtx) { if (editingMessage.value) {
editorCtx.getContents({
success: (res) => {
const index = messageList.value.findIndex(item => item.id === editingMessage.value.id) const index = messageList.value.findIndex(item => item.id === editingMessage.value.id)
if (index > -1) { if (index > -1) {
if (editingMessage.value.type === 'image') {
let finalImgPath = editingMessage.value.imgUrl;
let oldImgUrl = '';
if (editingNewImg.value) {
try {
// #ifdef APP-PLUS
const saveRes = await new Promise((resolve, reject) => {
uni.saveFile({
tempFilePath: editingNewImg.value,
success: resolve,
fail: reject
});
});
finalImgPath = saveRes.savedFilePath;
// #endif
// #ifndef APP-PLUS
finalImgPath = editingNewImg.value;
// #endif
oldImgUrl = messageList.value[index].imgUrl;
const imgInfo = await new Promise((resolve, reject) => {
uni.getImageInfo({
src: finalImgPath,
success: resolve,
fail: reject
});
});
let w = imgInfo.width;
let h = imgInfo.height;
let width = '100%';
let height = 'auto';
if (w && h) {
if (w > h) {
let scale = w / 200;
width = '200px';
height = (h / scale) + 'px';
} else {
let scale = h / 200;
height = '200px';
width = (w / scale) + 'px';
}
}
const newHtml = `<img src="${finalImgPath}" style="width: ${width}; height: ${height}; border-radius: 16rpx; display: block;" />`;
messageList.value[index].content = newHtml;
messageList.value[index].imgUrl = finalImgPath;
messageList.value[index].imgWidth = width;
messageList.value[index].imgHeight = height;
} catch (e) {
console.error('保存替换图片失败', e);
uni.showToast({ title: '保存图片失败', icon: 'none' });
return;
}
}
messageList.value[index].time = editingTime.value;
if (editingTimeMode.value === 'auto') {
delete messageList.value[index].timeMode;
} else {
messageList.value[index].timeMode = editingTimeMode.value;
}
delete messageList.value[index].hideTime;
if (editingSimKa.value) {
messageList.value[index].simIndex = Number(editingSimKa.value);
} else {
delete messageList.value[index].simIndex;
}
closeEditPopup();
saveChatList();
if (oldImgUrl && oldImgUrl.includes('_doc/')) {
// #ifdef APP-PLUS
uni.removeSavedFile({
filePath: oldImgUrl,
success: () => console.log('替换旧图片,删除成功:', oldImgUrl),
fail: (err) => console.warn('替换旧图片,删除失败:', oldImgUrl, err)
})
// #endif
}
return;
}
if (editorCtx) {
editorCtx.getContents({
success: (res) => {
messageList.value[index].content = res.html; messageList.value[index].content = res.html;
messageList.value[index].time = editingTime.value; messageList.value[index].time = editingTime.value;
// timeMode: 'auto'|'show'|'hide' // timeMode: 'auto'|'show'|'hide'
@ -367,7 +476,6 @@ const confirmEdit = () => {
} else { } else {
delete messageList.value[index].simIndex; delete messageList.value[index].simIndex;
} }
}
closeEditPopup(); closeEditPopup();
saveChatList(); saveChatList();
} }
@ -376,6 +484,10 @@ const confirmEdit = () => {
closeEditPopup(); closeEditPopup();
} }
} }
} else {
closeEditPopup();
}
}
/** /**
* 消息互换 * 消息互换
@ -1052,4 +1164,28 @@ const confirmAdd = async () => {
padding-bottom: 2rpx; padding-bottom: 2rpx;
z-index: 2; z-index: 2;
} }
.edit-image-replace-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f7f7f7;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
min-height: 200rpx;
}
.replace-img {
max-width: 100%;
max-height: 300rpx;
}
.replace-tip {
margin-top: 16rpx;
font-size: 24rpx;
color: #007AFF;
}
</style> </style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB