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

558 lines
24 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.

<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>
<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">
<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>
</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>
<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">
<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>
</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>
<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'
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',
// amount: 100.656578965,
// 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)
},
/**
* 处理 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] || '未知' },
/**
* 将链名称标准化为大写简称
* @param {string} chain - 后端返回的链标识,如 tron/eth/ethereum/bsc/polygon
* @returns {string} 大写显示,如 TRON/ETH/BSC/POLYGON
*/
formatChain(chain) {
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()
},
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}`
},
/**
* 金额显示保留最多6位小数直接截断不四舍五入不补尾随0始终返回非负字符串
* @param {number|string} value
* @returns {string}
*/
// 删除旧的 formatDec6统一使用 formatAmount
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();
},
/**
* 复制文本到剪贴板
* @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('复制失败,请手动选择复制')
}
},
/**
* 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; }
.value-row { display: inline-flex; align-items: center; gap: 6px; }
.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; }
</style>