|
@@ -30,6 +30,35 @@ const pagination = reactive({
|
|
|
void load()
|
|
void load()
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
|
|
|
+const ETH_TX_BASE_URL = 'https://etherscan.io/tx/'
|
|
|
|
|
+const TRON_TX_BASE_URL = 'https://tronscan.org/#/transaction/'
|
|
|
|
|
+
|
|
|
|
|
+const filters = reactive<{
|
|
|
|
|
+ status: string | null
|
|
|
|
|
+ // senderIsVaultAddress: string | null
|
|
|
|
|
+ // recipientIsVaultAddress: string | null
|
|
|
|
|
+}>({
|
|
|
|
|
+ status: null,
|
|
|
|
|
+ // senderIsVaultAddress: null,
|
|
|
|
|
+ // recipientIsVaultAddress: null,
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const statusOptions = computed(() => [
|
|
|
|
|
+ { label: t('vaultTx.filterAll'), value: null },
|
|
|
|
|
+ { label: t('vaultTx.status_unconfirmed'), value: 'unconfirmed' },
|
|
|
|
|
+ { label: t('vaultTx.status_confirming'), value: 'confirming' },
|
|
|
|
|
+ { label: t('vaultTx.status_confirmed'), value: 'confirmed' },
|
|
|
|
|
+ { label: t('vaultTx.status_finalized'), value: 'finalized' },
|
|
|
|
|
+ { label: t('vaultTx.status_dropped'), value: 'dropped' },
|
|
|
|
|
+ { label: t('vaultTx.status_replaced'), value: 'replaced' },
|
|
|
|
|
+ { label: t('vaultTx.status_failed'), value: 'failed' },
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+// const boolFilterOptions = computed(() => [
|
|
|
|
|
+// { label: t('vaultTx.filterAll'), value: null },
|
|
|
|
|
+// { label: t('vaultTx.boolYes'), value: 'true' },
|
|
|
|
|
+// { label: t('vaultTx.boolNo'), value: 'false' },
|
|
|
|
|
+// ])
|
|
|
|
|
|
|
|
function formatDash(v: unknown): string {
|
|
function formatDash(v: unknown): string {
|
|
|
if (v == null || v === '') return '—'
|
|
if (v == null || v === '') return '—'
|
|
@@ -41,17 +70,25 @@ function formatAmount(v: string | number | undefined): string {
|
|
|
return String(v)
|
|
return String(v)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 兼容秒级/毫秒级时间戳与 ISO 字符串 */
|
|
|
|
|
|
|
+/** 兼容秒级/毫秒级时间戳与 ISO 字符串 输出yyyy-MM-dd HH:mm:ss 格式 */
|
|
|
function formatTimestamp(v: string | number | undefined): string {
|
|
function formatTimestamp(v: string | number | undefined): string {
|
|
|
if (v == null || v === '') return '—'
|
|
if (v == null || v === '') return '—'
|
|
|
|
|
+ const format = (d: Date) => {
|
|
|
|
|
+ const pad = (n: number) => String(n).padStart(2, '0')
|
|
|
|
|
+ return (
|
|
|
|
|
+ `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
|
|
|
|
+ ` ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
if (typeof v === 'string' && Number.isNaN(Number(v))) {
|
|
if (typeof v === 'string' && Number.isNaN(Number(v))) {
|
|
|
const d = new Date(v)
|
|
const d = new Date(v)
|
|
|
- return Number.isNaN(d.getTime()) ? v : d.toLocaleString()
|
|
|
|
|
|
|
+ return Number.isNaN(d.getTime()) ? v : format(d)
|
|
|
}
|
|
}
|
|
|
const n = typeof v === 'number' ? v : Number(v)
|
|
const n = typeof v === 'number' ? v : Number(v)
|
|
|
if (Number.isNaN(n)) return String(v)
|
|
if (Number.isNaN(n)) return String(v)
|
|
|
const ms = n < 1e12 ? n * 1000 : n
|
|
const ms = n < 1e12 ? n * 1000 : n
|
|
|
- return new Date(ms).toLocaleString()
|
|
|
|
|
|
|
+ const d = new Date(ms)
|
|
|
|
|
+ return Number.isNaN(d.getTime()) ? String(v) : format(d)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function boolText(v: boolean | undefined): string {
|
|
function boolText(v: boolean | undefined): string {
|
|
@@ -62,7 +99,7 @@ function boolText(v: boolean | undefined): string {
|
|
|
|
|
|
|
|
/** 各列 width 之和,供 scroll-x 使用,保证超出视口时可横向滚动看全列 */
|
|
/** 各列 width 之和,供 scroll-x 使用,保证超出视口时可横向滚动看全列 */
|
|
|
const VAULT_TX_COL_WIDTHS = [
|
|
const VAULT_TX_COL_WIDTHS = [
|
|
|
- 120, 186, 200, 120, 96, 140, 200, 120, 96, 140, 120, 112, 128, 96, 260,
|
|
|
|
|
|
|
+ 120, 186, 200, 160,/* 120,*/ 96, 140, 200, 160, /*120,*/ 96, 140, 120, 112, 128, 96, 260,
|
|
|
] as const
|
|
] as const
|
|
|
|
|
|
|
|
const tableScrollX = VAULT_TX_COL_WIDTHS.reduce((a, w) => a + w, 0) + 120
|
|
const tableScrollX = VAULT_TX_COL_WIDTHS.reduce((a, w) => a + w, 0) + 120
|
|
@@ -99,14 +136,23 @@ const columns = computed<DataTableColumns<VaultTransactionItem>>(() => [
|
|
|
h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.senderAddress)),
|
|
h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.senderAddress)),
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
- title: () => t('vaultTx.colSenderInternal'),
|
|
|
|
|
- key: 'senderIsVaultAddress',
|
|
|
|
|
- width: 120,
|
|
|
|
|
|
|
+ title: () => t('vaultTx.colSenderLabel'),
|
|
|
|
|
+ key: 'senderLabel',
|
|
|
|
|
+ width: 160,
|
|
|
titleAlign: 'center',
|
|
titleAlign: 'center',
|
|
|
align: 'center',
|
|
align: 'center',
|
|
|
- render: (row) =>
|
|
|
|
|
- h('span', { class: 'vault-tx__cell-text' }, boolText(row.senderIsVaultAddress)),
|
|
|
|
|
|
|
+ ellipsis: { tooltip: true },
|
|
|
|
|
+ render: (row) => h('span', { class: 'vault-tx__cell-text' }, formatDash(row.senderLabel)),
|
|
|
},
|
|
},
|
|
|
|
|
+ // {
|
|
|
|
|
+ // title: () => t('vaultTx.colSenderInternal'),
|
|
|
|
|
+ // key: 'senderIsVaultAddress',
|
|
|
|
|
+ // width: 120,
|
|
|
|
|
+ // titleAlign: 'center',
|
|
|
|
|
+ // align: 'center',
|
|
|
|
|
+ // render: (row) =>
|
|
|
|
|
+ // h('span', { class: 'vault-tx__cell-text' }, boolText(row.senderIsVaultAddress)),
|
|
|
|
|
+ // },
|
|
|
{
|
|
{
|
|
|
title: () => t('vaultTx.colSenderUnit'),
|
|
title: () => t('vaultTx.colSenderUnit'),
|
|
|
key: 'senderAmountUnit',
|
|
key: 'senderAmountUnit',
|
|
@@ -137,14 +183,23 @@ const columns = computed<DataTableColumns<VaultTransactionItem>>(() => [
|
|
|
h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.recipientAddress)),
|
|
h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.recipientAddress)),
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
- title: () => t('vaultTx.colRecipientInternal'),
|
|
|
|
|
- key: 'recipientIsVaultAddress',
|
|
|
|
|
- width: 120,
|
|
|
|
|
|
|
+ title: () => t('vaultTx.colRecipientLabel'),
|
|
|
|
|
+ key: 'recipientLabel',
|
|
|
|
|
+ width: 160,
|
|
|
titleAlign: 'center',
|
|
titleAlign: 'center',
|
|
|
align: 'center',
|
|
align: 'center',
|
|
|
- render: (row) =>
|
|
|
|
|
- h('span', { class: 'vault-tx__cell-text' }, boolText(row.recipientIsVaultAddress)),
|
|
|
|
|
|
|
+ ellipsis: { tooltip: true },
|
|
|
|
|
+ render: (row) => h('span', { class: 'vault-tx__cell-text' }, formatDash(row.recipientLabel)),
|
|
|
},
|
|
},
|
|
|
|
|
+ // {
|
|
|
|
|
+ // title: () => t('vaultTx.colRecipientInternal'),
|
|
|
|
|
+ // key: 'recipientIsVaultAddress',
|
|
|
|
|
+ // width: 120,
|
|
|
|
|
+ // titleAlign: 'center',
|
|
|
|
|
+ // align: 'center',
|
|
|
|
|
+ // render: (row) =>
|
|
|
|
|
+ // h('span', { class: 'vault-tx__cell-text' }, boolText(row.recipientIsVaultAddress)),
|
|
|
|
|
+ // },
|
|
|
{
|
|
{
|
|
|
title: () => t('vaultTx.colRecipientUnit'),
|
|
title: () => t('vaultTx.colRecipientUnit'),
|
|
|
key: 'recipientAmountUnit',
|
|
key: 'recipientAmountUnit',
|
|
@@ -209,8 +264,22 @@ const columns = computed<DataTableColumns<VaultTransactionItem>>(() => [
|
|
|
titleAlign: 'center',
|
|
titleAlign: 'center',
|
|
|
align: 'center',
|
|
align: 'center',
|
|
|
ellipsis: { tooltip: true },
|
|
ellipsis: { tooltip: true },
|
|
|
- render: (row) =>
|
|
|
|
|
- h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.transactionId)),
|
|
|
|
|
|
|
+ render: (row) => {
|
|
|
|
|
+ const txid = row.transactionId?.trim()
|
|
|
|
|
+ if (!txid) return h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, '—')
|
|
|
|
|
+ const unit = row.recipientAmountUnit?.trim().toUpperCase()
|
|
|
|
|
+ const baseUrl = unit === 'ETH' ? ETH_TX_BASE_URL : TRON_TX_BASE_URL
|
|
|
|
|
+ return h(
|
|
|
|
|
+ 'a',
|
|
|
|
|
+ {
|
|
|
|
|
+ class: 'vault-tx__cell-text vault-tx__cell-mono vault-tx__cell-link',
|
|
|
|
|
+ href: `${baseUrl}${encodeURIComponent(txid)}`,
|
|
|
|
|
+ target: '_blank',
|
|
|
|
|
+ rel: 'noopener noreferrer',
|
|
|
|
|
+ },
|
|
|
|
|
+ txid
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
])
|
|
])
|
|
|
|
|
|
|
@@ -224,14 +293,21 @@ async function load(): Promise<void> {
|
|
|
try {
|
|
try {
|
|
|
const res = await fetchVaultTransactions({
|
|
const res = await fetchVaultTransactions({
|
|
|
vaultId: vault.currentVaultId,
|
|
vaultId: vault.currentVaultId,
|
|
|
- page: pagination.page,
|
|
|
|
|
- pageSize: pagination.pageSize,
|
|
|
|
|
|
|
+ status: filters.status,
|
|
|
|
|
+ // senderIsVaultAddress: filters.senderIsVaultAddress,
|
|
|
|
|
+ // recipientIsVaultAddress: filters.recipientIsVaultAddress,
|
|
|
|
|
+ page: {
|
|
|
|
|
+ current: pagination.page,
|
|
|
|
|
+ row: pagination.pageSize
|
|
|
|
|
+ }
|
|
|
|
|
+ // page: pagination.page,
|
|
|
|
|
+ // pageSize: ,
|
|
|
})
|
|
})
|
|
|
- if (res.code === ApiCode.StatusOK && Array.isArray(res.data?.list)) {
|
|
|
|
|
- rows.value = res.data.list
|
|
|
|
|
- const total = res.data.total
|
|
|
|
|
|
|
+ if (res.code === ApiCode.StatusOK && Array.isArray(res.data)) {
|
|
|
|
|
+ rows.value = res.data
|
|
|
|
|
+ const total = res.page.pageTotal
|
|
|
pagination.itemCount =
|
|
pagination.itemCount =
|
|
|
- typeof total === 'number' && Number.isFinite(total) ? total : res.data.list.length
|
|
|
|
|
|
|
+ typeof total === 'number' && Number.isFinite(total) ? total : res.data.length
|
|
|
} else {
|
|
} else {
|
|
|
rows.value = []
|
|
rows.value = []
|
|
|
pagination.itemCount = 0
|
|
pagination.itemCount = 0
|
|
@@ -250,6 +326,14 @@ watch(
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => [filters.status],
|
|
|
|
|
+ () => {
|
|
|
|
|
+ pagination.page = 1
|
|
|
|
|
+ void load()
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
void load()
|
|
void load()
|
|
|
})
|
|
})
|
|
@@ -270,6 +354,9 @@ async function onExport(): Promise<void> {
|
|
|
try {
|
|
try {
|
|
|
const res = await exportVaultTransactions({
|
|
const res = await exportVaultTransactions({
|
|
|
vaultId: vault.currentVaultId,
|
|
vaultId: vault.currentVaultId,
|
|
|
|
|
+ status: filters.status,
|
|
|
|
|
+ // senderIsVaultAddress: filters.senderIsVaultAddress,
|
|
|
|
|
+ // recipientIsVaultAddress: filters.recipientIsVaultAddress,
|
|
|
page: pagination.page,
|
|
page: pagination.page,
|
|
|
pageSize: pagination.pageSize,
|
|
pageSize: pagination.pageSize,
|
|
|
})
|
|
})
|
|
@@ -286,6 +373,12 @@ async function onExport(): Promise<void> {
|
|
|
exporting.value = false
|
|
exporting.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+function onResetFilters(): void {
|
|
|
|
|
+ filters.status = null
|
|
|
|
|
+ // filters.senderIsVaultAddress = null
|
|
|
|
|
+ // filters.recipientIsVaultAddress = null
|
|
|
|
|
+}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
@@ -310,6 +403,30 @@ async function onExport(): Promise<void> {
|
|
|
</div>
|
|
</div>
|
|
|
<div v-else class="vault-panel__body">
|
|
<div v-else class="vault-panel__body">
|
|
|
<div class="vault-panel__toolbar">
|
|
<div class="vault-panel__toolbar">
|
|
|
|
|
+ <div class="vault-panel__filters">
|
|
|
|
|
+ <n-select
|
|
|
|
|
+ v-model:value="filters.status"
|
|
|
|
|
+ :options="statusOptions"
|
|
|
|
|
+ :placeholder="t('vaultTx.colStatus')"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ style="width: 180px"
|
|
|
|
|
+ />
|
|
|
|
|
+<!-- <n-select-->
|
|
|
|
|
+<!-- v-model:value="filters.senderIsVaultAddress"-->
|
|
|
|
|
+<!-- :options="boolFilterOptions"-->
|
|
|
|
|
+<!-- :placeholder="t('vaultTx.colSenderInternal')"-->
|
|
|
|
|
+<!-- clearable-->
|
|
|
|
|
+<!-- style="width: 200px"-->
|
|
|
|
|
+<!-- />-->
|
|
|
|
|
+<!-- <n-select-->
|
|
|
|
|
+<!-- v-model:value="filters.recipientIsVaultAddress"-->
|
|
|
|
|
+<!-- :options="boolFilterOptions"-->
|
|
|
|
|
+<!-- :placeholder="t('vaultTx.colRecipientInternal')"-->
|
|
|
|
|
+<!-- clearable-->
|
|
|
|
|
+<!-- style="width: 200px"-->
|
|
|
|
|
+<!-- />-->
|
|
|
|
|
+ <n-button tertiary @click="onResetFilters">{{ t('vaultTx.filterReset') }}</n-button>
|
|
|
|
|
+ </div>
|
|
|
<n-button type="primary" secondary :loading="exporting" @click="onExport">
|
|
<n-button type="primary" secondary :loading="exporting" @click="onExport">
|
|
|
{{ t('vaultTx.export') }}
|
|
{{ t('vaultTx.export') }}
|
|
|
</n-button>
|
|
</n-button>
|
|
@@ -464,11 +581,22 @@ async function onExport(): Promise<void> {
|
|
|
.vault-panel__toolbar {
|
|
.vault-panel__toolbar {
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
padding: 0 14px 10px;
|
|
padding: 0 14px 10px;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.vault-panel__filters {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.vault-panel__table-frame {
|
|
.vault-panel__table-frame {
|
|
|
flex: 0 0 auto;
|
|
flex: 0 0 auto;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
@@ -590,6 +718,15 @@ async function onExport(): Promise<void> {
|
|
|
text-transform: uppercase;
|
|
text-transform: uppercase;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.vault-panel__table :deep(.vault-tx__cell-link) {
|
|
|
|
|
+ color: var(--vt-brand);
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.vault-panel__table :deep(.vault-tx__cell-link:hover) {
|
|
|
|
|
+ text-decoration: underline;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.vault-panel__table :deep(.vault-tx__cell-status) {
|
|
.vault-panel__table :deep(.vault-tx__cell-status) {
|
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
|
font-size: 12.5px;
|
|
font-size: 12.5px;
|