150 lines
12 KiB
Plaintext
150 lines
12 KiB
Plaintext
<template>
|
|
<view class="page" :style="pageStyle()">
|
|
<AppHeader title="消息中心" :back="false" />
|
|
<view class="message-tabs"><view v-for="item in tabs" :key="item.type" :class="tab == item.type ? 'message-tab active-tab' : 'message-tab'" @click="changeTab(item.type)"><text :class="tab == item.type ? 'tab-text active-text' : 'tab-text'">{{ item.label }}</text></view></view>
|
|
<!-- 扣除顶栏96、分类88和底栏130,避免列表末端落到固定导航下。 -->
|
|
<scroll-view scroll-y :style="contentStyle(314, false)" @scrolltolower="autoMore">
|
|
<view class="messages-content">
|
|
<view class="message-summary"><text class="summary-text">{{ loading ? '正在加载消息…' : (unreadCount > 0 ? unreadCount + ' 条未读消息' : (notices.length > 0 ? '消息已全部阅读' : '消息中心')) }}</text><view class="read-all" @click="markAllRead"><text :class="marking || loading || snapshotId == '0' || snapshotId.length == 0 ? 'read-all-text disabled-text' : 'read-all-text'">{{ marking ? '正在标记…' : '全部已读' }}</text></view></view>
|
|
<view v-if="error.length > 0" class="message-state"><text class="state-title">{{ error }}</text><text class="state-action" @click="reload">重新加载</text></view>
|
|
<view v-if="!loading && error.length == 0 && notices.length == 0" class="message-state"><text class="state-title">{{ tab == 'all' ? '暂无消息' : '暂无' + categoryLabel }}</text><text class="state-note">{{ tab == 'all' ? '评估状态更新后,会在这里通知您' : '当前分类暂无消息' }}</text></view>
|
|
<text v-if="actionError.length > 0" class="action-error">{{ actionError }}</text>
|
|
<view class="notice-list">
|
|
<view class="notice" v-for="(item, index) in notices" :key="item.id" :class="index == notices.length - 1 ? 'last-notice' : ''" @click="openNotice(item)">
|
|
<view class="notice-icon" :class="'notice-icon-' + item.icon"><view class="notice-icon-surface"><image class="notice-symbol" :src="'/packages/account/static/notifications/' + item.icon + '.svg'" mode="aspectFit" /></view><view v-if="!item.isRead" class="unread"></view></view>
|
|
<view class="notice-copy"><view class="notice-head"><text class="notice-title">{{ item.title }}</text><text class="notice-time">{{ item.time }}</text></view><text class="notice-desc">{{ item.content }}</text><text class="notice-category">{{ opening == item.id ? '正在打开…' : item.category }}</text></view>
|
|
</view>
|
|
</view>
|
|
<text v-if="paginationError.length > 0" class="action-error">{{ paginationError }}</text>
|
|
<text v-if="loadingMore" class="message-footer">正在加载更多…</text>
|
|
<text v-else-if="nextBeforeId.length > 0" class="message-footer state-action" @click="more">加载更多</text>
|
|
<text v-else-if="notices.length > 0" class="message-footer">已显示全部消息</text>
|
|
</view>
|
|
</scroll-view>
|
|
<BottomNav active="message" />
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { getSession } from '@/services/api.uts'
|
|
import { UserMessage, fetchMessages, fetchMessage, readMessage, readAllMessages } from '@/services/messages.uts'
|
|
import { usePageLayout } from '@/services/layout.uts'
|
|
import AppHeader from '@/components/AppHeader.uvue'
|
|
import BottomNav from '@/components/BottomNav.uvue'
|
|
const { contentStyle, pageStyle } = usePageLayout()
|
|
type MessageTab = { type: string, label: string }
|
|
const tabs: MessageTab[] = [{ type: 'all', label: '全部' }, { type: 'system', label: '系统通知' }, { type: 'service', label: '服务通知' }, { type: 'activity', label: '活动消息' }]
|
|
const tab = ref('all'); const notices = ref<UserMessage[]>([]); const unreadCount = ref(0)
|
|
const categoryLabel = computed((): string => tabs.find((item: MessageTab): boolean => item.type == tab.value)?.label ?? '')
|
|
const snapshotId = ref(''); const nextBeforeId = ref(''); const loading = ref(false); const loadingMore = ref(false)
|
|
const marking = ref(false); const opening = ref(''); const error = ref(''); const actionError = ref(''); const paginationError = ref('')
|
|
let visible = false; let generation = 0; let owner = ''
|
|
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeMessages=1' }) }
|
|
const reset = () => { notices.value = []; unreadCount.value = 0; snapshotId.value = ''; nextBeforeId.value = ''; error.value = ''; actionError.value = ''; paginationError.value = ''; marking.value = false; opening.value = ''; loading.value = false; loadingMore.value = false }
|
|
// UTS原生端不支持局部函数前向引用,账户切换回调在加载函数声明后绑定。
|
|
let reloadAfterAccountChange: () => void = () => {}
|
|
// 离页、换分类和账户切换后,旧请求不能恢复上一账户的消息或触发业务跳转。
|
|
function current(request: number, token: string): boolean {
|
|
if (!visible || request != generation) return false
|
|
if (getSession()?.getString('token') == token) return true
|
|
generation++; reset()
|
|
if (getSession() == null) login(); else reloadAfterAccountChange()
|
|
return false
|
|
}
|
|
async function load(append: boolean) {
|
|
if (!visible || loading.value || loadingMore.value || marking.value || opening.value.length > 0) return
|
|
const token = getSession()?.getString('token') ?? ''
|
|
if (token.length == 0) { reset(); login(); return }
|
|
if (append && token != owner) { generation++; reset(); reloadAfterAccountChange(); return }
|
|
owner = token
|
|
const request = append ? generation : ++generation
|
|
if (append) { if (nextBeforeId.value.length == 0) return; loadingMore.value = true; paginationError.value = '' }
|
|
else { reset(); loading.value = true }
|
|
try {
|
|
const result = await fetchMessages(tab.value, append ? nextBeforeId.value : '', append ? snapshotId.value : '')
|
|
if (!current(request, token)) return
|
|
notices.value = append ? notices.value.concat(result.items) : result.items
|
|
snapshotId.value = result.snapshotId; nextBeforeId.value = result.nextBeforeId; unreadCount.value = result.unreadCount
|
|
} catch (err) {
|
|
if (current(request, token)) {
|
|
const message = err instanceof Error ? err.message : '消息加载失败,请重试'
|
|
if (append) paginationError.value = message; else error.value = message
|
|
}
|
|
} finally { if (request == generation) { loading.value = false; loadingMore.value = false } }
|
|
}
|
|
function reload() { load(false) }
|
|
reloadAfterAccountChange = reload
|
|
const more = () => { load(true) }
|
|
// 自动触底失败后保留提示,由用户手动重试,避免滚动事件连续覆盖错误。
|
|
const autoMore = () => { if (paginationError.value.length == 0) more() }
|
|
const changeTab = (type: string) => { if (type == tab.value || marking.value || opening.value.length > 0) return; generation++; reset(); tab.value = type; reload() }
|
|
const openNotice = async (item: UserMessage) => {
|
|
if (opening.value.length > 0 || marking.value || loading.value || loadingMore.value) return
|
|
if (!current(generation, owner)) return
|
|
const token = getSession()?.getString('token') ?? ''; if (token.length == 0) { login(); return }
|
|
const request = generation; opening.value = item.id; actionError.value = ''
|
|
try {
|
|
const detail = await fetchMessage(item.id)
|
|
if (!current(request, token)) return
|
|
const unread = await readMessage(item.id)
|
|
if (!current(request, token)) return
|
|
item.isRead = true; unreadCount.value = unread
|
|
uni.showModal({ title: detail.item.title, content: detail.item.content + (detail.targetUnavailable ? '\n关联记录已不可用,请刷新或查看其他记录。' : ''),
|
|
showCancel: detail.targetUrl.length > 0, cancelText: '关闭', confirmText: detail.targetLabel.length > 0 ? detail.targetLabel : '知道了',
|
|
success: (result) => { if (result.confirm && detail.targetUrl.length > 0 && current(request, token)) uni.navigateTo({ url: detail.targetUrl }) } })
|
|
} catch (err) { if (current(request, token)) actionError.value = err instanceof Error ? err.message : '消息读取失败,请重试' }
|
|
finally { if (request == generation) opening.value = '' }
|
|
}
|
|
const markAllRead = async () => {
|
|
if (marking.value || opening.value.length > 0 || loading.value || loadingMore.value || snapshotId.value.length == 0 || snapshotId.value == '0') return
|
|
if (!current(generation, owner)) return
|
|
const token = getSession()?.getString('token') ?? ''; if (token.length == 0) { login(); return }
|
|
const request = generation; marking.value = true; actionError.value = ''
|
|
try {
|
|
const unread = await readAllMessages(snapshotId.value)
|
|
if (!current(request, token)) return
|
|
notices.value.forEach((item: UserMessage) => { item.isRead = true }); unreadCount.value = unread
|
|
} catch (err) { if (current(request, token)) actionError.value = err instanceof Error ? err.message : '标记失败,请重试' }
|
|
finally { if (request == generation) marking.value = false }
|
|
}
|
|
onShow(() => { visible = true; generation++; reset(); reload() })
|
|
onHide(() => { visible = false; generation++; reset() })
|
|
onUnload(() => { visible = false; generation++; reset() })
|
|
</script>
|
|
|
|
<style>
|
|
.message-tabs { height: 88rpx; flex-direction: row; background-color: #ffffff; border-bottom: 1rpx solid #e8eef6; padding: 0 18rpx; }
|
|
.message-tab { flex: 1; align-items: center; justify-content: center; border-bottom: 4rpx solid transparent; }
|
|
.active-tab { border-bottom-color: #0868f7; }
|
|
.tab-text { color: #66758a; font-size: 25rpx; }
|
|
.active-text { color: #0868f7; font-weight: 700; }
|
|
.messages-content { padding: 14rpx 28rpx 90rpx; }
|
|
.message-summary { flex-direction: row; align-items: center; justify-content: space-between; min-height: 66rpx; }
|
|
.summary-text { color: #66758a; font-size: 23rpx; }
|
|
.read-all { padding: 14rpx 0 14rpx 24rpx; }
|
|
.read-all-text { color: #0868f7; font-size: 23rpx; }
|
|
.disabled-text { color: #a1adbd; }
|
|
.message-state { padding: 56rpx 18rpx; align-items: center; }
|
|
.state-title { color: #52657e; font-size: 27rpx; text-align: center; line-height: 42rpx; }
|
|
.state-note { color: #8291a5; font-size: 23rpx; margin-top: 18rpx; text-align: center; line-height: 36rpx; }
|
|
.state-action { color: #0868f7; font-size: 25rpx; padding: 18rpx; }
|
|
.action-error { color: #b64d35; font-size: 23rpx; line-height: 36rpx; padding: 16rpx 0; }
|
|
.notice-list { background-color: #ffffff; border-radius: 18rpx; padding: 0 20rpx; }
|
|
.notice { padding: 26rpx 0; border-bottom: 1rpx solid #e8eef6; flex-direction: row; align-items: center; }
|
|
.last-notice { border-bottom-width: 0; }
|
|
.notice-icon { width: 76rpx; height: 76rpx; margin-right: 20rpx; position: relative; flex-shrink: 0; }
|
|
.notice-icon-surface { width: 100%; height: 100%; border-radius: 20rpx; background-color: #edf5ff; align-items: center; justify-content: center; }
|
|
.notice-icon-payout .notice-icon-surface { background-color: #eaf7f3; }
|
|
.notice-icon-gift .notice-icon-surface { background-color: #fff3e8; }
|
|
.notice-symbol { width: 46rpx; height: 46rpx; flex-shrink: 0; }
|
|
.notice-copy { flex: 1; min-width: 0; }
|
|
.notice-head { flex-direction: row; justify-content: space-between; align-items: flex-start; }
|
|
.notice-title { color: #172640; font-size: 28rpx; line-height: 40rpx; font-weight: 600; flex: 1; }
|
|
.notice-time { color: #66758a; font-size: 22rpx; line-height: 40rpx; margin-left: 12rpx; flex-shrink: 0; }
|
|
.notice-desc { color: #66758a; font-size: 25rpx; line-height: 38rpx; margin-top: 10rpx; }
|
|
.notice-category { color: #66758a; font-size: 22rpx; line-height: 32rpx; margin-top: 10rpx; }
|
|
/* 红点独立于圆角背景,避免跨端圆角裁切。 */
|
|
.unread { position: absolute; width: 15rpx; height: 15rpx; border-radius: 8rpx; background-color: #f04452; border: 2rpx solid #ffffff; right: -3rpx; top: -3rpx; z-index: 1; }
|
|
.message-footer { color: #66758a; font-size: 22rpx; text-align: center; margin-top: 28rpx; }
|
|
</style>
|