1133 lines
33 KiB
Vue
1133 lines
33 KiB
Vue
<template>
|
||
<view>
|
||
<view v-for="(message, index) in displayList" :key="message.id || index" class="message-item" :class="{
|
||
isMe: message.isMe,
|
||
'm-t-16': shouldApplyMt16(index) && !shouldShowTime(index),
|
||
'is-sort-mode': sortMode,
|
||
'sort-dragging': sortMode && dragIndex === index,
|
||
'sort-drop-before': sortMode && dragOverIndex === index && dragIndex !== index && dropPosition === 'before',
|
||
'sort-drop-after': sortMode && dragOverIndex === index && dragIndex !== index && dropPosition === 'after'
|
||
}">
|
||
|
||
<!-- 排序模式下的拖拽手柄 -->
|
||
<view v-if="sortMode" class="sort-handle-wrap" @longpress="onSortLongPress(index)"
|
||
@touchmove.stop.prevent="onSortTouchMove($event)" @touchend.stop="onSortTouchEnd()">
|
||
<view class="sort-handle-bar"></view>
|
||
<view class="sort-handle-bar"></view>
|
||
<view class="sort-handle-bar"></view>
|
||
</view>
|
||
|
||
<view style="flex: 1; overflow: hidden;">
|
||
<view class="time m-t-44" :id="'time-' + index" v-if="shouldShowTime(index)"
|
||
@longpress="!sortMode && onMessageLongPress(index, message, 'time')">
|
||
<view class="top-text" v-if="phone == 'iphone' && index == 0">信息 · 短信</view>
|
||
<view class="top-text" v-if="phone == 'huawei' && index == 0">短信/彩信</view>
|
||
<text v-if="phone == 'huawei'">{{ formatHuaweiTopTime(message.time) }}</text>
|
||
<text v-else>{{ formatChatTime(message.time) }} <text
|
||
v-if="(phone == 'oppo' || (phone == 'vivo' && message.isMe)) && message.simIndex">
|
||
{{ simInfo[`sim${message.simIndex}`] }}
|
||
</text></text>
|
||
|
||
<image style="width: 20rpx;height: 24rpx;margin-left: 8rpx; "
|
||
v-if="phone == 'oppo' || (phone == 'vivo' && message.isMe)"
|
||
:src="`/static/image/phone-message/huawei/chat-ka${message.simIndex}.png`">
|
||
</image>
|
||
</view>
|
||
<view class="chat-box" :id="'msg-' + index" :class="{
|
||
'tail-right': (shouldApplyTailRight(index) || isLastMeMessage(index)) && !isImageMsg(message),
|
||
'tail-left': shouldApplyTailLeft(index) && !isImageMsg(message),
|
||
'image-tail-left': shouldApplyTailLeft(index) && isImageMsg(message),
|
||
'delivered': isLastMeMessage(index)
|
||
}" @longpress="!sortMode && onMessageLongPress(index, message)">
|
||
<text v-if="message.isMe && phone == 'mi'" class="send-text">送达</text>
|
||
|
||
<view class="chat-bubble" :class="{ 'image-bubble': isImageMsg(message) }">
|
||
<view v-if="isImageMsg(message)" class="image-wrap" :class="{
|
||
'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 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>
|
||
</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 v-if="phone == 'huawei'" class="second-info">
|
||
<text>{{ formatHuaweiBottomTime(message.time) }}</text>
|
||
<image :src="`/static/image/phone-message/huawei/chat-ka${message.simIndex}.png`"></image>
|
||
</view>
|
||
<view v-if="(phone == 'oppo' || phone == 'vivo') && message.isMe" class="second-info">
|
||
<text v-if="message.isMe" class="delivered">{{ phone == 'vivo' ? '已发送' : '已送达' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, computed, watch, nextTick } from 'vue'
|
||
import { util } from '@/utils/common.js';
|
||
|
||
const handleImageClick = (message) => {
|
||
const src = getImageSrc(message);
|
||
if (!src) return;
|
||
|
||
const allImages = [];
|
||
const allTimes = [];
|
||
let clickIndex = 0;
|
||
|
||
displayList.value.forEach(msg => {
|
||
if (isImageMsg(msg)) {
|
||
const imgSrc = getImageSrc(msg);
|
||
if (imgSrc) {
|
||
if (imgSrc === src) {
|
||
clickIndex = allImages.length;
|
||
}
|
||
allImages.push(imgSrc);
|
||
allTimes.push(msg.time);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (props.phone === 'oppo' || props.phone === 'mi') {
|
||
emit('previewImage', { images: allImages, index: clickIndex, times: allTimes });
|
||
} else {
|
||
uni.previewImage({ urls: allImages, current: clickIndex });
|
||
}
|
||
}
|
||
|
||
const props = defineProps({
|
||
// 手机品牌
|
||
phone: {
|
||
type: String,
|
||
default: 'iphone'
|
||
},
|
||
messageList: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
// 是否处于拖拽排序模式
|
||
sortMode: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
})
|
||
|
||
const SIM_STORAGE_KEY = 'sim_info'
|
||
const emit = defineEmits(['onMessageLongPress', 'sort', 'previewImage'])
|
||
|
||
const simInfo = ref({
|
||
sim1: '中国电信',
|
||
sim2: '中国移动'
|
||
})
|
||
|
||
onMounted(() => {
|
||
try {
|
||
const cached = uni.getStorageSync(SIM_STORAGE_KEY)
|
||
simInfo.value = cached ? JSON.parse(cached) : { sim1: '中国电信', sim2: '中国移动' }
|
||
} catch (e) {
|
||
simInfo.value = { sim1: '中国电信', sim2: '中国移动' }
|
||
}
|
||
})
|
||
|
||
// 排序模式下的本地唦本列表
|
||
// 不直接修改 props.messageList,排序完成后 emit 给父组件
|
||
const localSortList = ref([])
|
||
const dragIndex = ref(-1)
|
||
const dragOverIndex = ref(-1)
|
||
const dropPosition = ref('after') // 'before'=插入目标上方, 'after'=插入目标下方
|
||
let isDragging = false
|
||
let sortItemRects = []
|
||
|
||
// 当 sortMode 切换时同步本地副本
|
||
watch(() => props.sortMode, (val) => {
|
||
if (val) {
|
||
let list = props.messageList;
|
||
// if (props.phone !== 'oppo') {
|
||
// list = list.filter(msg => !isImageMsg(msg));
|
||
// }
|
||
localSortList.value = list.map(item => ({ ...item }))
|
||
} else {
|
||
dragIndex.value = -1
|
||
dragOverIndex.value = -1
|
||
isDragging = false
|
||
sortItemRects = []
|
||
localSortList.value = []
|
||
}
|
||
})
|
||
|
||
// 实际渲染用的列表:排序模式下用内部副本,否则用原始列表(非 oppo 机型过滤图片)
|
||
const displayList = computed(() => {
|
||
if (props.sortMode) return localSortList.value
|
||
|
||
let list = props.messageList;
|
||
// if (props.phone !== 'oppo') {
|
||
// list = list.filter(msg => !isImageMsg(msg));
|
||
// }
|
||
return list;
|
||
})
|
||
|
||
// 判断消息isMe==true时上一条消息是否也是isMe==true,如果不是则应用m-t-16样式
|
||
const shouldApplyMt16 = (index) => {
|
||
if (index === 0) return false;
|
||
const currentMsg = displayList.value[index];
|
||
const prevMsg = displayList.value[index - 1];
|
||
if (currentMsg.isMe && !prevMsg.isMe) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
|
||
// 判断是否为最后一条自己发出的消息
|
||
const isLastMeMessage = (currentIndex) => {
|
||
const currentMsg = displayList.value[currentIndex];
|
||
if (!currentMsg.isMe) return false;
|
||
|
||
// 往后遍历,如果还能找到isMe=true的消息,说明当前这条不是最后一条
|
||
for (let i = currentIndex + 1; i < displayList.value.length; i++) {
|
||
if (displayList.value[i].isMe) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 判断 tail-right(右侧我方气泡尾巴)样式是否生效
|
||
const shouldApplyTailRight = (index) => {
|
||
const currentMsg = displayList.value[index];
|
||
if (!currentMsg.isMe) return false; // 不是我发的直接没右侧尾巴
|
||
|
||
// 如果这是整体列表最后一条消息,必然有尾巴
|
||
if (index === displayList.value.length - 1) return true;
|
||
|
||
const nextMsg = displayList.value[index + 1];
|
||
|
||
// 条件 c: 下一条消息 isMe == false (也就是被打断了)
|
||
if (!nextMsg.isMe) return true;
|
||
|
||
// 如果下一条消息上方显示了时间,则当前消息就是这一段的最后一条,应该有尾巴!
|
||
if (shouldShowTime(index + 1)) return true;
|
||
|
||
// 其他情况(连绵不断的连发,中间没超时间也没被对方打断),则隐藏中间环节气泡尾巴
|
||
return false;
|
||
}
|
||
|
||
// 判断 tail-left 样式是否生效
|
||
const shouldApplyTailLeft = (index) => {
|
||
const currentMsg = displayList.value[index];
|
||
if (currentMsg.isMe) return false;
|
||
|
||
// 如果这是整体列表最后一条消息,也就符合最后一条isMe==false的特征
|
||
if (index === displayList.value.length - 1) return true;
|
||
|
||
const nextMsg = displayList.value[index + 1];
|
||
|
||
// 条件 c: 下一条消息 isMe == true
|
||
if (nextMsg.isMe) return true;
|
||
|
||
// 如果下一条消息上方显示了时间,则当前消息就是这一段的最后一条,应该有尾巴!
|
||
if (shouldShowTime(index + 1)) return true;
|
||
|
||
// 否则说明后面还有相连的对方消息,不用加尾巴
|
||
return false;
|
||
}
|
||
|
||
// 获取格式化后的聊天时间显示
|
||
const formatChatTime = (timeStr) => {
|
||
if (!timeStr) return '';
|
||
const date = new Date(timeStr.replace(/-/g, '/'));
|
||
if (isNaN(date.getTime())) return timeStr;
|
||
|
||
const now = new Date();
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||
|
||
const diffDays = Math.floor((today.getTime() - target.getTime()) / (1000 * 60 * 60 * 24));
|
||
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const timeNum = `${hours}:${minutes}`;
|
||
|
||
if (diffDays === 0) {
|
||
if (props.phone == 'iphone' || props.phone == 'oppo') return `今天 ${timeNum}`;
|
||
return `${timeNum}`;
|
||
} else if (diffDays === 1) {
|
||
return `昨天 ${timeNum}`;
|
||
} else if (diffDays > 1 && diffDays < 7) {
|
||
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||
return `${weekDays[date.getDay()]} ${timeNum}`;
|
||
} else {
|
||
// 超过一周,同一年显示 月日 时分,跨年显示 年月日 时分
|
||
if (date.getFullYear() === now.getFullYear()) {
|
||
return `${date.getMonth() + 1}月${date.getDate()}日 ${timeNum}`;
|
||
} else {
|
||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${timeNum}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 华为顶部时间栏:只显示日期维度的信息,无具体时分秒
|
||
const formatHuaweiTopTime = (timeStr) => {
|
||
if (!timeStr) return '';
|
||
const date = new Date(timeStr.replace(/-/g, '/'));
|
||
if (isNaN(date.getTime())) return timeStr;
|
||
|
||
const now = new Date();
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||
const diffDays = Math.floor((today.getTime() - target.getTime()) / (1000 * 60 * 60 * 24));
|
||
|
||
if (diffDays === 0) {
|
||
return `今天`;
|
||
} else if (diffDays === 1) {
|
||
return `昨天`;
|
||
} else {
|
||
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||
const weekdayStr = weekDays[date.getDay()];
|
||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日${weekdayStr}`;
|
||
}
|
||
}
|
||
|
||
// 华为底部小尾巴附加时间:上下午时分、或者“刚刚”
|
||
const formatHuaweiBottomTime = (timeStr) => {
|
||
if (!timeStr) return '';
|
||
const date = new Date(timeStr.replace(/-/g, '/'));
|
||
if (isNaN(date.getTime())) return timeStr;
|
||
|
||
const now = new Date();
|
||
// 判断是否在一分钟内(差值在 60000 毫秒以内)
|
||
const diffMs = now.getTime() - date.getTime();
|
||
if (diffMs >= 0 && diffMs <= 60000) {
|
||
return '刚刚';
|
||
}
|
||
|
||
const ampm = date.getHours() >= 12 ? '下午' : '上午';
|
||
// 转换为 12 小时制
|
||
let displayHour = date.getHours() % 12;
|
||
displayHour = displayHour ? displayHour : 12; // 如果是 0(午夜) 则是12
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
|
||
return `${ampm}${displayHour}:${minutes}`;
|
||
}
|
||
|
||
// 判断是否显示时间
|
||
// timeMode: 'show'=强制显示, 'hide'=强制隐藏, 'auto'或无=按间隔逻辑判断
|
||
const shouldShowTime = (index) => {
|
||
const currentMsg = displayList.value[index];
|
||
const mode = currentMsg.timeMode;
|
||
|
||
// 强制隐藏(兼容旧 hideTime 字段)
|
||
if (mode === 'hide' || currentMsg.hideTime) return false;
|
||
|
||
// 强制显示
|
||
if (mode === 'show') return true;
|
||
|
||
// 以下为 auto / 默认 逻辑
|
||
if (index === 0) return true; // 第一条必定显示
|
||
|
||
// 排序模式下每条均显示时间
|
||
if (props.sortMode) return true;
|
||
|
||
const currentMsgTime = new Date(currentMsg.time.replace(/-/g, '/')).getTime();
|
||
const prevMsgTime = new Date(displayList.value[index - 1].time.replace(/-/g, '/')).getTime();
|
||
|
||
if (isNaN(currentMsgTime) || isNaN(prevMsgTime)) return true;
|
||
|
||
// 绝对值比较,兼容排序后时间倒序
|
||
return Math.abs(currentMsgTime - prevMsgTime) > 180000;
|
||
}
|
||
|
||
// 格式化文本:给网址和 5 位以上连续数字加上下划线样式,非我方消息同时赋予文字蓝色
|
||
const formatMessageContent = (content, isMe) => {
|
||
if (!content) return '';
|
||
|
||
// 匹配 url 或 5位以上连续数字
|
||
// URL: http(s)://... 或 xxx.xxx/...
|
||
const combinedRegex = /((?:https?:\/\/)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[a-zA-Z0-9_/%-]*)*)|(\b\d{5,}(?:\.\d+)?\b)/g;
|
||
|
||
return content.replace(combinedRegex, (match, p1, p2) => {
|
||
// 如果是数字且包含小数点(通常是金额),则不添加下划线样式
|
||
if (p2 && p2.includes('.')) {
|
||
return match;
|
||
}
|
||
|
||
let color = '#3A85F8'
|
||
if (props.phone == 'iphone') color = '#0B7AE3'
|
||
if (props.phone == 'mi') color = '#3A85F8'
|
||
if (props.phone == 'oppo') color = '#03A311'
|
||
const colorStyle = !isMe ? `color: ${color};` : '';
|
||
return `<span style="text-decoration: underline; ${colorStyle}">${match}</span>`;
|
||
});
|
||
}
|
||
|
||
// 判断该消息是否为纯图片消息
|
||
const isImageMsg = (message) => {
|
||
if (message.type === 'image') return true;
|
||
if (message.content && typeof message.content === 'string') {
|
||
const str = message.content.trim();
|
||
// 如果是以 <img 开头且没有其他多余文本
|
||
if (str.startsWith('<img') && str.endsWith('/>') && str.indexOf('<img') === str.lastIndexOf('<img')) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 提取图片 src
|
||
const getImageSrc = (message) => {
|
||
if (message.imgUrl) return message.imgUrl;
|
||
if (message.content && typeof message.content === 'string') {
|
||
const match = message.content.match(/src="([^"]+)"/);
|
||
if (match && match[1]) return match[1];
|
||
}
|
||
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) => {
|
||
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 {
|
||
width: message.imgWidth,
|
||
height: message.imgHeight
|
||
};
|
||
}
|
||
return {};
|
||
}
|
||
|
||
/**
|
||
* 长按信息
|
||
* @param index
|
||
* @param message
|
||
*/
|
||
const onMessageLongPress = (index, message, type = 'message') => {
|
||
emit('onLongPress', index, message, type)
|
||
}
|
||
|
||
// ===================== 拖拽排序逻辑 =====================
|
||
|
||
/**
|
||
* 刷新所有排序条目的 rect 坐标缓存
|
||
*/
|
||
const refreshSortRects = (callback) => {
|
||
const query = uni.createSelectorQuery()
|
||
const len = localSortList.value.length
|
||
let collected = []
|
||
let done = 0
|
||
for (let i = 0; i < len; i++) {
|
||
query.select('#msg-' + i).boundingClientRect(rect => {
|
||
collected[i] = rect
|
||
done++
|
||
if (done === len && callback) callback(collected)
|
||
})
|
||
}
|
||
query.exec()
|
||
}
|
||
|
||
/**
|
||
* 长按手柄开始拖拽
|
||
*/
|
||
const onSortLongPress = (idx) => {
|
||
dragIndex.value = idx
|
||
dragOverIndex.value = idx
|
||
isDragging = true
|
||
refreshSortRects(rects => {
|
||
sortItemRects = rects
|
||
})
|
||
uni.vibrateShort({ type: 'medium' })
|
||
}
|
||
|
||
/**
|
||
* 拖拽移动 - 根据触摸点 Y 坐标確定悬停位置和插入方向
|
||
*/
|
||
const onSortTouchMove = (e) => {
|
||
if (!isDragging || dragIndex.value === -1) return
|
||
if (!e.touches || !e.touches[0]) return
|
||
const touchY = e.touches[0].clientY
|
||
if (!sortItemRects || sortItemRects.length === 0) return
|
||
let overIdx = dragOverIndex.value
|
||
for (let i = 0; i < sortItemRects.length; i++) {
|
||
const rect = sortItemRects[i]
|
||
if (rect && touchY >= rect.top && touchY <= rect.bottom) {
|
||
overIdx = i
|
||
// 判断手指在该元素的上半区还是下半区
|
||
const mid = rect.top + rect.height / 2
|
||
dropPosition.value = touchY < mid ? 'before' : 'after'
|
||
break
|
||
}
|
||
}
|
||
dragOverIndex.value = overIdx
|
||
}
|
||
|
||
/**
|
||
* 拖拽结束 - 根据 dropPosition 決定插入到目标的上方还是下方
|
||
*/
|
||
const onSortTouchEnd = () => {
|
||
if (!isDragging) return
|
||
const from = dragIndex.value
|
||
const to = dragOverIndex.value
|
||
if (from !== -1 && to !== -1 && from !== to) {
|
||
const list = [...localSortList.value]
|
||
const [removed] = list.splice(from, 1)
|
||
// from 被移除后,如果 to 在 from 后面,to 需要 -1
|
||
let insertAt = to > from ? to - 1 : to
|
||
if (dropPosition.value === 'after') insertAt += 1
|
||
list.splice(insertAt, 0, removed)
|
||
localSortList.value = list
|
||
emit('sort', list.map(item => ({ ...item })))
|
||
nextTick(() => {
|
||
refreshSortRects(rects => {
|
||
sortItemRects = rects
|
||
})
|
||
})
|
||
}
|
||
dragIndex.value = -1
|
||
dragOverIndex.value = -1
|
||
dropPosition.value = 'after'
|
||
isDragging = false
|
||
}
|
||
</script>
|
||
<style lang="less" scoped>
|
||
/* ===== 排序模式公共样式(跨所有手机品牌通用)===== */
|
||
/* 排序模式下每条消息左右横排(手柄 + 气泡取剩余宽度)*/
|
||
.is-sort-mode {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: stretch;
|
||
}
|
||
|
||
/* 拖拽手柄区域 */
|
||
.sort-handle-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 60rpx;
|
||
flex-shrink: 0;
|
||
padding: 0 8rpx;
|
||
|
||
.sort-handle-bar {
|
||
margin: 3rpx 0;
|
||
}
|
||
}
|
||
|
||
.sort-handle-bar {
|
||
width: 30rpx;
|
||
height: 4rpx;
|
||
border-radius: 2rpx;
|
||
background-color: #CCCCCC;
|
||
}
|
||
|
||
/* 被拖拽中的条目:高亮蓝边 + 轻微缩进 */
|
||
.sort-dragging {
|
||
opacity: 0.7;
|
||
background-color: rgba(0, 122, 255, 0.06) !important;
|
||
border-left: 4rpx solid #007AFF;
|
||
}
|
||
|
||
/* 插入线:将拖拽项放在目标上方 */
|
||
.sort-drop-before {
|
||
border-top: 3rpx dashed #007AFF !important;
|
||
padding-top: 4rpx;
|
||
}
|
||
|
||
/* 插入线:将拖拽项放在目标下方 */
|
||
.sort-drop-after {
|
||
border-bottom: 3rpx dashed #007AFF !important;
|
||
padding-bottom: 4rpx;
|
||
}
|
||
|
||
/* 纯图片消息覆盖样式 */
|
||
.chat-bubble.image-bubble {
|
||
background-color: transparent !important;
|
||
padding: 0 !important;
|
||
border: none !important;
|
||
// border-radius: 16rpx !important;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.chat-image {
|
||
max-width: 400rpx;
|
||
border-radius: 16rpx;
|
||
display: block;
|
||
}
|
||
|
||
// 苹果样式
|
||
.iphone-style {
|
||
.m-t-16 {
|
||
margin-top: 16rpx !important;
|
||
}
|
||
|
||
.top-text {
|
||
text-align: center;
|
||
font-size: 20rpx;
|
||
color: #838383;
|
||
margin-top: 26rpx;
|
||
}
|
||
|
||
.time {
|
||
color: #838383;
|
||
font-size: 20rpx;
|
||
text-align: center;
|
||
margin-bottom: 20rpx;
|
||
margin-top: 36rpx;
|
||
}
|
||
|
||
.chat-box {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
|
||
.chat-bubble {
|
||
padding: 18rpx 22rpx;
|
||
background-color: #E9E9EB;
|
||
border-radius: 34rpx;
|
||
max-width: 600rpx;
|
||
font-size: 32rpx;
|
||
line-height: 38rpx;
|
||
color: #1A1A1A;
|
||
margin: 4rpx 30rpx 0;
|
||
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);
|
||
}
|
||
}
|
||
|
||
|
||
.isMe {
|
||
.chat-box {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.chat-bubble {
|
||
background-color: #34C85A;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
.tail-left {
|
||
position: relative;
|
||
}
|
||
|
||
.image-tail-left {
|
||
align-items: center;
|
||
|
||
::v-deep.image-bubble {
|
||
margin-right: 24rpx !important;
|
||
}
|
||
}
|
||
|
||
.tail-left::after {
|
||
position: absolute;
|
||
left: 18rpx;
|
||
bottom: 0;
|
||
content: '';
|
||
background: url('/static/image/phone-message/iphone/left-msg-box.png') no-repeat center bottom;
|
||
background-size: 100% 100%;
|
||
width: 28rpx;
|
||
height: 36rpx;
|
||
}
|
||
|
||
.tail-right {
|
||
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 {
|
||
position: absolute;
|
||
right: 18rpx;
|
||
bottom: -12rpx;
|
||
content: '已送达';
|
||
font-size: 20rpx;
|
||
line-height: 20rpx;
|
||
color: #8B8B8B;
|
||
transform: translateY(100%);
|
||
}
|
||
|
||
.tail-right::after {
|
||
position: absolute;
|
||
right: 18rpx;
|
||
bottom: 0;
|
||
content: '';
|
||
background: url('/static/image/phone-message/iphone/right-msg-box.png') no-repeat center bottom;
|
||
background-size: 100% 100%;
|
||
width: 28rpx;
|
||
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 {
|
||
.chat-image {
|
||
border-radius: 40rpx;
|
||
}
|
||
|
||
.top-text {
|
||
text-align: center;
|
||
font-size: 20rpx;
|
||
color: #838383;
|
||
margin-top: 26rpx;
|
||
}
|
||
|
||
.time {
|
||
padding: 0 54rpx;
|
||
color: #646464;
|
||
font-size: 24rpx;
|
||
line-height: 28rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.chat-box {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
|
||
.chat-bubble {
|
||
padding: 22rpx 42rpx;
|
||
background-color: #FFFFFF;
|
||
border-radius: 32rpx;
|
||
font-size: 32rpx;
|
||
line-height: 50rpx;
|
||
color: #1A1A1A;
|
||
max-width: 550rpx;
|
||
margin: 0 24rpx;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.message-item {
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.m-t-44 {
|
||
margin-top: 44rpx;
|
||
}
|
||
|
||
|
||
.isMe {
|
||
|
||
.time {
|
||
text-align: right;
|
||
}
|
||
|
||
.chat-box {
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
|
||
.send-text {
|
||
color: #646464;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.chat-bubble {
|
||
margin-left: 12rpx;
|
||
background-color: #3681FF;
|
||
color: #FFFFFF;
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
}
|
||
|
||
// oppo样式
|
||
.oppo-style {
|
||
.m-t-16 {
|
||
margin-top: 24rpx !important;
|
||
}
|
||
|
||
.time {
|
||
color: #727377;
|
||
font-size: 24rpx;
|
||
line-height: 24rpx;
|
||
text-align: center;
|
||
margin-bottom: 20rpx;
|
||
margin-top: 48rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.message-item {
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.chat-box {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
|
||
.chat-bubble {
|
||
padding: 28rpx 48rpx;
|
||
background-color: #FFFFFF;
|
||
border-radius: 32rpx;
|
||
max-width: 600rpx;
|
||
font-size: 32rpx;
|
||
line-height: 44rpx;
|
||
color: #1A1A1A;
|
||
margin: 0 32rpx;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
|
||
.isMe {
|
||
.chat-box {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.chat-bubble {
|
||
background-color: #00A1FF;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
.second-info {
|
||
font-size: 24rpx;
|
||
text-align: right;
|
||
padding: 0 34rpx;
|
||
margin-top: 14rpx;
|
||
padding-bottom: 6rpx;
|
||
|
||
.delivered {
|
||
font-size: 24rpx;
|
||
line-height: 24rpx;
|
||
color: #727377;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// 华为样式
|
||
.huawei-style {
|
||
.top-text {
|
||
text-align: center;
|
||
font-size: 22rpx;
|
||
line-height: 22rpx;
|
||
color: #545454;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.time {
|
||
text-align: center;
|
||
font-size: 22rpx;
|
||
line-height: 22rpx;
|
||
color: #545454;
|
||
margin-top: 40rpx;
|
||
}
|
||
|
||
.chat-box {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
|
||
.chat-bubble {
|
||
padding: 12rpx 24rpx;
|
||
background-color: #FFFFFF;
|
||
border-radius: 4rpx 28rpx 28rpx 28rpx;
|
||
max-width: 550rpx;
|
||
margin: 0 30rpx;
|
||
margin-top: 34rpx;
|
||
font-size: 30rpx;
|
||
line-height: 38rpx;
|
||
color: #1a1a1a;
|
||
font-weight: 500;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.second-info {
|
||
display: flex;
|
||
align-items: center;
|
||
color: #545454;
|
||
font-size: 20rpx;
|
||
line-height: 24rpx;
|
||
margin-top: 16rpx;
|
||
padding-left: 52rpx;
|
||
padding-right: 56rpx;
|
||
|
||
image {
|
||
width: 14rpx;
|
||
height: 18rpx;
|
||
margin-left: 8rpx;
|
||
}
|
||
}
|
||
|
||
|
||
.m-t-44 {
|
||
margin-top: 44rpx;
|
||
}
|
||
|
||
|
||
.isMe {
|
||
.second-info {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.message-item {
|
||
margin-top: 34rpx;
|
||
}
|
||
|
||
.chat-box {
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
|
||
.send-text {
|
||
color: #646464;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.chat-bubble {
|
||
margin-left: 12rpx;
|
||
background-color: #3681FF;
|
||
color: #FFFFFF;
|
||
border-radius: 28rpx 4rpx 28rpx 28rpx;
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
}
|
||
|
||
// vivo样式
|
||
.vivo-style {
|
||
.m-t-16 {
|
||
margin-top: 24rpx !important;
|
||
}
|
||
|
||
.chat-image {
|
||
border-radius: 32rpx;
|
||
}
|
||
|
||
.time {
|
||
color: #ACACAC;
|
||
font-size: 24rpx;
|
||
line-height: 24rpx;
|
||
text-align: center;
|
||
margin: 40rpx 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.message-item {
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.chat-box {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
|
||
.chat-bubble {
|
||
padding: 38rpx 38rpx 32rpx 40rpx;
|
||
background-color: #FFFFFF;
|
||
border-radius: 32rpx;
|
||
max-width: 100%;
|
||
font-size: 36rpx;
|
||
line-height: 48rpx;
|
||
color: #1A1A1A;
|
||
margin: 0 32rpx;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
|
||
.isMe {
|
||
.chat-box {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.chat-bubble {
|
||
background-color: #0078FE;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
.second-info {
|
||
font-size: 24rpx;
|
||
text-align: right;
|
||
padding: 0 34rpx;
|
||
margin-top: 12rpx;
|
||
padding-bottom: 6rpx;
|
||
|
||
.delivered {
|
||
font-size: 24rpx;
|
||
line-height: 24rpx;
|
||
color: #ACACAC;
|
||
}
|
||
}
|
||
|
||
}
|
||
</style>
|