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
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
<!-- 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>
|
||||
Reference in New Issue
Block a user