Files
QSY-lvjuxcx/pages/index/component/MessageCenterTab.vue
liangjian 6e721ccc7e refactor: 国际化文本迁移至全局 locales/messages.js
将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。

- 新增 utils/i18n.js 国际化工具函数
- 更新 store/$t.mixin.js 翻译混入逻辑
- 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
2026-07-24 13:43:41 +08:00

980 lines
31 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>