From 9c1c296d2e4c90f1a0d717f5768d5847aeeb57ae Mon Sep 17 00:00:00 2001 From: luo Date: Sat, 25 Jul 2026 11:08:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AB=99=E5=86=85=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/index.js | 3 +- api/modules/notification.js | 32 ++++ components/FunctionCard.vue | 28 +++ docs/产品迭代7月份/个人客户站内通知.md | 45 +++++ .../add-personal-notifications/proposal.md | 19 +++ .../specs/personal-notifications/spec.md | 56 ++++++ .../add-personal-notifications/tasks.md | 26 +++ pages.json | 6 + pages/index/index.vue | 22 ++- pages/notifications/notifications.vue | 160 ++++++++++++++++++ 10 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 api/modules/notification.js create mode 100644 docs/产品迭代7月份/个人客户站内通知.md create mode 100644 openspec/changes/add-personal-notifications/proposal.md create mode 100644 openspec/changes/add-personal-notifications/specs/personal-notifications/spec.md create mode 100644 openspec/changes/add-personal-notifications/tasks.md create mode 100644 pages/notifications/notifications.vue diff --git a/api/index.js b/api/index.js index c1b0a6a..1bf53a4 100644 --- a/api/index.js +++ b/api/index.js @@ -3,6 +3,7 @@ export { assetApi } from './modules/asset.js'; export { deviceApi } from './modules/device.js'; export { exchangeApi } from './modules/exchange.js'; export { orderApi } from './modules/order.js'; +export { notificationApi } from './modules/notification.js'; export { realnameApi } from './modules/realname.js'; export { walletApi } from './modules/wallet.js'; -export { wechatApi } from './modules/wechat.js'; \ No newline at end of file +export { wechatApi } from './modules/wechat.js'; diff --git a/api/modules/notification.js b/api/modules/notification.js new file mode 100644 index 0000000..b07aeb8 --- /dev/null +++ b/api/modules/notification.js @@ -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' + }); + } +}; diff --git a/components/FunctionCard.vue b/components/FunctionCard.vue index be23dd3..28ff139 100644 --- a/components/FunctionCard.vue +++ b/components/FunctionCard.vue @@ -29,6 +29,14 @@ 套餐订购 + + + 站内通知 + {{ unreadCount > 99 ? '99+' : unreadCount }} + + 站内通知 + @@ -132,6 +140,10 @@ walletBalance: { type: [Number, String], 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 { font-size: 28rpx; font-weight: 500; diff --git a/docs/产品迭代7月份/个人客户站内通知.md b/docs/产品迭代7月份/个人客户站内通知.md new file mode 100644 index 0000000..320bd82 --- /dev/null +++ b/docs/产品迭代7月份/个人客户站内通知.md @@ -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` 鉴权。 +- 未登录、参数错误、无权访问和服务异常沿用统一错误响应。 diff --git a/openspec/changes/add-personal-notifications/proposal.md b/openspec/changes/add-personal-notifications/proposal.md new file mode 100644 index 0000000..8f5bcc7 --- /dev/null +++ b/openspec/changes/add-personal-notifications/proposal.md @@ -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 diff --git a/openspec/changes/add-personal-notifications/specs/personal-notifications/spec.md b/openspec/changes/add-personal-notifications/specs/personal-notifications/spec.md new file mode 100644 index 0000000..0744350 --- /dev/null +++ b/openspec/changes/add-personal-notifications/specs/personal-notifications/spec.md @@ -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 diff --git a/openspec/changes/add-personal-notifications/tasks.md b/openspec/changes/add-personal-notifications/tasks.md new file mode 100644 index 0000000..ed3d06e --- /dev/null +++ b/openspec/changes/add-personal-notifications/tasks.md @@ -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. diff --git a/pages.json b/pages.json index cfba92c..7ffd700 100644 --- a/pages.json +++ b/pages.json @@ -44,6 +44,12 @@ "navigationBarTitleText": "我的订单" } }, + { + "path": "pages/notifications/notifications", + "style": { + "navigationBarTitleText": "站内通知" + } + }, { "path": "pages/bind/bind", "style": { diff --git a/pages/index/index.vue b/pages/index/index.vue index 591ae13..ecbcbcb 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -17,7 +17,8 @@ + :isDevice="userInfo.isDevice" :walletBalance="deviceInfo.walletBalance" :unreadCount="notificationUnreadCount" + @enter="enterDetail" @sync="onSync"> @@ -124,7 +125,8 @@ import FloatingButton from '@/components/FloatingButton.vue'; import { assetApi, - deviceApi + deviceApi, + notificationApi } from '@/api/index.js'; import { useUserStore @@ -210,6 +212,7 @@ let smsCodePhone = ref(''); let smsCode = ref(''); let indexEntryChecking = ref(false); + let notificationUnreadCount = ref(0); const formatDate = (dateStr) => { if (!dateStr) return '-'; @@ -699,6 +702,11 @@ const enterDetail = (name) => { switch (name) { + case 'notifications': + uni.navigateTo({ + url: '/pages/notifications/notifications' + }); + break; case 'package-order': uni.navigateTo({ 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(() => { initCurrentMonth(); }); onShow(() => { handleIndexEntry(); + loadNotificationUnreadCount(); }); diff --git a/pages/notifications/notifications.vue b/pages/notifications/notifications.vue new file mode 100644 index 0000000..b54c255 --- /dev/null +++ b/pages/notifications/notifications.vue @@ -0,0 +1,160 @@ + + + + +