Files
medical-mall/pages/main/messages.uvue

1118 lines
28 KiB
Plaintext
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="messages-page">
<!-- 顶部过滤栏 -->
<view class="top-filter-bar">
<scroll-view scroll-x class="tabs-scroll" show-scrollbar="false">
<view class="tabs-wrapper">
<view
v-for="tab in messageTabs"
:key="tab.id"
:class="['tab-item', { active: activeTab === tab.id }]"
@click="switchTab(tab.id)"
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="getTabUnread(tab.id) > 0" class="tab-badge">{{ getTabUnread(tab.id) > 99 ? '99+' : getTabUnread(tab.id) }}</text>
<view v-if="activeTab === tab.id" class="tab-active-line"></view>
</view>
</view>
</scroll-view>
<view class="plus-btn" @click.stop="toggleActionMenu">
<text class="plus-icon">+</text>
</view>
<!-- 下拉菜单 -->
<view v-if="showActionMenu" class="action-menu" @click.stop>
<view class="menu-arrow"></view>
<view class="menu-item" @click.stop="onMarkAllRead">
<text class="menu-icon">✓</text>
<text class="menu-text">一键已读</text>
</view>
<view class="menu-item" @click.stop="goBatchDeletePage">
<text class="menu-icon">🗑</text>
<text class="menu-text">会话批量删除</text>
</view>
<view class="menu-item" @click.stop="onMessageSettings">
<text class="menu-icon">⚙</text>
<text class="menu-text">消息设置</text>
</view>
</view>
</view>
<!-- 遮罩层:点击关闭菜单 -->
<view v-if="showActionMenu" class="menu-overlay" @click="closeActionMenu"></view>
<!-- 消息列表 -->
<scroll-view
scroll-y
class="messages-content"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
:scroll-top="scrollTop"
>
<!-- 近期消息(一周内) -->
<view
v-for="message in recentMessages"
:key="message.id"
class="message-row"
@click="handleMessageClick(message)"
>
<view class="message-avatar-wrapper">
<image
v-if="message.avatar != ''"
class="message-avatar"
:src="message.avatar"
mode="aspectFill"
@error="message.avatar = ''"
/>
<view v-else class="message-icon-default" :style="{ backgroundColor: message.color }">
<text class="message-icon-text">{{ message.icon }}</text>
</view>
</view>
<view class="message-body">
<view class="message-header-row">
<view class="message-title-area">
<text class="message-title">{{ message.title }}</text>
<text v-if="message.tagText != ''" class="message-tag">{{ message.tagText }}</text>
</view>
</view>
<text class="message-preview">{{ message.content }}</text>
</view>
<view class="message-meta">
<text class="message-time">{{ message.timeText }}</text>
<view v-if="!message.read && message.unreadCount > 0" class="unread-badge">{{ message.unreadCount > 99 ? '99+' : message.unreadCount }}</view>
<view v-else-if="!message.read" class="unread-dot-small"></view>
<view v-else class="unread-placeholder"></view>
</view>
</view>
<!-- 折叠旧消息 -->
<view v-if="oldMessages.length > 0" class="folded-header" @click="toggleOldMessages">
<text class="folded-text">已折叠7天前的消息</text>
<text class="folded-action">{{ oldMessagesExpanded ? '收起' : '展开' }}</text>
</view>
<!-- 旧消息 -->
<view v-if="oldMessagesExpanded" class="old-section">
<view
v-for="message in oldMessages"
:key="message.id + '_old'"
class="message-row"
@click="handleMessageClick(message)"
>
<view class="message-avatar-wrapper">
<image
v-if="message.avatar != ''"
class="message-avatar"
:src="message.avatar"
mode="aspectFill"
@error="message.avatar = ''"
/>
<view v-else class="message-icon-default" :style="{ backgroundColor: message.color }">
<text class="message-icon-text">{{ message.icon }}</text>
</view>
</view>
<view class="message-body">
<view class="message-header-row">
<view class="message-title-area">
<text class="message-title">{{ message.title }}</text>
<text v-if="message.tagText != ''" class="message-tag">{{ message.tagText }}</text>
</view>
</view>
<text class="message-preview">{{ message.content }}</text>
</view>
<view class="message-meta">
<text class="message-time">{{ message.timeText }}</text>
<view v-if="!message.read && message.unreadCount > 0" class="unread-badge">{{ message.unreadCount > 99 ? '99+' : message.unreadCount }}</view>
<view v-else-if="!message.read" class="unread-dot-small"></view>
<view v-else class="unread-placeholder"></view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="currentListEmpty" class="empty-messages">
<text class="empty-icon">💬</text>
<text class="empty-title">{{ activeTab === 'service' ? '暂无服务消息' : '暂无消息' }}</text>
<text class="empty-desc">暂时没有新消息</text>
</view>
<!-- 猜你喜欢 -->
<GuessYouLike title="猜你喜欢" :pageSize="8" />
<!-- 底部安全区域 -->
<view class="safe-area" style="height: 20px;"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type Notification, type ChatRoom } from '@/utils/supabaseService.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
// 统一消息模型
type UnifiedMessageItem = {
id: string,
conversationKey: string,
sourceType: string,
category: string,
title: string,
content: string,
avatar: string,
icon: string,
color: string,
timeText: string,
timestamp: number,
read: boolean,
unreadCount: number,
isOfficial: boolean,
tagText: string,
merchantId: string,
orderNo: string,
routeType: string,
rawId: string
}
type MessageTab = {
id: string,
name: string
}
// 响应式数据
const activeTab = ref<string>('shopping')
const refreshing = ref<boolean>(false)
const loading = ref<boolean>(false)
const scrollTop = ref<number>(0)
const showActionMenu = ref<boolean>(false)
const oldMessagesExpanded = ref<boolean>(false)
const locallyDeletedIds = reactive<Array<string>>([])
const unifiedMessages = ref<Array<UnifiedMessageItem>>([])
// Tab 定义
const messageTabs = reactive<Array<MessageTab>>([
{ id: 'shopping', name: '购物' },
{ id: 'service', name: '服务' }
])
// 时间格式化:今天 HH:mm / 昨天 / 星期几 / YYYY/MM/DD
const formatMessageTime = (isoString: string): { timeText: string; timestamp: number } => {
if (isoString == '') {
return { timeText: '', timestamp: 0 }
}
let timestamp = 0
try {
timestamp = new Date(isoString).getTime()
} catch (e) {
return { timeText: isoString, timestamp: 0 }
}
if (timestamp <= 0) {
return { timeText: isoString, timestamp: 0 }
}
const nowDate = new Date()
const todayStart = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate()).getTime()
const yesterdayStart = todayStart - 24 * 60 * 60 * 1000
const weekAgo = todayStart - 7 * 24 * 60 * 60 * 1000
let timeText = ''
if (timestamp >= todayStart) {
const dt = new Date(timestamp)
const h = dt.getHours()
const min = dt.getMinutes()
const hh = h < 10 ? '0' + h : '' + h
const mm = min < 10 ? '0' + min : '' + min
timeText = hh + ':' + mm
} else if (timestamp >= yesterdayStart) {
timeText = '昨天'
} else if (timestamp >= weekAgo) {
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
timeText = weekDays[new Date(timestamp).getDay()]
} else {
const dt2 = new Date(timestamp)
const y = dt2.getFullYear()
const mo = dt2.getMonth() + 1
const day = dt2.getDate()
timeText = y + '/' + (mo < 10 ? '0' + mo : '' + mo) + '/' + (day < 10 ? '0' + day : '' + day)
}
return { timeText, timestamp }
}
// 从通知检测分类
const detectCategoryFromNote = (note: Notification): string => {
const type = note.type
const combined = note.title + ' ' + note.content
if (type == 'logistics') return 'logistics'
if (type == 'order') return 'notice'
if (type == 'promotion') return 'promo'
if (type == 'system') return 'notice'
const logisticsKeywords = ['物流', '配送', '快递', '已发货', '已签收']
for (let i = 0; i < logisticsKeywords.length; i++) {
if (combined.indexOf(logisticsKeywords[i]) >= 0) return 'logistics'
}
const tradeKeywords = ['支付', '订单', '退款', '交易', '待付款', '已取消']
for (let i = 0; i < tradeKeywords.length; i++) {
if (combined.indexOf(tradeKeywords[i]) >= 0) return 'notice'
}
const serviceKeywords = ['服务', '预约', '上门', '维修', '安装']
for (let i = 0; i < serviceKeywords.length; i++) {
if (combined.indexOf(serviceKeywords[i]) >= 0) return 'service'
}
if (type == 'promotion') return 'promo'
return 'notice'
}
// 排序全部tab优先物流/交易+未读,其他按时间倒序
const sortMessages = (list: Array<UnifiedMessageItem>): Array<UnifiedMessageItem> => {
const arr: Array<UnifiedMessageItem> = []
for (let i = 0; i < list.length; i++) {
arr.push(list[i])
}
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
const a = arr[j]
const b = arr[j + 1]
let shouldSwap = false
if (activeTab.value == 'all') {
const aIsPriority = (a.sourceType == 'logistics' || a.sourceType == 'trade')
const bIsPriority = (b.sourceType == 'logistics' || b.sourceType == 'trade')
if (aIsPriority != bIsPriority) {
shouldSwap = !aIsPriority && bIsPriority
} else {
const aUnread = a.unreadCount > 0
const bUnread = b.unreadCount > 0
if (aUnread != bUnread) {
shouldSwap = !aUnread && bUnread
} else {
shouldSwap = a.timestamp < b.timestamp
}
}
} else {
shouldSwap = a.timestamp < b.timestamp
}
if (shouldSwap) {
const temp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
return arr
}
// 判断消息是否属于 shopping
const isShoppingMessage = (msg: UnifiedMessageItem): boolean => {
return msg.sourceType == 'chat' || msg.category == 'product' || msg.category == 'notice' || msg.category == 'logistics' || msg.category == 'promo'
}
// 判断消息是否属于 service
const isServiceMessage = (msg: UnifiedMessageItem): boolean => {
return msg.category == 'service'
}
// 过滤+排序后的消息
const filteredMessages = computed<Array<UnifiedMessageItem>>(() => {
const tab = activeTab.value
const matched: Array<UnifiedMessageItem> = []
for (let i = 0; i < unifiedMessages.value.length; i++) {
const msg = unifiedMessages.value[i]
if (locallyDeletedIds.indexOf(msg.id) >= 0) continue
if (tab == 'shopping' && isShoppingMessage(msg)) {
matched.push(msg)
} else if (tab == 'service' && isServiceMessage(msg)) {
matched.push(msg)
}
}
return sortMessages(matched)
})
// 辅助:获取最近两个聊天的 id 列表
const getTopChatIds = (all: Array<UnifiedMessageItem>): Array<string> => {
const topChats: Array<UnifiedMessageItem> = []
for (let i = 0; i < all.length; i++) {
if (all[i].sourceType == 'chat') {
topChats.push(all[i])
}
}
const ids: Array<string> = []
for (let i = 0; i < topChats.length && i < 2; i++) {
ids.push(topChats[i].id)
}
return ids
}
// 近期消息:官方消息(物流/交易/系统)+ 最近两个聊天 + 一周内其他消息
const recentMessages = computed<Array<UnifiedMessageItem>>(() => {
const oneWeek = 7 * 24 * 60 * 60 * 1000
const now = new Date().getTime()
const all = filteredMessages.value
const topChatIds = getTopChatIds(all)
const result: Array<UnifiedMessageItem> = []
for (let i = 0; i < all.length; i++) {
const msg = all[i]
const isOfficialAlwaysShow = (msg.sourceType == 'logistics' || msg.sourceType == 'trade' || msg.sourceType == 'notification')
const isTopChat = topChatIds.indexOf(msg.id) >= 0
const isRecent = msg.timestamp > 0 && (now - msg.timestamp) <= oneWeek
if (isOfficialAlwaysShow || isTopChat || isRecent) {
result.push(msg)
}
}
return result
})
// 旧消息:一周前且非官方且非前两个聊天的消息
const oldMessages = computed<Array<UnifiedMessageItem>>(() => {
const oneWeek = 7 * 24 * 60 * 60 * 1000
const now = new Date().getTime()
const all = filteredMessages.value
const topChatIds = getTopChatIds(all)
const result: Array<UnifiedMessageItem> = []
for (let i = 0; i < all.length; i++) {
const msg = all[i]
const isOfficialAlwaysShow = (msg.sourceType == 'logistics' || msg.sourceType == 'trade' || msg.sourceType == 'notification')
const isTopChat = topChatIds.indexOf(msg.id) >= 0
const isRecent = msg.timestamp > 0 && (now - msg.timestamp) <= oneWeek
if (!isOfficialAlwaysShow && !isTopChat && !isRecent) {
result.push(msg)
}
}
return result
})
// 当前列表是否为空
const currentListEmpty = computed<boolean>(() => {
if (loading.value) return false
return recentMessages.value.length === 0 && oldMessages.value.length === 0
})
// Tab 未读数
const getTabUnread = (tabId: string): number => {
let count = 0
for (let i = 0; i < unifiedMessages.value.length; i++) {
const msg = unifiedMessages.value[i]
if (locallyDeletedIds.indexOf(msg.id) >= 0) continue
if (!msg.read) {
if (tabId == 'shopping' && isShoppingMessage(msg)) {
count = count + msg.unreadCount
} else if (tabId == 'service' && isServiceMessage(msg)) {
count = count + msg.unreadCount
}
}
}
return count
}
// 加载消息
const loadMessages = async () => {
loading.value = true
try {
const dedupedMessages: Array<UnifiedMessageItem> = []
const keyIndexList: Array<{key: string, index: number}> = []
const addOrUpdateMessage = (item: UnifiedMessageItem) => {
let foundIndex = -1
for (let i = 0; i < keyIndexList.length; i++) {
if (keyIndexList[i].key == item.conversationKey) {
foundIndex = keyIndexList[i].index
break
}
}
if (foundIndex >= 0) {
if (item.timestamp > dedupedMessages[foundIndex].timestamp) {
dedupedMessages[foundIndex] = item
}
} else {
keyIndexList.push({ key: item.conversationKey, index: dedupedMessages.length })
dedupedMessages.push(item)
}
}
// 1. 获取通知
const notes = await supabaseService.getUserNotifications()
for (let i = 0; i < notes.length; i++) {
const note = notes[i]
const category = detectCategoryFromNote(note)
let sourceType = 'notification'
if (category == 'logistics') sourceType = 'logistics'
else if (category == 'notice' && note.type == 'order') sourceType = 'trade'
else if (category == 'promo') sourceType = 'promotion'
else if (category == 'service') sourceType = 'service'
let routeType = 'message-detail'
if (note.type == 'order') routeType = 'order-detail'
else if (note.type == 'promotion') routeType = 'coupons'
let orderNo = ''
if (note.extra_data != '') {
try {
const extraObj = JSON.parse(note.extra_data) as UTSJSONObject
const val = extraObj.get('order_id')
if (val != null && typeof val == 'string') {
orderNo = val
} else {
const val2 = extraObj.get('order_no')
if (val2 != null && typeof val2 == 'string') {
orderNo = val2
}
}
} catch (e) {
// ignore parse error
}
}
if (orderNo == '' && note.link_url != '') {
const idx = note.link_url.indexOf('id=')
if (idx >= 0) {
const rest = note.link_url.substring(idx + 3)
const endIdx = rest.indexOf('&')
orderNo = endIdx >= 0 ? rest.substring(0, endIdx) : rest
}
}
const timeInfo = formatMessageTime(note.created_at != null ? note.created_at : '')
const iconMap = new UTSJSONObject()
iconMap.set('logistics', '🚚')
iconMap.set('trade', '📦')
iconMap.set('promotion', '🎁')
iconMap.set('service', '🔧')
iconMap.set('notification', '📢')
const colorMap = new UTSJSONObject()
colorMap.set('logistics', '#07C160')
colorMap.set('trade', '#FF5000')
colorMap.set('promotion', '#FF9500')
colorMap.set('service', '#5856D6')
colorMap.set('notification', '#2196F3')
const iconVal = iconMap.get(sourceType)
const iconStr = (iconVal != null && typeof iconVal == 'string') ? (iconVal as string) : '📢'
const colorVal = colorMap.get(sourceType)
const colorStr = (colorVal != null && typeof colorVal == 'string') ? (colorVal as string) : '#2196F3'
const item: UnifiedMessageItem = {
id: note.id,
conversationKey: 'notification_' + note.type + '_' + note.id,
sourceType: sourceType,
category: category,
title: note.title,
content: note.content,
avatar: note.icon_url != null ? note.icon_url : '',
icon: iconStr,
color: colorStr,
timeText: timeInfo.timeText,
timestamp: timeInfo.timestamp,
read: note.is_read,
unreadCount: note.is_read ? 0 : 1,
isOfficial: (sourceType == 'logistics' || sourceType == 'trade' || sourceType == 'notification'),
tagText: (sourceType == 'logistics' || sourceType == 'trade' || sourceType == 'notification') ? '官方' : '',
merchantId: '',
orderNo: orderNo,
routeType: routeType,
rawId: note.id
}
addOrUpdateMessage(item)
}
// 2. 获取聊天会话
const rooms = await supabaseService.getChatRooms()
for (let i = 0; i < rooms.length; i++) {
const room = rooms[i]
const timeInfo = formatMessageTime(room.last_message_at != '' ? room.last_message_at : (room.updated_at != null ? room.updated_at : ''))
const item: UnifiedMessageItem = {
id: room.id,
conversationKey: 'chat_' + room.merchant_id,
sourceType: 'chat',
category: 'product',
title: room.shop_name,
content: room.last_message != null ? room.last_message : '暂无消息',
avatar: room.shop_logo != null ? room.shop_logo : '',
icon: '🏪',
color: '#FF9800',
timeText: timeInfo.timeText,
timestamp: timeInfo.timestamp,
read: room.unread_count === 0,
unreadCount: room.unread_count,
isOfficial: false,
tagText: '商家客服',
merchantId: room.merchant_id,
orderNo: '',
routeType: 'chat',
rawId: room.id
}
addOrUpdateMessage(item)
}
// 补充默认展示消息:物流、订单、两个聊天
let hasLogistics = false
let hasTrade = false
let chatCount = 0
for (let i = 0; i < dedupedMessages.length; i++) {
const msg = dedupedMessages[i]
if (msg.sourceType == 'logistics') hasLogistics = true
if (msg.sourceType == 'trade') hasTrade = true
if (msg.sourceType == 'chat') chatCount++
}
const now = new Date().getTime()
if (!hasLogistics) {
addOrUpdateMessage({
id: 'default_logistics_1',
conversationKey: 'default_logistics_1',
sourceType: 'logistics',
category: 'logistics',
title: '物流助手',
content: '您的订单已发货预计3天内送达',
avatar: '',
icon: '🚚',
color: '#07C160',
timeText: '刚刚',
timestamp: now,
read: false,
unreadCount: 1,
isOfficial: true,
tagText: '官方',
merchantId: '',
orderNo: 'DEFAULT001',
routeType: 'message-detail',
rawId: 'default_logistics_1'
})
}
if (!hasTrade) {
addOrUpdateMessage({
id: 'default_trade_1',
conversationKey: 'default_trade_1',
sourceType: 'trade',
category: 'notice',
title: '交易通知',
content: '您的订单待付款,请及时支付',
avatar: '',
icon: '📦',
color: '#FF5000',
timeText: '刚刚',
timestamp: now,
read: false,
unreadCount: 1,
isOfficial: true,
tagText: '官方',
merchantId: '',
orderNo: 'DEFAULT002',
routeType: 'order-detail',
rawId: 'default_trade_1'
})
}
while (chatCount < 2) {
chatCount++
const isFirst = chatCount == 1
addOrUpdateMessage({
id: 'default_chat_' + chatCount,
conversationKey: 'chat_default_' + chatCount,
sourceType: 'chat',
category: 'product',
title: isFirst ? '平台客服' : '官方客服',
content: isFirst ? '您好,有什么可以帮您?' : '欢迎咨询,我们随时为您服务',
avatar: '/static/icons/customer-service.png',
icon: '🏪',
color: '#FF9800',
timeText: '刚刚',
timestamp: now,
read: !isFirst,
unreadCount: isFirst ? 1 : 0,
isOfficial: false,
tagText: '商家客服',
merchantId: 'default_' + chatCount,
orderNo: '',
routeType: 'chat',
rawId: 'default_chat_' + chatCount
})
}
// 赋值到 unifiedMessages直接替换避免 reactive 数组追加问题)
unifiedMessages.value = dedupedMessages
} catch (e) {
console.error('加载消息失败', e)
} finally {
loading.value = false
}
}
// 点击消息项
const handleMessageClick = (message: UnifiedMessageItem) => {
// 本地标记已读
message.read = true
message.unreadCount = 0
// 跳转与数据库同步
if (message.sourceType == 'chat') {
supabaseService.markChatRoomRead(message.merchantId)
uni.navigateTo({
url: '/pages/mall/consumer/chat?merchantId=' + message.merchantId + '&merchantName=' + encodeURIComponent(message.title)
})
} else if (message.routeType == 'order-detail') {
supabaseService.markUserNotificationRead(message.rawId)
uni.navigateTo({
url: '/pages/mall/consumer/order-detail?id=' + (message.orderNo != '' ? message.orderNo : message.rawId)
})
} else if (message.routeType == 'coupons') {
supabaseService.markUserNotificationRead(message.rawId)
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
} else {
supabaseService.markUserNotificationRead(message.rawId)
uni.navigateTo({
url: '/pages/mall/consumer/message-detail?id=' + message.rawId + '&type=' + message.sourceType
})
}
}
// Tab 切换
const switchTab = (tabId: string) => {
activeTab.value = tabId
scrollTop.value = scrollTop.value === 0 ? 0.01 : 0
}
// + 菜单
const toggleActionMenu = () => {
console.log('[messages] toggleActionMenu before:', showActionMenu.value)
showActionMenu.value = !showActionMenu.value
console.log('[messages] toggleActionMenu after:', showActionMenu.value)
}
const closeActionMenu = () => {
showActionMenu.value = false
}
// 一键已读
const onMarkAllRead = async () => {
closeActionMenu()
console.log('[messages] onMarkAllRead')
const unreadList = unifiedMessages.value.filter((item: UnifiedMessageItem) => {
return !item.read || item.unreadCount > 0
})
if (unreadList.length === 0) {
uni.showToast({ title: '暂无未读消息', icon: 'none' })
return
}
uni.showLoading({ title: '处理中...' })
try {
const ok = await supabaseService.markAllMessagesRead()
for (let i = 0; i < unifiedMessages.value.length; i++) {
unifiedMessages.value[i].read = true
unifiedMessages.value[i].unreadCount = 0
}
uni.showToast({ title: '已全部标为已读', icon: 'success' })
} catch (error) {
console.error('[messages] 一键已读失败:', error)
uni.showToast({ title: '一键已读失败,请稍后重试', icon: 'none' })
} finally {
uni.hideLoading()
}
}
// 消息设置
const onMessageSettings = () => {
closeActionMenu()
console.log('[messages] onMessageSettings')
uni.showToast({ title: '消息设置开发中', icon: 'none' })
}
// 跳转批量删除页面
const goBatchDeletePage = () => {
closeActionMenu()
console.log('[messages] goBatchDeletePage tab:', activeTab.value)
uni.navigateTo({
url: '/pages/mall/consumer/message-batch-delete?tab=' + activeTab.value
})
}
// 展开/收起旧消息
const toggleOldMessages = () => {
oldMessagesExpanded.value = !oldMessagesExpanded.value
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadMessages()
refreshing.value = false
}, 800)
}
// 生命周期
onMounted(() => {
loadMessages()
})
onShow(() => {
loadMessages()
})
</script>
<style>
/* 页面基础 */
.messages-page {
width: 100%;
min-height: 100vh;
background-color: #fefefe;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* 顶部过滤栏 */
.top-filter-bar {
display: flex;
flex-direction: row;
align-items: center;
background-color: #fefefe;
border-bottom: 1px solid #f2f2f2;
height: 44px;
padding: 0 12px;
position: relative;
z-index: 9000;
flex-shrink: 0;
overflow: visible;
}
.tabs-scroll {
flex: 1;
height: 100%;
}
.tabs-wrapper {
display: flex;
flex-direction: row;
align-items: center;
height: 100%;
}
.tab-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 10px;
position: relative;
}
.tab-name {
font-size: 14px;
color: #333;
white-space: nowrap;
}
.tab-item.active .tab-name {
color: #ff3b30;
font-weight: 600;
}
.tab-active-line {
position: absolute;
bottom: 0;
left: calc(50% - 10px);
width: 20px;
height: 2px;
background-color: #ff3b30;
border-radius: 1px;
}
.tab-badge {
background-color: #ff3b30;
color: #fff;
font-size: 10px;
padding: 0 4px;
border-radius: 8px;
min-width: 14px;
height: 14px;
line-height: 14px;
text-align: center;
font-weight: bold;
margin-left: 4px;
}
/* + 按钮 */
.plus-btn {
width: 40px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 4px;
flex-shrink: 0;
z-index: 9020;
}
.plus-icon {
font-size: 24px;
color: #222222;
line-height: 24px;
}
/* 下拉菜单 */
.action-menu {
position: absolute;
right: 8px;
top: 40px;
width: 168px;
background-color: #222222;
border-radius: 10px;
padding-top: 6px;
padding-bottom: 6px;
z-index: 9050;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}
.menu-arrow {
position: absolute;
right: 18px;
top: -6px;
width: 12px;
height: 12px;
background-color: #222222;
transform: rotate(45deg);
}
.menu-item {
height: 44px;
padding-left: 14px;
padding-right: 14px;
display: flex;
flex-direction: row;
align-items: center;
}
.menu-icon {
width: 24px;
font-size: 15px;
color: #ffffff;
}
.menu-text {
font-size: 14px;
color: #ffffff;
}
.menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0);
z-index: 8990;
}
/* 消息列表区 */
.messages-content {
flex: 1;
height: 0;
width: 100%;
background-color: #fefefe;
}
/* 消息行 */
.message-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #f5f5f5;
}
.message-row:active {
opacity: 0.7;
}
/* 头像区域 */
.message-avatar-wrapper {
width: 48px;
height: 48px;
border-radius: 8px;
position: relative;
flex-shrink: 0;
margin-right: 12px;
}
.message-avatar {
width: 48px;
height: 48px;
border-radius: 8px;
}
.message-icon-default {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.message-icon-text {
font-size: 24px;
}
/* 消息主体 */
.message-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.message-header-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 4px;
}
.message-title-area {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
min-width: 0;
}
.message-title {
font-size: 16px;
color: #1a1a1a;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-tag {
font-size: 10px;
color: #ff3b30;
border: 1px solid #ff3b30;
padding: 0 4px;
border-radius: 2px;
margin-left: 6px;
flex-shrink: 0;
}
.message-preview {
font-size: 13px;
color: #8a8a8a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 右侧元信息 */
.message-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 8px;
flex-shrink: 0;
min-width: 40px;
}
.message-time {
font-size: 12px;
color: #999;
white-space: nowrap;
}
.unread-badge {
background-color: #ff3b30;
color: #fff;
font-size: 10px;
padding: 0 5px;
height: 16px;
line-height: 16px;
border-radius: 999px;
min-width: 16px;
text-align: center;
font-weight: bold;
margin-top: 4px;
}
.unread-dot-small {
width: 8px;
height: 8px;
border-radius: 4px;
background-color: #ff3b30;
margin-top: 6px;
}
.unread-placeholder {
height: 16px;
margin-top: 4px;
}
/* 折叠区 */
.folded-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 14px;
background-color: #fefefe;
}
.folded-text {
font-size: 13px;
color: #999;
}
.folded-action {
font-size: 13px;
color: #576b95;
margin-left: 8px;
}
.old-section {
background-color: #fff;
}
/* 空状态 */
.empty-messages {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 16px;
opacity: 0.3;
}
.empty-title {
font-size: 16px;
color: #333;
font-weight: bold;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #999;
}
.safe-area {
width: 100%;
}
</style>