短信完成80%

This commit is contained in:
tangxinyue 2026-03-16 17:32:44 +08:00
parent a88ddf46bc
commit 261918d871
16 changed files with 2241 additions and 921 deletions

View File

@ -1,9 +1,10 @@
<template>
<view class="page-container flex-column" :class="`${phone}-style`">
<view class="status-placeholder w100" :style="{ height: `${data.statusBarHeight}px` }"></view>
<view class="top-box">
<!-- <view class="top-placeholder" :style="{ height: data.topBoxHeight + 'px' }"></view> -->
<view class="top-box " :style="{ top: data.statusBarHeight + 'px' }">
<slot name="top">
<view class="top-container">
<view class="top-container" v-show="!sortMode">
<view class="h100 flex-align-center flex-justify-between">
<view class="left flex-align-center">
<image @click="util.goBack()" :src="`/static/image/phone-message/${phone}/back.png`">
@ -43,49 +44,67 @@
</slot>
</view>
<view class="center-box flex-1">
<slot></slot>
<scroll-view :scroll-top="scrollTop" scroll-y="true" scroll-with-animation :show-scrollbar="false"
class="scroll-view h100">
<slot></slot>
</scroll-view>
</view>
<view class="bottom-box">
<view class="bottom-box fixed-bottom-box" :style="{ bottom: data.keyboardHeight + 'px' }">
<slot name="bottom">
<view class="bottom-container flex-align-center">
<view class="bottom-container flex-align-center" v-show="!sortMode">
<image v-if="phone != 'huawei' && phone != 'vivo'" class="add-img shrink-0"
:src="`/static/image/phone-message/${phone}/chat-left.png`"></image>
<image v-if="phone == 'huawei'" class="add-img shrink-0"
:src="`/static/image/phone-message/huawei/emoji.png`"></image>
<view class="search-box flex-1 flex-align-center">
<image v-if="phone == 'huawei'" class="left-icon ka"
src="/static/image/phone-message/huawei/ka1.png"></image>
<image v-if="phone == 'huawei'" class="left-icon ka" @click="changeSim"
:src="`/static/image/phone-message/huawei/ka${simIndex}.png`"></image>
<image v-if="phone == 'huawei'" class="left-icon down"
src="/static/image/phone-message/huawei/down.png"></image>
<input class="input flex-1" :placeholder="showInfo.placeholder" type="text">
<image v-if="phone == 'iphone'" class="right-icon"
<!-- <input class="input flex-1" :placeholder="showInfo.placeholder" v-model="content"
@input="onInput"></input> -->
<textarea class="input flex-1" :adjust-position="false" fixed auto-height
:placeholder="showInfo.placeholder" v-model="content" @input="onInput" @focus="onFocus"
@blur="onBlur"></textarea>
<!-- <editor class="input flex-1" :placeholder="showInfo.placeholder"></editor> -->
<image v-if="phone == 'iphone' && !isSend" class="right-icon"
src="/static/image/phone-message/iphone/mic.png"></image>
<image v-if="phone == 'oppo'" class="right-icon" src="/static/image/phone-message/oppo/ka1.png">
<image v-if="phone == 'iphone' && isSend" class="right-send-icon"
src="/static/image/phone-message/iphone/send.png" @click="sendMessage"></image>
<image v-if="phone == 'oppo'" class="right-icon"
:src="`/static/image/phone-message/oppo/ka${simIndex}.png`" @click="changeSim">
</image>
</view>
<view v-if="phone != 'iphone'" class="flex-align-center">
<image v-if="phone == 'mi'" class="right-icon" src="/static/image/phone-message/mi/mic.png">
<image v-if="phone == 'mi'" class="right-icon"
:src="`/static/image/phone-message/mi/${isSend ? 'send' : 'unsend'}.png`"
@click="sendMessage">
</image>
<image v-if="phone == 'oppo'" class="right-icon"
src="/static/image/phone-message/oppo/send.png">
:src="`/static/image/phone-message/oppo/${isSend ? 'send' : 'unsend'}.png`"
@click="sendMessage">
</image>
<image v-if="phone == 'huawei'" class="right-icon"
src="/static/image/phone-message/huawei/chat-add.png">
</image>
<image v-if="phone == 'huawei'" class="right-icon m-l-34"
src="/static/image/phone-message/huawei/send.png">
:src="`/static/image/phone-message/huawei/${isSend ? 'send' : 'unsend'}.png`"
@click="sendMessage">
</image>
<text v-if="phone == 'vivo'" class="send-text">发送</text>
<text v-if="phone == 'vivo'" class="send-text" :class="{ 'send-text-active': isSend }"
@click="sendMessage">发送</text>
</view>
</view>
</slot>
</view>
<view class="bottom-placeholder" :style="{ height: (data.bottomBoxHeight + data.keyboardHeight) + 'px' }">
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ref, reactive, onMounted, computed, toRefs } from 'vue'
import { onLoad, onPageScroll } from "@dcloudio/uni-app";
import { stringUtil, util } from '@/utils/common.js';
import { stringUtil, dateUtil, util } from '@/utils/common.js';
const props = defineProps({
//
phone: {
@ -102,13 +121,100 @@ const props = defineProps({
content: "您关注的商品降价啦,快来抢购吧!",
time: "2026-02-20 20:55:12"
}
},
//
sortMode: {
type: Boolean,
default: false
}
})
const data = reactive({
statusBarHeight: 0,
topBoxHeight: 0,
bottomBoxHeight: 0,
isSend: false,
content: "",
simIndex: 1,
scrollTop: 0,
keyboardHeight: 0
})
let { isSend, content, simIndex, scrollTop } = toRefs(data)
const emit = defineEmits(['send'])
onMounted(() => {
// DOM
setTimeout(() => {
data.scrollTop = 99999 + Math.random();
}, 100);
})
const onInput = (e) => {
content.value = e.detail.value
if (content.value.length > 0) {
isSend.value = true
} else {
isSend.value = false
}
setTimeout(() => {
uni.createSelectorQuery().select('.bottom-box').boundingClientRect(rect => {
if (rect) {
data.bottomBoxHeight = rect.height;
}
}).exec();
}, 50);
}
const onFocus = (e) => {
if (e.detail.height) {
data.keyboardHeight = e.detail.height;
//
setTimeout(() => {
data.scrollTop = 99999 + Math.random();
}, 100);
}
}
const onBlur = () => {
data.keyboardHeight = 0;
}
/**
* 切换sim卡
*/
const changeSim = () => {
simIndex.value = simIndex.value == 1 ? 2 : 1
}
/**
* 发送消息
*/
const sendMessage = () => {
if (!isSend.value) return
const date = dateUtil.now("YYYY-MM-DD HH:mm")
const params = {
content: `<p>${content.value}</p>`,
simIndex: simIndex.value,
time: date,
isMe: true
}
emit('send', params)
data.content = ""
data.isSend = false
// DOM
setTimeout(() => {
data.scrollTop = 99999 + Math.random();
uni.createSelectorQuery().select('.bottom-box').boundingClientRect(rect => {
if (rect) {
data.bottomBoxHeight = rect.height;
}
}).exec();
}, 100);
}
//
const showInfo = computed(() => {
let placeholder
@ -139,8 +245,24 @@ onMounted(() => {
const systemInfo = uni.getSystemInfoSync();
data.statusBarHeight = systemInfo.statusBarHeight || 0;
// top-box bottom-box
setTimeout(() => {
uni.createSelectorQuery().select('.top-box').boundingClientRect(rect => {
if (rect) {
data.topBoxHeight = rect.height;
}
}).exec();
uni.createSelectorQuery().select('.bottom-box').boundingClientRect(rect => {
if (rect) {
data.bottomBoxHeight = rect.height;
}
}).exec();
}, 50);
})
</script>
<style>
@import '@/common/main.css';
@ -162,10 +284,26 @@ onMounted(() => {
.center-box {
overflow: hidden;
overflow-y: scroll;
// overflow-y: scroll;
padding-bottom: 12px;
}
.fixed-bottom-box {
position: fixed;
width: 100%;
background: #fff;
bottom: 0;
left: 0;
}
.fixed-top-box {
position: fixed;
width: 100%;
z-index: 9;
left: 0;
top: 0;
}
//
.iphone-style {
@ -242,17 +380,32 @@ onMounted(() => {
}
.search-box {
height: 70rpx;
min-height: 70rpx;
border-radius: 35rpx 35rpx 35rpx 35rpx;
border: 2rpx solid #DFDFDF;
padding: 0 26rpx;
padding: 8rpx 26rpx;
::v-deep .ql-container {
// height: 100%;
min-height: 48rpx !important;
font-size: 32rpx;
}
::v-deep .ql-blank::before {
color: #C3C3C3;
font-size: 32rpx;
line-height: inherit;
font-style: normal;
}
.input {
::v-deep .input-placeholder {
color: #C3C3C3;
font-size: 32rpx;
line-height: 32rpx;
}
height: 100%;
width: auto;
min-height: 46rpx;
max-height: 160rpx;
overflow: hidden;
overflow-y: scroll;
// min-height: 48rpx !important;
}
.right-icon {
@ -260,6 +413,12 @@ onMounted(() => {
height: 32rpx;
margin-left: 20rpx;
}
.right-send-icon {
width: 52rpx;
height: 52rpx;
margin-left: 20rpx;
}
}
}
@ -338,18 +497,31 @@ onMounted(() => {
}
.search-box {
height: 96rpx;
min-height: 96rpx;
border-radius: 28rpx;
padding: 0 42rpx;
padding: 12rpx 42rpx;
margin: 0 20rpx;
background-color: #E8E8E8;
::v-deep .ql-container {
height: 100%;
min-height: auto;
}
::v-deep .ql-blank::before {
color: #8B8B8B;
font-size: 30rpx;
line-height: inherit;
font-style: normal;
}
.input {
::v-deep .input-placeholder {
color: #8B8B8B;
font-size: 30rpx;
line-height: 28rpx;
}
height: 100%;
width: auto;
min-height: 46rpx;
max-height: 160rpx;
overflow: hidden;
overflow-y: scroll;
}
.right-icon {
@ -457,18 +629,31 @@ onMounted(() => {
}
.search-box {
height: 84rpx;
min-height: 84rpx;
border-radius: 42rpx;
padding: 0 38rpx;
margin: 0 16rpx;
background-color: #E6E6E6;
padding: 12rpx 38rpx;
::v-deep .ql-container {
height: 100%;
min-height: auto;
}
::v-deep .ql-blank::before {
color: #6C6C6E;
font-size: 32rpx;
line-height: inherit;
font-style: normal;
}
.input {
::v-deep .input-placeholder {
color: #6C6C6E;
font-size: 32rpx;
line-height: 32rpx;
}
height: 100%;
width: auto;
min-height: 46rpx;
max-height: 160rpx;
overflow: hidden;
overflow-y: scroll;
}
.right-icon {
@ -581,9 +766,9 @@ onMounted(() => {
}
.search-box {
height: 76rpx;
min-height: 76rpx;
border-radius: 42rpx;
padding: 0 28rpx;
padding: 10rpx 28rpx;
margin: 0 30rpx;
background-color: #F4F4F4;
@ -599,12 +784,24 @@ onMounted(() => {
}
.input {
margin-left: 34rpx;
height: 100%;
width: auto;
min-height: 46rpx;
max-height: 160rpx;
overflow: hidden;
overflow-y: scroll;
margin-left: 32rpx;
::v-deep .input-placeholder {
::v-deep .ql-container {
height: 100%;
min-height: auto;
}
::v-deep .ql-blank::before {
color: #606060;
font-size: 30rpx;
line-height: 32rpx;
line-height: inherit;
font-style: normal;
}
}
@ -689,17 +886,30 @@ onMounted(() => {
padding: 20rpx 48rpx;
.search-box {
height: 64rpx;
min-height: 64rpx;
border-radius: 16rpx;
padding: 0 18rpx;
padding: 8rpx 18rpx;
margin-right: 42rpx;
background-color: #F2F2F2;
.input {
::v-deep .input-placeholder {
height: 100%;
width: auto;
min-height: 46rpx;
max-height: 160rpx;
overflow: hidden;
overflow-y: scroll;
::v-deep .ql-container {
height: 100%;
min-height: auto;
}
::v-deep .ql-blank::before {
color: #C8C8C8;
font-size: 30rpx;
line-height: 30rpx;
line-height: inherit;
font-style: normal;
}
}
}
@ -709,6 +919,10 @@ onMounted(() => {
color: #9CC9FF;
font-size: 34rpx;
}
.send-text-active {
color: #0078FE;
}
}
}

View File

@ -0,0 +1,857 @@
<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, $event)"
@touchmove.stop.prevent="onSortTouchMove(index, $event)" @touchend.stop="onSortTouchEnd(index, $event)">
<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" v-if="shouldShowTime(index)">
<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"> 中国联通
</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),
'tail-left': shouldApplyTailLeft(index),
'delivered': isLastMeMessage(index)
}" @longpress="!sortMode && onMessageLongPress(index, message)">
<text v-if="message.isMe && phone == 'mi'" class="send-text">送达</text>
<view class="chat-bubble">
<view v-html="formatMessageContent(message.content, message.isMe)"></view>
</view>
</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">已送达</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
import { onLoad, onPageScroll } from "@dcloudio/uni-app";
import { stringUtil, util } from '@/utils/common.js';
const props = defineProps({
//
phone: {
type: String,
default: 'iphone'
},
messageList: {
type: Array,
default: []
},
//
sortMode: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['onMessageLongPress', 'sort'])
//
// 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) {
localSortList.value = props.messageList.map(item => ({ ...item }))
} else {
dragIndex.value = -1
dragOverIndex.value = -1
isDragging = false
sortItemRects = []
localSortList.value = []
}
})
//
const displayList = computed(() => {
if (props.sortMode) return localSortList.value
return props.messageList
})
// isMe==trueisMe==truem-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;
}
// isMe==trueisMe==truem-t-16
const shouldApplyNextIsMe = (index) => {
const currentMsg = displayList.value[index];
//
if (index >= displayList.value.length - 1) return false;
const nextMsg = displayList.value[index + 1];
if (currentMsg.isMe && nextMsg.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;
// 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 = 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;
// 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 < displayList.value.length; i++) {
if (!displayList.value[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 (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'
const colorStyle = !isMe ? `color: ${color};` : '';
return `<span style="text-decoration: underline; ${colorStyle}">${match}</span>`;
});
}
/**
* 长按信息
* @param index
* @param message
*/
const onMessageLongPress = (index, message) => {
emit('onLongPress', index, message)
}
// ===================== =====================
/**
* 刷新所有排序条目的 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, e) => {
dragIndex.value = idx
dragOverIndex.value = idx
isDragging = true
refreshSortRects(rects => {
sortItemRects = rects
})
uni.vibrateShort({ type: 'medium' })
}
/**
* 拖拽移动 - 根据触摸点 Y 坐标確定悬停位置和插入方向
*/
const onSortTouchMove = (idx, 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 = (idx, e) => {
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;
}
//
.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>

View File

@ -12,7 +12,7 @@
> 99
? '99+' : (item.unReadNumber || 1) }}</text>
</view>
<image class="img shrink-0"
<image class="img avatar shrink-0"
:src="item.img || `/static/image/phone-message/${phone}/default.png`">
</image>
</view>
@ -20,9 +20,12 @@
<view class="main-box flex-1">
<view class="title-box flex-between">
<text class="title">{{ item.title }}</text>
<text class="time">{{ formatDate(item.time) }}</text>
<text class="time">{{ formatDate(item.chatList?.[item.chatList?.length -
1]?.time || item.time)
}}</text>
</view>
<view class="content">{{ item.content }}</view>
<view class="content"
v-html="item.chatList?.[item.chatList?.length - 1]?.content || ''"></view>
</view>
<view class="box-right h100 flex-column flex-align-center">
<image v-if="phone == 'iphone'" class="m-t-4"
@ -35,14 +38,18 @@
</view>
<template v-slot:right>
<view class="flex flex-align-center" style="margin-left: 1px;">
<view v-if="phone == 'iphone'" class="btn flex-center flex-align-center edit">
<image src="/static/image/phone-message/iphone/mute.png"></image>
<view class="btn flex-center flex-align-center edit" style="color: #fff;"
@click="editItem(item)">
编辑
<!-- <image :src="`/static/image/phone-message/iphone/delete.png`">
</image> -->
</view>
<view class="btn flex-center flex-align-center delete">
<view class="btn flex-center flex-align-center delete" @click="deleteItem(item)">
<image
:src="`/static/image/phone-message/${phone == 'huawei' || phone == 'vivo' ? 'huawei' : 'iphone'}/delete.png`">
</image>
</view>
</view>
</template>
@ -53,99 +60,6 @@
<script>
import { stringUtil } from '@/utils/common.js';
const defaultList = [
{
id: stringUtil.uuid(),
unRead: true,
unReadNumber: 100,
noNotice: true,
img: "",
title: "系统通知",
content: "您的账户于今日 08:30 在异地登录,请确认是否为本人操作。",
time: "2026-03-11 08:31:00"
},
{
id: stringUtil.uuid(),
unRead: true,
noNotice: true,
img: "",
title: "12306",
content: "市燃管办温馨提示:依法安全文明燃放烟花爆竹,共护平安幸福家园呵呵哈哈哈还好共护平安幸福家园呵呵哈哈哈还好",
time: "2026-03-10 14:30:00"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "支付宝支付",
content: "支付成功您在XX超市消费了 45.00 元。",
time: "2026-03-10 18:20:15"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "花呗提醒",
content: "本月账单已出,请在 9 号前还款,避免逾期记录。",
time: "2026-03-09 09:00:00"
},
{
id: stringUtil.uuid(),
unRead: true,
noNotice: false,
img: "",
title: "顺丰速运",
content: "您的快递已到达XX驿站凭取件码 8899 取件。",
time: "2026-03-01 10:15:22"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: true,
img: "",
title: "蚂蚁森林",
content: "您的好友偷走了你 5g 绿色能量,快去看看吧!",
time: "2026-02-28 07:45:10"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "余额宝",
content: "昨日收益已到账,恭喜您获得收益 0.58 元。",
time: "2026-02-27 06:30:00"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "10086",
content: "【流量使用提示】您本月的套餐流量已使用达 80%,请注意余额。",
time: "2026-02-26 15:10:45"
},
{
id: stringUtil.uuid(),
unRead: true,
noNotice: false,
img: "",
title: "饿了么",
content: "您的外卖已送达,祝您用餐愉快,别忘了给骑手五星好评哦~",
time: "2026-02-25 12:40:33"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "淘宝通知",
content: "您关注的商品降价啦,快来抢购吧!",
time: "2026-02-20 20:55:12"
}
]
</script>
<script setup>
import {
@ -158,7 +72,7 @@ import {
dateUtil
} from '@/utils/common.js';
//
const emit = defineEmits(['item-click'])
const emit = defineEmits(['item-click', 'delete-item', 'edit-item'])
const props = defineProps({
//
phone: {
@ -167,7 +81,7 @@ const props = defineProps({
},
list: {
type: Array,
default: () => defaultList
default: () => []
}
})
@ -200,6 +114,21 @@ const formatDate = (date) => {
const clickItem = (item) => {
emit('item-click', item)
}
/**
* 删除列表元素
*/
const deleteItem = (item) => {
emit('delete-item', item)
}
/**
* 修改元素
* @param item
*/
const editItem = (item) => {
emit('edit-item', item)
}
</script>
<style>
@import '@/common/main.css';
@ -225,6 +154,14 @@ const clickItem = (item) => {
background-color: #FFFFFF;
}
.avatar {
border-radius: 50%;
}
.edit {
background-color: #5855D6;
}
//
.iphone-style {
@ -266,6 +203,7 @@ const clickItem = (item) => {
width: 84rpx;
height: 84rpx;
margin-left: 16rpx;
border-radius: 50%;
}
.border-box {

View File

@ -2,7 +2,9 @@
<view :class="`${phone}-style`">
<!-- 导航样式 -->
<view class="nav-bar-box" :class="{ 'border-nav-bar-box': isScroll }">
<NavBar :isBack="false" :bgColor="isScroll ? data.navBar.bgColor : '#fff'">
<NavBar :isBack="false" :bgColor="isScroll ? data.navBar.bgColor : '#fff'" :buttonGroup="buttonGroup"
@button-click="util.clickTitlePopupButton" tipLayerType="message-list-tip" isTipLayer
tipLayerText="添加短信" @add="emit('add')">
<!-- 左侧文字图标 -->
<template v-slot:left>
<view v-if="phone == 'iphone'" class="flex flex-align-center">
@ -108,6 +110,15 @@ import {
util
} from '@/utils/common.js';
const emit = defineEmits(['add'])
const buttonGroup = [{
name: "添加短信",
click: () => {
emit('add')
}
}]
const props = defineProps({
//
phone: {

View File

@ -53,8 +53,11 @@
<view class="detail-info-container">
<template v-for="item in billData.itemInfoList" :key="item.id">
<view class="info-item-box" v-if="item.key != 'paymentReward'">
<view class="item-label">
{{ item.label }}
<view class="item-label" :class="{ 'switchable-label': item.key === 'createTime' }"
@click="toggleCreateTimeLabel(item)">
<text>{{ item.label }}</text>
<image v-if="item.key === 'createTime'" class="switch-icon"
src="/static/image/bill/add-bill/edit.png"></image>
</view>
<view v-if="item.type != 'link'" class="info-item-input" @click="onClickItemInfo(item)">
<!-- 隐藏的text用于测量宽度 -->
@ -882,6 +885,15 @@ const onRightClick = async () => {
}, 500)
}
/**
* 切换创建时间/支付时间标签
*/
const toggleCreateTimeLabel = (item) => {
if (item.key === 'createTime') {
item.label = item.label === '创建时间' ? '支付时间' : '创建时间'
}
}
// itemInfo
const onClickItemInfo = async (item, action) => {
console.log(item)
@ -1230,6 +1242,22 @@ page {
font-size: 26rpx;
color: var(--text-secondary);
}
.switchable-label {
display: inline-flex;
align-items: center;
padding: 4rpx 10rpx 4rpx 10rpx;
margin-left: -10rpx;
border-radius: 8rpx;
border: 1px dashed #d9d9d9;
background-color: #fcfcfc;
.switch-icon {
width: 18rpx;
height: 18rpx;
margin-left: 6rpx;
}
}
}
.info-item-input {

View File

@ -256,7 +256,7 @@ const otherList = [{
path: "/pages/other/card/card"
},
{
icon: "/static/image/index/qita/card.png",
icon: "/static/image/index/qita/msg.png",
name: "短信",
path: "/pages/common/call-and-message-entry/call-and-message-entry?type=message"
},

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,234 @@
[
{
"id": 12365458895222,
"unRead": true,
"unReadNumber": 2,
"noNotice": true,
"img": "",
"title": "京东快递",
"content": "【京东快递】您的快递已送达博朗郡二期...",
"chatList": [
{
"id": "uuid-001-a1",
"time": "2026-03-15 10:24",
"content": "<p>【京东快递】快递尾号29398已放在博朗郡二期惠美家便利店快递员电话15320547739。</p>",
"isMe": false,
"simKa": 1
},
{
"id": "uuid-001-a2",
"time": "2026-03-16 08:31",
"content": "<p>【京东快递】您的包裹已签收,感谢使用京东快递,期待再次为您服务。</p>",
"isMe": false,
"simKa": 1
}
],
"time": "2026-03-16 08:31:00"
},
{
"id": 12365458895223,
"unRead": false,
"unReadNumber": 0,
"noNotice": false,
"img": "",
"title": "招商银行",
"content": "【招商银行】账户变动提醒",
"chatList": [
{
"id": "uuid-002-b1",
"time": "2026-03-15 12:00",
"content": "<p>【招商银行】您尾号8888的账户于03月15日11:58收入人民币90000.00元余额965600.50元。</p>",
"isMe": false,
"simKa": 1
}
],
"time": "2026-03-15 12:00:00"
},
{
"id": 12365458895224,
"unRead": true,
"unReadNumber": 1,
"noNotice": false,
"img": "",
"title": "中国移动",
"content": "【中国移动】流量使用周报",
"chatList": [
{
"id": "uuid-003-c1",
"time": "2026-03-14 09:00",
"content": "<p>【中国移动】截至14日09时您本月已使用通用流量18.5GB剩余3.2GB。</p>",
"isMe": false,
"simKa": 1
},
{
"id": "uuid-003-c2",
"time": "2026-03-14 18:30",
"content": "<p>【中国移动】流量包订购成功您已成功领取5GB周末流量包立即生效。</p>",
"isMe": false,
"simKa: 1"
}
],
"time": "2026-03-14 18:30:00"
},
{
"id": 12365458895225,
"unRead": false,
"unReadNumber": 0,
"noNotice": false,
"img": "",
"title": "验证码中心",
"content": "【抖音】您的验证码是 882931",
"chatList": [
{
"id": "uuid-004-d1",
"time": "2026-03-16 01:10",
"content": "<p>【抖音】验证码 882931用于手机绑定。5分钟内有效请勿泄露给他人。</p>",
"isMe": false,
"simKa": 1
}
],
"time": "2026-03-16 01:10:00"
},
{
"id": 12365458895226,
"unRead": false,
"unReadNumber": 0,
"noNotice": true,
"img": "",
"title": "美团外卖",
"content": "【美团】订单配送通知",
"chatList": [
{
"id": "uuid-005-e1",
"time": "2026-03-15 18:30",
"content": "<p>【美团】商家已接单,骑手正在赶往商家,请耐心等待。</p>",
"isMe": false,
"simKa": 2
},
{
"id": "uuid-005-e2",
"time": "2026-03-15 19:15",
"content": "<p>【美团】骑手已送达!祝您用餐愉快,给个五星好评吧!</p>",
"isMe": false,
"simKa": 2
}
],
"time": "2026-03-15 19:15:00"
},
{
"id": 12365458895227,
"unRead": true,
"unReadNumber": 3,
"noNotice": false,
"img": "",
"title": "建设银行",
"content": "【建设银行】还款提醒",
"chatList": [
{
"id": "uuid-006-f1",
"time": "2026-03-10 10:00",
"content": "<p>【建设银行】您03月信用卡账单已出应还款额为¥3,200.00。</p>",
"isMe": false,
"simKa": 1
},
{
"id": "uuid-006-f2",
"time": "2026-03-13 14:00",
"content": "<p>【建设银行】温馨提醒您的账单将于3天后到期请确保扣款账户余额充足。</p>",
"isMe": false,
"simKa": 1
},
{
"id": "uuid-006-f3",
"time": "2026-03-16 09:00",
"content": "<p>【建设银行】扣款成功。感谢您使用建设银行信用卡。</p>",
"isMe": false,
"simKa": 1
}
],
"time": "2026-03-16 09:00:00"
},
{
"id": 12365458895228,
"unRead": false,
"unReadNumber": 0,
"noNotice": false,
"img": "",
"title": "12306",
"content": "【12306】购票成功通知",
"chatList": [
{
"id": "uuid-007-g1",
"time": "2026-03-11 11:00",
"content": "<p>【12306】订单EG12345678支付成功。北京南-上海虹桥03月20日14:00开。</p>",
"isMe": false,
"simKa": 2
}
],
"time": "2026-03-11 11:00:00"
},
{
"id": 12365458895229,
"unRead": false,
"unReadNumber": 0,
"noNotice": true,
"img": "",
"title": "腾讯科技",
"content": "【腾讯科技】安全提醒",
"chatList": [
{
"id": "uuid-008-h1",
"time": "2026-03-13 22:30",
"content": "<p>【腾讯科技】您的QQ账号在异地登录登录地点广州。如非本人操作请及时改密。</p>",
"isMe": false,
"simKa": 1
}
],
"time": "2026-03-13 22:30:00"
},
{
"id": 12365458895230,
"unRead": true,
"unReadNumber": 1,
"noNotice": false,
"img": "",
"title": "菜鸟驿站",
"content": "【菜鸟驿站】取件通知",
"chatList": [
{
"id": "uuid-009-i1",
"time": "2026-03-16 10:15",
"content": "<p>【菜鸟驿站】您的中通包裹已到达。凭取件码 8-2-4002 领取,地址:博朗郡东门。</p>",
"isMe": false,
"simKa: 1"
}
],
"time": "2026-03-16 10:15:00"
},
{
"id": 12365458895231,
"unRead": false,
"unReadNumber": 0,
"noNotice": false,
"img": "",
"title": "阿里云",
"content": "【阿里云】资源包即将到期",
"chatList": [
{
"id": "uuid-010-j1",
"time": "2026-03-12 09:00",
"content": "<p>【阿里云】尊敬的用户您的OSS存储包将于7天后到期请及时续费。</p>",
"isMe": false,
"simKa": 1
},
{
"id": "uuid-010-j2",
"time": "2026-03-15 15:00",
"content": "<p>【阿里云】续费成功。您的OSS存储包有效期已延长至2027年03月。</p>",
"isMe": false,
"simKa": 1
}
],
"time": "2026-03-15 15:00:00"
}
]

View File

@ -1,20 +1,78 @@
<template>
<view>
<MessageNavBar :phone="data.phone" :isScroll="data.isScroll">
<MessageList :phone="data.phone" :list="defaultList" @item-click="itemClick"></MessageList>
<MessageNavBar :phone="data.phone" :isScroll="data.isScroll" @add="openAddPopup">
<MessageList :phone="data.phone" :list="defaultList" @item-click="itemClick" @delete-item="deleteItem"
@edit-item="editItem">
</MessageList>
</MessageNavBar>
<!-- 添加短信弹窗 -->
<view v-if="showAddPopup" class="add-mask" @tap="closeAddPopup">
<view class="add-popup" @tap.stop>
<view class="add-header">{{ editingItem ? '编辑短信' : '新建短信' }}</view>
<view class="add-body">
<view class="add-row">
<text class="add-label">头像URL</text>
<view class="image-box" style="width: 84rpx;height: 84rpx;" @tap="selectImage">
<image v-if="addForm.img" class="image w100 h100" :src="addForm.img" mode="aspectFill">
</image>
<image v-else class="image w100 h100" src="/static/image/phone-message/add.png"
mode="aspectFill">
</image>
</view>
</view>
<view class="add-row">
<text class="add-label required">联系人</text>
<input class="add-input" v-model="addForm.title" placeholder="请输入名称或号码" />
</view>
<view class="add-row between" v-if="data.phone == 'iphone'">
<text class="add-label">取消通知</text>
<switch :checked="addForm.noNotice" @change="addForm.noNotice = !addForm.noNotice" />
</view>
<view class="add-row between">
<text class="add-label">是否未读</text>
<switch :checked="addForm.unRead" @change="addForm.unRead = !addForm.unRead" />
</view>
<view class="add-row" v-if="addForm.unRead && data.phone == 'oppo' || data.phone == 'huawei'">
<text class="add-label">未读数量</text>
<input class="add-input" type="number" v-model="addForm.unReadNumber" placeholder="请输入未读数量" />
</view>
<view class="add-row" v-if="addForm.chatList.length == 0 && editingItem">
<text class="add-label">消息时间</text>
<view class="time-picker-group">
<picker mode="date" :value="addForm.date" @change="onAddDateChange">
<view class="time-picker-item">
<text>{{ addForm.date || '选择日期' }}</text>
</view>
</picker>
<picker mode="time" :value="addForm.timeOfDay" @change="onAddTimeChange">
<view class="time-picker-item">
<text>{{ addForm.timeOfDay || '选择时刻' }}</text>
</view>
</picker>
</view>
</view>
</view>
<view class="add-footer">
<view class="add-btn cancel" @tap="closeAddPopup">取消</view>
<view class="add-btn confirm" @tap="confirmAdd">确定</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import MessageNavBar from '@/components/message/list/message-nav-bar.vue'
import MessageList from '@/components/message/list/list.vue'
import defaultData from './defaultData.json'
import {
ref,
reactive
} from 'vue'
import {
onLoad,
onShow,
onPageScroll
} from "@dcloudio/uni-app";
import {
@ -22,99 +80,7 @@ import {
util
} from '@/utils/common.js';
const defaultList = [
{
id: stringUtil.uuid(),
unRead: true,
unReadNumber: 100,
noNotice: true,
img: "",
title: "系统通知",
content: "您的账户于今日 08:30 在异地登录,请确认是否为本人操作。",
time: "2026-03-11 08:31:00"
},
{
id: stringUtil.uuid(),
unRead: true,
noNotice: true,
img: "",
title: "12306",
content: "市燃管办温馨提示:依法安全文明燃放烟花爆竹,共护平安幸福家园呵呵哈哈哈还好共护平安幸福家园呵呵哈哈哈还好",
time: "2026-03-10 14:30:00"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "支付宝支付",
content: "支付成功您在XX超市消费了 45.00 元。",
time: "2026-03-10 18:20:15"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "花呗提醒",
content: "本月账单已出,请在 9 号前还款,避免逾期记录。",
time: "2026-03-09 09:00:00"
},
{
id: stringUtil.uuid(),
unRead: true,
noNotice: false,
img: "",
title: "顺丰速运",
content: "您的快递已到达XX驿站凭取件码 8899 取件。",
time: "2026-03-01 10:15:22"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: true,
img: "",
title: "蚂蚁森林",
content: "您的好友偷走了你 5g 绿色能量,快去看看吧!",
time: "2026-02-28 07:45:10"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "余额宝",
content: "昨日收益已到账,恭喜您获得收益 0.58 元。",
time: "2026-02-27 06:30:00"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "10086",
content: "【流量使用提示】您本月的套餐流量已使用达 80%,请注意余额。",
time: "2026-02-26 15:10:45"
},
{
id: stringUtil.uuid(),
unRead: true,
noNotice: false,
img: "",
title: "饿了么",
content: "您的外卖已送达,祝您用餐愉快,别忘了给骑手五星好评哦~",
time: "2026-02-25 12:40:33"
},
{
id: stringUtil.uuid(),
unRead: false,
noNotice: false,
img: "",
title: "淘宝通知",
content: "您关注的商品降价啦,快来抢购吧!",
time: "2026-02-20 20:55:12"
}
]
const defaultList = ref(defaultData)
const data = reactive({
navBar: {
@ -125,26 +91,27 @@ const data = reactive({
isScroll: false
})
const options = [{
text: '取消',
style: {
backgroundColor: '#007aff'
}
}, {
text: '确认',
style: {
backgroundColor: '#dd524d'
}
}]
const STORAGE_KEY = 'message_list'
onLoad((options) => {
if (options.phone) {
data.phone = options.phone
}
})
onShow(() => {
//
try {
const cached = uni.getStorageSync(STORAGE_KEY)
if (cached) {
defaultList.value = JSON.parse(cached)
}
} catch (e) { }
})
onPageScroll((e) => {
console.log(e.scrollTop)
if (e.scrollTop > 60) {
data.isScroll = true
} else {
@ -152,8 +119,153 @@ onPageScroll((e) => {
}
})
/**
* 删除元素
* @param item
*/
const deleteItem = (item) => {
uni.showModal({
title: '提示',
content: '确定要删除吗?',
success: (res) => {
if (res.confirm) {
defaultList.value.splice(defaultList.value.findIndex(i => i.id === item.id), 1)
uni.setStorageSync(STORAGE_KEY, JSON.stringify(defaultList.value))
}
}
})
}
/**
* 选择图片
*/
const selectImage = () => {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
addForm.img = res.tempFilePaths[0]
}
})
}
const itemClick = (item) => {
util.goPage(`/pages/message/chat-page/chat-page?phone=${data.phone}&data=${JSON.stringify(item)}`)
util.goPage(`/pages/message/chat-page/chat-page?phone=${data.phone}&id=${item.id}`)
}
// ===== =====
const showAddPopup = ref(false)
const addForm = reactive({
title: '',
img: '',
content: '',
date: '',
timeOfDay: ''
})
// null==
const editingItem = ref(null)
const openAddPopup = () => {
//
const now = new Date()
const pad = v => String(v).padStart(2, '0')
editingItem.value = null
addForm.title = ''
addForm.img = ''
addForm.content = ''
addForm.unRead = false
addForm.unReadNumber = 1
addForm.noNotice = false
addForm.chatList = []
addForm.date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
addForm.timeOfDay = `${pad(now.getHours())}:${pad(now.getMinutes())}`
showAddPopup.value = true
}
const closeAddPopup = () => {
showAddPopup.value = false
}
const onAddDateChange = (e) => {
addForm.date = e.detail.value
}
const onAddTimeChange = (e) => {
addForm.timeOfDay = e.detail.value
}
/**
* 编辑元素回填表单并打开弹窗
*/
const editItem = (item) => {
editingItem.value = item
const lastMsg = item.chatList && item.chatList.length ? item.chatList[item.chatList.length - 1] : null
const timeStr = lastMsg ? lastMsg.time : (item.time || '')
const parts = timeStr.split(' ')
addForm.title = item.title || ''
addForm.img = item.img || ''
addForm.content = ''
addForm.unRead = !!item.unRead
addForm.unReadNumber = item.unReadNumber || 0
addForm.date = parts[0] || ''
addForm.timeOfDay = parts[1] || ''
addForm.chatList = item.chatList || []
showAddPopup.value = true
}
const confirmAdd = () => {
if (!addForm.title.trim()) {
uni.showToast({ title: '请输入联系人名称', icon: 'none' })
return
}
console.log(addForm)
const time = `${addForm.date} ${addForm.timeOfDay}`.trim()
if (editingItem.value) {
// ===== =====
const idx = defaultList.value.findIndex(i => i.id === editingItem.value.id)
if (idx > -1) {
defaultList.value[idx].title = addForm.title.trim()
defaultList.value[idx].img = addForm.img.trim()
defaultList.value[idx].unRead = addForm.unRead
defaultList.value[idx].unReadNumber = addForm.unReadNumber
defaultList.value[idx].noNotice = addForm.noNotice
defaultList.value[idx].time = time
}
} else {
// ===== =====
const newItem = {
id: Date.now(),
unRead: addForm.unRead,
unReadNumber: addForm.unReadNumber,
noNotice: addForm.noNotice,
img: addForm.img.trim(),
title: addForm.title.trim(),
chatList: addForm.content.trim() ? [
{
id: stringUtil.uuid(),
time: time,
content: `<p>${addForm.content.trim()}</p>`,
isMe: false
}
] : [],
time: time
}
defaultList.value.unshift(newItem)
}
//
defaultList.value.sort((a, b) => {
const timeA = new Date(((a.chatList && a.chatList.length ? a.chatList[a.chatList.length - 1].time : null) || a.time || '').replace(/-/g, '/')).getTime()
const timeB = new Date(((b.chatList && b.chatList.length ? b.chatList[b.chatList.length - 1].time : null) || b.time || '').replace(/-/g, '/')).getTime()
return timeB - timeA
})
try {
uni.setStorageSync(STORAGE_KEY, JSON.stringify(defaultList.value))
} catch (e) { }
closeAddPopup()
}
</script>
@ -209,4 +321,126 @@ page {
margin-right: 30rpx;
}
}
/* ===== 添加短信弹窗 ===== */
.add-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
}
.add-popup {
width: 88%;
background-color: #ffffff;
border-radius: 24rpx;
overflow: hidden;
}
.add-header {
padding: 36rpx 40rpx 20rpx;
font-size: 34rpx;
font-weight: bold;
color: #1A1A1A;
text-align: center;
}
.add-body {
padding: 8rpx 0;
}
.add-row {
display: flex;
align-items: center;
padding: 20rpx 32rpx;
gap: 16rpx;
}
.between {
justify-content: space-between;
}
.required {
position: relative;
}
.required::before {
position: absolute;
left: -10px;
content: '*';
top: 0;
color: #EA0000;
}
.add-label {
font-size: 28rpx;
color: #1A1A1A;
width: 150rpx;
flex-shrink: 0;
}
.add-input {
flex: 1;
height: 70rpx;
background-color: #F6F6F6;
border-radius: 14rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #1A1A1A;
::v-deep .uni-input {
color: #aaaaaa;
}
}
.add-footer {
padding: 32rpx 24rpx;
display: flex;
}
.add-btn {
flex: 1;
height: 90rpx;
line-height: 90rpx;
text-align: center;
font-size: 32rpx;
background-color: #F1F1F1;
margin: 0 16rpx;
border-radius: 12rpx;
}
.add-btn.cancel {
color: #767676;
}
.add-btn.confirm {
color: #fff;
background-color: #1777FF;
}
.time-picker-group {
display: flex;
flex-direction: row;
gap: 16rpx;
flex: 1;
}
.time-picker-item {
flex: 1;
height: 70rpx;
line-height: 70rpx;
background-color: #F8F8F8;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #333333;
text-align: center;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B