Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
861ffa371b | ||
|
|
f6a33121f2 | ||
|
|
133a4d18ef | ||
|
|
9c1c296d2e | ||
|
|
3929156ef4 | ||
|
|
ad3c9b964d |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.html linguist-detectable=false
|
||||
*.vue linguist-detectable=true
|
||||
@@ -3,7 +3,7 @@ name: 构建并部署前端到生产环境
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- dev
|
||||
- test
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: 设置镜像标签
|
||||
id: tag
|
||||
run: |
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
if [ "${{ github.ref }}" = "refs/heads/develop" ]; then
|
||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
|
||||
echo "tag=dev" >> $GITHUB_OUTPUT
|
||||
@@ -52,8 +52,8 @@ jobs:
|
||||
docker push ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
||||
docker push ${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
|
||||
- name: 部署到本地(仅 main 分支)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
- name: 部署到本地(仅 develop 分支)
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
mkdir -p ${{ env.DEPLOY_DIR }}
|
||||
|
||||
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -2,9 +2,22 @@
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
npminstall-debug.log
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Common local files
|
||||
dist-ssr
|
||||
*.local
|
||||
.cursorrules
|
||||
.claude
|
||||
.codex
|
||||
.env.development
|
||||
.env.production
|
||||
.scratch
|
||||
.agents
|
||||
Thumbs.db
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
@@ -91,4 +104,4 @@ dist/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -9,14 +9,17 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
# 设置工作目录
|
||||
WORKDIR /build
|
||||
|
||||
# 设置 npm 镜像源
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
# 安装 pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
|
||||
|
||||
# 复制 package.json(利用 Docker 缓存)
|
||||
COPY package.json ./
|
||||
# 设置 pnpm 镜像源
|
||||
RUN pnpm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 复制 package.json 和 pnpm-lock.yaml(利用 Docker 缓存)
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# 安装所有依赖
|
||||
RUN npm install --legacy-peer-deps
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# 安装 git (用于获取 commit 信息)
|
||||
RUN apk add --no-cache git
|
||||
@@ -24,21 +27,18 @@ RUN apk add --no-cache git
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 列出文件确认复制成功
|
||||
RUN ls -la
|
||||
|
||||
# 创建 src 目录软链接 (兼容 uni-app 新版本)
|
||||
RUN mkdir -p src && \
|
||||
ln -sf /build/manifest.json /build/src/manifest.json && \
|
||||
ln -sf /build/pages.json /build/src/pages.json
|
||||
|
||||
# 构建 H5 生产版本
|
||||
RUN npm run build:h5
|
||||
RUN pnpm run build:h5
|
||||
|
||||
# ================================
|
||||
# 阶段 2: 运行阶段
|
||||
# ================================
|
||||
FROM --platform=linux/amd64 nginx:alpine
|
||||
FROM --platform=linux/amd64 nginx:1.31.2-alpine
|
||||
|
||||
# 使用阿里云镜像源加速
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
@@ -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';
|
||||
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 class="function-name">套餐订购</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/notification.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"
|
||||
tabindex="0">
|
||||
<view class="function-icon">
|
||||
@@ -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;
|
||||
|
||||
@@ -9,6 +9,9 @@ services:
|
||||
- '3003:80'
|
||||
networks:
|
||||
- device-voice-network
|
||||
tmpfs:
|
||||
- /run:rw
|
||||
- /tmp:rw
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://127.0.0.1:80/health']
|
||||
interval: 30s
|
||||
|
||||
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": "我的订单"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/notifications/notifications",
|
||||
"style": {
|
||||
"navigationBarTitleText": "站内通知"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/bind/bind",
|
||||
"style": {
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
<WifiCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" @modify="modifyWifi" @copy="copy" />
|
||||
|
||||
<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弹窗 -->
|
||||
<up-popup :show="showModifyWifi" mode="center" @close="showModifyWifi=false">
|
||||
<view class="wifi-popup">
|
||||
@@ -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();
|
||||
});
|
||||
</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/notification.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>
|
||||
BIN
static/notification.png
Normal file
BIN
static/notification.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
Reference in New Issue
Block a user