584 lines
15 KiB
Vue
584 lines
15 KiB
Vue
<template>
|
|
<view class="message-detail-page tn-safe-area-inset-bottom">
|
|
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
|
|
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
|
|
</tn-nav-bar>
|
|
|
|
<view class="page-main tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '48rpx' }">
|
|
<view class="hero-panel tn-margin-bottom-lg" :class="heroThemeClass">
|
|
<view class="hero-top-row">
|
|
<view class="hero-badge" :class="heroBadgeClass">{{ categoryText }}</view>
|
|
<view class="hero-icon-wrap">
|
|
<text :class="categoryIcon"></text>
|
|
</view>
|
|
</view>
|
|
<view class="hero-title tn-margin-top">{{ localizedTitle || t('messageFallbackTitle') }}</view>
|
|
<view class="hero-meta-row tn-margin-top">
|
|
<view class="hero-meta-chip">
|
|
<text class="tn-icon-time tn-margin-right-xs"></text>
|
|
<text>{{ detail.time || '--' }}</text>
|
|
</view>
|
|
<view class="hero-meta-chip">
|
|
<text class="tn-icon-my tn-margin-right-xs"></text>
|
|
<text>{{ sourceText }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="article-header tn-margin-bottom-lg">
|
|
<view class="article-title">{{ localizedTitle || t('messageFallbackTitle') }}</view>
|
|
<view class="article-meta tn-margin-top">
|
|
<view class="article-tag">{{ categoryText }}</view>
|
|
<view class="article-time">{{ detail.time || '--' }}</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="content-panel tn-margin-bottom-lg">
|
|
<view class="panel-title">{{ t('contentTitle') }}</view>
|
|
<rich-text class="article-content tn-margin-top" :nodes="contentNodes"></rich-text>
|
|
</view>
|
|
|
|
<view v-if="localizedDesc && localizedDesc !== localizedContent" class="content-panel tn-margin-bottom-lg">
|
|
<view class="panel-title">{{ t('summaryTitle') }}</view>
|
|
<view class="panel-desc tn-margin-top">{{ localizedDesc }}</view>
|
|
</view>
|
|
|
|
<view class="meta-panel tn-margin-bottom-lg">
|
|
<view class="meta-row">
|
|
<text class="meta-label">{{ t('sourceLabel') }}</text>
|
|
<text class="meta-value">{{ sourceText }}</text>
|
|
</view>
|
|
<view class="meta-row">
|
|
<text class="meta-label">{{ t('categoryLabel') }}</text>
|
|
<text class="meta-value">{{ categoryText }}</text>
|
|
</view>
|
|
<view v-if="detail.code" class="meta-row">
|
|
<text class="meta-label">{{ t('codeLabel') }}</text>
|
|
<view class="meta-action-wrap">
|
|
<text class="meta-value">{{ detail.code }}</text>
|
|
<view class="copy-btn" @click="copyCode">
|
|
<text class="tn-text-xs tn-text-bold">{{ t('copy') }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view v-if="detail.statusHint" class="meta-row">
|
|
<text class="meta-label">{{ t('statusLabel') }}</text>
|
|
<text class="meta-value">{{ detail.statusHint }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="action-row">
|
|
<view class="ghost-btn" @click="goBackList">
|
|
<text class="tn-text-bold">{{ t('backList') }}</text>
|
|
</view>
|
|
<view v-if="detail.path" class="action-btn" @click="handleGoBusiness">
|
|
<text class="tn-text-bold">{{ actionButtonText }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import {
|
|
clearMessageDetailPayload,
|
|
findMessageDetailPayload,
|
|
getMessageDetailPayload
|
|
} from '../../utils/messages'
|
|
import { normalizeRichTextImages } from '../../utils/rich-text'
|
|
|
|
function escapeHtml(value = '') {
|
|
return String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
}
|
|
|
|
function normalizeContentToRichText(value = '', emptyText = '') {
|
|
const text = String(value || '').trim()
|
|
if (!text) {
|
|
return `<p>${escapeHtml(emptyText || '')}</p>`
|
|
}
|
|
if (/<[a-z][\s\S]*>/i.test(text)) {
|
|
return normalizeRichTextImages(
|
|
text,
|
|
'display:block;width:100%;max-width:100%;height:auto;box-sizing:border-box;border-radius:16rpx;margin:16rpx 0;vertical-align:top;'
|
|
)
|
|
}
|
|
return `<div style="white-space:pre-wrap;line-height:1.9;color:#334155;font-size:30rpx;">${escapeHtml(text)}</div>`
|
|
}
|
|
|
|
export default {
|
|
i18n: {
|
|
zh: {
|
|
navTitle: '消息详情',
|
|
messageFallbackTitle: '消息通知',
|
|
contentTitle: '通知内容',
|
|
summaryTitle: '摘要',
|
|
sourceLabel: '消息来源',
|
|
categoryLabel: '消息分类',
|
|
codeLabel: '通知编码',
|
|
copy: '复制',
|
|
statusLabel: '当前状态',
|
|
backList: '返回消息页',
|
|
categorySystem: '系统通知',
|
|
categoryPayment: '缴费账单',
|
|
categoryButler: '管家消息',
|
|
actionSystem: '前往相关页面',
|
|
actionPayment: '去处理账单',
|
|
actionButler: '去查看服务',
|
|
actionDefault: '继续前往业务页',
|
|
remoteSource: '系统通知接口',
|
|
localSource: '本地业务消息',
|
|
expiredDesc: '当前消息详情已失效,请返回消息中心重新打开。',
|
|
expiredHint: '消息缓存已失效',
|
|
codeCopied: '通知编号已复制',
|
|
emptyContent: '暂无详细内容'
|
|
},
|
|
en: {
|
|
navTitle: 'Message Detail',
|
|
messageFallbackTitle: 'Notification',
|
|
contentTitle: 'Content',
|
|
summaryTitle: 'Summary',
|
|
sourceLabel: 'Source',
|
|
categoryLabel: 'Category',
|
|
codeLabel: 'Notice Code',
|
|
copy: 'Copy',
|
|
statusLabel: 'Status',
|
|
backList: 'Back to Messages',
|
|
categorySystem: 'System Notice',
|
|
categoryPayment: 'Payment Bill',
|
|
categoryButler: 'Butler Message',
|
|
actionSystem: 'Open Related Page',
|
|
actionPayment: 'Handle Bill',
|
|
actionButler: 'View Service',
|
|
actionDefault: 'Continue to Page',
|
|
remoteSource: 'Remote Notification API',
|
|
localSource: 'Local Business Message',
|
|
expiredDesc: 'This message detail has expired. Please return to the message center and open it again.',
|
|
expiredHint: 'Message cache expired',
|
|
codeCopied: 'Notice code copied',
|
|
emptyContent: 'No detailed content available'
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
lookupId: '',
|
|
lookupCode: '',
|
|
detail: {
|
|
id: '',
|
|
title: '',
|
|
desc: '',
|
|
content: '',
|
|
time: '',
|
|
category: 'system',
|
|
source: 'local',
|
|
code: '',
|
|
path: '',
|
|
statusHint: ''
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
language() {
|
|
return this.$getLanguage()
|
|
},
|
|
localizedTitle() {
|
|
if (this.language === 'en') {
|
|
return this.detail.titleEn || this.detail.title || this.detail.titleZh || ''
|
|
}
|
|
return this.detail.titleZh || this.detail.title || this.detail.titleEn || ''
|
|
},
|
|
localizedDesc() {
|
|
if (this.language === 'en') {
|
|
return this.detail.descEn || this.detail.desc || this.detail.descZh || ''
|
|
}
|
|
return this.detail.descZh || this.detail.desc || this.detail.descEn || ''
|
|
},
|
|
localizedContent() {
|
|
if (this.language === 'en') {
|
|
return this.detail.contentEn || this.detail.content || this.detail.contentZh || this.localizedDesc
|
|
}
|
|
return this.detail.contentZh || this.detail.content || this.detail.contentEn || this.localizedDesc
|
|
},
|
|
categoryText() {
|
|
const map = {
|
|
system: this.t('categorySystem'),
|
|
payment: this.t('categoryPayment'),
|
|
butler: this.t('categoryButler')
|
|
}
|
|
return map[this.detail.category] || this.t('messageFallbackTitle')
|
|
},
|
|
contentNodes() {
|
|
return normalizeContentToRichText(this.localizedContent || this.localizedDesc || '', this.t('emptyContent'))
|
|
},
|
|
categoryIcon() {
|
|
const map = {
|
|
system: 'tn-icon-notice-fill',
|
|
payment: 'tn-icon-coupon-fill',
|
|
butler: 'tn-icon-service-fill'
|
|
}
|
|
return map[this.detail.category] || 'tn-icon-notice-fill'
|
|
},
|
|
heroThemeClass() {
|
|
const map = {
|
|
system: 'hero-system',
|
|
payment: 'hero-payment',
|
|
butler: 'hero-butler'
|
|
}
|
|
return map[this.detail.category] || 'hero-system'
|
|
},
|
|
heroBadgeClass() {
|
|
const map = {
|
|
system: 'badge-system',
|
|
payment: 'badge-payment',
|
|
butler: 'badge-butler'
|
|
}
|
|
return map[this.detail.category] || 'badge-system'
|
|
},
|
|
actionButtonText() {
|
|
const map = {
|
|
system: this.t('actionSystem'),
|
|
payment: this.t('actionPayment'),
|
|
butler: this.t('actionButler')
|
|
}
|
|
return map[this.detail.category] || this.t('actionDefault')
|
|
},
|
|
sourceText() {
|
|
return this.detail.source === 'remote' ? this.t('remoteSource') : this.t('localSource')
|
|
}
|
|
},
|
|
onLoad(options = {}) {
|
|
this.lookupId = options.id || ''
|
|
this.lookupCode = options.code || ''
|
|
this.restoreDetail()
|
|
},
|
|
watch: {
|
|
language() {
|
|
this.restoreDetail()
|
|
}
|
|
},
|
|
onUnload() {
|
|
clearMessageDetailPayload(this.language)
|
|
},
|
|
methods: {
|
|
t(key, params) {
|
|
return this.$tm('pkgService.message_detail.message_detail', key, params)
|
|
},
|
|
restoreDetail() {
|
|
const payload = getMessageDetailPayload(this.language)
|
|
const fallbackPayload = findMessageDetailPayload(this.lookupId, this.lookupCode, this.language)
|
|
const resolvedPayload = payload && Object.keys(payload).length ? payload : fallbackPayload
|
|
|
|
if (resolvedPayload && typeof resolvedPayload === 'object' && Object.keys(resolvedPayload).length) {
|
|
this.detail = {
|
|
...this.detail,
|
|
...resolvedPayload
|
|
}
|
|
return
|
|
}
|
|
|
|
this.detail = {
|
|
...this.detail,
|
|
id: this.lookupId || this.lookupCode || '',
|
|
code: this.lookupCode || '',
|
|
titleZh: this.t('messageFallbackTitle'),
|
|
titleEn: this.t('messageFallbackTitle'),
|
|
descZh: this.t('expiredDesc'),
|
|
descEn: this.t('expiredDesc'),
|
|
contentZh: this.t('expiredDesc'),
|
|
contentEn: this.t('expiredDesc'),
|
|
title: this.t('messageFallbackTitle'),
|
|
desc: this.t('expiredDesc'),
|
|
content: this.t('expiredDesc'),
|
|
source: 'remote',
|
|
statusHint: this.t('expiredHint')
|
|
}
|
|
},
|
|
goBackList() {
|
|
uni.navigateBack()
|
|
},
|
|
copyCode() {
|
|
if (!this.detail.code) {
|
|
return
|
|
}
|
|
uni.setClipboardData({
|
|
data: String(this.detail.code),
|
|
success: () => {
|
|
uni.showToast({
|
|
title: this.t('codeCopied'),
|
|
icon: 'success'
|
|
})
|
|
}
|
|
})
|
|
},
|
|
handleGoBusiness() {
|
|
if (!this.detail.path) {
|
|
return
|
|
}
|
|
uni.navigateTo({
|
|
url: this.detail.path
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.message-detail-page {
|
|
min-height: 100vh;
|
|
background: linear-gradient(145deg, #F4F7FA 0%, #EAEFF5 100%);
|
|
}
|
|
|
|
.hero-panel,
|
|
.content-panel,
|
|
.meta-panel {
|
|
background: rgba(255, 255, 255, 0.84);
|
|
border-radius: 32rpx;
|
|
padding: 32rpx;
|
|
box-shadow: 0 16rpx 48rpx rgba(15, 23, 42, 0.04), inset 0 2rpx 0 rgba(255, 255, 255, 1);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
.hero-panel {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.hero-panel::after {
|
|
content: '';
|
|
position: absolute;
|
|
right: -60rpx;
|
|
top: -80rpx;
|
|
width: 240rpx;
|
|
height: 240rpx;
|
|
border-radius: 50%;
|
|
opacity: 0.28;
|
|
}
|
|
|
|
.hero-system {
|
|
background: linear-gradient(135deg, rgba(37, 99, 235, 0.16) 0%, rgba(255, 255, 255, 0.88) 56%);
|
|
}
|
|
|
|
.hero-system::after {
|
|
background: radial-gradient(circle, rgba(59, 130, 246, 0.42) 0%, rgba(59, 130, 246, 0) 70%);
|
|
}
|
|
|
|
.hero-payment {
|
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.16) 0%, rgba(255, 255, 255, 0.88) 56%);
|
|
}
|
|
|
|
.hero-payment::after {
|
|
background: radial-gradient(circle, rgba(245, 158, 11, 0.34) 0%, rgba(245, 158, 11, 0) 70%);
|
|
}
|
|
|
|
.hero-butler {
|
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.16) 0%, rgba(255, 255, 255, 0.88) 56%);
|
|
}
|
|
|
|
.hero-butler::after {
|
|
background: radial-gradient(circle, rgba(16, 185, 129, 0.34) 0%, rgba(16, 185, 129, 0) 70%);
|
|
}
|
|
|
|
.hero-top-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.hero-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 10rpx 20rpx;
|
|
border-radius: 999rpx;
|
|
font-size: 20rpx;
|
|
letter-spacing: 4rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.badge-system {
|
|
color: #1d4ed8;
|
|
background: rgba(29, 78, 216, 0.12);
|
|
}
|
|
|
|
.badge-payment {
|
|
color: #b45309;
|
|
background: rgba(245, 158, 11, 0.14);
|
|
}
|
|
|
|
.badge-butler {
|
|
color: #047857;
|
|
background: rgba(16, 185, 129, 0.14);
|
|
}
|
|
|
|
.hero-icon-wrap {
|
|
width: 88rpx;
|
|
height: 88rpx;
|
|
border-radius: 24rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 40rpx;
|
|
color: #ffffff;
|
|
background: linear-gradient(135deg, #0f172a 0%, #475569 100%);
|
|
box-shadow: 0 16rpx 32rpx rgba(15, 23, 42, 0.14);
|
|
}
|
|
|
|
.hero-title {
|
|
font-size: 44rpx;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
line-height: 1.4;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.hero-meta,
|
|
.panel-desc,
|
|
.meta-label,
|
|
.meta-value {
|
|
font-size: 24rpx;
|
|
line-height: 1.8;
|
|
color: #64748b;
|
|
}
|
|
|
|
.hero-meta-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 16rpx;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.hero-meta-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 10rpx 18rpx;
|
|
border-radius: 999rpx;
|
|
font-size: 22rpx;
|
|
color: #475569;
|
|
background: rgba(148, 163, 184, 0.1);
|
|
}
|
|
|
|
.article-header {
|
|
padding: 40rpx 32rpx;
|
|
background: #ffffff;
|
|
border-radius: 32rpx;
|
|
box-shadow: 0 16rpx 48rpx rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.article-title {
|
|
font-size: 40rpx;
|
|
line-height: 1.5;
|
|
font-weight: 900;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.article-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.article-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 8rpx 18rpx;
|
|
border-radius: 10rpx;
|
|
color: #1d4ed8;
|
|
font-size: 22rpx;
|
|
font-weight: 700;
|
|
background: rgba(29, 78, 216, 0.1);
|
|
}
|
|
|
|
.article-time {
|
|
font-size: 24rpx;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 30rpx;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.meta-row + .meta-row {
|
|
margin-top: 18rpx;
|
|
}
|
|
|
|
.meta-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 24rpx;
|
|
}
|
|
|
|
.meta-action-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14rpx;
|
|
}
|
|
|
|
.meta-value {
|
|
color: #111827;
|
|
text-align: right;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.copy-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 72rpx;
|
|
height: 44rpx;
|
|
padding: 0 14rpx;
|
|
border-radius: 999rpx;
|
|
color: #1d4ed8;
|
|
background: rgba(29, 78, 216, 0.1);
|
|
}
|
|
|
|
.article-content {
|
|
font-size: 30rpx;
|
|
color: #334155;
|
|
line-height: 1.9;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 92rpx;
|
|
border-radius: 999rpx;
|
|
color: #0f172a;
|
|
background: linear-gradient(135deg, $brand-primary 0%, #c7eb65 100%);
|
|
box-shadow: 0 16rpx 32rpx rgba($brand-primary, 0.25);
|
|
}
|
|
|
|
.action-row {
|
|
display: flex;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.action-row > view {
|
|
flex: 1;
|
|
}
|
|
|
|
.ghost-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 92rpx;
|
|
border-radius: 999rpx;
|
|
color: #0f172a;
|
|
background: rgba(255, 255, 255, 0.84);
|
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.04);
|
|
}
|
|
</style>
|