将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。 - 新增 utils/i18n.js 国际化工具函数 - 更新 store/$t.mixin.js 翻译混入逻辑 - 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
980 lines
31 KiB
Vue
980 lines
31 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">{{ $tr('消息中心') }}</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">{{ $tr('消息中心') }}</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">{{ $tr('系统通知') }}</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">{{ $tr('缴费账单') }}</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">{{ $tr('管家消息') }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="inspectionList.length" class="section-card tn-margin-bottom">
|
||
<view class="section-head">
|
||
<view>
|
||
<view class="section-title">{{ $tr('巡查提醒') }}</view>
|
||
<view class="section-desc">{{ $tr('根据当前入住订单自动汇总巡查提醒记录') }}</view>
|
||
</view>
|
||
<view class="section-chip is-primary">{{ inspectionReminderCount }} {{ $tr('条提醒') }}</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 || $tr('未关联房间') }} · {{ 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">{{ $tr('检查人:') }}{{ record.checkerUserCode || '--' }}</view>
|
||
<view class="inspection-action">{{ $tr('查看详情') }}</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">{{ $tr('近期消息') }}</view>
|
||
<view class="tn-text-sm tn-color-gray" @click="handleReadAll">
|
||
<text class="tn-icon-delete tn-margin-right-xs"></text>{{ $tr('通知已读') }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="msg-list">
|
||
<view v-if="loading" class="empty-card">
|
||
<view class="tn-text-bold tn-text-lg tn-color-black">{{ $tr('消息加载中') }}</view>
|
||
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ $tr('正在同步系统通知和巡查提醒,请稍候。') }}</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">{{ $tr('暂无消息') }}</view>
|
||
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ $tr('系统通知和巡查提醒会在这里显示。') }}</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 || $tr('未关联房间') }} · {{ 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">{{ $tr('检查人') }}</text>
|
||
<text class="inspect-detail-value">{{ activeInspectionRecord.checkerUserCode || '--' }}</text>
|
||
</view>
|
||
<view class="inspect-detail-row">
|
||
<text class="inspect-detail-label">{{ $tr('检查结果') }}</text>
|
||
<text class="inspect-detail-value">{{ activeInspectionRecord.resultText }}</text>
|
||
</view>
|
||
<view class="inspect-detail-row">
|
||
<text class="inspect-detail-label">{{ $tr('当前状态') }}</text>
|
||
<text class="inspect-detail-value">{{ activeInspectionRecord.statusText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="activeInspectionRecord.remark" class="inspect-block">
|
||
<view class="inspect-block-title">{{ $tr('检查备注') }}</view>
|
||
<view class="inspect-block-content">{{ activeInspectionRecord.remark }}</view>
|
||
</view>
|
||
|
||
<view v-if="activeInspectionRecord.items.length" class="inspect-block">
|
||
<view class="inspect-block-title">{{ $tr('检查项明细') }}</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">{{ $tr('检查项 {index}', { index: index + 1 }) }}</view>
|
||
<view class="inspect-item-tag">{{ getCheckTypeLabel(detail.checkType) }}</view>
|
||
</view>
|
||
<view v-if="detail.problemDescription" class="inspect-item-text">{{ $tr('问题:') }}{{ detail.problemDescription }}</view>
|
||
<view v-if="detail.remark" class="inspect-item-text">{{ $tr('备注:') }}{{ detail.remark }}</view>
|
||
<view v-if="detail.compensationAmount !== '' && detail.compensationAmount !== null && detail.compensationAmount !== undefined" class="inspect-item-text">
|
||
{{ $tr('赔付金额:') }}{{ 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">{{ $tr('我知道了') }}</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
|
||
},
|
||
}
|
||
},
|
||
computed: {
|
||
language() {
|
||
return this.$getLanguage()
|
||
},
|
||
subtitleText() {
|
||
const text = this.$tr('你有 {count} 条未读消息待处理').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)
|
||
},
|
||
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.$tr('系统通知'),
|
||
titleEn: rawTitleEn || this.$tr('系统通知'),
|
||
descZh: rawDescZh || this.$tr('您有一条新的系统消息'),
|
||
descEn: rawDescEn || this.$tr('您有一条新的系统消息'),
|
||
contentZh: rawDescZh || this.$tr('您有一条新的系统消息'),
|
||
contentEn: rawDescEn || this.$tr('您有一条新的系统消息'),
|
||
title: title || this.$tr('系统通知'),
|
||
desc: desc || this.$tr('您有一条新的系统消息'),
|
||
content: desc || this.$tr('您有一条新的系统消息'),
|
||
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.$tr('巡查提醒,请查看详情')
|
||
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.$tr('日常检查'),
|
||
1: this.$tr('退房检查'),
|
||
2: this.$tr('专项检查')
|
||
}
|
||
return map[Number(value)] || this.$tr('检查记录')
|
||
},
|
||
getCheckRecordStatusLabel(value) {
|
||
const map = {
|
||
0: this.$tr('待确认'),
|
||
1: this.$tr('处理中'),
|
||
2: this.$tr('已完成')
|
||
}
|
||
return map[Number(value)] || this.$tr('状态待定')
|
||
},
|
||
getCheckResultStatusLabel(value) {
|
||
const map = {
|
||
0: this.$tr('正常'),
|
||
1: this.$tr('需整改'),
|
||
2: this.$tr('待复核')
|
||
}
|
||
return map[Number(value)] || this.$tr('检查结果')
|
||
},
|
||
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.$tr('巡查提醒需单独查看处理')
|
||
: this.$tr('暂无未读通知'),
|
||
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.$tr('系统通知已标记,巡查提醒需单独处理')
|
||
: this.$tr('系统通知已全部标记为已读'),
|
||
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>
|