feat: 迭代三大模块 + 全系统 Bug 修复

新功能:
- 账单筛选统计:多维度筛选面板 + 保存方案 + 结果统计
- 通知公告:系统公告/群组通知/个人提醒 + 通知中心页面
- 管理员后台:数据看板 + 用户管理 + 发布公告

Bug 修复:
- 统计页 fetchPrevMonthData/fetchMaxSingle 添加 group_id
- 群组视图添加 t.group_id 过滤,防止私人数据泄露
- 通知列表添加群组通知条件
- 通知标记已读添加错误处理
- 管理员 API 添加 affectedRows 检查
- 筛选面板金额为 0 时正确回填
- profile onShow 补充 userStore.fetchUserInfo
- 全系统样式修复(overflow/box-sizing)
This commit is contained in:
wangxiaogang
2026-06-06 17:55:29 +08:00
parent bc6eca1e9b
commit 83571de723
35 changed files with 2255 additions and 37 deletions

View File

@@ -11,8 +11,22 @@
<view class="tab" :class="{ active: filterType === 'expense' }" @tap="switchFilter('expense')">支出</view>
<view class="tab" :class="{ active: filterType === 'income' }" @tap="switchFilter('income')">收入</view>
</view>
<view class="filter-btn" :class="{ active: hasAdvancedFilter }" @tap="showFilter = true">
<Icon name="filter" :size="28" :color="hasAdvancedFilter ? '#FF8C69' : '#8B7E7E'" />
<text class="filter-btn-text">筛选</text>
</view>
</view>
<!-- 筛选结果统计 -->
<view class="filter-stats" v-if="hasAdvancedFilter && !loading">
<text class="filter-stats-text"> {{ total }} </text>
<text class="filter-stats-text" v-if="filteredExpense > 0">支出 {{ formatAmount(filteredExpense) }}</text>
<text class="filter-stats-text" v-if="filteredIncome > 0">收入 {{ formatAmount(filteredIncome) }}</text>
</view>
<!-- 筛选面板 -->
<FilterPanel v-if="showFilter" :initialFilters="advancedFilters" @close="showFilter = false" @apply="onFilterApply" />
<view class="tx-list" v-if="!txStore.loading || txList.length > 0">
<view v-for="group in groupedTx" :key="group.date" class="date-group">
<view class="group-header">
@@ -81,8 +95,10 @@ import { waitForReady } from '@/utils/app-ready'
import TransactionItem from '@/components/TransactionItem/TransactionItem.vue'
import Icon from '@/components/Icon/Icon.vue'
import Skeleton from '@/components/Skeleton/Skeleton.vue'
import FilterPanel from './filter-panel.vue'
import { statusBarHeight } from '@/utils/system'
import { formatAmount, formatDate } from '@/utils/format'
import type { FilterParams } from '@/api/filter'
const txStore = useTransactionStore()
const userStore = useUserStore()
@@ -95,6 +111,16 @@ const total = ref(0)
const loadError = ref(false)
let loadSeq = 0 // 请求序号,防止并发覆盖
// 高级筛选
const showFilter = ref(false)
const advancedFilters = ref<FilterParams>({})
const filteredExpense = ref(0)
const filteredIncome = ref(0)
const hasAdvancedFilter = computed(() => {
const f = advancedFilters.value
return !!(f.category_ids?.length || f.startDate || f.endDate || f.minAmount || f.maxAmount || f.keyword)
})
const noMore = computed(() => txList.value.length >= total.value && total.value > 0)
const groupedTx = computed(() => {
@@ -138,9 +164,17 @@ async function loadTx(reset = false, silent = false) {
const seq = ++loadSeq
const params: any = { page: page.value, pageSize }
if (filterType.value !== 'all') params.type = filterType.value
// 合并高级筛选参数
const af = advancedFilters.value
if (af.category_ids?.length === 1) params.category_id = af.category_ids[0]
if (af.startDate) params.startDate = af.startDate
if (af.endDate) params.endDate = af.endDate
if (af.minAmount) params.minAmount = af.minAmount
if (af.maxAmount) params.maxAmount = af.maxAmount
if (af.keyword) params.keyword = af.keyword
try {
loadError.value = false
await txStore.fetchTransactions(params)
const data = await txStore.fetchTransactions(params)
// 丢弃过期请求的结果
if (seq !== loadSeq) return
if (reset) {
@@ -149,6 +183,10 @@ async function loadTx(reset = false, silent = false) {
txList.value = [...txList.value, ...txStore.transactions]
}
total.value = txStore.total
if (data) {
filteredExpense.value = data.filteredExpense || 0
filteredIncome.value = data.filteredIncome || 0
}
} catch (e) {
if (seq !== loadSeq) return
console.error('Load bills error:', e)
@@ -156,6 +194,12 @@ async function loadTx(reset = false, silent = false) {
}
}
function onFilterApply(filters: FilterParams) {
advancedFilters.value = filters
showFilter.value = false
loadTx(true)
}
function onEdit(item: any) {
if (groupStore.isGroupMode && item.user_id !== userStore.userInfo?.id) {
uni.showToast({ title: '只能编辑自己的记录', icon: 'none' })
@@ -227,7 +271,13 @@ onPullDownRefresh(() => {
padding: 16rpx 0 24rpx;
}
.tabs-wrap { display: flex; justify-content: center; margin: 0 0 32rpx; }
.tabs-wrap {
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 24rpx;
gap: 16rpx;
}
.tabs {
display: inline-flex;
@@ -251,6 +301,41 @@ onPullDownRefresh(() => {
}
}
.filter-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 12rpx 20rpx;
background: #FFF0E6;
border-radius: 20rpx;
border: 2rpx solid #F0E0D6;
&.active {
border-color: #FF8C69;
background: #FFE8E0;
}
&:active { opacity: 0.7; }
}
.filter-btn-text {
font-size: 24rpx;
color: #8B7E7E;
.active & { color: #FF8C69; }
}
.filter-stats {
display: flex;
gap: 24rpx;
padding: 0 40rpx 16rpx;
}
.filter-stats-text {
font-size: 24rpx;
font-family: 'Fredoka', sans-serif;
color: #8B7E7E;
}
.tx-list { padding: 0 40rpx; }
.date-group {