Files
one-pipe-system/src/views/asset-management/asset-information/components/WalletTransactionCard.vue
luo 4d023e7666
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m35s
fix: update some files
2026-08-19 18:14:20 +08:00

548 lines
16 KiB
Vue

<template>
<ElCard shadow="never" class="info-card wallet-info" v-loading="walletLoading">
<template #header>
<div class="card-header wallet-header">
<div class="wallet-header-top">
<span class="wallet-title">钱包</span>
<ElTag
v-if="walletInfo"
class="wallet-status-tag"
:type="getWalletStatusType(walletInfo.status)"
size="large"
>
{{ walletInfo.status_text || '-' }}
</ElTag>
</div>
<div v-if="walletInfo" class="wallet-balance-summary">
<div class="balance-item">
<span class="balance-label">总余额</span>
<strong>{{ formatAmount(walletInfo.balance) }}</strong>
</div>
<div class="balance-item">
<span class="balance-label">可用</span>
<strong>{{ formatAmount(walletInfo.available_balance) }}</strong>
</div>
<div class="balance-item">
<span class="balance-label">冻结</span>
<strong>{{ formatAmount(walletInfo.frozen_balance) }}</strong>
</div>
</div>
<div v-if="!props.boundDeviceId" class="filter-section">
<ElSelect
v-model="filterForm.transaction_type"
placeholder="交易类型"
clearable
class="transaction-type-select"
@change="handleFilterChange"
>
<ElOption label="充值" value="recharge" />
<ElOption label="扣款" value="deduct" />
<ElOption label="退款" value="refund" />
</ElSelect>
<ElDatePicker
v-model="filterForm.date_range"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
class="date-range-picker"
/>
<ElButton type="primary" @click="handleQuery">查询</ElButton>
<ElButton @click="handleReset()">重置</ElButton>
<ElButton
v-if="
isPlatformAuditUser &&
walletInfo?.wallet_id &&
hasAuth(AUDIT_PERMISSIONS.assetInfoWalletFinance)
"
@click="openWalletAudit"
>资金链路</ElButton
>
<ElButton
v-if="
isPlatformAuditUser &&
walletInfo?.resource_type &&
walletInfo?.resource_id &&
hasAuth(AUDIT_PERMISSIONS.assetInfoWalletAsset)
"
@click="openAssetAudit"
>资产审计</ElButton
>
</div>
</div>
</template>
<!-- 绑定设备提示 -->
<ElEmpty v-if="props.boundDeviceId" :image-size="100">
<template #description>
<span>该卡已绑定设备,请点击 </span>
<ElButton
type="primary"
link
style="font-size: 15px; font-weight: 600"
@click="emit('navigateToDevice', props.boundDeviceNo!)"
>{{ props.boundDeviceNo }}</ElButton
>
<span> 进行查看</span>
</template>
</ElEmpty>
<div v-else v-loading="loading" class="wallet-table-wrapper">
<ElTable v-if="hasTransactions" :data="transactionList" class="wallet-table" border>
<ElTableColumn prop="reference_no" label="业务编号" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.reference_no" class="reference-no-link">
{{ scope.row.reference_no }}
</span>
<span v-else>--</span>
</template>
</ElTableColumn>
<ElTableColumn label="交易类型" width="100">
<template #default="scope">
<ElTag :type="getTransactionTypeTag(scope.row.transaction_type)" size="small">
{{ scope.row.transaction_type_text }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="变动金额" width="120">
<template #default="scope">
<span
:style="{
color: scope.row.amount > 0 ? '#67c23a' : '#f56c6c',
fontWeight: 600
}"
>
{{ formatAmount(scope.row.amount) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="变动前余额" width="120">
<template #default="scope">
{{ formatAmount(scope.row.balance_before) }}
</template>
</ElTableColumn>
<ElTableColumn label="变动后余额" width="120">
<template #default="scope">
{{ formatAmount(scope.row.balance_after) }}
</template>
</ElTableColumn>
<ElTableColumn label="关联业务" width="120">
<template #default="scope">
<ElTag v-if="scope.row.reference_type" type="info" size="small">
{{ scope.row.reference_type === 'recharge' ? '充值' : '订单' }}
</ElTag>
<span v-else>--</span>
</template>
</ElTableColumn>
<ElTableColumn prop="remark" label="备注" min-width="180" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.remark || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" width="170" show-overflow-tooltip>
<template #default="scope">
{{ formatDateTime(scope.row.created_at) }}
</template>
</ElTableColumn>
</ElTable>
<div v-else class="wallet-empty-state">
<ElEmpty v-if="hasLoadedTransactions" description="暂无钱包流水数据" :image-size="100" />
</div>
<!-- 分页器 -->
<div v-if="total > 0" class="pagination-wrapper">
<ElPagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.page_size"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</ElCard>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import {
ElCard,
ElTable,
ElTableColumn,
ElTag,
ElButton,
ElSelect,
ElOption,
ElDatePicker,
ElPagination,
ElEmpty,
ElMessage
} from 'element-plus'
import { useAssetFormatters } from '../composables/useAssetFormatters'
import { formatDateTime } from '@/utils/business/format'
import { AssetService } from '@/api/modules'
import type {
AssetWalletTransactionItem,
AssetWalletTransactionListResponse,
AssetWalletResponse,
AssetWalletTransactionParams,
TransactionType
} from '@/types/api'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import { isPlatformAuditAccount } from '@/utils/business/auditAccess'
import {
resolveAuditResourceTarget,
resolveFinanceAuditTarget
} from '@/utils/business/auditNavigation'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
interface Props {
assetIdentifier: string
searchToken?: number
walletInfo: AssetWalletResponse | null
walletLoading?: boolean
boundDeviceId?: number | null
boundDeviceNo?: string
}
const props = withDefaults(defineProps<Props>(), {
walletLoading: false,
boundDeviceId: undefined,
boundDeviceNo: ''
})
const { hasAuth } = useAuth()
const userStore = useUserStore()
const isPlatformAuditUser = computed(() =>
isPlatformAuditAccount(userStore.info.user_type, userStore.isSuperAdmin)
)
const openWalletAudit = () => {
const target = resolveFinanceAuditTarget('wallet_id', props.walletInfo?.wallet_id)
if (target) openAuditInvestigation(target)
}
const openAssetAudit = () => {
const target = resolveAuditResourceTarget({
resourceType: props.walletInfo?.resource_type || '',
internalId: props.walletInfo?.resource_id
})
if (target) openAuditInvestigation(target)
}
const emit = defineEmits<{
(e: 'navigateToDevice', deviceNo: string): void
}>()
const { formatAmount, getTransactionTypeTag, getWalletStatusType } = useAssetFormatters()
const DEFAULT_PAGE_SIZE = 20
const createDefaultPagination = () => ({ page: 1, page_size: DEFAULT_PAGE_SIZE })
const createDefaultFilterForm = () => ({
transaction_type: null as TransactionType | null,
date_range: null as [Date, Date] | null
})
const createDefaultQueryParams = (): AssetWalletTransactionParams => ({
page: 1,
page_size: DEFAULT_PAGE_SIZE,
transaction_type: null,
start_time: null,
end_time: null
})
// State
const loading = ref(false)
const hasLoadedTransactions = ref(false)
const transactionList = ref<AssetWalletTransactionItem[]>([])
const total = ref(0)
const hasTransactions = computed(() => transactionList.value.length > 0)
const pagination = ref(createDefaultPagination())
const filterForm = ref(createDefaultFilterForm())
const queryParams = ref<AssetWalletTransactionParams>(createDefaultQueryParams())
const clearTransactions = () => {
hasLoadedTransactions.value = false
transactionList.value = []
total.value = 0
pagination.value = createDefaultPagination()
}
const normalizeTransactionResponse = (data?: AssetWalletTransactionListResponse | null) => ({
items: data?.items ?? data?.list ?? [],
total: data?.total ?? 0,
page: data?.page ?? 1,
pageSize: data?.size ?? data?.page_size ?? queryParams.value.page_size ?? DEFAULT_PAGE_SIZE
})
const loadTransactions = async (preserveDisplayState: boolean = true) => {
if (!props.assetIdentifier || props.boundDeviceId) {
clearTransactions()
return
}
if (!preserveDisplayState) {
clearTransactions()
}
try {
loading.value = true
const response = await AssetService.getWalletTransactions(
props.assetIdentifier,
queryParams.value
)
if (response.code === 0 && response.data) {
const normalized = normalizeTransactionResponse(response.data)
transactionList.value = normalized.items
total.value = normalized.total
pagination.value = {
page: normalized.page,
page_size: normalized.pageSize
}
queryParams.value.page = normalized.page
queryParams.value.page_size = normalized.pageSize
} else {
transactionList.value = []
total.value = 0
pagination.value = createDefaultPagination()
}
hasLoadedTransactions.value = true
} catch (error: any) {
if (error?.response?.status === 403) {
ElMessage.warning('企业账号禁止查看钱包流水')
} else {
console.error('获取钱包流水失败:', error)
}
} finally {
loading.value = false
}
}
const resetQueryState = () => {
filterForm.value = createDefaultFilterForm()
queryParams.value = createDefaultQueryParams()
pagination.value = createDefaultPagination()
}
const handleQuery = () => {
queryParams.value.page = 1
pagination.value.page = 1
queryParams.value.transaction_type = filterForm.value.transaction_type
if (filterForm.value.date_range && filterForm.value.date_range.length === 2) {
queryParams.value.start_time = filterForm.value.date_range[0].toISOString()
queryParams.value.end_time = filterForm.value.date_range[1].toISOString()
} else {
queryParams.value.start_time = null
queryParams.value.end_time = null
}
void loadTransactions(true)
}
const handleFilterChange = () => handleQuery()
const handleReset = (preserveDisplayState: boolean = true) => {
resetQueryState()
if (props.boundDeviceId) {
clearTransactions()
return
}
void loadTransactions(preserveDisplayState)
}
const handlePageSizeChange = (size: number) => {
queryParams.value.page_size = size
queryParams.value.page = 1
pagination.value.page = 1
void loadTransactions(true)
}
const handlePageChange = (page: number) => {
queryParams.value.page = page
void loadTransactions(true)
}
watch(
() => [props.assetIdentifier, props.searchToken, props.boundDeviceId] as const,
([assetIdentifier], previousValue) => {
if (!assetIdentifier) {
resetQueryState()
clearTransactions()
return
}
const previousAssetIdentifier = previousValue?.[0]
const preserveDisplayState =
Boolean(previousAssetIdentifier) && assetIdentifier === previousAssetIdentifier
handleReset(preserveDisplayState)
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
.info-card {
&.wallet-info {
.wallet-header {
display: flex;
flex-direction: column;
gap: 16px;
align-items: stretch;
min-width: 0;
.wallet-header-top {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.wallet-title {
font-size: 16px;
font-weight: 600;
white-space: nowrap;
}
.wallet-balance-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
width: 100%;
min-width: 0;
}
.balance-item {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
min-width: 0;
padding: 8px 12px;
white-space: nowrap;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
.balance-label {
font-size: 13px;
color: var(--el-text-color-secondary);
}
strong {
font-size: 18px;
font-weight: 700;
line-height: 24px;
color: var(--el-text-color-primary);
}
}
.wallet-status-tag {
padding: 8px 14px;
font-size: 14px;
font-weight: 700;
}
.filter-section {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: flex-end;
width: 100%;
min-width: 0;
.transaction-type-select {
width: 150px;
}
.date-range-picker {
width: min(360px, 100%);
}
}
}
.wallet-table-wrapper {
overflow-x: auto;
.wallet-empty-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 220px;
}
.wallet-table {
:deep(.el-table__header) {
th {
font-weight: 600;
color: var(--el-text-color-primary, #374151);
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
overflow-x: auto;
}
}
@media (width <= 1200px) {
.wallet-header {
.filter-section {
justify-content: flex-end;
}
}
}
@media (width <= 768px) {
.wallet-header {
.filter-section {
flex-direction: column;
align-items: stretch;
:deep(.el-select),
:deep(.el-date-editor),
.el-button,
.el-tag {
width: 100% !important;
}
.el-button {
margin-left: 0;
}
}
.wallet-balance-summary {
grid-template-columns: 1fr;
gap: 8px;
}
}
.wallet-table-wrapper {
.wallet-table {
min-width: 900px;
}
.pagination-wrapper {
justify-content: flex-start;
overflow-x: auto;
}
}
}
@media (width <= 480px) {
.wallet-table-wrapper {
padding: 0 12px;
margin-right: -12px;
margin-left: -12px;
}
}
}
}
</style>