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:
wangxiaogang
2026-06-06 22:02:28 +08:00
parent 83571de723
commit a35689cdda
20 changed files with 1058 additions and 202 deletions

View File

@@ -112,6 +112,26 @@
<Icon name="edit" :size="48" color="#BFB3B3" />
<text class="state-text">还没有记录快去记一笔吧</text>
</view>
<!-- 强提醒公告弹窗 -->
<view class="modal-mask" v-if="urgentNotice" @tap="dismissUrgent">
<view class="urgent-modal" @tap.stop>
<view class="urgent-header">
<Icon name="bell" :size="32" color="#FF8C69" />
<text class="urgent-title">{{ urgentNotice.title }}</text>
</view>
<scroll-view class="urgent-body" scroll-y>
<image v-if="urgentNotice.image_url" class="urgent-image" :src="urgentNotice.image_url" mode="widthFix" />
<text class="urgent-content">{{ urgentNotice.content }}</text>
<view v-if="urgentNotice.link_url" class="urgent-link" @tap="openLink(urgentNotice.link_url)">
<text class="urgent-link-text">查看详情</text>
</view>
</scroll-view>
<view class="urgent-footer" @tap="dismissUrgent">
<text class="urgent-btn-text">我知道了</text>
</view>
</view>
</view>
</view>
</template>
@@ -160,6 +180,27 @@ const greeting = computed(() => {
return '晚上好'
})
// 强提醒公告:取第一条未读的
const urgentNotice = computed(() => notifStore.urgentNotifications[0] || null)
function dismissUrgent() {
if (urgentNotice.value) {
notifStore.markRead(urgentNotice.value.id)
}
}
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 loadData(silent = false) {
await waitForReady()
try {
@@ -167,15 +208,21 @@ async function loadData(silent = false) {
loadError.value = false
const now = new Date()
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
// 关键数据:任一失败则显示错误
await Promise.all([
statsStore.fetchOverview(),
budgetStore.fetchBudget(),
txStore.fetchTransactions({ page: 1 }),
fetchTodayData(today),
userStore.fetchUserInfo(),
notifStore.fetchUnreadCount()
fetchTodayData(today)
])
recentTx.value = txStore.transactions.slice(0, 5)
// 非关键数据:失败不影响页面显示
Promise.allSettled([
userStore.fetchUserInfo(),
notifStore.fetchUnreadCount(),
notifStore.fetchUrgentNotifications()
])
} catch (e) {
console.error('Load error:', e)
if (!silent) loadError.value = true
@@ -516,4 +563,85 @@ function goBills() {
font-size: 28rpx;
color: #BFB3B3;
}
/* 强提醒公告弹窗 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 300;
display: flex;
align-items: center;
justify-content: center;
}
.urgent-modal {
width: 80%;
max-height: 70vh;
background: #FFFFFF;
border-radius: 32rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.urgent-header {
display: flex;
align-items: center;
gap: 12rpx;
padding: 32rpx 32rpx 16rpx;
}
.urgent-title {
font-size: 32rpx;
font-weight: 600;
color: #2D1B1B;
flex: 1;
}
.urgent-body {
flex: 1;
padding: 0 32rpx 24rpx;
max-height: 50vh;
}
.urgent-image {
width: 100%;
border-radius: 16rpx;
margin-bottom: 16rpx;
}
.urgent-content {
font-size: 28rpx;
color: #2D1B1B;
line-height: 1.6;
}
.urgent-link {
margin-top: 16rpx;
padding: 12rpx 0;
}
.urgent-link-text {
font-size: 28rpx;
color: #FF8C69;
text-decoration: underline;
}
.urgent-footer {
padding: 24rpx 32rpx;
border-top: 2rpx solid #F0E0D6;
text-align: center;
&:active { background: #FFF8F0; }
}
.urgent-btn-text {
font-size: 30rpx;
font-weight: 600;
color: #FF8C69;
}
</style>