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 {
|
export interface Budget {
|
||||||
id: number
|
id: number
|
||||||
amount: number
|
amount: number
|
||||||
|
myAmount?: number
|
||||||
month: string
|
month: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,8 @@
|
|||||||
@touchmove="!disableSwipe && onTouchMove($event)"
|
@touchmove="!disableSwipe && onTouchMove($event)"
|
||||||
@touchend="!disableSwipe && onTouchEnd()"
|
@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
|
<CategoryIcon
|
||||||
v-else
|
|
||||||
:label="item.category_name?.[0] || '?'"
|
:label="item.category_name?.[0] || '?'"
|
||||||
:color="item.category_color || '#BFB3B3'"
|
:color="item.category_color || '#BFB3B3'"
|
||||||
:bg-color="(item.category_color || '#BFB3B3') + '20'"
|
:bg-color="(item.category_color || '#BFB3B3') + '20'"
|
||||||
@@ -23,6 +18,8 @@
|
|||||||
<view class="info">
|
<view class="info">
|
||||||
<text class="name">{{ item.note || item.category_name || '未分类' }}</text>
|
<text class="name">{{ item.note || item.category_name || '未分类' }}</text>
|
||||||
<text class="meta">
|
<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>
|
<text v-if="isOtherCreator" class="creator-name">{{ item.creator_nickname }} · </text>
|
||||||
{{ item.category_name || '未分类' }}{{ hideDate ? '' : ' · ' + formatDate(item.date) }}
|
{{ item.category_name || '未分类' }}{{ hideDate ? '' : ' · ' + formatDate(item.date) }}
|
||||||
</text>
|
</text>
|
||||||
@@ -151,27 +148,26 @@ function onTouchEnd() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.creator-avatar {
|
.creator-mini-avatar {
|
||||||
width: 80rpx;
|
width: 28rpx;
|
||||||
height: 80rpx;
|
height: 28rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creator-mini-initial {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #FFF0E6;
|
background: #FFF0E6;
|
||||||
display: flex;
|
font-size: 16rpx;
|
||||||
|
color: #FF8C69;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
vertical-align: middle;
|
||||||
flex-shrink: 0;
|
margin-right: 4rpx;
|
||||||
}
|
|
||||||
|
|
||||||
.creator-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator-initial {
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #FF8C69;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useCategoryStore } from '@/stores/category'
|
import { useCategoryStore } from '@/stores/category'
|
||||||
import { useTransactionStore } from '@/stores/transaction'
|
import { useTransactionStore } from '@/stores/transaction'
|
||||||
import { waitForReady } from '@/utils/app-ready'
|
import { waitForReady } from '@/utils/app-ready'
|
||||||
@@ -150,6 +151,11 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 返回页面时刷新分类(可能新增了分类)
|
||||||
|
onShow(() => {
|
||||||
|
catStore.fetchCategories()
|
||||||
|
})
|
||||||
|
|
||||||
function onDateChange(e: any) {
|
function onDateChange(e: any) {
|
||||||
selectedDate.value = e.detail.value
|
selectedDate.value = e.detail.value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,27 +12,45 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="content">
|
<view class="content">
|
||||||
<!-- 当前预算预览 -->
|
<!-- 群组模式:群组总预算(只读) -->
|
||||||
<view class="preview-card">
|
<view class="preview-card" v-if="groupStore.isGroupMode && !loading">
|
||||||
<view class="preview-header">
|
<view class="preview-header">
|
||||||
<text class="preview-label">本月预算</text>
|
<text class="preview-label">群组预算</text>
|
||||||
<text class="preview-month">{{ currentMonth }}</text>
|
<text class="preview-month">{{ currentMonth }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="preview-amount" v-if="!loading">
|
<text class="preview-amount">
|
||||||
{{ budget ? formatAmount(budget.amount) : '未设置' }}
|
{{ budget ? formatAmount(budget.amount) : '未设置' }}
|
||||||
</text>
|
</text>
|
||||||
<view class="preview-skeleton" v-else></view>
|
|
||||||
<BudgetBar
|
<BudgetBar
|
||||||
v-if="budget"
|
v-if="budget"
|
||||||
:total="budget.amount"
|
:total="budget.amount"
|
||||||
:used="monthExpense"
|
:used="monthExpense"
|
||||||
:showLabel="true"
|
: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>
|
<text class="preview-hint" v-else>设置预算,掌控开支</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 快捷金额 -->
|
<!-- 快捷金额 -->
|
||||||
<view class="section-title">快捷金额</view>
|
<view class="section-title">{{ groupStore.isGroupMode ? '设置我的预算' : '快捷金额' }}</view>
|
||||||
<view class="preset-grid">
|
<view class="preset-grid">
|
||||||
<view
|
<view
|
||||||
class="preset-item"
|
class="preset-item"
|
||||||
@@ -65,6 +83,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useBudgetStore } from '@/stores/budget'
|
import { useBudgetStore } from '@/stores/budget'
|
||||||
import { useStatsStore } from '@/stores/stats'
|
import { useStatsStore } from '@/stores/stats'
|
||||||
|
import { useGroupStore } from '@/stores/group'
|
||||||
import { waitForReady } from '@/utils/app-ready'
|
import { waitForReady } from '@/utils/app-ready'
|
||||||
import { formatAmount, getCurrentMonth } from '@/utils/format'
|
import { formatAmount, getCurrentMonth } from '@/utils/format'
|
||||||
import { statusBarHeight } from '@/utils/system'
|
import { statusBarHeight } from '@/utils/system'
|
||||||
@@ -74,11 +93,15 @@ import Icon from '@/components/Icon/Icon.vue'
|
|||||||
|
|
||||||
const budgetStore = useBudgetStore()
|
const budgetStore = useBudgetStore()
|
||||||
const statsStore = useStatsStore()
|
const statsStore = useStatsStore()
|
||||||
|
const groupStore = useGroupStore()
|
||||||
|
|
||||||
const currentMonth = getCurrentMonth()
|
const currentMonth = getCurrentMonth()
|
||||||
const budget = ref<any>(null)
|
const budget = ref<any>(null)
|
||||||
const monthExpense = ref(0)
|
const monthExpense = ref(0)
|
||||||
|
const myBudgetAmount = ref(0)
|
||||||
|
const myExpense = ref(0)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
const initialLoaded = ref(false)
|
||||||
const amountStr = ref('')
|
const amountStr = ref('')
|
||||||
|
|
||||||
const presets = [1000, 2000, 3000, 5000, 10000]
|
const presets = [1000, 2000, 3000, 5000, 10000]
|
||||||
@@ -87,6 +110,23 @@ function formatPreset(val: number) {
|
|||||||
return val >= 10000 ? (val / 10000) + '万' : val.toLocaleString()
|
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 () => {
|
onMounted(async () => {
|
||||||
await waitForReady()
|
await waitForReady()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -95,27 +135,24 @@ onMounted(async () => {
|
|||||||
budgetStore.fetchBudget(currentMonth),
|
budgetStore.fetchBudget(currentMonth),
|
||||||
statsStore.fetchOverview(currentMonth)
|
statsStore.fetchOverview(currentMonth)
|
||||||
])
|
])
|
||||||
budget.value = budgetStore.budget
|
syncBudgetData()
|
||||||
monthExpense.value = statsStore.overview.expense
|
|
||||||
if (budget.value) {
|
|
||||||
amountStr.value = (budget.value.amount / 100).toString()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Budget page load error:', e)
|
console.error('Budget page load error:', e)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
initialLoaded.value = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 返回页面时刷新预算数据
|
// 返回页面时静默刷新
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
|
if (!initialLoaded.value) return
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
budgetStore.fetchBudget(currentMonth),
|
budgetStore.fetchBudget(currentMonth),
|
||||||
statsStore.fetchOverview(currentMonth)
|
statsStore.fetchOverview(currentMonth)
|
||||||
])
|
])
|
||||||
budget.value = budgetStore.budget
|
syncBudgetData()
|
||||||
monthExpense.value = statsStore.overview.expense
|
|
||||||
} catch {}
|
} catch {}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -135,7 +172,7 @@ async function onConfirm() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await budgetStore.setBudget(amount, currentMonth)
|
await budgetStore.setBudget(amount, currentMonth)
|
||||||
budget.value = budgetStore.budget
|
syncBudgetData()
|
||||||
uni.showToast({ title: '预算已设置', icon: 'success' })
|
uni.showToast({ title: '预算已设置', icon: 'success' })
|
||||||
setTimeout(() => uni.navigateBack(), 1500)
|
setTimeout(() => uni.navigateBack(), 1500)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useCategoryStore, type Category } from '@/stores/category'
|
import { useCategoryStore, type Category } from '@/stores/category'
|
||||||
import { waitForReady } from '@/utils/app-ready'
|
import { waitForReady } from '@/utils/app-ready'
|
||||||
import { getCategoryTransactionCount } from '@/api/category'
|
import { getCategoryTransactionCount } from '@/api/category'
|
||||||
@@ -144,9 +145,17 @@ const migrateTargets = computed(() => {
|
|||||||
return currentCategories.value.filter(c => c.id !== deletingCat.value?.id)
|
return currentCategories.value.filter(c => c.id !== deletingCat.value?.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const initialLoaded = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await waitForReady()
|
await waitForReady()
|
||||||
catStore.fetchCategories()
|
await catStore.fetchCategories()
|
||||||
|
initialLoaded.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回页面时静默刷新
|
||||||
|
onShow(() => {
|
||||||
|
if (initialLoaded.value) catStore.fetchCategories()
|
||||||
})
|
})
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useGroupStore } from '@/stores/group'
|
import { useGroupStore } from '@/stores/group'
|
||||||
import { waitForReady } from '@/utils/app-ready'
|
import { waitForReady } from '@/utils/app-ready'
|
||||||
import { statusBarHeight, capsuleRight } from '@/utils/system'
|
import { statusBarHeight, capsuleRight } from '@/utils/system'
|
||||||
@@ -86,10 +87,17 @@ const groupStore = useGroupStore()
|
|||||||
const newGroupName = ref('')
|
const newGroupName = ref('')
|
||||||
const inviteCode = ref('')
|
const inviteCode = ref('')
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
const initialLoaded = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await waitForReady()
|
await waitForReady()
|
||||||
groupStore.fetchGroups()
|
await groupStore.fetchGroups()
|
||||||
|
initialLoaded.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回页面时静默刷新
|
||||||
|
onShow(() => {
|
||||||
|
if (initialLoaded.value) groupStore.fetchGroups()
|
||||||
})
|
})
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useTransactionStore } from '@/stores/transaction'
|
import { useTransactionStore } from '@/stores/transaction'
|
||||||
import { useStatsStore } from '@/stores/stats'
|
import { useStatsStore } from '@/stores/stats'
|
||||||
import { useBudgetStore } from '@/stores/budget'
|
import { useBudgetStore } from '@/stores/budget'
|
||||||
@@ -140,6 +141,7 @@ const budget = computed(() => budgetStore.budget)
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const showAbout = ref(false)
|
const showAbout = ref(false)
|
||||||
const showIdentity = ref(false)
|
const showIdentity = ref(false)
|
||||||
|
const initialLoaded = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await waitForReady()
|
await waitForReady()
|
||||||
@@ -156,9 +158,23 @@ onMounted(async () => {
|
|||||||
console.error('Profile load error:', e)
|
console.error('Profile load error:', e)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
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() {
|
function goBudget() {
|
||||||
uni.navigateTo({ url: '/pages/budget/index' })
|
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
|
const group_id = req.query.group_id as string | undefined
|
||||||
|
|
||||||
if (group_id && group_id !== 'null') {
|
if (group_id && group_id !== 'null') {
|
||||||
// 群组视图:所有成员的预算总和
|
// 群组视图:所有成员的预算总和 + 当前用户个人预算
|
||||||
const [memberCheck] = await pool.query(
|
const [memberCheck] = await pool.query(
|
||||||
'SELECT id FROM group_members WHERE group_id = ? AND user_id = ?',
|
'SELECT id FROM group_members WHERE group_id = ? AND user_id = ?',
|
||||||
[group_id, req.userId]
|
[group_id, req.userId]
|
||||||
@@ -24,15 +24,20 @@ router.get('/', async (req: AuthRequest, res: Response) => {
|
|||||||
return res.status(403).json({ code: 40300, message: '无权访问此群组' })
|
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
|
`SELECT COALESCE(SUM(amount), 0) as amount, month
|
||||||
FROM budgets
|
FROM budgets
|
||||||
WHERE user_id IN (SELECT user_id FROM group_members WHERE group_id = ?) AND month = ?
|
WHERE user_id IN (SELECT user_id FROM group_members WHERE group_id = ?) AND month = ?
|
||||||
GROUP BY month`,
|
GROUP BY month`,
|
||||||
[group_id, month]
|
[group_id, month]
|
||||||
)
|
)
|
||||||
const budget = (rows as any[])[0] || { amount: 0, month }
|
const [myRows] = await pool.query(
|
||||||
res.json({ code: 0, data: budget })
|
'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 {
|
} else {
|
||||||
// 个人视图
|
// 个人视图
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
@@ -40,6 +45,9 @@ router.get('/', async (req: AuthRequest, res: Response) => {
|
|||||||
[req.userId, month]
|
[req.userId, month]
|
||||||
)
|
)
|
||||||
const budget = (rows as any[])[0] || null
|
const budget = (rows as any[])[0] || null
|
||||||
|
if (budget) {
|
||||||
|
budget.myAmount = budget.amount
|
||||||
|
}
|
||||||
res.json({ code: 0, data: budget })
|
res.json({ code: 0, data: budget })
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user