chore: initial import

This commit is contained in:
liangjian
2026-06-29 09:42:53 +08:00
commit 9fea127de1
241 changed files with 82767 additions and 0 deletions

View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
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>