Files
gamegroup2/frontend/src/components/ledger/LedgerCard.vue
T
congsh c5413644f9 feat: phase 3 - ledger and asset management
Add group expense tracking (ledger) and public asset inventory (asset) features.
Ledger supports income/expense recording with monthly summary. Asset tracks
group equipment with free-form holder transfer. Both are independent pages
accessible from GroupView navigation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 19:42:04 +08:00

313 lines
7.0 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.
<script setup lang="ts">
import { computed } from 'vue'
import { Edit, Delete } from '@element-plus/icons-vue'
import type { Ledger } from '@/types'
import { LedgerTypeMap, LedgerCategoryMap, displayName } from '@/types'
import { pb } from '@/api/pocketbase'
import { useGroupStore } from '@/stores/group'
const props = defineProps<{
ledger: Ledger
}>()
const emit = defineEmits<{
edit: [ledgerId: string]
delete: [ledgerId: string]
}>()
const groupStore = useGroupStore()
const isCreator = computed(() => {
return props.ledger.creator === pb.authStore.model?.id
})
const isGroupOwner = computed(() => {
return pb.authStore.model?.id === groupStore.currentGroup?.owner
})
const typeLabel = computed(() => LedgerTypeMap[props.ledger.type])
const categoryLabel = computed(() => LedgerCategoryMap[props.ledger.category])
const isIncome = computed(() => props.ledger.type === 'income')
const formattedAmount = computed(() => {
const prefix = isIncome.value ? '+' : '-'
return `${prefix}¥${props.ledger.amount.toFixed(2)}`
})
const formattedDate = computed(() => {
if (!props.ledger.occurredAt) return ''
return props.ledger.occurredAt.slice(0, 10)
})
const creatorName = computed(() => {
return displayName(props.ledger.expand?.creator)
})
const relatedMemberNames = computed(() => {
const members = props.ledger.expand?.relatedMembers
if (!members || members.length === 0) return []
return members.map((m) => displayName(m))
})
</script>
<template>
<div class="ledger-card">
<div class="ledger-card__main">
<!-- 左侧类型标签 + 金额 -->
<div class="ledger-card__left">
<span
class="ledger-card__type-tag"
:class="isIncome ? 'ledger-card__type-tag--income' : 'ledger-card__type-tag--expense'"
>
{{ typeLabel }}
</span>
<span
class="ledger-card__amount"
:class="isIncome ? 'ledger-card__amount--income' : 'ledger-card__amount--expense'"
>
{{ formattedAmount }}
</span>
</div>
<!-- 中间描述分类日期 -->
<div class="ledger-card__center">
<div class="ledger-card__desc">{{ ledger.description || '无备注' }}</div>
<div class="ledger-card__meta">
<span class="ledger-card__category-tag">{{ categoryLabel }}</span>
<span class="ledger-card__date">{{ formattedDate }}</span>
</div>
</div>
<!-- 右侧关联成员 + 创建者 -->
<div class="ledger-card__right">
<div v-if="relatedMemberNames.length > 0" class="ledger-card__members">
<span
v-for="(name, index) in relatedMemberNames.slice(0, 3)"
:key="index"
class="ledger-card__member"
>
{{ name }}
</span>
<span v-if="relatedMemberNames.length > 3" class="ledger-card__member ledger-card__member--more">
+{{ relatedMemberNames.length - 3 }}
</span>
</div>
<div class="ledger-card__creator">
{{ creatorName }}
</div>
</div>
</div>
<!-- 操作按钮 -->
<div v-if="isCreator || isGroupOwner" class="ledger-card__actions">
<button v-if="isCreator" class="ledger-card__action-btn" @click.stop="emit('edit', ledger.id)">
<el-icon><Edit /></el-icon>
编辑
</button>
<button class="ledger-card__action-btn ledger-card__action-btn--danger" @click.stop="emit('delete', ledger.id)">
<el-icon><Delete /></el-icon>
删除
</button>
</div>
</div>
</template>
<style scoped>
.ledger-card {
background: var(--gg-bg-card);
border: 1px solid var(--gg-border);
border-radius: var(--gg-radius-md, 8px);
padding: 14px 18px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.ledger-card:hover {
border-color: var(--gg-primary-light);
box-shadow: 0 0 16px rgba(5, 150, 105, 0.08);
}
.ledger-card__main {
display: flex;
align-items: flex-start;
gap: 16px;
}
/* 左侧 */
.ledger-card__left {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
min-width: 80px;
}
.ledger-card__type-tag {
display: inline-block;
padding: 2px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
line-height: 18px;
}
.ledger-card__type-tag--income {
background: rgba(16, 185, 129, 0.1);
color: var(--gg-success);
}
.ledger-card__type-tag--expense {
background: rgba(239, 68, 68, 0.1);
color: var(--gg-danger);
}
.ledger-card__amount {
font-size: 18px;
font-weight: 700;
white-space: nowrap;
}
.ledger-card__amount--income {
color: var(--gg-success);
}
.ledger-card__amount--expense {
color: var(--gg-danger);
}
/* 中间 */
.ledger-card__center {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.ledger-card__desc {
font-size: 14px;
font-weight: 500;
color: var(--gg-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ledger-card__meta {
display: flex;
align-items: center;
gap: 8px;
}
.ledger-card__category-tag {
display: inline-block;
padding: 1px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
line-height: 18px;
background: rgba(5, 150, 105, 0.08);
color: var(--gg-primary);
}
.ledger-card__date {
font-size: 12px;
color: var(--gg-text-muted);
}
/* 右侧 */
.ledger-card__right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
min-width: 0;
}
.ledger-card__members {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
justify-content: flex-end;
}
.ledger-card__member {
display: inline-block;
padding: 1px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
line-height: 18px;
background: var(--gg-bg-elevated);
color: var(--gg-text-secondary);
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ledger-card__member--more {
background: var(--gg-bg-elevated);
color: var(--gg-text-muted);
max-width: none;
}
.ledger-card__creator {
font-size: 12px;
color: var(--gg-text-muted);
}
/* 操作按钮 */
.ledger-card__actions {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--gg-border);
}
.ledger-card__action-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--gg-text-secondary);
font-size: 12px;
cursor: pointer;
transition: color 0.2s, background 0.2s;
}
.ledger-card__action-btn:hover {
color: var(--gg-primary);
background: rgba(5, 150, 105, 0.08);
}
.ledger-card__action-btn--danger:hover {
color: var(--gg-danger);
background: rgba(239, 68, 68, 0.08);
}
@media (max-width: 640px) {
.ledger-card__main {
flex-direction: column;
gap: 10px;
}
.ledger-card__left {
flex-direction: row;
align-items: center;
justify-content: space-between;
min-width: auto;
width: 100%;
}
.ledger-card__right {
align-items: flex-start;
}
}
</style>