chore: establish V1.1 recovery baseline

This commit is contained in:
2026-09-09 22:05:00 +08:00
commit b46b48eb33
947 changed files with 155471 additions and 0 deletions
@@ -0,0 +1,97 @@
/**
* 通知中心页 U25
*/
const api = require('../../utils/api');
Page({
data: {
notifications: [],
unreadCount: 0,
loading: true,
page: 1,
hasMore: true
},
onLoad() {
this.loadNotifications();
},
onShow() {
this.loadUnreadCount();
},
async loadNotifications() {
this.setData({ loading: true });
try {
const token = wx.getStorageSync('accessToken');
const res = await api.get(
`/notifications?page=${this.data.page}&pageSize=20`,
{},
{ Authorization: `Bearer ${token}` }
);
if (res) {
const isNewPage = this.data.page === 1;
this.setData({
notifications: isNewPage ? res.notifications : [...this.data.notifications, ...res.notifications],
unreadCount: res.unreadCount,
hasMore: res.notifications.length >= 20,
loading: false
});
}
} catch (err) {
this.setData({ loading: false });
}
},
async loadUnreadCount() {
try {
const token = wx.getStorageSync('accessToken');
const res = await api.get('/notifications/unread-count', {}, { Authorization: `Bearer ${token}` });
this.setData({ unreadCount: res?.unreadCount || 0 });
} catch (e) { /* ignore */ }
},
async onMarkRead(e) {
const notificationId = e.currentTarget.dataset.id;
try {
const token = wx.getStorageSync('accessToken');
await api.put(`/notifications/${notificationId}/read`, {}, { Authorization: `Bearer ${token}` });
this.setData({
notifications: this.data.notifications.map(n =>
n.notificationId === notificationId ? { ...n, read: true } : n
),
unreadCount: Math.max(0, this.data.unreadCount - 1)
});
} catch (err) { /* ignore */ }
},
async onMarkAllRead() {
try {
const token = wx.getStorageSync('accessToken');
await api.put('/notifications/read-all', {}, { Authorization: `Bearer ${token}` });
this.setData({ unreadCount: 0 });
} catch (err) { /* ignore */ }
},
async onReachBottom() {
if (this.data.hasMore && !this.data.loading) {
this.setData({ page: this.data.page + 1 });
this.loadNotifications();
}
},
onPullDownRefresh() {
this.setData({ page: 1 });
this.loadNotifications().then(() => wx.stopPullDownRefresh());
},
formatTime(ts) {
if (!ts) return '';
const d = new Date(ts);
const now = new Date();
if (d.toDateString() === now.toDateString()) {
return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
}
return `${d.getMonth() + 1}/${d.getDate()} ${d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
}
});
@@ -0,0 +1,5 @@
{
"navigationBarTitleText": "通知",
"enablePullDownRefresh": true,
"usingComponents": {}
}
@@ -0,0 +1,36 @@
<!-- 通知中心页 U25 -->
<view class="page">
<view class="header">
<text class="title">通知</text>
<text class="unread-badge" wx:if="{{unreadCount > 0}}" bindtap="onMarkAllRead">
{{unreadCount}}条未读 · 全部已读
</text>
<text wx:else class="unread-badge empty">暂无未读</text>
</view>
<scroll-view scroll-y class="list" bindscrolltolower="onReachBottom" refresher-enabled bindrefresherrefresh="onPullDownRefresh">
<view wx:for="{{notifications}}" wx:key="notificationId" class="notif-item {{item.read ? 'read' : 'unread'}}"
bindtap="onMarkRead" data-id="{{item.notificationId}}">
<view class="notif-dot" wx:if="{{!item.read}}"></view>
<view class="notif-content">
<view class="notif-header">
<text class="notif-title">{{item.title}}</text>
<text class="notif-time">{{formatTime(item.createdAt)}}</text>
</view>
<text class="notif-body">{{item.content}}</text>
<view class="notif-type-tag {{item.type}}">
{{getTypeText(item.type)}}
</view>
</view>
</view>
<view wx:if="{{!loading && notifications.length === 0}}" class="empty">
<image class="empty-img" src="/assets/images/empty-notification.png" mode="aspectFit" />
<text class="empty-text">暂无通知</text>
</view>
<view wx:if="{{loading && notifications.length === 0}}" class="loading">
<text>加载中...</text>
</view>
</scroll-view>
</view>
@@ -0,0 +1,127 @@
.page {
min-height: 100vh;
background: #FFF8F7;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #1A1A1A;
}
.unread-badge {
font-size: 24rpx;
color: #E96B6B;
padding: 8rpx 16rpx;
background: #FFF3F2;
border-radius: 20rpx;
}
.unread-badge.empty {
color: #999;
background: #f5f5f5;
}
.list {
height: calc(100vh - 120rpx);
}
.notif-item {
display: flex;
align-items: flex-start;
padding: 28rpx 32rpx;
background: #fff;
border-bottom: 1rpx solid #f5f5f5;
gap: 16rpx;
}
.notif-item.unread {
background: #FFFAF9;
}
.notif-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #E96B6B;
flex-shrink: 0;
margin-top: 12rpx;
}
.notif-content {
flex: 1;
min-width: 0;
}
.notif-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.notif-title {
font-size: 28rpx;
font-weight: bold;
color: #1A1A1A;
}
.notif-time {
font-size: 22rpx;
color: #999;
}
.notif-body {
font-size: 24rpx;
color: #666;
line-height: 1.6;
margin-bottom: 12rpx;
}
.notif-type-tag {
display: inline-block;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
background: #F5F5F5;
color: #999;
}
.notif-type-tag.match { background: #E8F5E9; color: #2E7D32; }
.notif-type-tag.review { background: #E3F2FD; color: #1565C0; }
.notif-type-tag.activity { background: #FFF3E0; color: #E65100; }
.notif-type-tag.risk { background: #FFEBEE; color: #C62828; }
.notif-type-tag.account { background: #F3E5F5; color: #6A1B9A; }
.empty {
text-align: center;
padding: 120rpx 40rpx;
}
.empty-img {
width: 160rpx;
height: 160rpx;
margin-bottom: 24rpx;
opacity: 0.5;
}
.empty-text {
display: block;
font-size: 28rpx;
color: #999;
}
.loading {
text-align: center;
padding: 40rpx;
color: #999;
}