548 lines
16 KiB
Vue
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>
|