This commit is contained in:
@@ -3,6 +3,7 @@ export { assetApi } from './modules/asset.js';
|
|||||||
export { deviceApi } from './modules/device.js';
|
export { deviceApi } from './modules/device.js';
|
||||||
export { exchangeApi } from './modules/exchange.js';
|
export { exchangeApi } from './modules/exchange.js';
|
||||||
export { orderApi } from './modules/order.js';
|
export { orderApi } from './modules/order.js';
|
||||||
|
export { notificationApi } from './modules/notification.js';
|
||||||
export { realnameApi } from './modules/realname.js';
|
export { realnameApi } from './modules/realname.js';
|
||||||
export { walletApi } from './modules/wallet.js';
|
export { walletApi } from './modules/wallet.js';
|
||||||
export { wechatApi } from './modules/wechat.js';
|
export { wechatApi } from './modules/wechat.js';
|
||||||
32
api/modules/notification.js
Normal file
32
api/modules/notification.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import request from '@/utils/request.js';
|
||||||
|
|
||||||
|
export const notificationApi = {
|
||||||
|
getList(page = 1, page_size = 20) {
|
||||||
|
return request({
|
||||||
|
url: '/api/c/v1/notifications',
|
||||||
|
method: 'GET',
|
||||||
|
data: { page, page_size }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getUnreadCount() {
|
||||||
|
return request({
|
||||||
|
url: '/api/c/v1/notifications/unread-count',
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
markRead(id) {
|
||||||
|
return request({
|
||||||
|
url: `/api/c/v1/notifications/${id}/read`,
|
||||||
|
method: 'PUT'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
markAllRead() {
|
||||||
|
return request({
|
||||||
|
url: '/api/c/v1/notifications/read-all',
|
||||||
|
method: 'PUT'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -29,6 +29,14 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="function-name">套餐订购</view>
|
<view class="function-name">套餐订购</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'notifications')" role="button"
|
||||||
|
tabindex="0">
|
||||||
|
<view class="function-icon function-icon-badge">
|
||||||
|
<image src="/static/tips.png" mode="aspectFit" alt="站内通知"></image>
|
||||||
|
<view v-if="unreadCount > 0" class="unread-badge">{{ unreadCount > 99 ? '99+' : unreadCount }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="function-name">站内通知</view>
|
||||||
|
</view>
|
||||||
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'order-list')" role="button"
|
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'order-list')" role="button"
|
||||||
tabindex="0">
|
tabindex="0">
|
||||||
<view class="function-icon">
|
<view class="function-icon">
|
||||||
@@ -132,6 +140,10 @@
|
|||||||
walletBalance: {
|
walletBalance: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: 0
|
default: 0
|
||||||
|
},
|
||||||
|
unreadCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,6 +200,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.function-icon-badge { position: relative; }
|
||||||
|
.unread-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -8rpx;
|
||||||
|
right: -12rpx;
|
||||||
|
min-width: 34rpx;
|
||||||
|
height: 34rpx;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18rpx;
|
||||||
|
line-height: 34rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.function-name {
|
.function-name {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
45
docs/产品迭代7月份/个人客户站内通知.md
Normal file
45
docs/产品迭代7月份/个人客户站内通知.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 个人客户站内通知
|
||||||
|
|
||||||
|
为个人客户增加通知入口,支持查询通知、查看未读数以及标记已读。所有接口均需要登录,使用 `Bearer Token` 鉴权。
|
||||||
|
|
||||||
|
## 查询通知列表
|
||||||
|
|
||||||
|
`GET /api/c/v1/notifications`
|
||||||
|
|
||||||
|
查询当前客户可见的未过期业务通知,按创建时间和通知 ID 倒序排列。
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `page` | 否 | 页码,默认 1,范围 1~10000 |
|
||||||
|
| `page_size` | 否 | 每页数量,默认 20,范围 1~50 |
|
||||||
|
|
||||||
|
返回:包含通知列表 `items`、当前页 `page`、每页数量 `size` 和总数 `total`。通知项包含标题、正文、类型、类别、级别、关联资源、已读状态及创建/已读时间。
|
||||||
|
|
||||||
|
## 标记单条通知已读
|
||||||
|
|
||||||
|
`PUT /api/c/v1/notifications/{id}/read`
|
||||||
|
|
||||||
|
将当前客户可见的指定通知标记为已读。通知不存在、属于其他客户或已经已读时,也返回成功,接口幂等。
|
||||||
|
|
||||||
|
返回:`{ "success": true }`
|
||||||
|
|
||||||
|
## 全部标记已读
|
||||||
|
|
||||||
|
`PUT /api/c/v1/notifications/read-all`
|
||||||
|
|
||||||
|
将当前客户可见的未过期未读业务通知全部标记为已读,重复调用幂等。
|
||||||
|
|
||||||
|
返回:`{ "updated_count": 3 }`
|
||||||
|
|
||||||
|
## 查询未读数
|
||||||
|
|
||||||
|
`GET /api/c/v1/notifications/unread-count`
|
||||||
|
|
||||||
|
查询当前客户未过期业务通知的未读数量;平台同步和系统运维通知不计入结果。
|
||||||
|
|
||||||
|
返回:`{ "count": 3, "display_count": "3" }`,数量超过 99 时 `display_count` 为 `99+`。
|
||||||
|
|
||||||
|
## 通用约定
|
||||||
|
|
||||||
|
- 所有接口均使用 `Bearer Token` 鉴权。
|
||||||
|
- 未登录、参数错误、无权访问和服务异常沿用统一错误响应。
|
||||||
19
openspec/changes/add-personal-notifications/proposal.md
Normal file
19
openspec/changes/add-personal-notifications/proposal.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Change: Add personal customer notifications
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
个人客户缺少统一的站内通知入口,无法查看业务通知、获取未读数量或管理通知已读状态。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 增加个人客户通知列表查询接口,支持分页和固定倒序排列。
|
||||||
|
- 增加单条通知已读和全部通知已读接口。
|
||||||
|
- 增加个人客户通知未读数查询接口。
|
||||||
|
- 限制通知数据仅返回当前认证客户可见且未过期的业务通知。
|
||||||
|
- 统一返回通知内容、关联资源、通知级别、已读状态及时间信息。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `personal-notifications`
|
||||||
|
- Affected code: 个人客户通知 API、通知列表入口、未读数展示和已读状态处理
|
||||||
|
- No breaking changes to existing APIs
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Personal customers SHALL be able to query notifications
|
||||||
|
|
||||||
|
The system SHALL provide `GET /api/c/v1/notifications` for authenticated personal customers. The endpoint SHALL return only visible, unexpired business notifications for the current customer, ordered by creation time descending and notification ID descending.
|
||||||
|
|
||||||
|
#### Scenario: Query the first notification page
|
||||||
|
- **WHEN** an authenticated customer requests the notification list without pagination parameters
|
||||||
|
- **THEN** the system SHALL return page 1 with up to 20 notifications
|
||||||
|
- **AND** the response SHALL include `items`, `page`, `size`, and `total`
|
||||||
|
|
||||||
|
#### Scenario: Apply valid pagination
|
||||||
|
- **WHEN** the request includes `page` from 1 to 10000 and `page_size` from 1 to 50
|
||||||
|
- **THEN** the system SHALL return the requested page and page size
|
||||||
|
|
||||||
|
#### Scenario: Reject invalid pagination
|
||||||
|
- **WHEN** `page` or `page_size` is outside its allowed range
|
||||||
|
- **THEN** the system SHALL return a parameter validation error
|
||||||
|
|
||||||
|
### Requirement: Notification items SHALL expose client-readable status and metadata
|
||||||
|
|
||||||
|
Each notification item SHALL include `id`, `title`, `body`, `type`, `category`, `severity`, `ref_type`, `ref_id`, `ref_key`, `is_read`, `created_at`, and nullable `read_at`.
|
||||||
|
|
||||||
|
#### Scenario: Return an unread notification
|
||||||
|
- **WHEN** a visible notification has not been read by the current customer
|
||||||
|
- **THEN** the system SHALL return `is_read: false` and `read_at: null`
|
||||||
|
|
||||||
|
### Requirement: Personal customers SHALL be able to mark one notification as read
|
||||||
|
|
||||||
|
The system SHALL provide `PUT /api/c/v1/notifications/{id}/read` for authenticated personal customers. The operation SHALL be idempotent and SHALL only update a notification visible to the current customer.
|
||||||
|
|
||||||
|
#### Scenario: Mark a visible unread notification
|
||||||
|
- **WHEN** the customer marks a visible unread notification as read
|
||||||
|
- **THEN** the system SHALL set its read state and return `{ "success": true }`
|
||||||
|
|
||||||
|
#### Scenario: Mark an unavailable or already-read notification
|
||||||
|
- **WHEN** the notification does not exist, belongs to another customer, or is already read
|
||||||
|
- **THEN** the system SHALL return `{ "success": true }` without exposing ownership information
|
||||||
|
|
||||||
|
### Requirement: Personal customers SHALL be able to mark all notifications as read
|
||||||
|
|
||||||
|
The system SHALL provide `PUT /api/c/v1/notifications/read-all` to mark all visible, unexpired, unread business notifications for the current customer as read.
|
||||||
|
|
||||||
|
#### Scenario: Mark all unread notifications
|
||||||
|
- **WHEN** the customer calls the mark-all-read endpoint
|
||||||
|
- **THEN** the system SHALL return the number of notifications actually updated in `updated_count`
|
||||||
|
- **AND** repeated calls SHALL remain successful and return zero when nothing needs updating
|
||||||
|
|
||||||
|
### Requirement: Personal customers SHALL be able to query unread count
|
||||||
|
|
||||||
|
The system SHALL provide `GET /api/c/v1/notifications/unread-count`. The count SHALL include only visible, unexpired business notifications and SHALL exclude platform sync and system operations notifications.
|
||||||
|
|
||||||
|
#### Scenario: Return unread count and badge text
|
||||||
|
- **WHEN** an authenticated customer queries the unread count
|
||||||
|
- **THEN** the system SHALL return numeric `count` and string `display_count`
|
||||||
|
- **AND** `display_count` SHALL be `99+` when `count` exceeds 99
|
||||||
26
openspec/changes/add-personal-notifications/tasks.md
Normal file
26
openspec/changes/add-personal-notifications/tasks.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
## 1. API and Data Contract
|
||||||
|
|
||||||
|
- [x] 1.1 Define the client-side personal notification item and paginated list contract
|
||||||
|
- [x] 1.2 Define the client-side unread count and read-operation contracts
|
||||||
|
- [x] 1.3 Add authenticated notification API wrappers
|
||||||
|
|
||||||
|
## 2. Notification Behavior
|
||||||
|
|
||||||
|
- [ ] 2.1 Return only current customer's visible, unexpired business notifications
|
||||||
|
- [ ] 2.2 Apply creation-time and notification-ID descending ordering
|
||||||
|
- [ ] 2.3 Support idempotent single-read and read-all operations
|
||||||
|
- [ ] 2.4 Exclude platform sync and system operations notifications from unread count
|
||||||
|
|
||||||
|
## 3. Client Entry
|
||||||
|
|
||||||
|
- [x] 3.1 Add the personal notification list entry and pagination handling
|
||||||
|
- [x] 3.2 Add unread-count display and refresh behavior
|
||||||
|
- [x] 3.3 Mark notifications read from list actions and support mark-all-read
|
||||||
|
|
||||||
|
## 4. Verification
|
||||||
|
|
||||||
|
- [ ] 4.1 Test authentication, pagination limits, ordering, and visibility isolation
|
||||||
|
- [ ] 4.2 Test idempotent read behavior and unread-count updates
|
||||||
|
- [x] 4.3 Verify the H5 build and unified success/error response handling
|
||||||
|
|
||||||
|
> Note: The repository contains the H5 client only. Tasks 2.1-2.4 and 4.1-4.2 require implementation and verification in the backend service repository.
|
||||||
@@ -44,6 +44,12 @@
|
|||||||
"navigationBarTitleText": "我的订单"
|
"navigationBarTitleText": "我的订单"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/notifications/notifications",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "站内通知"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/bind/bind",
|
"path": "pages/bind/bind",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
<WifiCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" @modify="modifyWifi" @copy="copy" />
|
<WifiCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" @modify="modifyWifi" @copy="copy" />
|
||||||
|
|
||||||
<FunctionCard :realNameStatus="realNameStatus" :alreadyBindPhone="alreadyBindPhone"
|
<FunctionCard :realNameStatus="realNameStatus" :alreadyBindPhone="alreadyBindPhone"
|
||||||
:isDevice="userInfo.isDevice" :walletBalance="deviceInfo.walletBalance" @enter="enterDetail" @sync="onSync">
|
:isDevice="userInfo.isDevice" :walletBalance="deviceInfo.walletBalance" :unreadCount="notificationUnreadCount"
|
||||||
|
@enter="enterDetail" @sync="onSync">
|
||||||
<!-- 修改WIFI弹窗 -->
|
<!-- 修改WIFI弹窗 -->
|
||||||
<up-popup :show="showModifyWifi" mode="center" @close="showModifyWifi=false">
|
<up-popup :show="showModifyWifi" mode="center" @close="showModifyWifi=false">
|
||||||
<view class="wifi-popup">
|
<view class="wifi-popup">
|
||||||
@@ -124,7 +125,8 @@
|
|||||||
import FloatingButton from '@/components/FloatingButton.vue';
|
import FloatingButton from '@/components/FloatingButton.vue';
|
||||||
import {
|
import {
|
||||||
assetApi,
|
assetApi,
|
||||||
deviceApi
|
deviceApi,
|
||||||
|
notificationApi
|
||||||
} from '@/api/index.js';
|
} from '@/api/index.js';
|
||||||
import {
|
import {
|
||||||
useUserStore
|
useUserStore
|
||||||
@@ -210,6 +212,7 @@
|
|||||||
let smsCodePhone = ref('');
|
let smsCodePhone = ref('');
|
||||||
let smsCode = ref('');
|
let smsCode = ref('');
|
||||||
let indexEntryChecking = ref(false);
|
let indexEntryChecking = ref(false);
|
||||||
|
let notificationUnreadCount = ref(0);
|
||||||
|
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
if (!dateStr) return '-';
|
if (!dateStr) return '-';
|
||||||
@@ -699,6 +702,11 @@
|
|||||||
|
|
||||||
const enterDetail = (name) => {
|
const enterDetail = (name) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
case 'notifications':
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/notifications/notifications'
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'package-order':
|
case 'package-order':
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/package-order/package-order'
|
url: '/pages/package-order/package-order'
|
||||||
@@ -767,12 +775,22 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadNotificationUnreadCount = async () => {
|
||||||
|
try {
|
||||||
|
const data = await notificationApi.getUnreadCount();
|
||||||
|
notificationUnreadCount.value = Number(data?.count || 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载通知未读数失败', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initCurrentMonth();
|
initCurrentMonth();
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
handleIndexEntry();
|
handleIndexEntry();
|
||||||
|
loadNotificationUnreadCount();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
160
pages/notifications/notifications.vue
Normal file
160
pages/notifications/notifications.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="toolbar flex-row-sb">
|
||||||
|
<view class="summary">共 {{ total }} 条通知</view>
|
||||||
|
<button class="btn-apple btn-secondary mark-all" :disabled="unreadCount === 0" @tap="markAllRead">
|
||||||
|
全部已读
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="notifications.length === 0 && !loading" class="empty-state">
|
||||||
|
<image src="/static/tips.png" mode="aspectFit" class="empty-icon" />
|
||||||
|
<view class="empty-title">暂无通知</view>
|
||||||
|
<view class="empty-desc">当前没有可查看的业务通知</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="notification-list">
|
||||||
|
<view v-for="item in notifications" :key="item.id" class="card notification-card"
|
||||||
|
:class="{ unread: !item.is_read }" @tap="markItemRead(item)">
|
||||||
|
<view class="notification-header flex-row-sb">
|
||||||
|
<view class="notification-title">{{ item.title || '业务通知' }}</view>
|
||||||
|
<view v-if="!item.is_read" class="unread-dot"></view>
|
||||||
|
</view>
|
||||||
|
<view class="notification-body">{{ item.body || '-' }}</view>
|
||||||
|
<view class="notification-footer flex-row-sb">
|
||||||
|
<view class="notification-meta">{{ getCategoryText(item.category) }} · {{ getSeverityText(item.severity) }}</view>
|
||||||
|
<view class="notification-time">{{ formatDate(item.created_at) }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="notifications.length > 0" class="load-more" @tap="loadMore">
|
||||||
|
<text v-if="loading">加载中...</text>
|
||||||
|
<text v-else-if="noMore">没有更多了</text>
|
||||||
|
<text v-else>点击加载更多</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import { notificationApi } from '@/api/index.js';
|
||||||
|
|
||||||
|
const pageSize = 20;
|
||||||
|
const notifications = reactive([]);
|
||||||
|
const page = ref(1);
|
||||||
|
const total = ref(0);
|
||||||
|
const unreadCount = ref(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
const noMore = ref(false);
|
||||||
|
|
||||||
|
const formatDate = (value) => {
|
||||||
|
if (!value) return '-';
|
||||||
|
return value.replace('T', ' ').slice(0, 16);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryText = (category) => ({
|
||||||
|
approval: '审批',
|
||||||
|
expiry: '临期',
|
||||||
|
sync: '同步',
|
||||||
|
system: '系统'
|
||||||
|
}[category] || '通知');
|
||||||
|
|
||||||
|
const getSeverityText = (severity) => ({
|
||||||
|
info: '提示',
|
||||||
|
warning: '警告',
|
||||||
|
error: '错误',
|
||||||
|
critical: '严重'
|
||||||
|
}[severity] || '提示');
|
||||||
|
|
||||||
|
const loadUnreadCount = async () => {
|
||||||
|
try {
|
||||||
|
const data = await notificationApi.getUnreadCount();
|
||||||
|
unreadCount.value = Number(data?.count || 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载通知未读数失败', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadNotifications = async (append = false) => {
|
||||||
|
if (loading.value || (append && noMore.value)) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await notificationApi.getList(page.value, pageSize);
|
||||||
|
const items = data?.items || [];
|
||||||
|
if (append) {
|
||||||
|
notifications.push(...items);
|
||||||
|
} else {
|
||||||
|
notifications.splice(0, notifications.length, ...items);
|
||||||
|
}
|
||||||
|
total.value = Number(data?.total || 0);
|
||||||
|
noMore.value = notifications.length >= total.value || items.length < pageSize;
|
||||||
|
if (!noMore.value) page.value += 1;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载通知列表失败', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const markItemRead = async (item) => {
|
||||||
|
if (item.is_read) return;
|
||||||
|
try {
|
||||||
|
await notificationApi.markRead(item.id);
|
||||||
|
item.is_read = true;
|
||||||
|
item.read_at = new Date().toISOString();
|
||||||
|
unreadCount.value = Math.max(unreadCount.value - 1, 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('标记通知已读失败', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const markAllRead = async () => {
|
||||||
|
if (!unreadCount.value) return;
|
||||||
|
try {
|
||||||
|
await notificationApi.markAllRead();
|
||||||
|
notifications.forEach(item => {
|
||||||
|
item.is_read = true;
|
||||||
|
item.read_at = item.read_at || new Date().toISOString();
|
||||||
|
});
|
||||||
|
unreadCount.value = 0;
|
||||||
|
uni.showToast({ title: '已全部标记为已读', icon: 'success' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('全部标记通知已读失败', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
if (!noMore.value) loadNotifications(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadNotifications();
|
||||||
|
loadUnreadCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
loadUnreadCount();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container { padding-bottom: 40rpx; }
|
||||||
|
.toolbar { margin-bottom: var(--space-md); }
|
||||||
|
.summary { color: var(--text-tertiary); font-size: 24rpx; }
|
||||||
|
.mark-all { margin: 0; padding: 0 24rpx; height: 64rpx; line-height: 64rpx; font-size: 24rpx; }
|
||||||
|
.notification-card { margin-bottom: var(--space-md); border-left: 6rpx solid transparent; }
|
||||||
|
.notification-card.unread { border-left-color: var(--primary); }
|
||||||
|
.notification-header { margin-bottom: var(--space-sm); }
|
||||||
|
.notification-title { color: var(--text-primary); font-size: 30rpx; font-weight: 600; flex: 1; }
|
||||||
|
.unread-dot { width: 14rpx; height: 14rpx; margin-left: 16rpx; border-radius: 50%; background: var(--primary); }
|
||||||
|
.notification-body { color: var(--text-secondary); font-size: 26rpx; line-height: 1.6; white-space: pre-wrap; }
|
||||||
|
.notification-footer { margin-top: var(--space-md); }
|
||||||
|
.notification-meta, .notification-time { color: var(--text-tertiary); font-size: 22rpx; }
|
||||||
|
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 140rpx 40rpx; }
|
||||||
|
.empty-icon { width: 100rpx; height: 100rpx; opacity: 0.6; margin-bottom: 24rpx; }
|
||||||
|
.empty-title { color: var(--text-primary); font-size: 30rpx; font-weight: 600; }
|
||||||
|
.empty-desc { color: var(--text-tertiary); font-size: 24rpx; margin-top: 12rpx; }
|
||||||
|
.load-more { padding: var(--space-lg); color: var(--text-tertiary); text-align: center; font-size: 24rpx; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user