c5413644f9
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>
304 lines
6.8 KiB
Vue
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>
|