Files
QSY-lvjuxcx/pages/index/component/MessageCenterTab.vue
2026-06-29 09:42:53 +08:00

1081 lines
35 KiB
Vue

<template>
<view class="message-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="false" :bottomShadow="false" backgroundColor="transparent" :customBackStyle="{ transition: 'all 0.3s ease' }">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('title') }}</view>
</tn-nav-bar>
<view class="message-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="page-header tn-margin-bottom-xl">
<view class="tn-text-bold tn-color-black page-title">{{ t('title') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ subtitleText }}</view>
</view>
<view class="msg-category tn-flex tn-flex-row-between tn-margin-bottom">
<view class="category-item tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="category-icon-box active-box">
<text class="tn-icon-notice-fill tn-text-xxl"></text>
<view v-if="stats.systemUnread" class="badge tn-flex tn-flex-col-center tn-flex-row-center">{{ stats.systemUnread }}</view>
</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-black">{{ t('system') }}</view>
</view>
<view class="category-item tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="category-icon-box normal-box">
<text class="tn-icon-coupon-fill tn-text-xxl"></text>
<view v-if="stats.paymentUnread" class="badge tn-flex tn-flex-col-center tn-flex-row-center">{{ stats.paymentUnread }}</view>
</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-gray">{{ t('payment') }}</view>
</view>
<view class="category-item tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="category-icon-box normal-box">
<text class="tn-icon-service-fill tn-text-xxl"></text>
<view v-if="stats.butlerUnread" class="badge tn-flex tn-flex-col-center tn-flex-row-center">{{ stats.butlerUnread }}</view>
</view>
<view class="tn-text-sm tn-margin-top-sm tn-text-bold tn-color-gray">{{ t('butler') }}</view>
</view>
</view>
<view v-if="inspectionList.length" class="section-card tn-margin-bottom">
<view class="section-head">
<view>
<view class="section-title">{{ t('inspectionTitle') }}</view>
<view class="section-desc">{{ t('inspectionDesc') }}</view>
</view>
<view class="section-chip is-primary">{{ inspectionReminderCount }} {{ t('inspectionCountUnit') }}</view>
</view>
<view
v-for="(record, index) in inspectionList"
:key="record.code || index"
class="inspection-card tn-margin-top-sm"
@click="openInspectionPopup(record)"
>
<view class="inspection-head">
<view class="inspection-main-wrap">
<view class="inspection-icon">
<text class="tn-icon-home-fill tn-color-white"></text>
</view>
<view class="inspection-main">
<view class="inspection-title">{{ record.typeText }}</view>
<view class="inspection-meta">{{ record.roomCode || t('unassignedRoom') }} · {{ record.timeText }}</view>
</view>
</view>
<view class="inspection-chips">
<view class="inspection-status-chip" :class="record.resultClass">{{ record.resultText }}</view>
<view class="inspection-status-chip" :class="record.statusClass">{{ record.statusText }}</view>
</view>
</view>
<view class="inspection-desc">{{ record.summaryText }}</view>
<view class="inspection-footer">
<view class="inspection-footer-text">{{ t('checkerLabel') }}{{ record.checkerUserCode || '--' }}</view>
<view class="inspection-action">{{ t('viewDetail') }}</view>
</view>
</view>
</view>
<view class="tn-flex tn-flex-row-between tn-flex-col-bottom tn-margin-bottom">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('recentMessages') }}</view>
<view class="tn-text-sm tn-color-gray" @click="handleReadAll">
<text class="tn-icon-delete tn-margin-right-xs"></text>{{ t('markNotificationRead') }}
</view>
</view>
<view class="msg-list">
<view v-if="loading" class="empty-card">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('loadingTitle') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ t('loadingDesc') }}</view>
</view>
<view
v-for="(msg, index) in messages"
:key="msg.id || msg.code || index"
class="msg-card tn-flex tn-flex-col-center tn-padding tn-margin-bottom-sm"
@click="openMessage(msg)"
>
<view class="msg-avatar tn-flex tn-flex-col-center tn-flex-row-center" :class="msg.unread ? 'avatar-unread' : ''">
<text :class="msg.icon" class="tn-text-xl tn-color-white"></text>
<view v-if="msg.unread" class="unread-dot"></view>
</view>
<view class="msg-info tn-flex-1 tn-margin-left">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1 msg-title">{{ msg.title }}</view>
<view class="tn-color-gray tn-text-xs">{{ msg.time }}</view>
</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs clamp-text-1">{{ msg.desc }}</view>
</view>
</view>
<view v-if="!loading && !messages.length && !inspectionList.length" class="empty-card">
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('emptyTitle') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ t('emptyDesc') }}</view>
</view>
</view>
<view style="height: 100rpx;"></view>
</view>
<view v-if="inspectionPopupVisible && activeInspectionRecord" class="inspect-popup">
<view class="inspect-popup-mask" @click="closeInspectionPopup"></view>
<view class="inspect-popup-panel">
<view class="inspect-popup-head">
<view>
<view class="inspect-popup-title">{{ activeInspectionRecord.typeText }}</view>
<view class="inspect-popup-subtitle">{{ activeInspectionRecord.roomCode || t('unassignedRoom') }} · {{ activeInspectionRecord.timeText }}</view>
</view>
<view class="inspect-popup-close" @click="closeInspectionPopup">
<text class="tn-icon-close"></text>
</view>
</view>
<scroll-view scroll-y class="inspect-popup-body">
<view class="inspect-detail-card">
<view class="inspect-detail-row">
<text class="inspect-detail-label">{{ t('inspectionChecker') }}</text>
<text class="inspect-detail-value">{{ activeInspectionRecord.checkerUserCode || '--' }}</text>
</view>
<view class="inspect-detail-row">
<text class="inspect-detail-label">{{ t('inspectionResult') }}</text>
<text class="inspect-detail-value">{{ activeInspectionRecord.resultText }}</text>
</view>
<view class="inspect-detail-row">
<text class="inspect-detail-label">{{ t('inspectionStatus') }}</text>
<text class="inspect-detail-value">{{ activeInspectionRecord.statusText }}</text>
</view>
</view>
<view v-if="activeInspectionRecord.remark" class="inspect-block">
<view class="inspect-block-title">{{ t('inspectionRemarkTitle') }}</view>
<view class="inspect-block-content">{{ activeInspectionRecord.remark }}</view>
</view>
<view v-if="activeInspectionRecord.items.length" class="inspect-block">
<view class="inspect-block-title">{{ t('inspectionItemsTitle') }}</view>
<view
v-for="(detail, index) in activeInspectionRecord.items"
:key="detail.code || index"
class="inspect-item"
>
<view class="inspect-item-head">
<view class="inspect-item-title">{{ t('inspectionItemPrefix', { index: index + 1 }) }}</view>
<view class="inspect-item-tag">{{ getCheckTypeLabel(detail.checkType) }}</view>
</view>
<view v-if="detail.problemDescription" class="inspect-item-text">{{ t('problemLabel') }}{{ detail.problemDescription }}</view>
<view v-if="detail.remark" class="inspect-item-text">{{ t('remarkLabel') }}{{ detail.remark }}</view>
<view v-if="detail.compensationAmount !== '' && detail.compensationAmount !== null && detail.compensationAmount !== undefined" class="inspect-item-text">
{{ t('compensationLabel') }}{{ detail.compensationAmount }}
</view>
<view v-if="detail.imageUrls.length" class="inspect-image-list">
<image
v-for="(url, imageIndex) in detail.imageUrls"
:key="`${detail.code || index}-${imageIndex}`"
class="inspect-image"
:src="url"
mode="aspectFill"
@click.stop="previewImages(detail.imageUrls, imageIndex)"
></image>
</view>
</view>
</view>
</scroll-view>
<view class="inspect-popup-foot">
<view class="inspect-popup-btn" @click="closeInspectionPopup">{{ t('acknowledged') }}</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getSessionSnapshot } from '../../../utils/app-state'
import { checkRecordByChecked, notificationPage, notificationRead } from '@/api/index.js'
import {
countPendingInspectionRecords,
extractInspectionList,
formatMessageDateTime,
isPendingInspectionRecord,
resolveNotificationList,
resolveNotificationUnreadTotal
} from '../../../utils/message-helper'
import { compactEnglishText } from '../../../utils/select-room-normalizers'
import { saveMessageDetailPayload, saveMessageListSnapshot } from '../../../utils/messages'
export default {
name: 'MessageCenterTab',
data() {
return {
loading: false,
messages: [],
remoteMessages: [],
remoteUnreadCount: 0,
inspectionList: [],
inspectionPopupVisible: false,
activeInspectionRecord: null,
stats: {
totalUnread: 0,
systemUnread: 0,
paymentUnread: 0,
butlerUnread: 0
},
i18n: {
zh: {
title: '消息中心',
subtitle: '你有 {count} 条未读消息待处理',
system: '系统通知',
payment: '缴费账单',
butler: '管家消息',
inspectionTitle: '巡查提醒',
inspectionDesc: '根据当前入住订单自动汇总巡查提醒记录',
inspectionCountUnit: '条提醒',
recentMessages: '近期消息',
markNotificationRead: '通知已读',
loadingTitle: '消息加载中',
loadingDesc: '正在同步系统通知和巡查提醒,请稍候。',
emptyTitle: '暂无消息',
emptyDesc: '系统通知和巡查提醒会在这里显示。',
noUnreadNotification: '暂无未读通知',
inspectionNeedManualHandle: '巡查提醒需单独查看处理',
remoteMarkedRead: '系统通知已全部标记为已读',
remoteMarkedReadWithInspection: '系统通知已标记,巡查提醒需单独处理',
unassignedRoom: '未关联房间',
checkerLabel: '检查人:',
viewDetail: '查看详情',
inspectionChecker: '检查人',
inspectionResult: '检查结果',
inspectionStatus: '当前状态',
inspectionRemarkTitle: '检查备注',
inspectionItemsTitle: '检查项明细',
inspectionItemPrefix: '检查项 {index}',
problemLabel: '问题:',
remarkLabel: '备注:',
compensationLabel: '赔付金额:',
acknowledged: '我知道了',
notificationFallbackTitle: '系统通知',
notificationFallbackDesc: '您有一条新的系统消息',
inspectionSummaryFallback: '巡查提醒,请查看详情',
checkTypeRoutine: '日常检查',
checkTypeCheckout: '退房检查',
checkTypeSpecial: '专项检查',
checkTypeDefault: '检查记录',
checkStatusPending: '待确认',
checkStatusProcessing: '处理中',
checkStatusDone: '已完成',
checkStatusDefault: '状态待定',
checkResultNormal: '正常',
checkResultFix: '需整改',
checkResultReview: '待复核',
checkResultDefault: '检查结果'
},
en: {
title: 'Messages',
subtitle: 'You have {count} unread messages',
system: 'System',
payment: 'Payment',
butler: 'Butler',
inspectionTitle: 'Inspection',
inspectionDesc: 'Inspection reminders are summarized automatically.',
inspectionCountUnit: 'alerts',
recentMessages: 'Recent',
markNotificationRead: 'Read Notifications',
loadingTitle: 'Loading Messages',
loadingDesc: 'Syncing notifications and inspections.',
emptyTitle: 'No Messages',
emptyDesc: 'Notifications and inspection reminders will appear here.',
noUnreadNotification: 'No unread notifications',
inspectionNeedManualHandle: 'Inspection reminders need manual handling',
remoteMarkedRead: 'Notifications marked as read',
remoteMarkedReadWithInspection: 'Notifications marked as read, inspection reminders still need handling',
unassignedRoom: 'Room Unlinked',
checkerLabel: 'Inspector: ',
viewDetail: 'Details',
inspectionChecker: 'Inspector',
inspectionResult: 'Result',
inspectionStatus: 'Status',
inspectionRemarkTitle: 'Inspection Remark',
inspectionItemsTitle: 'Inspection Items',
inspectionItemPrefix: 'Item {index}',
problemLabel: 'Problem: ',
remarkLabel: 'Remark: ',
compensationLabel: 'Compensation: ',
acknowledged: 'Got It',
notificationFallbackTitle: 'System Notification',
notificationFallbackDesc: 'You have a new system message',
inspectionSummaryFallback: 'Inspection reminder, tap to view details.',
checkTypeRoutine: 'Routine Check',
checkTypeCheckout: 'Checkout Check',
checkTypeSpecial: 'Special Check',
checkTypeDefault: 'Inspection Record',
checkStatusPending: 'Pending',
checkStatusProcessing: 'Processing',
checkStatusDone: 'Completed',
checkStatusDefault: 'Status Pending',
checkResultNormal: 'Normal',
checkResultFix: 'Needs Fix',
checkResultReview: 'Pending Review',
checkResultDefault: 'Inspection Result'
}
}
}
},
computed: {
language() {
return this.$getLanguage()
},
subtitleText() {
const text = this.t('subtitle').replace('{count}', this.stats.totalUnread)
return this.language === 'en' ? compactEnglishText(text, 48) : text
},
inspectionReminderCount() {
return countPendingInspectionRecords(this.inspectionList)
}
},
watch: {
language() {
this.refreshMessages(true)
}
},
onShow() {
this.load()
},
methods: {
load(force = false) {
return this.refreshMessages(force)
},
t(key, params) {
return this.$tm('pages.index.component.MessageCenterTab', key, params)
},
getStoreUser() {
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
},
getStoreCurrentOrder() {
return (this.$store && this.$store.state && this.$store.state.vuex_current_order) || null
},
isCurrentLogin() {
const storeUser = this.getStoreUser()
if (storeUser && Object.keys(storeUser).length) {
return true
}
return !!getSessionSnapshot().isLogin
},
resetMessageState() {
this.messages = []
this.remoteMessages = []
this.remoteUnreadCount = 0
this.inspectionList = []
this.inspectionPopupVisible = false
this.activeInspectionRecord = null
this.stats = {
totalUnread: 0,
systemUnread: 0,
paymentUnread: 0,
butlerUnread: 0
}
uni.$emit('messageBadgeCountChange', 0)
},
getCurrentUserCode() {
const session = getSessionSnapshot()
const storeUser = this.getStoreUser()
const user = Object.keys(storeUser).length
? storeUser
: (session.currentUser || session.registeredUser || session.resolvedUser || {})
return user.code || user.userCode || ''
},
getCurrentOrderNo() {
const session = getSessionSnapshot()
const room = session.myRoomData || {}
const user = session.currentUser || session.registeredUser || session.resolvedUser || {}
const currentOrder = this.getStoreCurrentOrder() || (user && user.currentOrder) || {}
return String(room.orderNo || currentOrder.orderNo || '').trim()
},
isPendingInspectionRecord(record) {
return isPendingInspectionRecord(record)
},
refreshMessages(force = false) {
if (!this.isCurrentLogin()) {
this.resetMessageState()
return Promise.resolve()
}
const now = Date.now()
if (!force && this._refreshMessagesPromise) {
return this._refreshMessagesPromise
}
if (!force && this._lastRefreshMessagesAt && now - this._lastRefreshMessagesAt < 600) {
return Promise.resolve()
}
this._lastRefreshMessagesAt = now
this.loading = true
this._refreshMessagesPromise = Promise.all([
this.fetchRemoteMessages(),
this.fetchInspectionList()
])
.catch((error) => {
console.error('refresh messages failed', error)
})
.finally(() => {
this.buildMessageView()
this.loading = false
this._refreshMessagesPromise = null
})
return this._refreshMessagesPromise
},
async fetchRemoteMessages() {
const [listResponse, unreadResponse] = await Promise.all([
notificationPage({
PageIndex: 1,
PageSize: 20
}, this.language).catch(() => []),
notificationPage({
IsRead: false,
PageIndex: 1,
PageSize: 1,
Search: '',
IsExport: false
}, this.language).catch(() => [])
])
this.remoteMessages = resolveNotificationList(listResponse).map(item => this.normalizeRemoteMessage(item))
this.remoteUnreadCount = resolveNotificationUnreadTotal(unreadResponse)
},
fetchInspectionList() {
const checkedUserCode = this.getCurrentUserCode()
const orderNo = this.getCurrentOrderNo()
if (!orderNo) {
this.inspectionList = []
return Promise.resolve()
}
return checkRecordByChecked({
checkedUserCode,
orderNo
}, this.language)
.then((response) => {
const list = extractInspectionList(response)
this.inspectionList = list
.map(item => this.normalizeInspectionRecord(item))
.sort((prev, next) => new Date(next.rawCreateTime || 0).getTime() - new Date(prev.rawCreateTime || 0).getTime())
})
.catch((error) => {
console.error('fetch inspection list failed', error)
this.inspectionList = []
})
},
normalizeRemoteMessage(item = {}) {
const rawTitleZh = item.titleZh || item.title || ''
const rawTitleEn = item.titleEn || item.title || item.titleZh || ''
const rawDescZh = item.content || item.desc || ''
const rawDescEn = item.contentEn || item.content || item.desc || ''
const rawTitle = this.language === 'zh' ? rawTitleZh : rawTitleEn
const rawDesc = this.language === 'zh' ? rawDescZh : rawDescEn
const title = this.language === 'en' ? compactEnglishText(rawTitle, 28) : rawTitle
const desc = this.language === 'en' ? compactEnglishText(rawDesc, 60) : rawDesc
return {
id: item.code || item.id || `remote-${Date.now()}`,
code: item.code || '',
category: this.resolveRemoteCategory(item, title, desc),
titleZh: rawTitleZh || this.t('notificationFallbackTitle'),
titleEn: rawTitleEn || this.t('notificationFallbackTitle'),
descZh: rawDescZh || this.t('notificationFallbackDesc'),
descEn: rawDescEn || this.t('notificationFallbackDesc'),
contentZh: rawDescZh || this.t('notificationFallbackDesc'),
contentEn: rawDescEn || this.t('notificationFallbackDesc'),
title: title || this.t('notificationFallbackTitle'),
desc: desc || this.t('notificationFallbackDesc'),
content: desc || this.t('notificationFallbackDesc'),
time: formatMessageDateTime(item.createTime || item.updateTime || ''),
rawTime: item.createTime || item.updateTime || '',
unread: item.isRead !== true,
icon: this.resolveRemoteIcon(item, title, desc),
actionUrl: item.path || item.actionUrl || '',
path: item.path || item.actionUrl || '',
source: 'remote'
}
},
normalizeInspectionRecord(record = {}) {
const items = Array.isArray(record.items)
? record.items.map(item => ({
code: item && item.code ? item.code : '',
remark: item && item.remark ? item.remark : '',
checkType: item && item.checkType !== undefined ? item.checkType : '',
problemDescription: item && item.problemDescription ? item.problemDescription : '',
compensationAmount: item && item.compensationAmount,
imageUrls: Array.isArray(item && item.imageUrls) ? item.imageUrls.filter(Boolean) : []
}))
: []
const issueTexts = items.map(item => item.problemDescription || item.remark || '').filter(Boolean)
const summaryText = record.remark || issueTexts[0] || this.t('inspectionSummaryFallback')
return {
code: record.code || '',
roomCode: record.roomCode || '',
checkedUserCode: record.checkedUserCode || '',
checkerUserCode: record.checkerUserCode || '',
checkedUserAgree: record.checkedUserAgree,
status: record.status,
resultStatus: record.resultStatus,
remark: record.remark || '',
items,
typeText: this.getCheckTypeLabel(record.checkType),
resultText: this.getCheckResultStatusLabel(record.resultStatus),
resultClass: this.getCheckResultStatusClass(record.resultStatus),
statusText: this.getCheckRecordStatusLabel(record.status),
statusClass: this.getCheckRecordStatusClass(record.status),
summaryText,
rawCreateTime: record.createTime || '',
timeText: formatMessageDateTime(record.createTime),
pending: this.isPendingInspectionRecord(record)
}
},
resolveRemoteCategory(item = {}, title = '', desc = '') {
const text = `${item.category || ''} ${title} ${desc}`.toLowerCase()
if (/(充值|账单|支付|payment|bill|fee)/i.test(text)) {
return 'payment'
}
if (/(管家|butler|service|报修|入住|合同|申请)/i.test(text)) {
return 'butler'
}
return 'system'
},
resolveRemoteIcon(item = {}, title = '', desc = '') {
const category = this.resolveRemoteCategory(item, title, desc)
if (category === 'payment') {
return 'tn-icon-coupon-fill'
}
if (category === 'butler') {
return 'tn-icon-service-fill'
}
return 'tn-icon-notice-fill'
},
getCheckTypeLabel(value) {
const map = {
0: this.t('checkTypeRoutine'),
1: this.t('checkTypeCheckout'),
2: this.t('checkTypeSpecial')
}
return map[Number(value)] || this.t('checkTypeDefault')
},
getCheckRecordStatusLabel(value) {
const map = {
0: this.t('checkStatusPending'),
1: this.t('checkStatusProcessing'),
2: this.t('checkStatusDone')
}
return map[Number(value)] || this.t('checkStatusDefault')
},
getCheckResultStatusLabel(value) {
const map = {
0: this.t('checkResultNormal'),
1: this.t('checkResultFix'),
2: this.t('checkResultReview')
}
return map[Number(value)] || this.t('checkResultDefault')
},
getCheckResultStatusClass(value) {
const map = {
0: 'is-success',
1: 'is-warning',
2: 'is-primary'
}
return map[Number(value)] || 'is-muted'
},
getCheckRecordStatusClass(value) {
const map = {
0: 'is-warning',
1: 'is-primary',
2: 'is-success'
}
return map[Number(value)] || 'is-muted'
},
buildMessageView() {
const remoteStats = this.getRemoteStats()
const inspectionPendingCount = this.inspectionReminderCount
this.messages = [...this.remoteMessages].sort((prev, next) => {
return new Date(next.rawTime || 0).getTime() - new Date(prev.rawTime || 0).getTime()
})
saveMessageListSnapshot(this.messages, this.language)
this.stats = {
totalUnread: remoteStats.totalUnread + inspectionPendingCount,
systemUnread: remoteStats.systemUnread,
paymentUnread: remoteStats.paymentUnread,
butlerUnread: remoteStats.butlerUnread + inspectionPendingCount
}
uni.$emit('messageBadgeCountChange', this.stats.totalUnread)
},
getRemoteStats() {
const unreadMessages = this.remoteMessages.filter(item => item.unread)
return {
totalUnread: this.remoteUnreadCount || unreadMessages.length,
systemUnread: unreadMessages.filter(item => item.category === 'system').length,
paymentUnread: unreadMessages.filter(item => item.category === 'payment').length,
butlerUnread: unreadMessages.filter(item => item.category === 'butler').length
}
},
async handleReadAll() {
if (!this.isCurrentLogin()) {
this.resetMessageState()
return
}
const unreadRemoteCodes = this.remoteMessages.filter(item => item.unread && item.code).map(item => item.code)
if (!unreadRemoteCodes.length) {
uni.showToast({
title: this.inspectionReminderCount
? this.t('inspectionNeedManualHandle')
: this.t('noUnreadNotification'),
icon: 'none'
})
return
}
if (unreadRemoteCodes.length) {
await Promise.all(unreadRemoteCodes.map(code => {
return notificationRead(code, this.language).catch(error => {
console.error('mark remote message read failed', error)
return null
})
}))
}
this.remoteMessages = this.remoteMessages.map(item => ({
...item,
unread: false
}))
this.remoteUnreadCount = 0
this.buildMessageView()
uni.showToast({
title: this.inspectionReminderCount
? this.t('remoteMarkedReadWithInspection')
: this.t('remoteMarkedRead'),
icon: 'none'
})
},
openMessage(msg) {
if (!this.isCurrentLogin()) {
this.resetMessageState()
return
}
if (msg.unread && msg.source === 'remote' && msg.code) {
return notificationRead(msg.code, this.language)
.then(() => {
this.remoteMessages = this.remoteMessages.map(item => ({
...item,
unread: item.id === msg.id ? false : item.unread
}))
this.remoteUnreadCount = Math.max(this.remoteUnreadCount - 1, 0)
})
.catch((error) => {
console.error('read remote message failed', error)
})
.finally(() => {
this.navigateToMessageDetail(msg)
})
}
this.navigateToMessageDetail(msg)
},
navigateToMessageDetail(msg) {
this.buildMessageView()
const detailPayload = {
id: msg.id || msg.code || '',
titleZh: msg.titleZh || msg.title || '',
titleEn: msg.titleEn || msg.title || '',
descZh: msg.descZh || msg.desc || '',
descEn: msg.descEn || msg.desc || '',
contentZh: msg.contentZh || msg.content || msg.desc || '',
contentEn: msg.contentEn || msg.content || msg.desc || '',
title: msg.title || '',
desc: msg.desc || '',
content: msg.content || msg.desc || '',
time: msg.time || '',
category: msg.category || 'system',
source: msg.source || 'remote',
code: msg.code || '',
path: msg.actionUrl || msg.path || ''
}
saveMessageDetailPayload(detailPayload, this.language)
uni.navigateTo({
url: `/pkgService/message_detail/message_detail?id=${encodeURIComponent(msg.id || msg.code || '')}`
})
},
openInspectionPopup(record) {
this.activeInspectionRecord = record
this.inspectionPopupVisible = true
},
closeInspectionPopup() {
this.inspectionPopupVisible = false
this.activeInspectionRecord = null
},
previewImages(urls, currentIndex = 0) {
const safeUrls = Array.isArray(urls) ? urls.filter(Boolean) : []
if (!safeUrls.length) {
return
}
uni.previewImage({
urls: safeUrls,
current: safeUrls[currentIndex] || safeUrls[0]
})
}
}
}
</script>
<style scoped lang="scss">
.message-page {
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
min-height: 100vh;
}
.page-header {
padding-top: 20rpx;
}
.page-title {
font-size: 56rpx;
letter-spacing: 2rpx;
line-height: 1.25;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.msg-category {
padding: 0 20rpx;
}
.category-icon-box {
width: 120rpx;
height: 120rpx;
border-radius: 40rpx;
display: flex;
justify-content: center;
align-items: center;
position: relative;
transition: all 0.2s;
}
.active-box {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #FFFFFF;
box-shadow: 0 10rpx 20rpx rgba($brand-primary, 0.25);
}
.normal-box {
background-color: #FFFFFF;
color: $brand-primary;
border: 1rpx solid rgba($brand-primary, 0.15);
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
}
.badge {
position: absolute;
top: -10rpx;
right: -10rpx;
background-color: #FF5A5F;
color: #FFFFFF;
font-size: 22rpx;
width: 40rpx;
height: 40rpx;
border-radius: 20rpx;
font-weight: bold;
border: 4rpx solid #FFFFFF;
}
.section-card,
.msg-card,
.empty-card,
.inspect-detail-card,
.inspect-block,
.inspect-item {
background-color: #FFFFFF;
border-radius: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
}
.section-card,
.empty-card {
padding: 32rpx;
}
.section-head,
.inspection-head,
.inspection-footer,
.inspect-detail-row,
.inspect-item-head {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-title,
.inspect-popup-title,
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: #111827;
}
.section-desc,
.inspection-meta,
.inspection-desc,
.inspection-footer-text,
.empty-desc,
.inspect-popup-subtitle,
.inspect-detail-label,
.inspect-block-content,
.inspect-item-text {
color: #6B7280;
font-size: 24rpx;
line-height: 1.6;
}
.section-chip {
padding: 8rpx 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
font-weight: 700;
}
.section-chip.is-primary {
color: $brand-primary;
background: rgba($brand-primary, 0.1);
border: 1rpx solid rgba($brand-primary, 0.2);
}
.inspection-card {
padding: 24rpx;
background: rgba($brand-primary, 0.02);
border-radius: 16rpx;
border: 1rpx solid rgba($brand-primary, 0.05);
}
.inspection-main-wrap {
display: flex;
align-items: center;
flex: 1;
}
.inspection-icon {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
margin-right: 18rpx;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.2);
}
.inspection-title,
.inspect-item-title {
font-size: 28rpx;
font-weight: 700;
color: #111827;
}
.inspection-chips {
display: flex;
align-items: center;
gap: 10rpx;
margin-left: 16rpx;
}
.inspection-status-chip,
.inspect-item-tag {
padding: 8rpx 16rpx;
border-radius: 999rpx;
font-size: 20rpx;
font-weight: 700;
}
.is-success {
color: #15803d;
background: rgba(34, 197, 94, 0.12);
}
.is-warning {
color: #b45309;
background: rgba(245, 158, 11, 0.16);
}
.is-primary {
color: $brand-primary;
background: rgba($brand-primary, 0.12);
}
.is-muted {
color: #64748b;
background: rgba(148, 163, 184, 0.14);
}
.inspection-desc,
.inspection-footer {
margin-top: 16rpx;
}
.inspection-action {
color: $brand-primary;
font-size: 24rpx;
font-weight: 700;
}
.msg-card {
transition: transform 0.2s;
}
.msg-card:active {
transform: scale(0.98);
}
.msg-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 24rpx;
background-color: rgba($brand-primary, 0.2);
position: relative;
color: $brand-primary;
}
.avatar-unread {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #FFFFFF;
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.2);
}
.msg-avatar .tn-text-xl {
color: inherit !important;
}
.unread-dot {
position: absolute;
top: -6rpx;
right: -6rpx;
width: 20rpx;
height: 20rpx;
border-radius: 10rpx;
background-color: #FF5A5F;
border: 4rpx solid #FFFFFF;
}
.msg-title {
max-width: 350rpx;
}
.clamp-text-1 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.inspect-popup {
position: fixed;
inset: 0;
z-index: 1000;
}
.inspect-popup-mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.45);
}
.inspect-popup-panel {
position: absolute;
left: 24rpx;
right: 24rpx;
top: 8vh;
bottom: 8vh;
background: #ffffff;
border-radius: 32rpx;
padding: 32rpx;
display: flex;
flex-direction: column;
}
.inspect-popup-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.inspect-popup-close {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(148, 163, 184, 0.12);
display: flex;
align-items: center;
justify-content: center;
}
.inspect-popup-body {
flex: 1;
margin-top: 24rpx;
}
.inspect-detail-card,
.inspect-block,
.inspect-item {
padding: 24rpx;
}
.inspect-detail-row + .inspect-detail-row,
.inspect-item-text + .inspect-item-text {
margin-top: 16rpx;
}
.inspect-detail-value,
.inspect-block-title {
font-size: 26rpx;
color: #111827;
font-weight: 700;
}
.inspect-block {
margin-top: 20rpx;
}
.inspect-block-content {
margin-top: 12rpx;
}
.inspect-item {
margin-top: 16rpx;
background: #f8fafc;
box-shadow: none;
}
.inspect-image-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 16rpx;
}
.inspect-image {
width: 160rpx;
height: 160rpx;
border-radius: 20rpx;
}
.inspect-popup-foot {
padding-top: 24rpx;
}
.inspect-popup-btn {
height: 84rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 28rpx;
font-weight: 700;
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.25);
}
</style>