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:
@@ -4,6 +4,7 @@ import { request } from '@/utils/request'
|
||||
export interface Budget {
|
||||
id: number
|
||||
amount: number
|
||||
myAmount?: number
|
||||
month: string
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user