Files
device-voice-h5/pages/notifications/notifications.vue
luo 697ff9c33c
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m16s
fix: some
2026-10-13 18:02:13 +08:00

191 lines
7.1 KiB
Vue

<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 || markingAll" @tap="markAllRead">
全部已读
</button>
</view>
<view v-if="!notificationsLoaded || (loading && notifications.length === 0)" class="loading-state">
<up-loading-icon text="加载中" size="30"></up-loading-icon>
</view>
<view v-else-if="notifications.length === 0" 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" @tap="markItemRead(item)">
<view class="notification-header flex-row-sb">
<view class="notification-title">{{ item.title || '业务通知' }}</view>
<button class="read-status" :class="item.is_read ? 'status-read' : 'status-unread'"
@tap.stop="markItemRead(item)">{{ item.is_read ? '已读' : '未读' }}</button>
</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 } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { notificationApi } from '@/api/index.js';
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
const pageSize = 20;
const notifications = reactive([]);
const page = ref(1);
const total = ref(0);
const unreadCount = ref(0);
const loading = ref(false);
const notificationsLoaded = ref(false);
const noMore = ref(false);
const markingAll = ref(false);
let listRequestId = 0;
let countRequestId = 0;
const readPending = new Set();
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 () => {
const context = getSessionContext();
const requestId = ++countRequestId;
try {
const data = await notificationApi.getUnreadCount();
if (!isCurrentSession(context) || requestId !== countRequestId) return;
unreadCount.value = Number(data?.count || 0);
} catch (error) {
console.error('加载通知未读数失败', error);
}
};
const loadNotifications = async (append = false) => {
if (append && (loading.value || noMore.value)) return;
const context = getSessionContext();
const requestId = ++listRequestId;
const requestedPage = append ? page.value : 1;
loading.value = true;
try {
const data = await notificationApi.getList(requestedPage, pageSize);
if (!isCurrentSession(context) || requestId !== listRequestId) return;
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;
page.value = requestedPage + 1;
} catch (error) {
console.error('加载通知列表失败', error);
} finally {
if (isCurrentSession(context) && requestId === listRequestId) {
loading.value = false;
notificationsLoaded.value = true;
}
}
};
const markItemRead = async (item) => {
if (item.is_read || readPending.has(item.id) || markingAll.value) return;
const context = getSessionContext();
readPending.add(item.id);
try {
await notificationApi.markRead(item.id);
if (!isCurrentSession(context)) return;
item.is_read = true;
item.read_at = new Date().toISOString();
await loadUnreadCount();
} catch (error) {
console.error('标记通知已读失败', error);
} finally {
readPending.delete(item.id);
}
};
const markAllRead = async () => {
if (!unreadCount.value || markingAll.value) return;
const context = getSessionContext();
markingAll.value = true;
try {
await notificationApi.markAllRead();
if (!isCurrentSession(context)) return;
await Promise.all([loadNotifications(), loadUnreadCount()]);
if (!isCurrentSession(context)) return;
uni.showToast({ title: '已全部标记为已读', icon: 'success' });
} catch (error) {
console.error('全部标记通知已读失败', error);
} finally {
markingAll.value = false;
}
};
const loadMore = () => {
if (!noMore.value) loadNotifications(true);
};
onShow(async () => {
notifications.splice(0);
total.value = 0;
unreadCount.value = 0;
page.value = 1;
noMore.value = false;
notificationsLoaded.value = false;
await Promise.all([loadNotifications(), 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); }
.notification-header { margin-bottom: var(--space-sm); gap: 20rpx; align-items: flex-start; }
.notification-title { color: var(--text-primary); font-size: 30rpx; font-weight: 600; flex: 1; }
.read-status { flex-shrink: 0; margin: 0; padding: 6rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 500; line-height: 1.5; }
.read-status::after { border: none; }
.status-unread { color: var(--primary-dark); background: var(--success-light); }
.status-read { color: var(--text-tertiary); background: var(--gray-100); }
.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; }
.loading-state { display: flex; align-items: center; justify-content: center; min-height: 400rpx; }
.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>