|
@@ -0,0 +1,334 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="notification-page">
|
|
|
|
|
+ <view v-if="isEmpty" class="empty-state">
|
|
|
|
|
+ <wd-empty icon="no-content" tip="暂无消息" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-else class="notification-list">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="item in notificationList"
|
|
|
|
|
+ :key="item.id"
|
|
|
|
|
+ class="notification-card"
|
|
|
|
|
+ :class="{ 'is-unread': item.read === 0 }"
|
|
|
|
|
+ @click="handleDetail(item.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="icon-box">
|
|
|
|
|
+ <wd-icon name="notification" size="24px"></wd-icon>
|
|
|
|
|
+ <view v-if="item.read === 0" class="unread-dot" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="content-box">
|
|
|
|
|
+ <view class="title-row">
|
|
|
|
|
+ <text class="title">{{ item.title || '消息通知' }}</text>
|
|
|
|
|
+ <text class="time">{{ item.sendTime }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <text class="desc">{{ item.text }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-if="loading" class="load-state">
|
|
|
|
|
+ <wd-loading size="30rpx" />
|
|
|
|
|
+ <text class="load-text">加载中</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-else-if="finished && notificationList.length > 0" class="load-state">
|
|
|
|
|
+ <text class="load-text">没有更多了</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { computed, ref } from 'vue'
|
|
|
|
|
+
|
|
|
|
|
+import { onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app'
|
|
|
|
|
+
|
|
|
|
|
+import type { NotificationRecord } from '@/services/modules/mine/notification'
|
|
|
|
|
+import { getInternalMsgApi } from '@/services/modules/mine/notification'
|
|
|
|
|
+
|
|
|
|
|
+type NotificationViewRecord = NotificationRecord & {
|
|
|
|
|
+ text: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const page = ref({
|
|
|
|
|
+ current: 1,
|
|
|
|
|
+ size: 10,
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const finished = ref(false)
|
|
|
|
|
+const initialized = ref(false)
|
|
|
|
|
+const notificationList = ref<NotificationViewRecord[]>([])
|
|
|
|
|
+
|
|
|
|
|
+const isEmpty = computed(() => {
|
|
|
|
|
+ return initialized.value && !loading.value && notificationList.value.length === 0
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const resetList = (): void => {
|
|
|
|
|
+ page.value.current = 1
|
|
|
|
|
+ page.value.total = 0
|
|
|
|
|
+ finished.value = false
|
|
|
|
|
+ initialized.value = false
|
|
|
|
|
+ notificationList.value = []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const safeCodePointToString = (codePoint: number): string => {
|
|
|
|
|
+ if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return String.fromCodePoint(codePoint)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const decodeHtmlEntitiesOnce = (value: string): string => {
|
|
|
|
|
+ return value
|
|
|
|
|
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex: string) => {
|
|
|
|
|
+ return safeCodePointToString(Number.parseInt(hex, 16))
|
|
|
|
|
+ })
|
|
|
|
|
+ .replace(/&#(\d+);/g, (_, decimal: string) => {
|
|
|
|
|
+ return safeCodePointToString(Number.parseInt(decimal, 10))
|
|
|
|
|
+ })
|
|
|
|
|
+ .replace(/ /gi, ' ')
|
|
|
|
|
+ .replace(/&/gi, '&')
|
|
|
|
|
+ .replace(/</gi, '<')
|
|
|
|
|
+ .replace(/>/gi, '>')
|
|
|
|
|
+ .replace(/"/gi, '"')
|
|
|
|
|
+ .replace(/'/gi, "'")
|
|
|
|
|
+ .replace(/'/gi, "'")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const decodeHtmlEntities = (value: string): string => {
|
|
|
|
|
+ if (!value) return ''
|
|
|
|
|
+
|
|
|
|
|
+ return decodeHtmlEntitiesOnce(decodeHtmlEntitiesOnce(value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getPreviewText = (html: string, maxLength = 80): string => {
|
|
|
|
|
+ if (!html) return ''
|
|
|
|
|
+
|
|
|
|
|
+ const hasImage = /<img[\s\S]*?>/i.test(html)
|
|
|
|
|
+
|
|
|
|
|
+ const rawText = html
|
|
|
|
|
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
|
|
|
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
|
|
|
+ .replace(/<br\s*\/?>/gi, ' ')
|
|
|
|
|
+ .replace(/<\/p>/gi, ' ')
|
|
|
|
|
+ .replace(/<[^>]+>/g, '')
|
|
|
|
|
+
|
|
|
|
|
+ const text = decodeHtmlEntities(rawText).replace(/\s+/g, ' ').trim()
|
|
|
|
|
+
|
|
|
|
|
+ if (text) {
|
|
|
|
|
+ return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return hasImage ? '【图片】' : '暂无内容'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const normalizeRecord = (record: NotificationRecord): NotificationViewRecord => {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...record,
|
|
|
|
|
+ text: getPreviewText(record.content),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getNotificationList = async (): Promise<void> => {
|
|
|
|
|
+ if (loading.value || finished.value) return
|
|
|
|
|
+
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getInternalMsgApi({
|
|
|
|
|
+ current: page.value.current,
|
|
|
|
|
+ size: page.value.size,
|
|
|
|
|
+ sendType: 2,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (res.code !== 0) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: res.msg || '获取消息列表失败',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const records = res.data.records.map(normalizeRecord)
|
|
|
|
|
+
|
|
|
|
|
+ notificationList.value =
|
|
|
|
|
+ page.value.current === 1 ? records : notificationList.value.concat(records)
|
|
|
|
|
+
|
|
|
|
|
+ page.value.total = res.data.total
|
|
|
|
|
+
|
|
|
|
|
+ finished.value =
|
|
|
|
|
+ notificationList.value.length >= res.data.total || records.length < page.value.size
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '获取消息列表失败',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ initialized.value = true
|
|
|
|
|
+ uni.stopPullDownRefresh()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleDetail = (id: number): void => {
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: `/pages-mine/notification/detail?id=${id}`,
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onShow(() => {
|
|
|
|
|
+ resetList()
|
|
|
|
|
+ void getNotificationList()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onReachBottom(() => {
|
|
|
|
|
+ if (loading.value || finished.value) return
|
|
|
|
|
+
|
|
|
|
|
+ page.value.current += 1
|
|
|
|
|
+ void getNotificationList()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onPullDownRefresh(() => {
|
|
|
|
|
+ resetList()
|
|
|
|
|
+ void getNotificationList()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.notification-page {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ padding: 24rpx 24rpx 48rpx;
|
|
|
|
|
+ background: #f6f7fb;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.notification-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.notification-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 28rpx;
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+ border: 1rpx solid rgba(230, 234, 242, 0.95);
|
|
|
|
|
+ border-radius: 24rpx;
|
|
|
|
|
+ box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.045);
|
|
|
|
|
+
|
|
|
|
|
+ &.is-unread {
|
|
|
|
|
+ border-color: rgba(47, 128, 237, 0.18);
|
|
|
|
|
+ box-shadow: 0 10rpx 28rpx rgba(47, 128, 237, 0.08);
|
|
|
|
|
+
|
|
|
|
|
+ .title {
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:active {
|
|
|
|
|
+ transform: scale(0.985);
|
|
|
|
|
+ background: #f9fafc;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.icon-box {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 72rpx;
|
|
|
|
|
+ height: 72rpx;
|
|
|
|
|
+ margin-right: 22rpx;
|
|
|
|
|
+ color: #2f80ed;
|
|
|
|
|
+ background: #eef5ff;
|
|
|
|
|
+ border-radius: 22rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.unread-dot {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: -4rpx;
|
|
|
|
|
+ right: -4rpx;
|
|
|
|
|
+ width: 16rpx;
|
|
|
|
|
+ height: 16rpx;
|
|
|
|
|
+ background: #ff3b30;
|
|
|
|
|
+ border: 4rpx solid #ffffff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content-box {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.title-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ gap: 18rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.title {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ line-height: 44rpx;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.time {
|
|
|
|
|
+ flex: none;
|
|
|
|
|
+ max-width: 260rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: #98a2b3;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ line-height: 34rpx;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.desc {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ margin-top: 16rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: #667085;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ line-height: 40rpx;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.load-state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ height: 88rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.load-text {
|
|
|
|
|
+ color: #9aa3b2;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-height: 72vh;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|