chore: initial import
This commit is contained in:
583
pkgService/message_detail/message_detail.vue
Normal file
583
pkgService/message_detail/message_detail.vue
Normal file
@@ -0,0 +1,583 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user