文件
shanghaipeiwan/pwuniapp/pages/user/notifications.vue

469 行
12 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="notif-page" :class="['theme-' + currentThemeKey]" :style="navStyle">
<!-- 顶部沉浸式自定义导航栏 -->
<view class="custom-nav-bar">
<view class="nav-content">
<view class="btn-nav-back btn-active" @click="handleBack">
<image class="nav-back-icon" :src="ICONS.chevronLeft" mode="aspectFit" />
</view>
<view class="nav-title-wrap">
<text class="nav-title">消息通知</text>
</view>
<view class="nav-action btn-active" v-if="unreadCount > 0" @click="handleReadAll">
<text class="nav-action-text">全部已读</text>
</view>
<view class="nav-capsule-placeholder" v-else></view>
</view>
</view>
<scroll-view
scroll-y
class="notif-scroll"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<view class="notif-body">
<view class="notif-summary">
<text class="summary-title">通知中心</text>
<text class="summary-sub">{{ unreadCount > 0 ? `你有 ${unreadCount} 条未读通知` : '暂无未读通知,订单/提现/报名进展都会在这里留痕' }}</text>
</view>
<view v-if="isLoading" class="notif-empty">
<text class="empty-text">加载中…</text>
</view>
<view v-else-if="list.length === 0" class="notif-empty">
<text class="empty-title">暂无消息</text>
<text class="empty-text">下单、支付、接单、提现到账等节点,平台都会在此提醒你</text>
</view>
<view v-else class="notif-list">
<view
v-for="item in list"
:key="item.id"
class="notif-card btn-active"
:class="{ 'is-unread': !item.isRead }"
@click="handleOpen(item)"
>
<view class="notif-dot" v-if="!item.isRead"></view>
<view class="notif-main">
<view class="notif-head">
<text class="notif-title">{{ item.title }}</text>
<text class="notif-time">{{ formatTime(item.createdAt) }}</text>
</view>
<text class="notif-content">{{ item.content }}</text>
</view>
</view>
</view>
<view v-if="isLoadingMore" class="load-more-tip">
<text>正在加载更多…</text>
</view>
<view v-else-if="!hasMore && list.length > 0" class="load-more-tip">
<text>没有更多了</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { getNavBarInfo } from '@/common/nav.js';
import { getNotifications, markNotificationsRead } from '@/common/api.js';
import { useShare } from '@/common/share.js';
useShare({
title: '同城陪伴 · 消息与通知中心',
timelineTitle: '同城陪伴 · 专业游戏陪玩与生活陪伴',
path: '/pages/index/index'
});
const currentThemeKey = ref(getCurrentThemeKey());
const navInfo = ref(getNavBarInfo());
const isLoading = ref(true);
const isRefreshing = ref(false);
const isLoadingMore = ref(false);
const list = ref([]);
const unreadCount = ref(0);
const page = ref(1);
const pageSize = 20;
const hasMore = ref(false);
let requestSeq = 0;
const navStyle = computed(() => ({
'--safe-header-gap': `${navInfo.value.totalHeaderHeight}px`,
'--capsule-right-space': `${navInfo.value.capsuleRightSpace}px`,
'--capsule-height': `${navInfo.value.capsuleHeight}px`
}));
const currentUserId = () => uni.getStorageSync('userId') || uni.getStorageSync('uid') || 0;
const formatTime = (v) => {
if (!v) return '';
const s = String(v).replace('T', ' ').replace('Z', '');
return s.length >= 16 ? s.slice(0, 16) : s;
};
const loadData = async (reset = true) => {
if (!reset && isLoadingMore.value) return;
const uid = currentUserId();
if (!uid) {
isLoading.value = false;
return;
}
const seq = ++requestSeq;
const targetPage = reset ? 1 : page.value + 1;
isLoadingMore.value = true;
if (reset) isLoading.value = true;
try {
const res = await getNotifications(uid, 50, { page: targetPage, pageSize });
if (seq !== requestSeq) return;
const incoming = Array.isArray(res?.list) ? res.list : [];
if (reset) {
list.value = incoming;
hasMore.value = Boolean(res?.hasMore);
} else {
const seen = new Set(list.value.map((x) => x.id));
list.value = list.value.concat(incoming.filter((x) => !seen.has(x.id)));
hasMore.value = Boolean(res?.hasMore);
}
unreadCount.value = Number(res?.unread || 0);
page.value = targetPage;
} catch (_) {
if (seq === requestSeq && reset) {
list.value = [];
unreadCount.value = 0;
hasMore.value = false;
}
} finally {
if (seq === requestSeq) {
isLoading.value = false;
isLoadingMore.value = false;
isRefreshing.value = false;
}
}
};
const loadMore = () => {
if (hasMore.value && !isLoadingMore.value) {
loadData(false);
}
};
const onRefresh = () => {
isRefreshing.value = true;
loadData(true);
};
const handleReadAll = async () => {
const uid = currentUserId();
if (!uid) return;
try {
await markNotificationsRead(uid);
list.value = list.value.map((x) => ({ ...x, isRead: 1 }));
unreadCount.value = 0;
uni.showToast({ title: '已全部标记为已读', icon: 'none' });
} catch (err) {
uni.showToast({ title: err.message || '操作失败', icon: 'none' });
}
};
const handleOpen = async (item) => {
const uid = currentUserId();
if (!item.isRead && uid) {
try {
await markNotificationsRead(uid, [item.id]);
item.isRead = 1;
unreadCount.value = Math.max(0, unreadCount.value - 1);
} catch (_) {}
}
const link = String(item.link || '');
if (!link) return;
const tabPages = [
'/pages/index/index',
'/pages/activity/list',
'/pages/order/create',
'/pages/community/index',
'/pages/user/index',
'/pages/expert/hall',
'/pages/expert/radar'
];
if (tabPages.includes(link)) {
uni.switchTab({ url: link, fail: () => {} });
} else {
uni.navigateTo({ url: link, fail: () => {} });
}
};
const handleBack = () => {
const pages = getCurrentPages();
if (pages && pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({ url: '/pages/user/index' });
}
};
onShow(() => {
navInfo.value = getNavBarInfo();
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
loadData();
});
</script>
<style lang="scss" scoped>
/* 颜色全部取自全局主题变量(App.vue 按 theme-warm 覆盖),随主题切换自动适配 */
/* 导航栏规范与其它页面一致:总高=安全区,内容行贴底对齐胶囊,标题绝对居中 */
.notif-page {
height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
background: var(--bg-primary, #0a0f1d);
}
.custom-nav-bar {
position: relative;
flex-shrink: 0;
height: var(--safe-header-gap, 88px);
background: var(--nav-bg, #0a0f1d);
border-bottom: 1px solid var(--border-subtle, #1e293b);
}
.nav-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: var(--capsule-height, 32px);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
box-sizing: border-box;
}
.btn-nav-back {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
background: var(--bg-surface-elevated, rgba(255, 255, 255, 0.08));
display: flex;
align-items: center;
justify-content: center;
}
.nav-back-icon {
width: 38rpx;
height: 38rpx;
}
.nav-title-wrap {
position: absolute;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
.nav-title {
color: var(--text-primary, #f8fafc);
font-size: 32rpx;
font-weight: 700;
letter-spacing: 0.5rpx;
}
.nav-action {
min-width: 96rpx;
text-align: right;
}
.nav-action-text {
color: var(--color-brand, #8b9cff);
font-size: 26rpx;
}
.nav-capsule-placeholder {
width: var(--capsule-right-space, 0rpx);
}
.notif-scroll {
flex: 1;
min-height: 0;
box-sizing: border-box;
}
.notif-body {
padding: 12px 16px 40px;
}
.notif-summary {
padding: 16px;
border-radius: 16px;
background: var(--bg-surface, #141a30);
border: 1px solid var(--border-subtle, #1e293b);
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 14px;
}
.summary-title {
color: var(--text-primary, #fff);
font-size: 16px;
font-weight: 700;
}
.summary-sub {
color: var(--text-secondary, #9aa7c7);
font-size: 12px;
line-height: 1.5;
}
.notif-empty {
padding: 48px 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.empty-title {
color: var(--text-primary, #e6eaf5);
font-size: 15px;
font-weight: 600;
}
.empty-text {
color: var(--text-muted, #7d89a8);
font-size: 12px;
text-align: center;
line-height: 1.6;
}
.notif-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.load-more-tip {
padding: 18px 0 8px;
text-align: center;
color: var(--text-muted, #6d7997);
font-size: 12px;
}
.notif-card {
position: relative;
padding: 14px 14px 14px 18px;
border-radius: 14px;
background: var(--bg-surface, #141a30);
border: 1px solid var(--border-subtle, #1e293b);
display: flex;
align-items: flex-start;
gap: 10px;
}
.notif-card.is-unread {
background: var(--bg-surface-elevated, #182143);
}
.notif-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff5f57;
margin-top: 6px;
}
.notif-main {
flex: 1;
min-width: 0;
}
.notif-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.notif-title {
color: var(--text-primary, #fff);
font-size: 14px;
font-weight: 600;
}
.notif-time {
color: var(--text-muted, #6d7997);
font-size: 11px;
flex-shrink: 0;
}
.notif-content {
display: block;
margin-top: 6px;
color: var(--text-secondary, #a8b3d1);
font-size: 12px;
line-height: 1.6;
}
.btn-active:active {
opacity: 0.75;
}
/* ===== Premium visual layer: 消息通知 ===== */
.notif-page {
background:
radial-gradient(110% 38% at 50% -6%, rgba(99, 102, 241, 0.12), transparent 62%),
var(--surface-0);
}
.notif-page .custom-nav-bar {
background: rgba(7, 11, 20, 0.88);
backdrop-filter: blur(18px);
border-bottom-color: rgba(255, 255, 255, 0.05);
}
.notif-page .notif-summary {
background: linear-gradient(145deg, rgba(24, 34, 58, 0.88) 0%, rgba(13, 20, 36, 0.92) 100%);
border-color: var(--hairline);
box-shadow: 0 12rpx 28rpx rgba(0, 0, 0, 0.16);
}
.notif-page .notif-card {
background: rgba(15, 22, 38, 0.78);
border-color: var(--hairline);
border-radius: 16px;
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.12);
}
.notif-page .notif-card.is-unread {
background: linear-gradient(145deg, rgba(30, 41, 66, 0.92) 0%, rgba(18, 27, 46, 0.94) 100%);
border-color: rgba(129, 140, 248, 0.22);
}
.notif-page .notif-card .notif-title {
font-size: 14px;
letter-spacing: 0.2rpx;
}
.notif-page .notif-card .notif-content {
color: #A7B3CB;
}
.notif-page .notif-dot {
box-shadow: 0 0 10rpx rgba(255, 95, 87, 0.45);
}
.notif-page.theme-warm {
background:
radial-gradient(100% 36% at 50% -6%, rgba(234, 88, 12, 0.06), transparent 62%),
#FDFBF7;
}
.notif-page.theme-warm .custom-nav-bar {
background: rgba(253, 251, 247, 0.92);
border-bottom-color: #F0ECE4;
}
.notif-page.theme-warm .notif-summary {
background: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 12rpx 28rpx rgba(28, 25, 23, 0.05);
}
.notif-page.theme-warm .notif-card {
background: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 10rpx 24rpx rgba(28, 25, 23, 0.04);
}
.notif-page.theme-warm .notif-card.is-unread {
background: #FFFDF8;
border-color: #FED7AA;
}
.notif-page.theme-warm .notif-card .notif-content {
color: #78716C;
}
</style>