Files
xiaocai/client/src/components/TransactionItem/TransactionItem.vue
wangxiaogang 8bd4af9b9a feat: onShow静默刷新 + 群组预算双显 + TransactionItem优化
onShow 静默刷新(首次有loading,后续静默):
- budget: 添加 initialLoaded 守卫,避免首次双重请求
- profile: 添加 onShow 刷新概览/预算/账单/群组
- group-manage: 添加 onShow 刷新群组列表
- category-manage: 添加 onShow 刷新分类列表
- add: 添加 onShow 刷新分类(可能新增了分类)

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

TransactionItem 群组模式优化:
- 主图标始终显示分类图标(不再显示创建者头像)
- 他人记录在昵称旁显示小头像(28rpx圆形)
2026-06-04 17:45:37 +08:00

211 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="tx-item-wrap">
<view
class="tx-item"
:style="{ transform: `translateX(${offsetX}rpx)` }"
@tap="$emit('item-tap', item)"
@touchstart="!disableSwipe && onTouchStart($event)"
@touchmove="!disableSwipe && onTouchMove($event)"
@touchend="!disableSwipe && onTouchEnd()"
>
<!-- 始终显示分类图标 -->
<CategoryIcon
:label="item.category_name?.[0] || '?'"
:color="item.category_color || '#BFB3B3'"
:bg-color="(item.category_color || '#BFB3B3') + '20'"
size="sm"
/>
<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>
</view>
<text class="amount" :class="item.type">{{ item.type === 'expense' ? '-' : '+' }}{{ formatAmount(item.amount) }}</text>
</view>
<view class="delete-btn" :class="{ visible: showDelete }" @tap="$emit('delete', item)">
<Icon name="trash" :size="32" color="#FFFFFF" />
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { formatAmount, formatDate } from '@/utils/format'
import { API_BASE } from '@/config'
import Icon from '@/components/Icon/Icon.vue'
import CategoryIcon from '@/components/CategoryIcon/CategoryIcon.vue'
const props = withDefaults(defineProps<{
item: {
id: number
user_id: number
amount: number
type: 'expense' | 'income'
note?: string
date: string
category_name?: string
category_icon?: string
category_color?: string
creator_nickname?: string
creator_avatar?: string
}
hideDate?: boolean
disableSwipe?: boolean
/** 是否显示创建者信息(群组模式) */
showCreator?: boolean
/** 当前用户 ID用于判断是否为本人记录 */
currentUserId?: number
}>(), { hideDate: false, disableSwipe: false, showCreator: false, currentUserId: 0 })
/** 是否为他人的记录(群组模式下,非本人的才显示创建者信息) */
const isOtherCreator = computed(() => {
if (!props.showCreator || !props.item.creator_nickname) return false
return !props.currentUserId || props.item.user_id !== props.currentUserId
})
defineEmits(['item-tap', 'delete'])
const creatorAvatarUrl = computed(() => {
if (!props.item.creator_avatar) return ''
if (props.item.creator_avatar.startsWith('http')) return props.item.creator_avatar
// 截取服务端根地址(去掉 /api
const base = API_BASE.replace(/\/api$/, '')
return base + '/api/user/avatar/' + props.item.creator_avatar
})
const startX = ref(0)
const offsetX = ref(0)
const showDelete = ref(false)
function onTouchStart(e: any) {
startX.value = e.touches[0].clientX
}
function onTouchMove(e: any) {
const diff = e.touches[0].clientX - startX.value
if (diff < 0) {
offsetX.value = Math.max(diff * 2, -160)
} else if (showDelete.value) {
offsetX.value = Math.min(diff * 2 - 160, 0)
}
}
function onTouchEnd() {
if (offsetX.value < -80) {
offsetX.value = -160
showDelete.value = true
} else {
offsetX.value = 0
showDelete.value = false
}
}
</script>
<style lang="scss" scoped>
.tx-item-wrap {
position: relative;
overflow: hidden;
margin-bottom: 8rpx;
}
.tx-item {
display: flex;
align-items: center;
padding: 28rpx 24rpx;
gap: 24rpx;
border-radius: 20rpx;
transition: transform 0.2s ease;
background: #FFFFFF;
position: relative;
z-index: 1;
&:active {
background: #FFF0E6;
}
}
.delete-btn {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 160rpx;
background: #FF6B6B;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 20rpx 20rpx 0;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 0;
&.visible {
opacity: 1;
}
}
.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;
font-size: 16rpx;
color: #FF8C69;
align-items: center;
justify-content: center;
vertical-align: middle;
margin-right: 4rpx;
}
.info {
flex: 1;
min-width: 0;
}
.name {
font-size: 28rpx;
font-weight: 500;
color: #2D1B1B;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
font-size: 24rpx;
color: #8B7E7E;
display: block;
margin-top: 4rpx;
}
.creator-name {
color: #FF8C69;
font-weight: 500;
}
.amount {
font-family: 'Fredoka', sans-serif;
font-size: 32rpx;
font-weight: 600;
font-variant-numeric: tabular-nums;
white-space: nowrap;
&.expense { color: #2D1B1B; }
&.income { color: #7BC67E; }
}
</style>