feat: onShow静默刷新 + 群组预算双显 + TransactionItem优化

onShow 静默刷新(首次有loading,后续静默):
- budget: 添加 initialLoaded 守卫,避免首次双重请求
- profile: 添加 onShow 刷新概览/预算/账单/群组
- group-manage: 添加 onShow 刷新群组列表
- category-manage: 添加 onShow 刷新分类列表
- add: 添加 onShow 刷新分类(可能新增了分类)

群组模式预算双显:
- 服务端 GET /budget 群组模式额外返回 myAmount(本人预算)
- 客户端 Budget 类型添加 myAmount 字段
- 预算页群组模式:群组总预算卡片(只读)+ 我的预算卡片(可编辑)
- 个人模式保持原样

TransactionItem 群组模式优化:
- 主图标始终显示分类图标(不再显示创建者头像)
- 他人记录在昵称旁显示小头像(28rpx圆形)
This commit is contained in:
2026-06-04 17:45:37 +08:00
parent e110cd5c5d
commit 8bd4af9b9a
8 changed files with 125 additions and 44 deletions

View File

@@ -4,6 +4,7 @@ import { request } from '@/utils/request'
export interface Budget {
id: number
amount: number
myAmount?: number
month: string
}

View File

@@ -8,13 +8,8 @@
@touchmove="!disableSwipe && onTouchMove($event)"
@touchend="!disableSwipe && onTouchEnd()"
>
<!-- 头像群组模式下显示他人创建者 -->
<view v-if="isOtherCreator" class="creator-avatar">
<image v-if="creatorAvatarUrl" :src="creatorAvatarUrl" class="creator-img" mode="aspectFill" />
<text v-else class="creator-initial">{{ item.creator_nickname?.[0] }}</text>
</view>
<!-- 始终显示分类图标 -->
<CategoryIcon
v-else
:label="item.category_name?.[0] || '?'"
:color="item.category_color || '#BFB3B3'"
:bg-color="(item.category_color || '#BFB3B3') + '20'"
@@ -23,6 +18,8 @@
<view class="info">
<text class="name">{{ item.note || item.category_name || '未分类' }}</text>
<text class="meta">
<image v-if="isOtherCreator && creatorAvatarUrl" :src="creatorAvatarUrl" class="creator-mini-avatar" mode="aspectFill" />
<text v-else-if="isOtherCreator" class="creator-mini-initial">{{ item.creator_nickname?.[0] }}</text>
<text v-if="isOtherCreator" class="creator-name">{{ item.creator_nickname }} · </text>
{{ item.category_name || '未分类' }}{{ hideDate ? '' : ' · ' + formatDate(item.date) }}
</text>
@@ -151,27 +148,26 @@ function onTouchEnd() {
}
}
.creator-avatar {
width: 80rpx;
height: 80rpx;
.creator-mini-avatar {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
vertical-align: middle;
margin-right: 4rpx;
}
.creator-mini-initial {
display: inline-flex;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #FFF0E6;
display: flex;
font-size: 16rpx;
color: #FF8C69;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.creator-img {
width: 100%;
height: 100%;
}
.creator-initial {
font-size: 28rpx;
font-weight: 600;
color: #FF8C69;
vertical-align: middle;
margin-right: 4rpx;
}
.info {

View File

@@ -55,6 +55,7 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useCategoryStore } from '@/stores/category'
import { useTransactionStore } from '@/stores/transaction'
import { waitForReady } from '@/utils/app-ready'
@@ -150,6 +151,11 @@ onMounted(async () => {
}
})
// 返回页面时刷新分类(可能新增了分类)
onShow(() => {
catStore.fetchCategories()
})
function onDateChange(e: any) {
selectedDate.value = e.detail.value
}

View File

@@ -12,27 +12,45 @@
</view>
<view class="content">
<!-- 当前预算预览 -->
<view class="preview-card">
<!-- 群组模式群组总预算只读 -->
<view class="preview-card" v-if="groupStore.isGroupMode && !loading">
<view class="preview-header">
<text class="preview-label">本月预算</text>
<text class="preview-label">群组预算</text>
<text class="preview-month">{{ currentMonth }}</text>
</view>
<text class="preview-amount" v-if="!loading">
<text class="preview-amount">
{{ budget ? formatAmount(budget.amount) : '未设置' }}
</text>
<view class="preview-skeleton" v-else></view>
<BudgetBar
v-if="budget"
:total="budget.amount"
:used="monthExpense"
:showLabel="true"
/>
<text class="preview-hint" v-else>群组成员均未设置预算</text>
</view>
<!-- 我的预算 / 个人模式预算 -->
<view class="preview-card">
<view class="preview-header">
<text class="preview-label">{{ groupStore.isGroupMode ? '我的预算' : '本月预算' }}</text>
<text class="preview-month">{{ currentMonth }}</text>
</view>
<text class="preview-amount" v-if="!loading">
{{ myBudgetAmount > 0 ? formatAmount(myBudgetAmount) : '未设置' }}
</text>
<view class="preview-skeleton" v-else></view>
<BudgetBar
v-if="myBudgetAmount > 0"
:total="myBudgetAmount"
:used="myExpense"
:showLabel="true"
/>
<text class="preview-hint" v-else>设置预算掌控开支</text>
</view>
<!-- 快捷金额 -->
<view class="section-title">快捷金额</view>
<view class="section-title">{{ groupStore.isGroupMode ? '设置我的预算' : '快捷金额' }}</view>
<view class="preset-grid">
<view
class="preset-item"
@@ -65,6 +83,7 @@ import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useBudgetStore } from '@/stores/budget'
import { useStatsStore } from '@/stores/stats'
import { useGroupStore } from '@/stores/group'
import { waitForReady } from '@/utils/app-ready'
import { formatAmount, getCurrentMonth } from '@/utils/format'
import { statusBarHeight } from '@/utils/system'
@@ -74,11 +93,15 @@ import Icon from '@/components/Icon/Icon.vue'
const budgetStore = useBudgetStore()
const statsStore = useStatsStore()
const groupStore = useGroupStore()
const currentMonth = getCurrentMonth()
const budget = ref<any>(null)
const monthExpense = ref(0)
const myBudgetAmount = ref(0)
const myExpense = ref(0)
const loading = ref(true)
const initialLoaded = ref(false)
const amountStr = ref('')
const presets = [1000, 2000, 3000, 5000, 10000]
@@ -87,6 +110,23 @@ function formatPreset(val: number) {
return val >= 10000 ? (val / 10000) + '万' : val.toLocaleString()
}
function syncBudgetData() {
budget.value = budgetStore.budget
monthExpense.value = statsStore.overview.expense
// 群组模式用 myAmount个人模式用 amount
if (budget.value) {
myBudgetAmount.value = groupStore.isGroupMode ? (budget.value.myAmount || 0) : budget.value.amount
myExpense.value = groupStore.isGroupMode ? statsStore.overview.expense : statsStore.overview.expense
} else {
myBudgetAmount.value = 0
myExpense.value = 0
}
// 预填输入框(个人预算)
if (myBudgetAmount.value > 0) {
amountStr.value = (myBudgetAmount.value / 100).toString()
}
}
onMounted(async () => {
await waitForReady()
loading.value = true
@@ -95,27 +135,24 @@ onMounted(async () => {
budgetStore.fetchBudget(currentMonth),
statsStore.fetchOverview(currentMonth)
])
budget.value = budgetStore.budget
monthExpense.value = statsStore.overview.expense
if (budget.value) {
amountStr.value = (budget.value.amount / 100).toString()
}
syncBudgetData()
} catch (e) {
console.error('Budget page load error:', e)
} finally {
loading.value = false
initialLoaded.value = true
}
})
// 返回页面时刷新预算数据
// 返回页面时静默刷新
onShow(async () => {
if (!initialLoaded.value) return
try {
await Promise.all([
budgetStore.fetchBudget(currentMonth),
statsStore.fetchOverview(currentMonth)
])
budget.value = budgetStore.budget
monthExpense.value = statsStore.overview.expense
syncBudgetData()
} catch {}
})
@@ -135,7 +172,7 @@ async function onConfirm() {
}
try {
await budgetStore.setBudget(amount, currentMonth)
budget.value = budgetStore.budget
syncBudgetData()
uni.showToast({ title: '预算已设置', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1500)
} catch {

View File

@@ -111,6 +111,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useCategoryStore, type Category } from '@/stores/category'
import { waitForReady } from '@/utils/app-ready'
import { getCategoryTransactionCount } from '@/api/category'
@@ -144,9 +145,17 @@ const migrateTargets = computed(() => {
return currentCategories.value.filter(c => c.id !== deletingCat.value?.id)
})
const initialLoaded = ref(false)
onMounted(async () => {
await waitForReady()
catStore.fetchCategories()
await catStore.fetchCategories()
initialLoaded.value = true
})
// 返回页面时静默刷新
onShow(() => {
if (initialLoaded.value) catStore.fetchCategories()
})
function goBack() {

View File

@@ -76,6 +76,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useGroupStore } from '@/stores/group'
import { waitForReady } from '@/utils/app-ready'
import { statusBarHeight, capsuleRight } from '@/utils/system'
@@ -86,10 +87,17 @@ const groupStore = useGroupStore()
const newGroupName = ref('')
const inviteCode = ref('')
const saving = ref(false)
const initialLoaded = ref(false)
onMounted(async () => {
await waitForReady()
groupStore.fetchGroups()
await groupStore.fetchGroups()
initialLoaded.value = true
})
// 返回页面时静默刷新
onShow(() => {
if (initialLoaded.value) groupStore.fetchGroups()
})
function goBack() {

View File

@@ -118,6 +118,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useTransactionStore } from '@/stores/transaction'
import { useStatsStore } from '@/stores/stats'
import { useBudgetStore } from '@/stores/budget'
@@ -140,6 +141,7 @@ const budget = computed(() => budgetStore.budget)
const loading = ref(true)
const showAbout = ref(false)
const showIdentity = ref(false)
const initialLoaded = ref(false)
onMounted(async () => {
await waitForReady()
@@ -156,9 +158,23 @@ onMounted(async () => {
console.error('Profile load error:', e)
} finally {
loading.value = false
initialLoaded.value = true
}
})
// 返回页面时静默刷新
onShow(async () => {
if (!initialLoaded.value) return
try {
await Promise.all([
statsStore.fetchOverview(),
budgetStore.fetchBudget(),
txStore.fetchTransactions({ page: 1, pageSize: 1 }),
groupStore.fetchGroups()
])
} catch {}
})
function goBudget() {
uni.navigateTo({ url: '/pages/budget/index' })
}

View File

@@ -15,7 +15,7 @@ router.get('/', async (req: AuthRequest, res: Response) => {
const group_id = req.query.group_id as string | undefined
if (group_id && group_id !== 'null') {
// 群组视图:所有成员的预算总和
// 群组视图:所有成员的预算总和 + 当前用户个人预算
const [memberCheck] = await pool.query(
'SELECT id FROM group_members WHERE group_id = ? AND user_id = ?',
[group_id, req.userId]
@@ -24,15 +24,20 @@ router.get('/', async (req: AuthRequest, res: Response) => {
return res.status(403).json({ code: 40300, message: '无权访问此群组' })
}
const [rows] = await pool.query(
const [groupRows] = await pool.query(
`SELECT COALESCE(SUM(amount), 0) as amount, month
FROM budgets
WHERE user_id IN (SELECT user_id FROM group_members WHERE group_id = ?) AND month = ?
GROUP BY month`,
[group_id, month]
)
const budget = (rows as any[])[0] || { amount: 0, month }
res.json({ code: 0, data: budget })
const [myRows] = await pool.query(
'SELECT COALESCE(amount, 0) as amount FROM budgets WHERE user_id = ? AND month = ?',
[req.userId, month]
)
const groupBudget = (groupRows as any[])[0] || { amount: 0, month }
const myAmount = (myRows as any[])[0]?.amount || 0
res.json({ code: 0, data: { amount: groupBudget.amount, myAmount, month } })
} else {
// 个人视图
const [rows] = await pool.query(
@@ -40,6 +45,9 @@ router.get('/', async (req: AuthRequest, res: Response) => {
[req.userId, month]
)
const budget = (rows as any[])[0] || null
if (budget) {
budget.myAmount = budget.amount
}
res.json({ code: 0, data: budget })
}
} catch (err) {