alipay-emulator/pages/message/chat-page/chat-page.vue

834 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view :class="`${data.phone}-style`">
<ChatLayout :phone="data.phone" :chatInfo="data.data">
<!-- 弹出操作层及遮罩 -->
<view v-if="showActionPopup" class="action-mask" @tap="closeActionPopup">
<view class="action-popup" :style="{ top: popupTop + 'px', left: popupLeft + 'px' }">
<view class="action-item" @tap.stop="handleEdit">
<image class="action-icon" src="/static/image/phone-message/bianji.png"></image>
<text>编辑</text>
</view>
<view class="action-item" @tap.stop="handleSwap">
<image class="action-icon" src="/static/image/phone-message/huhuan.png"></image>
<text>消息互换</text>
</view>
<view class="action-item" @tap.stop="handleDelete">
<image class="action-icon" src="/static/image/phone-message/shanchu.png"></image>
<text>删除</text>
</view>
<!-- 向上指的三角形,因为要求在长按元素下方 -->
<view class="triangle"></view>
</view>
</view>
<view v-for="(message, index) in messageList" :key="index" class="message-item"
:class="{ isMe: message.isMe, 'm-t-16': shouldApplyMt16(index) && !shouldShowTime(index) }">
<view class="time m-t-44" v-if="shouldShowTime(index)">
<view class="top-text" v-if="data.phone == 'iphone' && index == 0">信息 · 短信</view>
<view class="top-text" v-if="data.phone == 'huawei' && index == 0">短信/彩信</view>
<text v-if="data.phone == 'huawei'">{{ formatHuaweiTopTime(message.time) }}</text>
<text v-else>{{ formatChatTime(message.time) }} <text
v-if="data.phone == 'oppo' || (data.phone == 'vivo' && message.isMe)"> 中国联通
</text></text>
<image style="width: 20rpx;height: 24rpx;margin-left: 8rpx; "
v-if="data.phone == 'oppo' || (data.phone == 'vivo' && message.isMe)"
src="/static/image/phone-message/huawei/chat-ka2.png">
</image>
</view>
<view class="chat-box" :id="'msg-' + index" :class="{
'tail-right': shouldApplyTailRight(index),
'tail-left': shouldApplyTailLeft(index),
'delivered': isLastMeMessage(index)
}" @longpress="onMessageLongPress(index, message)">
<text v-if="message.isMe && data.phone == 'mi'" class="send-text">送达</text>
<view class="chat-bubble">
<view v-html="formatMessageContent(message.content, message.isMe)"></view>
</view>
</view>
<view v-if="data.phone == 'huawei'" class="second-info">
<text>{{ formatHuaweiBottomTime(message.time) }}</text>
<image src="/static/image/phone-message/huawei/chat-ka2.png"></image>
</view>
<view v-if="(data.phone == 'oppo' || data.phone == 'vivo') && message.isMe" class="second-info">
<text v-if="message.isMe" class="delivered">已送达</text>
</view>
</view>
</ChatLayout>
</view>
</template>
<script setup>
import ChatLayout from '@/components/message/chat/chat-layout.vue'
import {
ref,
reactive
} from 'vue'
import {
onLoad,
onPageScroll
} from "@dcloudio/uni-app";
import {
stringUtil,
util
} from '@/utils/common.js';
const messageList = [
{
time: "2026-01-01 00:24",
content: "<p>【京东快递】快递尾号29398已放在博朗郡二期惠美家便利店快递员电话15320547739查看签收照片或反馈异常3.cn/2Beu-fXr</p>",
isMe: false,
simKa: 1
},
{
time: "2026-01-01 00:24",
content: "<p>啊哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈</p>",
isMe: false,
simKa: 2
},
{
time: "2026-01-01 00:24",
content: "<p>啊哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈</p>",
isMe: true
}, {
time: "2026-01-01 00:24",
content: "<p>1</p>",
isMe: true
}
, {
time: "2026-01-01 00:24",
content: "<p>15555555</p>",
isMe: true
},
{
time: "2026-01-02 11:24",
content: "<p>测试测试测试测试测试测试彩色测试哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈</p>",
isMe: false
}, {
time: "2026-01-02 11:24",
content: "<p>测试测试测试测试测试测试彩色测试哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈</p>",
isMe: false
}, {
time: "2026-01-12 11:24",
content: "<p>【京东金融】您的白条付费额度已提升5000.00元不分期使用0.065%/日不用不收费3.cn/j/1Gx-uEat 拒收请回复R</p>",
isMe: false
}, {
time: "2026-01-20 00:24",
content: "<p>dhhahahhahs.cn</p>",
isMe: true
}, {
time: "2026-03-12 00:24",
content: "<p>dhhahahhahs.cn</p>",
isMe: true
}, {
time: "2026-03-13 00:24",
content: "<p>dhhahahhahs.cn</p>",
isMe: false
},
]
// 判断消息isMe==true时上一条消息是否也是isMe==true如果不是则应用m-t-16样式
const shouldApplyMt16 = (index) => {
if (index === 0) return false;
const currentMsg = messageList[index];
const prevMsg = messageList[index - 1];
if (currentMsg.isMe && !prevMsg.isMe) {
return true;
}
return false;
}
// 判断消息isMe==true时下一条消息是否也是isMe==true如果不是则应用m-t-16样式
const shouldApplyNextIsMe = (index) => {
const currentMsg = messageList[index];
// 拦截:如果已经是数组的最后一条消息,必然不存在下一条消息
if (index >= messageList.length - 1) return false;
const nextMsg = messageList[index + 1];
if (currentMsg.isMe && nextMsg.isMe) {
return true;
}
return false;
}
// 判断是否为最后一条自己发出的消息
const isLastMeMessage = (currentIndex) => {
const currentMsg = messageList[currentIndex];
if (!currentMsg.isMe) return false;
// 往后遍历如果还能找到isMe=true的消息说明当前这条不是最后一条
for (let i = currentIndex + 1; i < messageList.length; i++) {
if (messageList[i].isMe) {
return false;
}
}
return true;
}
// 判断 tail-right右侧我方气泡尾巴样式是否生效
const shouldApplyTailRight = (index) => {
const currentMsg = messageList[index];
if (!currentMsg.isMe) return false; // 不是我发的直接没右侧尾巴
// 如果这是整体列表最后一条消息,必然有尾巴
if (index === messageList.length - 1) return true;
const nextMsg = messageList[index + 1];
// 条件 c: 下一条消息 isMe == false (也就是被打断了)
if (!nextMsg.isMe) return true;
// 条件 a: 下一条消息间隔三分钟以上 (180000 毫秒) - 时间分割线导致当前段落结束
const currentMsgTime = new Date(currentMsg.time.replace(/-/g, '/')).getTime();
const nextMsgTime = new Date(nextMsg.time.replace(/-/g, '/')).getTime();
if (!isNaN(currentMsgTime) && !isNaN(nextMsgTime) && (nextMsgTime - currentMsgTime > 180000)) {
return true;
}
// 其他情况(连绵不断的连发,中间没超时间也没被对方打断),则隐藏中间环节气泡尾巴
return false;
}
// 判断 tail-left 样式是否生效
const shouldApplyTailLeft = (index) => {
const currentMsg = messageList[index];
if (currentMsg.isMe) return false;
// 如果这是整体列表最后一条消息也就符合最后一条isMe==false的特征
if (index === messageList.length - 1) return true;
const nextMsg = messageList[index + 1];
// 条件 c: 下一条消息 isMe == true
if (nextMsg.isMe) return true;
// 条件 a: 下一条消息间隔三分钟以上 (180000 毫秒)
const currentMsgTime = new Date(currentMsg.time.replace(/-/g, '/')).getTime();
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 < messageList.length; i++) {
if (!messageList[i].isMe) {
return false; // 往后还有不是自己发的消息,所以当前并非最后一条 isMe==false
}
}
return true;
}
// 获取格式化后的聊天时间显示
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 (data.phone == 'iphone' || data.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}`;
}
// 判断是否显示时间 (间隔大于5分钟)
const shouldShowTime = (index) => {
if (index === 0) return true; // 第一条必定显示时间
const currentMsgTime = new Date(messageList[index].time.replace(/-/g, '/')).getTime();
const prevMsgTime = new Date(messageList[index - 1].time.replace(/-/g, '/')).getTime();
// 如果无法解析时间,降级为显示
if (isNaN(currentMsgTime) || isNaN(prevMsgTime)) return true;
// 5 分钟 = 5 * 60 * 1000 = 300000 毫秒
return (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_/%-]*)*)|(\d{5,})/g;
return content.replace(combinedRegex, (match) => {
// 为了防止直接替换标签内属性(当前仅存在简单的 <p> 标签无此隐患)
let color = '#3A85F8'
if (data.phone == 'iphone') color = ' #0B7AE3'
if (data.phone == 'mi') color = '#3A85F8'
const colorStyle = !isMe ? `color: ${color};` : '';
return `<span style="text-decoration: underline; ${colorStyle}">${match}</span>`;
});
}
const showActionPopup = ref(false)
const popupTop = ref(0)
const popupLeft = ref(0)
const selectedMessage = ref(null)
const onMessageLongPress = (index, message) => {
selectedMessage.value = message;
uni.createSelectorQuery().select('#msg-' + index).boundingClientRect(rect => {
if (rect) {
// 将弹窗定位在元素正下方 (bottom边界 + 一点点边距)
popupTop.value = rect.bottom + 10;
// 弹窗水平居中于该内容
let left = rect.left + rect.width / 2;
// 获取系统信息,防止弹出框超出屏幕左右侧
uni.getSystemInfo({
success: (info) => {
let popupWidth = 150; // 预估弹出层的固定宽度(可根据实际情况微调)
if (left < popupWidth / 2 + 10) left = popupWidth / 2 + 10;
if (left > info.windowWidth - popupWidth / 2 - 10) left = info.windowWidth - popupWidth / 2 - 10;
popupLeft.value = left;
showActionPopup.value = true;
}
})
}
}).exec();
}
const closeActionPopup = () => {
showActionPopup.value = false;
selectedMessage.value = null;
}
const handleEdit = () => {
uni.showToast({ title: '点击了编辑', icon: 'none' });
closeActionPopup();
}
const handleSwap = () => {
uni.showToast({ title: '点击了消息互换', icon: 'none' });
closeActionPopup();
}
const handleDelete = () => {
uni.showToast({ title: '点击了删除', icon: 'none' });
closeActionPopup();
}
const data = reactive({
phone: "iphone",
data: {}
})
onLoad((options) => {
console.log(options)
if (options.phone) {
data.phone = options.phone
}
if (options.data) {
data.data = JSON.parse(options.data)
}
})
</script>
<style lang="less" scoped>
.action-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.action-popup {
position: fixed;
background-color: #4C4C4C;
border-radius: 12rpx;
padding: 20rpx 30rpx;
display: flex;
transform: translateX(-50%);
width: 540rpx;
z-index: 10;
}
.action-popup .triangle {
position: absolute;
top: -12rpx;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 12rpx solid transparent;
border-right: 12rpx solid transparent;
border-bottom: 14rpx solid #4C4C4C;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
color: #FFFFFF;
font-size: 20rpx;
margin: 0 20rpx;
}
.action-icon {
width: 36rpx;
height: 36rpx;
margin-bottom: 8rpx;
}
// 苹果样式
.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;
}
}
.isMe {
.chat-box {
justify-content: flex-end;
}
.chat-bubble {
background-color: #34C85A;
color: #fff;
}
}
.tail-left {
position: relative;
}
.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::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;
}
}
// 小米样式
.mi-style {
.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;
}
.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>