Files
webs/power_leasing/src/views/account/fundsFlow.vue

558 lines
24 KiB
Vue
Raw Normal View History

2025-10-20 10:15:13 +08:00
<template>
<div class="funds-page">
<h3 class="title" style="margin-bottom: 18px; text-align: left;">资金流水</h3>
<div class="tabs-card">
<el-tabs v-model="active" @tab-click="handleTab">
<el-tab-pane label="充值记录" name="recharge">
<div class="list-wrap">
<div class="list-header">
<span class="list-title">全部充值 ({{ rechargeRows.length }})</span>
<el-button type="primary" size="small" @click="loadRecharge">刷新</el-button>
</div>
<div class="record-list" v-loading="loading.recharge">
<div v-for="(row, idx) in rechargeRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('recharge', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">
<el-tooltip
v-if="formatAmount(row.amount, row.fromSymbol).truncated"
:content="`${formatAmount(row.amount, row.fromSymbol).full} ${(row.fromSymbol || 'USDT').toUpperCase()}`"
placement="top"
>
<span>
+ {{ formatAmount(row.amount, row.fromSymbol).text }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
+ {{ formatAmount(row.amount, row.fromSymbol).text }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}
</span>
</div>
2025-10-20 10:15:13 +08:00
<div class="chain">{{ formatChain(row.fromChain) }}</div>
</div>
<div class="item-right">
<div class="status"><el-tag :type="getRechargeStatusType(row.status)" size="small">{{ getRechargeStatusText(row.status) }}</el-tag></div>
<div class="time">{{ formatFullTime(row.createTime) }}</div>
</div>
</div>
<div v-show="isExpanded('recharge', row, idx)" class="expand-panel">
<div class="expand-grid">
2025-10-31 14:09:58 +08:00
<div class="expand-item">
<span class="label">充值地址</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.fromAddress">{{ row.fromAddress }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.fromAddress, '充值地址')">复制</el-button>
</div>
</div>
<div class="expand-item" v-if="row.txHash">
<span class="label">交易哈希</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.txHash, '交易哈希')">复制</el-button>
</div>
</div>
2025-10-20 10:15:13 +08:00
</div>
</div>
</div>
<div v-if="!rechargeRows.length" class="empty">暂无充值记录</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="提现记录" name="withdraw">
<div class="list-wrap">
<div class="list-header">
<span class="list-title">全部提现 ({{ withdrawRows.length }})</span>
<el-button type="primary" size="small" @click="loadWithdraw">刷新</el-button>
</div>
<div class="record-list" v-loading="loading.withdraw">
<div v-for="(row, idx) in withdrawRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('withdraw', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">
<el-tooltip
v-if="formatAmount(row.amount, row.toSymbol).truncated"
:content="`${formatAmount(row.amount, row.toSymbol).full} ${(row.toSymbol || 'USDT').toUpperCase()}`"
placement="top"
>
<span>
- {{ formatAmount(row.amount, row.toSymbol).text }} {{ (row.toSymbol || 'USDT').toUpperCase() }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
- {{ formatAmount(row.amount, row.toSymbol).text }} {{ (row.toSymbol || 'USDT').toUpperCase() }}
</span>
</div>
2025-10-20 10:15:13 +08:00
<div class="chain">{{ formatChain(row.toChain) }}</div>
</div>
<div class="item-right">
<div class="status"><el-tag :type="getWithdrawStatusType(row.status)" size="small">{{ getWithdrawStatusText(row.status) }}</el-tag></div>
<div class="time">{{ formatFullTime(row.createTime) }}</div>
</div>
</div>
<div v-show="isExpanded('withdraw', row, idx)" class="expand-panel">
<div class="expand-grid">
2025-10-31 14:09:58 +08:00
<div class="expand-item">
<span class="label">收款地址</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.toAddress">{{ row.toAddress }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.toAddress, '收款地址')">复制</el-button>
</div>
</div>
<div class="expand-item" v-if="row.txHash">
<span class="label">交易哈希</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.txHash, '交易哈希')">复制</el-button>
</div>
</div>
2025-10-20 10:15:13 +08:00
</div>
</div>
</div>
<div v-if="!withdrawRows.length" class="empty">暂无提现记录</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="消费记录" name="consume">
<div class="list-wrap">
<div class="list-header">
<span class="list-title">全部消费 ({{ consumeRows.length }})</span>
<el-button type="primary" size="small" @click="loadConsume">刷新</el-button>
</div>
<div class="record-list" v-loading="loading.consume">
<div v-for="(row, idx) in consumeRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('consume', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">
<el-tooltip
v-if="formatAmount(row.realAmount, row.fromSymbol).truncated"
:content="`${formatAmount(row.realAmount, row.fromSymbol).full} ${(row.fromSymbol || 'USDT').toUpperCase()}`"
placement="top"
>
<span>
- {{ formatAmount(row.realAmount, row.fromSymbol).text }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
- {{ formatAmount(row.realAmount, row.fromSymbol).text }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}
</span>
</div>
2025-10-20 10:15:13 +08:00
<div class="chain">{{ formatChain(row.fromChain) }}</div>
</div>
<div class="item-right">
<div class="status"><el-tag :type="getPayStatusType(row.status)" size="small">{{ getPayStatusText(row.status) }}</el-tag></div>
<div class="time">{{ formatFullTime(row.createTime || row.time) }}</div>
</div>
</div>
<div v-show="isExpanded('consume', row, idx)" class="expand-panel">
<div class="expand-grid">
<div class="expand-item"><span class="label">订单号</span><span class="value mono">{{ row.orderId || '' }}</span></div>
<div class="expand-item"><span class="label">支付地址</span><span class="value mono-ellipsis" :title="row.fromAddress">{{ row.fromAddress || '' }}</span></div>
<div class="expand-item"><span class="label">收款地址</span><span class="value mono-ellipsis" :title="row.toAddress">{{ row.toAddress || '' }}</span></div>
<div class="expand-item" v-if="row.txHash"><span class="label">交易哈希</span><span class="value mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span></div>
</div>
</div>
</div>
<div v-if="!consumeRows.length" class="empty">暂无消费记录</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
<el-row>
<el-col :span="24" style="display: flex; justify-content: center">
<el-pagination
style="margin: 0 auto; margin-top: 10px"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="pageSizes"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import { transactionRecord } from '../../api/wallet'
import { truncateAmountByCoin } from '../../utils/amount'
2025-10-20 10:15:13 +08:00
export default {
name: 'AccountFundsFlow',
data() {
return {
active: 'recharge',
loading: { recharge: false, withdraw: false, consume: false },
rechargeRows: [
// {
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// fromAddress: 'djdddksfhsfj',
// fromChain: 'tron',
// fromSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
// {
// createTime: '2024-01-15 14:30:25',
2025-10-31 14:09:58 +08:00
// amount: 100.656578965,
2025-10-20 10:15:13 +08:00
// fromAddress: 'djdddksfhsfj',
// fromChain: 'tron',
// fromSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
],
withdrawRows: [
// {
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// toAddress: 'djdddksfhsfj',
// toChain: 'tron',
// toSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
// {
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// toAddress: 'djdddksfhsfj',
// toChain: 'tron',
// toSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
],
consumeRows: [
// {
// orderId: '1234567890',
// fromAddress: 'djdddksfhsfj',
// fromChain: 'tron',
// fromSymbol: 'USDT',
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// toAddress: 'djdddksfhsfj',
// toChain: 'tron',
// toSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
// {
// orderId: '1234567890',
// fromAddress: 'djdddksfhsfj',
// fromChain: 'tron',
// fromSymbol: 'USDT',
// createTime: '2024-01-15 14:30:25',
// realAmount: 100, //后端告知只有消费记录金额用这个字段
// toAddress: 'djdddksfhsfj',
// toChain: 'tron',
// toSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
],
// 手风琴展开集合
expandedKeys: new Set(),
total: 0,
pageSizes: [10, 20, 50],
currentPage: 1,
// 分页和筛选参数
pagination: {
pageNum: 1,
pageSize: 10,
status: 2,
},
}
},
mounted() {
const tab = (this.$route && this.$route.query && this.$route.query.tab) || 'recharge'
if (['recharge', 'withdraw', 'consume'].includes(tab)) this.active = tab
// 初始化按当前 Tab 的状态加载列表
this.pagination.status = this.getStatusByTab(this.active)
this.loadList()
},
methods: {
/**
* 金额格式化不补0不四舍五入
*/
formatAmount(value, coin) {
return truncateAmountByCoin(value, coin)
},
2025-10-20 10:15:13 +08:00
/**
* 处理 Tab 切换清空展开状态确保手风琴行为
* @param {any} pane - 当前 paneElement UI 传入
* @param {Event} evt - 事件对象
*/
handleTab(pane, evt) {
this.expandedKeys.clear()
// 触发视图刷新
this.expandedKeys = new Set(this.expandedKeys)
// 按 Tab 切换刷新对应数据
const tabName = (pane && pane.name) || this.active
this.pagination.status = this.getStatusByTab(tabName)
this.pagination.pageNum = 1
this.currentPage = 1
this.loadList()
},
/**
* 生成唯一键优先使用后端提供的稳定字段其次回退到索引
* @param {Record<string, any>} row - 当前行数据
* @param {number} [idx] - v-for 提供的索引
* @returns {string}
*/
getRowKey(row, idx) {
const indexPart = idx != null ? `#${idx}` : ''
if (!row) return String(idx != null ? idx : '')
const stable = row.__key || row.id || row.txHash || row.orderId || `${row.createTime || ''}-${row.updateTime || ''}`
// 始终附加索引,避免重复 id/txHash 导致多条记录共用同一键
if (stable == null || stable === '') return String(idx != null ? idx : '')
return `${String(stable)}${indexPart}`
},
/**
* 判断当前行是否展开手风琴全局仅允许一个展开
* @param {string} type - 列表类型recharge/withdraw/consume
* @param {Record<string, any>} row - 当前行数据
* @param {number} [idx] - 行索引
* @returns {boolean}
*/
isExpanded(type, row, idx) {
const key = `${type}-${this.getRowKey(row, idx)}`
return this.expandedKeys.has(key)
},
/**
* 切换展开手风琴若点击同一行则收起否则清空后仅展开当前行
* @param {string} type - 列表类型
* @param {Record<string, any>} row - 当前行数据
* @param {number} [idx] - 行索引
*/
toggleExpand(type, row, idx) {
const key = `${type}-${this.getRowKey(row, idx)}`
if (this.expandedKeys.has(key)) {
this.expandedKeys.clear()
} else {
this.expandedKeys.clear()
this.expandedKeys.add(key)
}
// 触发视图刷新
this.expandedKeys = new Set(this.expandedKeys)
},
// 统一加载:根据 pagination.status 请求并填充对应列表
async loadList() {
const status = Number(this.pagination.status)
const typeKey = this.getTypeKeyByStatus(status)
if (!typeKey) return
this.loading[typeKey] = true
try {
const res = await transactionRecord({
pageNum: this.pagination.pageNum,
pageSize: this.pagination.pageSize,
status
})
const rows = res?.rows || res?.data?.rows || []
this.total = res?.total || res?.data?.total || (Array.isArray(rows) ? rows.length : 0)
const mapped = (Array.isArray(rows) ? rows : []).map((r, i) => ({
...r,
__key: r.id || r.txHash || r.orderId || `${i}`
}))
if (status === 2) this.rechargeRows = mapped
else if (status === 1) this.withdrawRows = mapped
else this.consumeRows = mapped
// 刷新时收起展开
this.expandedKeys.clear()
this.expandedKeys = new Set(this.expandedKeys)
} finally {
this.loading[typeKey] = false
}
},
/**
* 包装根据传入状态加载并同步 Tab/分页重置
* @param {0|1|2} status - 0 支付1 提现2 充值
*/
loadByStatus(status) {
this.pagination.status = status
this.active = this.getTabByStatus(status)
this.pagination.pageNum = 1
this.currentPage = 1
return this.loadList()
},
// 兼容现有按钮点击
loadRecharge() { return this.loadByStatus(2) },
loadWithdraw() { return this.loadByStatus(1) },
loadConsume() { return this.loadByStatus(0) },
statusClass(status) { return { 0: 'failed', 1: 'success', 2: 'pending' }[status] || 'neutral' },
getRechargeStatusType(s) { return ({ 0: 'danger', 1: 'success', 2: 'warning' })[s] || 'info' },
getRechargeStatusText(s) { return ({ 0: '充值失败', 1: '充值成功', 2: '充值中', 3: '证书校验失败' })[s] || '未知' },
getWithdrawStatusType(s) { return ({ 0: 'danger', 1: 'success', 2: 'warning' })[s] || 'info' },
getWithdrawStatusText(s) { return ({ 0: '提现失败', 1: '提现成功', 2: '提现中', 3: '证书校验失败' })[s] || '未知' },
getPayStatusType(s) { return ({ 0: 'danger', 1: 'success', 2: 'warning', 3: 'danger' })[s] || 'info' },
getPayStatusText(s) { return ({ 0: '支付失败', 1: '支付成功', 2: '待校验', 3: '证书校验失败' })[s] || '未知' },
2025-10-31 14:09:58 +08:00
/**
* 将链名称标准化为大写简称
* @param {string} chain - 后端返回的链标识 tron/eth/ethereum/bsc/polygon
* @returns {string} 大写显示 TRON/ETH/BSC/POLYGON
*/
2025-10-20 10:15:13 +08:00
formatChain(chain) {
2025-10-31 14:09:58 +08:00
if (!chain) return ''
const key = String(chain).toLowerCase()
const map = { tron: 'TRON', trx: 'TRON', eth: 'ETH', ethereum: 'ETH', bsc: 'BSC', polygon: 'POLYGON', matic: 'POLYGON' }
return (map[key] || String(chain)).toUpperCase()
2025-10-20 10:15:13 +08:00
},
formatFullTime(time) { if (!time) return ''; try { return new Date(time).toLocaleString('zh-CN') } catch (e) { return String(time) } },
formatTime(time) { return this.formatFullTime(time) },
formatTrunc(value, decimals = 2) {
const num = Number(value)
if (!Number.isFinite(num)) return '0'
const d = Math.max(0, Number(decimals) || 0)
const factor = Math.pow(10, d)
const truncated = Math.trunc(num * factor) / factor
const str = String(truncated)
if (d === 0) return str
const [intPart, decPart = ''] = str.split('.')
const padded = decPart.padEnd(d, '0')
return `${intPart}.${padded}`
},
2025-10-31 14:09:58 +08:00
/**
* 金额显示保留最多6位小数直接截断不四舍五入不补尾随0始终返回非负字符串
* @param {number|string} value
* @returns {string}
*/
// 删除旧的 formatDec6统一使用 formatAmount
2025-10-20 10:15:13 +08:00
handleSizeChange(val) {
console.log(`每页 ${val}`);
this.pagination.pageSize = val;
this.pagination.pageNum = 1;
this.currentPage = 1;
this.loadList();
},
handleCurrentChange(val) {
console.log(`当前页: ${val}`);
this.pagination.pageNum = val;
this.loadList();
},
2025-10-31 14:09:58 +08:00
/**
* 复制文本到剪贴板
* @param {string} text - 需要复制的内容
* @param {string} [label] - 语义标签用于提示文案
*/
async handleCopy(text, label = '内容') {
try {
const value = String(text || '')
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(value)
} else {
const ta = document.createElement('textarea')
ta.value = value
ta.style.position = 'fixed'
ta.style.left = '-9999px'
document.body.appendChild(ta)
ta.focus()
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
}
this.$message.success(`${label}已复制`)
} catch (e) {
this.$message.error('复制失败,请手动选择复制')
}
},
2025-10-20 10:15:13 +08:00
/**
* Tab 名称转状态码
* @param {string} tabName - 'recharge' | 'withdraw' | 'consume'
* @returns {0|1|2}
*/
getStatusByTab(tabName) {
if (tabName === 'recharge') return 2
if (tabName === 'withdraw') return 1
return 0
},
/**
* 状态码转 Tab 名称
* @param {number} status - 0|1|2
* @returns {'recharge'|'withdraw'|'consume'}
*/
getTabByStatus(status) {
if (Number(status) === 2) return 'recharge'
if (Number(status) === 1) return 'withdraw'
return 'consume'
},
/**
* 状态码转 loading/列表 key
* @param {number} status - 0|1|2
* @returns {'recharge'|'withdraw'|'consume'|''}
*/
getTypeKeyByStatus(status) {
if (Number(status) === 2) return 'recharge'
if (Number(status) === 1) return 'withdraw'
if (Number(status) === 0) return 'consume'
return ''
},
}
}
</script>
<style scoped>
.funds-page { padding: 8px; }
.tabs-card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; }
.list-wrap { padding: 6px 0; }
.list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }
.list-title { font-size: 14px; font-weight: 600; color: #333; }
.record-list { display: flex; flex-direction: column; gap: 10px; max-height: 62vh; overflow-y: auto; }
.record-item { background: #f8f9fa; border-radius: 8px; padding: 12px; border: 1px solid transparent; transition: all .15s ease; }
.record-item:hover { background: #eef2f7; border-color: #409eff; box-shadow: 0 4px 12px rgba(64,158,255,.12); transform: translateY(-1px); }
.record-item.pending { border-left: 4px solid #e6a23c; }
.record-item.success { border-left: 4px solid #67c23a; }
.record-item.failed { border-left: 4px solid #f56c6c; }
.item-main { display: flex; justify-content: space-between; align-items: center; }
.item-left .amount { font-size: 16px; font-weight: 700; color: #111; margin-bottom: 2px; }
.item-left .chain { font-size: 12px; color: #666; }
.item-right { text-align: right; }
.status { margin-bottom: 2px; }
.time { font-size: 12px; color: #999; }
.expand-panel { background: #fff; border: 1px dashed #e5e7eb; border-radius: 8px; padding: 10px; margin-top: 8px; }
.expand-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 24px; }
.expand-item { display: grid; grid-template-columns: 80px 1fr; gap: 6px; align-items: center; }
.label { color: #666; font-size: 13px; text-align: right; }
.value { color: #333; font-size: 13px; text-align: left; }
2025-10-31 14:09:58 +08:00
.value-row { display: inline-flex; align-items: center; gap: 6px; }
2025-10-20 10:15:13 +08:00
.mono { font-family: "Monaco", "Menlo", monospace; }
.mono-ellipsis { font-family: "Monaco", "Menlo", monospace; max-width: 480px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty { text-align: center; color: #999; padding: 20px 0; }
.amount-more { font-size: 12px; color: #94a3b8; margin-left: 4px; }
2025-10-20 10:15:13 +08:00
</style>