feat: 全面公告系统 + 全系统 Bug 修复
全面公告系统: - notifications 表新增置顶/强提醒/定时发布/过期/配图/链接字段 - 后端增强:7 个接口(列表/置顶/未读数/已读/发布/编辑/删除) - 首页强提醒公告弹窗 - 通知中心:置顶标记、配图展示、链接跳转、管理员操作 - 管理员发布表单:标题+内容+链接+配图+置顶+强提醒+定时+过期 Bug 修复: - 401 重试队列竞态导致请求永久挂起 - 多分类筛选条件被静默丢弃 - 网络错误导致群组选择丢失 - Numpad 输入 '.' 显示异常 - transaction store 错误时清空数据 - 首页/预算页 Promise.all 部分失败阻塞关键数据 - CSS prefers-reduced-motion 类名不匹配 - 通知 read-all 遗漏群组通知 - auth.ts 错误日志缺少 stack trace - stats 页面 v-for key 使用 index - add 页面重复调用 fetchCategories
This commit is contained in:
@@ -33,14 +33,30 @@
|
||||
</view>
|
||||
|
||||
<view v-else class="notification-list">
|
||||
<view v-for="item in list" :key="item.id" class="notification-item" :class="{ unread: !item.is_read }" @tap="handleTap(item)">
|
||||
<view v-for="item in list" :key="item.id" class="notification-item" :class="{ unread: !item.is_read, pinned: item.is_pinned }" @tap="handleTap(item)">
|
||||
<view class="notif-icon" :style="{ background: getNotifBg(item.type) }">
|
||||
<Icon :name="getIconName(item.type)" :size="28" :color="getIconColor(item.type)" />
|
||||
</view>
|
||||
<view class="notif-content">
|
||||
<text class="notif-title">{{ item.title }}</text>
|
||||
<view class="notif-title-row">
|
||||
<text v-if="item.is_pinned" class="pin-badge">📌</text>
|
||||
<text class="notif-title">{{ item.title }}</text>
|
||||
</view>
|
||||
<text class="notif-body" v-if="item.content">{{ item.content }}</text>
|
||||
<text class="notif-time">{{ formatTime(item.created_at) }}</text>
|
||||
<image v-if="item.image_url" class="notif-image" :src="item.image_url" mode="widthFix" />
|
||||
<view class="notif-meta">
|
||||
<text class="notif-time">{{ formatTime(item.created_at) }}</text>
|
||||
<text v-if="item.link_url" class="notif-link" @tap.stop="openLink(item.link_url)">查看详情</text>
|
||||
</view>
|
||||
<!-- 管理员操作 -->
|
||||
<view v-if="isAdmin && item.type === 'system'" class="admin-actions">
|
||||
<view class="admin-btn" @tap.stop="handleTogglePin(item)">
|
||||
<text class="admin-btn-text">{{ item.is_pinned ? '取消置顶' : '置顶' }}</text>
|
||||
</view>
|
||||
<view class="admin-btn danger" @tap.stop="handleDeleteNotif(item)">
|
||||
<text class="admin-btn-text danger-text">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!item.is_read" class="unread-dot"></view>
|
||||
</view>
|
||||
@@ -56,13 +72,16 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import { getNotifications, markRead, markAllRead } from '@/api/notification'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getNotifications, markRead, markAllRead, deleteNotification, updateNotification } from '@/api/notification'
|
||||
import { waitForReady } from '@/utils/app-ready'
|
||||
import { statusBarHeight, capsuleRight } from '@/utils/system'
|
||||
import Icon from '@/components/Icon/Icon.vue'
|
||||
import type { Notification } from '@/api/notification'
|
||||
|
||||
const notifStore = useNotificationStore()
|
||||
const userStore = useUserStore()
|
||||
const isAdmin = computed(() => userStore.userInfo?.role === 'admin')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'all', label: '全部' },
|
||||
@@ -174,6 +193,48 @@ async function handleMarkAllRead() {
|
||||
}
|
||||
}
|
||||
|
||||
function openLink(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => uni.showToast({ title: '链接已复制', icon: 'success' })
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
/** 管理员:切换置顶状态 */
|
||||
async function handleTogglePin(item: Notification) {
|
||||
try {
|
||||
await updateNotification(item.id, { is_pinned: !item.is_pinned })
|
||||
item.is_pinned = item.is_pinned ? 0 : 1
|
||||
uni.showToast({ title: item.is_pinned ? '已置顶' : '已取消置顶', icon: 'success' })
|
||||
} catch {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
/** 管理员:删除公告 */
|
||||
function handleDeleteNotif(item: Notification) {
|
||||
uni.showModal({
|
||||
title: '删除公告',
|
||||
content: `确定删除「${item.title}」?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await deleteNotification(item.id)
|
||||
list.value = list.value.filter(n => n.id !== item.id)
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
} catch {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
@@ -301,6 +362,16 @@ onReachBottom(() => {
|
||||
|
||||
.notif-content { flex: 1; min-width: 0; }
|
||||
|
||||
.notif-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.pin-badge {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.notif-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
@@ -318,11 +389,22 @@ onReachBottom(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notif-image {
|
||||
width: 100%;
|
||||
border-radius: 12rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.notif-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.notif-time {
|
||||
font-size: 22rpx;
|
||||
color: #BFB3B3;
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.unread-dot {
|
||||
@@ -334,6 +416,17 @@ onReachBottom(() => {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.notif-link {
|
||||
font-size: 22rpx;
|
||||
color: #FF8C69;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pinned {
|
||||
border-color: #FF8C69;
|
||||
background: #FFFAF7;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 32rpx 0;
|
||||
@@ -343,4 +436,25 @@ onReachBottom(() => {
|
||||
font-size: 24rpx;
|
||||
color: #BFB3B3;
|
||||
}
|
||||
|
||||
.admin-actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.admin-btn {
|
||||
padding: 8rpx 16rpx;
|
||||
background: #FFF0E6;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&.danger { background: #FFE8E8; }
|
||||
&:active { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.admin-btn-text {
|
||||
font-size: 22rpx;
|
||||
color: #FF8C69;
|
||||
&.danger-text { color: #FF6B6B; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user