Files
xiaocai/client/src/components/TransactionItem/TransactionItem.vue

237 lines
5.8 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" :class="{ 'show-hint': swipeHint && !disableSwipe }">
<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>
<view class="meta">
<view v-if="isOtherCreator" class="creator-tag">
<image v-if="creatorAvatarUrl" :src="creatorAvatarUrl" class="creator-mini-avatar" mode="aspectFill" />
<text v-else class="creator-mini-initial">{{ item.creator_nickname?.[0] }}</text>
<text class="creator-name">{{ item.creator_nickname }}</text>
</view>
<text>{{ (isOtherCreator ? ' · ' : '') + (item.category_name || '未分类') }}{{ hideDate ? '' : ' · ' + formatDate(item.date) }}</text>
</view>
</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
/** 是否显示左滑提示动画由父组件控制仅首个可删除项传入true */
swipeHint?: boolean
}>(), { hideDate: false, disableSwipe: false, showCreator: false, currentUserId: 0, swipeHint: false })
/** 是否为他人的记录(群组模式下,非本人的才显示创建者信息) */
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') || props.item.creator_avatar.startsWith('blob:')) 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>
@import '@/styles/mixins.scss';
.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: $surface;
position: relative;
z-index: 1;
&:active {
background: $surface-warm;
}
}
// 左滑提示:仅在 swipeHint=true 且 disableSwipe=false 时播放一次
.show-hint .tx-item {
animation: swipe-hint 0.6s ease 1.5s both;
}
@keyframes swipe-hint {
0% { transform: translateX(0); }
40% { transform: translateX(-40rpx); }
100% { transform: translateX(0); }
}
.delete-btn {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 160rpx;
background: $danger;
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-tag {
margin-right: 4rpx;
display: inline;
}
.creator-mini-avatar {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
margin-right: 4rpx;
position: relative;
top: 4rpx;
}
.creator-mini-initial {
display: flex;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: $primary-light;
font-size: 16rpx;
color: $primary;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.info {
flex: 1;
min-width: 0;
}
.name {
font-size: 28rpx;
font-weight: 500;
color: $text;
display: block;
@include text-ellipsis;
}
.meta {
font-size: 24rpx;
color: $text-sec;
display: block;
margin-top: 4rpx;
}
.creator-name {
color: $primary;
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: $text; }
&.income { color: $success; }
}
@media (prefers-reduced-motion: reduce) {
.show-hint .tx-item {
animation: none;
}
}
</style>