Files
gamegroup2/frontend/src/views-mobile/AssetMobile.vue
T
锦麟 王 13e87110ae feat(mobile): stage 8 - ledger + assets + blacklist
- migrate LedgerMobile.vue (monthly summary + list + add + swipe-delete)
- migrate AssetMobile.vue (list + add with image + transfer + delete)
- migrate BlacklistMobile.vue (game/player dual tabs + add + delete)
- router: wire LedgerView/AssetView/BlacklistView mobile views
- verified: ledger/asset stores + assets/gameBlacklist/playerBlacklist APIs + types maps all match uat

build verified: vue-tsc + vite build pass
2026-06-18 11:20:52 +08:00

231 lines
8.3 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.
<!-- src/views-mobile/AssetMobile.vue -->
<!-- 手机端资产列表 + 添加 + 转移 + 删除 -->
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { useAssetStore } from '@/stores/asset'
import { useGroupStore } from '@/stores/group'
import { getAssetImageUrl } from '@/api/assets'
import { AssetTypeMap } from '@/types'
import type { AssetType } from '@/types'
import { displayName } from '@/types'
import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant'
const route = useRoute()
const groupId = route.params.groupId as string
const assetStore = useAssetStore()
const groupStore = useGroupStore()
const assets = computed(() => assetStore.assets)
const members = computed(() => groupStore.currentMembers)
onMounted(async () => {
await groupStore.setCurrentGroup(groupId)
await assetStore.loadAssets(groupId)
await assetStore.startSubscription(groupId)
})
onUnmounted(() => {
assetStore.stopSubscription()
})
function imageUrl(assetId: string, filename: string): string {
if (!filename) return ''
return getAssetImageUrl(assetId, filename, '200x200')
}
// 添加
const showAdd = ref(false)
const addForm = ref({ name: '', type: 'other' as AssetType, description: '' })
const addImage = ref<File | null>(null)
const addLoading = ref(false)
function onImageSelect(items: any) {
const arr = Array.isArray(items) ? items : [items]
addImage.value = arr[0]?.file || null
}
async function handleAdd() {
if (!addForm.value.name.trim()) {
showFailToast('请输入名称')
return
}
addLoading.value = true
try {
await assetStore.addAsset({
group: groupId,
name: addForm.value.name.trim(),
type: addForm.value.type,
description: addForm.value.description.trim(),
image: addImage.value || undefined
})
showSuccessToast('添加成功')
showAdd.value = false
addForm.value = { name: '', type: 'other', description: '' }
addImage.value = null
} catch (e: any) {
showFailToast(e.message || '添加失败')
} finally {
addLoading.value = false
}
}
// 转移
const showTransfer = ref(false)
const transferAssetId = ref('')
const transferUserId = ref('')
function openTransfer(assetId: string) {
transferAssetId.value = assetId
transferUserId.value = ''
showTransfer.value = true
}
async function handleTransfer() {
if (!transferUserId.value) {
showFailToast('请选择成员')
return
}
try {
await assetStore.transfer(transferAssetId.value, transferUserId.value)
showSuccessToast('已转移')
showTransfer.value = false
} catch (e: any) {
showFailToast(e.message || '转移失败')
}
}
// 删除
async function handleDelete(assetId: string) {
showConfirmDialog({ title: '删除', message: '确定删除这个资产吗?' })
.then(async () => {
try {
await assetStore.removeAsset(assetId)
showSuccessToast('已删除')
} catch (e: any) {
showFailToast('删除失败')
}
}).catch(() => {})
}
const assetTypes = Object.keys(AssetTypeMap) as AssetType[]
</script>
<template>
<div class="asset-mobile">
<div v-if="assets.length === 0" class="empty">
<van-empty description="暂无资产" image-size="100" />
</div>
<div v-else class="asset-list">
<div v-for="a in assets" :key="a.id" class="asset-card">
<div class="asset-main">
<img
v-if="a.image"
:src="imageUrl(a.id, a.image)"
class="asset-img"
alt=""
/>
<div v-else class="asset-img-placeholder">
<van-icon name="gift-o" size="28" />
</div>
<div class="asset-info">
<div class="asset-name">{{ a.name }}</div>
<div class="asset-meta">
<van-tag plain size="medium">{{ AssetTypeMap[a.type] }}</van-tag>
</div>
<div v-if="a.expand?.currentHolder" class="asset-holder">
持有: {{ displayName(a.expand.currentHolder) }}
</div>
</div>
</div>
<div class="asset-actions">
<van-button size="mini" plain round @click="openTransfer(a.id)">转移</van-button>
<van-button size="mini" plain type="danger" round @click="handleDelete(a.id)">删除</van-button>
</div>
</div>
</div>
<!-- 浮动添加 -->
<div class="fab" @click="showAdd = true">
<van-icon name="plus" size="24" />
</div>
<!-- 添加弹层 -->
<van-popup v-model:show="showAdd" position="bottom" round closeable :style="{ height: '70%' }">
<div class="popup-content">
<div class="popup-title">添加资产</div>
<van-cell-group inset>
<van-field v-model="addForm.name" label="名称" placeholder="资产名称" required />
<van-field name="select" label="类型">
<template #input>
<select v-model="addForm.type" class="type-select">
<option v-for="t in assetTypes" :key="t" :value="t">{{ AssetTypeMap[t] }}</option>
</select>
</template>
</van-field>
<van-field v-model="addForm.description" label="描述" placeholder="可选" />
</van-cell-group>
<div class="upload-area">
<van-uploader :after-read="onImageSelect" :max-count="1" accept="image/*" />
</div>
<div class="popup-actions">
<van-button type="primary" block round :loading="addLoading" @click="handleAdd">添加</van-button>
</div>
</div>
</van-popup>
<!-- 转移弹层 -->
<van-action-sheet v-model:show="showTransfer" title="转移给">
<div class="transfer-list">
<div
v-for="m in members"
:key="m.id"
class="transfer-item"
:class="{ active: transferUserId === m.id }"
@click="transferUserId = m.id"
>
<img :src="m.avatar || '/default-avatar.svg'" class="transfer-avatar" alt="" />
<span>{{ displayName(m) }}</span>
<van-icon v-if="transferUserId === m.id" name="success" class="transfer-check" />
</div>
<div class="transfer-actions">
<van-button type="primary" block round @click="handleTransfer">确认转移</van-button>
</div>
</div>
</van-action-sheet>
</div>
</template>
<style scoped>
.asset-mobile { padding: 12px; min-height: 60vh; }
.empty { padding: 30px 0; }
.asset-list { display: flex; flex-direction: column; gap: 10px; }
.asset-card { background: var(--gg-bg-card); border-radius: var(--gg-radius-md); padding: 14px; box-shadow: var(--gg-shadow); }
.asset-main { display: flex; gap: 12px; }
.asset-img { width: 60px; height: 60px; border-radius: var(--gg-radius-sm); object-fit: cover; flex-shrink: 0; }
.asset-img-placeholder { width: 60px; height: 60px; border-radius: var(--gg-radius-sm); background: var(--gg-bg-elevated); display: flex; align-items: center; justify-content: center; color: var(--gg-text-muted); flex-shrink: 0; }
.asset-info { flex: 1; min-width: 0; }
.asset-name { font-size: 15px; font-weight: 600; color: var(--gg-text); }
.asset-meta { margin-top: 6px; }
.asset-holder { font-size: 12px; color: var(--gg-text-muted); margin-top: 4px; }
.asset-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
.fab { position: fixed; right: 20px; bottom: calc(76px + env(safe-area-inset-bottom, 0px)); width: 52px; height: 52px; border-radius: 50%; background: var(--gg-gradient-green); color: #fff; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(5,150,105,0.4); z-index: 20; }
.popup-content { padding: 16px 0 24px; display: flex; flex-direction: column; height: 100%; overflow-y: auto; }
.popup-title { text-align: center; font-size: 16px; font-weight: 600; padding: 8px 0 16px; }
.type-select { border: 1px solid var(--gg-border); border-radius: var(--gg-radius-sm); padding: 4px 8px; font-size: 14px; }
.upload-area { padding: 16px; }
.popup-actions { padding: 16px; }
.transfer-list { padding: 8px 0; }
.transfer-item { display: flex; align-items: center; gap: 10px; padding: 12px 24px; }
.transfer-item.active { background: rgba(5,150,105,0.06); color: var(--gg-primary); }
.transfer-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; }
.transfer-check { margin-left: auto; color: var(--gg-primary); }
.transfer-actions { padding: 16px 24px; }
</style>