Files
gamegroup2/frontend/src/components/asset/AssetList.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

304 lines
6.8 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useAssetStore } from '@/stores/asset'
import { useGroupStore } from '@/stores/group'
import { AssetTypeMap } from '@/types'
import type { Asset, AssetType } from '@/types'
import AssetCard from './AssetCard.vue'
import CreateAssetDialog from './CreateAssetDialog.vue'
import TransferAssetDialog from './TransferAssetDialog.vue'
const assetStore = useAssetStore()
const groupStore = useGroupStore()
// 类型过滤
const typeFilter = ref<string>('')
// 类型选项
const typeOptions = computed(() => {
return [
{ value: '', label: '全部' },
...(Object.entries(AssetTypeMap) as [AssetType, string][]).map(([value, label]) => ({
value,
label
}))
]
})
// 过滤后的资产列表
const filteredAssets = computed(() => {
if (!typeFilter.value) return assetStore.assets
return assetStore.assets.filter(a => a.type === typeFilter.value)
})
// 对话框状态
const showCreateDialog = ref(false)
const showTransferDialog = ref(false)
// 编辑/转移目标
const editingAsset = ref<Asset | undefined>(undefined)
const transferringAsset = ref<Asset | null>(null)
// 加载数据
async function loadData() {
const groupId = groupStore.currentGroupId
if (!groupId) return
await assetStore.loadAssets(groupId)
}
// 订阅变更
async function setupSubscription() {
const groupId = groupStore.currentGroupId
if (!groupId) return
await assetStore.startSubscription(groupId)
}
// 登记资产
function handleCreate() {
editingAsset.value = undefined
showCreateDialog.value = true
}
// 编辑资产
function handleEdit(assetId: string) {
const asset = assetStore.assets.find(a => a.id === assetId)
if (asset) {
editingAsset.value = asset
showCreateDialog.value = true
}
}
// 转移资产
function handleTransfer(assetId: string) {
const asset = assetStore.assets.find(a => a.id === assetId)
if (asset) {
transferringAsset.value = asset
showTransferDialog.value = true
}
}
// 资产删除后刷新
function handleDelete() {
// store 已经从列表中移除了,无需额外操作
}
// 保存成功回调
function handleSaved() {
showCreateDialog.value = false
editingAsset.value = undefined
}
// 转移成功回调
function handleTransferred() {
showTransferDialog.value = false
transferringAsset.value = null
}
onMounted(async () => {
if (groupStore.currentGroupId) {
await loadData()
await setupSubscription()
}
})
watch(() => groupStore.currentGroupId, async (newId, oldId) => {
if (newId && newId !== oldId) {
await loadData()
await setupSubscription()
}
})
onUnmounted(() => {
assetStore.stopSubscription()
})
</script>
<template>
<div class="asset-list">
<!-- 顶部操作栏 -->
<div class="asset-list__header">
<h3 class="asset-list__title">资产库</h3>
<button class="asset-list__create-btn" @click="handleCreate">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="asset-list__create-icon">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
登记资产
</button>
</div>
<!-- 类型过滤 -->
<div class="asset-list__filter">
<el-select
v-model="typeFilter"
placeholder="筛选类型"
size="default"
style="width: 160px"
clearable
>
<el-option
v-for="opt in typeOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<span class="asset-list__count"> {{ filteredAssets.length }} </span>
</div>
<!-- 加载状态 -->
<div v-if="assetStore.loading" v-loading="true" class="asset-list__loading"></div>
<!-- 空状态 -->
<div
v-else-if="filteredAssets.length === 0"
class="asset-list__empty"
>
<svg class="asset-list__empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" y1="22.08" x2="12" y2="12" />
</svg>
<p class="asset-list__empty-text">暂无资产</p>
<p class="asset-list__empty-hint">点击登记资产添加第一个资产</p>
</div>
<!-- 资产网格 -->
<div v-else class="asset-list__grid">
<AssetCard
v-for="asset in filteredAssets"
:key="asset.id"
:asset="asset"
@edit="handleEdit"
@transfer="handleTransfer"
@delete="handleDelete"
/>
</div>
<!-- 创建/编辑弹窗 -->
<CreateAssetDialog
v-model="showCreateDialog"
:group-id="groupStore.currentGroupId"
:edit-asset="editingAsset"
@saved="handleSaved"
/>
<!-- 转移弹窗 -->
<TransferAssetDialog
v-model="showTransferDialog"
:asset="transferringAsset"
:members="groupStore.currentMembers"
@transferred="handleTransferred"
/>
</div>
</template>
<style scoped>
.asset-list {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 顶部操作栏 */
.asset-list__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.asset-list__title {
font-size: 18px;
font-weight: 700;
color: var(--gg-text);
margin: 0;
}
.asset-list__create-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 16px;
border: none;
border-radius: var(--gg-radius-sm);
background: var(--gg-gradient-green);
color: #ffffff;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.asset-list__create-btn:hover {
opacity: 0.85;
}
.asset-list__create-icon {
width: 16px;
height: 16px;
}
/* 过滤栏 */
.asset-list__filter {
display: flex;
align-items: center;
gap: 12px;
}
.asset-list__count {
font-size: 13px;
color: var(--gg-text-muted);
}
/* 加载状态 */
.asset-list__loading {
min-height: 200px;
}
/* 网格布局 */
.asset-list__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 14px;
}
/* 空状态 */
.asset-list__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 20px;
text-align: center;
}
.asset-list__empty-icon {
width: 48px;
height: 48px;
color: var(--gg-text-muted);
margin-bottom: 12px;
opacity: 0.5;
}
.asset-list__empty-text {
font-size: 15px;
font-weight: 600;
color: var(--gg-text-secondary);
margin: 0 0 4px;
}
.asset-list__empty-hint {
font-size: 13px;
color: var(--gg-text-muted);
margin: 0;
}
/* 响应式 */
@media (max-width: 640px) {
.asset-list__grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px;
}
}
</style>