1081 lines
35 KiB
Vue
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>
|