浏览代码

feat: 哈希可以跳转,添加筛选,动态域名地址,添加列表字段

ljc 2 月之前
父节点
当前提交
65278f2ba9
共有 9 个文件被更改,包括 207 次插入26 次删除
  1. 1 0
      .env.development
  2. 11 0
      src/locales/en-US.ts
  3. 11 0
      src/locales/zh-CN.ts
  4. 8 1
      src/service/http.ts
  5. 2 1
      src/service/salesExchange.ts
  6. 9 1
      src/service/vault.ts
  7. 2 0
      src/types/vault.ts
  8. 160 23
      src/views/Vault/vaultList.vue
  9. 3 0
      vite.config.ts

+ 1 - 0
.env.development

@@ -1,4 +1,5 @@
 # 与 gypsy-crm-frontend-admin 开发环境默认 Host 对齐,可按环境修改
 # VITE_API_HOST82=https://ad.44a5c8109e4.com
 VITE_API_HOST82=http://103.158.191.66:9300
+#VITE_API_HOST82=http://192.168.0.25:9300
 VITE_API_HOST80=https://secure.44a5c8109e4.com

+ 11 - 0
src/locales/en-US.ts

@@ -41,13 +41,24 @@ export default {
     empty: 'No transactions',
     boolYes: 'Yes',
     boolNo: 'No',
+    filterAll: 'All',
+    filterReset: 'Reset',
+    status_unconfirmed: 'Unconfirmed',
+    status_confirming: 'Confirming',
+    status_confirmed: 'Confirmed',
+    status_finalized: 'Finalized',
+    status_dropped: 'Dropped',
+    status_replaced: 'Replaced',
+    status_failed: 'Failed',
     colStatus: 'Status',
     colCreated: 'Created',
     colSenderAddr: 'Sender address',
+    colSenderLabel: 'Sender name',
     colSenderInternal: 'Sender internal',
     colSenderUnit: 'Sender asset',
     colSenderAmount: 'Sender amount',
     colRecipientAddr: 'Recipient address',
+    colRecipientLabel: 'Recipient name',
     colRecipientInternal: 'Recipient internal',
     colRecipientUnit: 'Recipient asset',
     colRecipientAmount: 'Recipient amount',

+ 11 - 0
src/locales/zh-CN.ts

@@ -37,13 +37,24 @@ export default {
     empty: '暂无交易记录',
     boolYes: '是',
     boolNo: '否',
+    filterAll: '全部',
+    filterReset: '重置',
+    status_unconfirmed: '未确认',
+    status_confirming: '确认中',
+    status_confirmed: '已确认',
+    status_finalized: '已最终确认',
+    status_dropped: '丢弃',
+    status_replaced: '覆盖',
+    status_failed: '执行失败',
     colStatus: '交易状态',
     colCreated: '创建时间',
     colSenderAddr: '转出钱包地址',
+    colSenderLabel: '付款钱包名称',
     colSenderInternal: '转出是否内部钱包',
     colSenderUnit: '转出币种',
     colSenderAmount: '转出金额',
     colRecipientAddr: '收款钱包地址',
+    colRecipientLabel: '收款钱包名称',
     colRecipientInternal: '收款是否内部钱包',
     colRecipientUnit: '收款币种',
     colRecipientAmount: '收到金额',

+ 8 - 1
src/service/http.ts

@@ -8,6 +8,13 @@ export function setSessionExpireHandler(fn: () => void): void {
   sessionExpireHandler = fn
 }
 
+export function getApiHost(): string | undefined {
+  const fromEnv = import.meta.env.VITE_API_HOST82
+  if (import.meta.env.DEV) return fromEnv
+  // if (fromEnv) return fromEnv
+  return window.location.origin
+}
+
 function applyDefaultHeaders(instance: AxiosInstance): void {
   const token = sessionStorage.getItem('access_token')
   if (token) {
@@ -25,7 +32,7 @@ function applyDefaultHeaders(instance: AxiosInstance): void {
 }
 
 export const http = axios.create({
-  baseURL: import.meta.env.VITE_API_HOST82,
+  baseURL: getApiHost(),
   timeout: 120000,
 })
 

+ 2 - 1
src/service/salesExchange.ts

@@ -4,13 +4,14 @@
 import axios from 'axios'
 import { ApiCode } from '@/config'
 import type { ApiResponse } from '@/types/api'
+import {getApiHost} from './http'
 
 interface ExchangeData {
   accessToken: string
 }
 
 export async function exchangeSalesAccessToken(rawToken: string): Promise<ApiResponse<ExchangeData>> {
-  const url = `${import.meta.env.VITE_API_HOST82}/custom/sales/node/list`
+  const url = `${getApiHost() ?? ''}/custom/sales/node/list`
   const headers: Record<string, string> = {
     'Access-Token': rawToken,
     'X-System': 'A',

+ 9 - 1
src/service/vault.ts

@@ -9,6 +9,9 @@ export function fetchVaultsList() {
 
 export interface FetchVaultTransactionsBody {
   vaultId?: string | number | null
+  status?: string | null
+  senderIsVaultAddress?: string | null
+  recipientIsVaultAddress?: string | null
   /** 页码,从 1 开始 */
   page?: number
   pageSize?: number
@@ -23,13 +26,18 @@ export interface VaultTransactionsResult {
 function buildVaultTransactionsPayload(body: FetchVaultTransactionsBody): Record<string, unknown> {
   const payload: Record<string, unknown> = {}
   if (body.vaultId != null) payload.vaultId = body.vaultId
+  if (body.status != null && String(body.status).trim() !== '') payload.status = body.status
+  // if (body.senderIsVaultAddress != null && String(body.senderIsVaultAddress).trim() !== '')
+  //   payload.senderIsVaultAddress = body.senderIsVaultAddress
+  // if (body.recipientIsVaultAddress != null && String(body.recipientIsVaultAddress).trim() !== '')
+  //   payload.recipientIsVaultAddress = body.recipientIsVaultAddress
   if (body.page != null) payload.page = body.page
   if (body.pageSize != null) payload.pageSize = body.pageSize
   return payload
 }
 
 export function fetchVaultTransactions(body: FetchVaultTransactionsBody = {}) {
-  return postJson<VaultTransactionsResult>('/vaultody/vaults/transactions', buildVaultTransactionsPayload(body))
+  return postJson<VaultTransactionsResult>('/vaultody/transaction/search/list', buildVaultTransactionsPayload(body))
 }
 
 function parseFilenameFromContentDisposition(cd: string | undefined): string | null {

+ 2 - 0
src/types/vault.ts

@@ -8,10 +8,12 @@ export interface VaultTransactionItem {
   status?: string
   createdTimestamp?: string | number
   senderAddress?: string
+  senderLabel?: string
   senderIsVaultAddress?: boolean
   senderAmountUnit?: string
   senderAmount?: string | number
   recipientAddress?: string
+  recipientLabel?: string
   recipientIsVaultAddress?: boolean
   recipientAmountUnit?: string
   recipientAmount?: string | number

+ 160 - 23
src/views/Vault/vaultList.vue

@@ -30,6 +30,35 @@ const pagination = reactive({
     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 {
   if (v == null || v === '') return '—'
@@ -41,17 +70,25 @@ function formatAmount(v: string | number | undefined): string {
   return String(v)
 }
 
-/** 兼容秒级/毫秒级时间戳与 ISO 字符串 */
+/** 兼容秒级/毫秒级时间戳与 ISO 字符串 输出yyyy-MM-dd HH:mm:ss 格式 */
 function formatTimestamp(v: string | number | undefined): string {
   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))) {
     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)
   if (Number.isNaN(n)) return String(v)
   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 {
@@ -62,7 +99,7 @@ function boolText(v: boolean | undefined): string {
 
 /** 各列 width 之和,供 scroll-x 使用,保证超出视口时可横向滚动看全列 */
 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
 
 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)),
   },
   {
-    title: () => t('vaultTx.colSenderInternal'),
-    key: 'senderIsVaultAddress',
-    width: 120,
+    title: () => t('vaultTx.colSenderLabel'),
+    key: 'senderLabel',
+    width: 160,
     titleAlign: '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'),
     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)),
   },
   {
-    title: () => t('vaultTx.colRecipientInternal'),
-    key: 'recipientIsVaultAddress',
-    width: 120,
+    title: () => t('vaultTx.colRecipientLabel'),
+    key: 'recipientLabel',
+    width: 160,
     titleAlign: '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'),
     key: 'recipientAmountUnit',
@@ -209,8 +264,22 @@ const columns = computed<DataTableColumns<VaultTransactionItem>>(() => [
     titleAlign: 'center',
     align: 'center',
     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 {
     const res = await fetchVaultTransactions({
       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 =
-        typeof total === 'number' && Number.isFinite(total) ? total : res.data.list.length
+        typeof total === 'number' && Number.isFinite(total) ? total : res.data.length
     } else {
       rows.value = []
       pagination.itemCount = 0
@@ -250,6 +326,14 @@ watch(
   }
 )
 
+watch(
+  () => [filters.status],
+  () => {
+    pagination.page = 1
+    void load()
+  }
+)
+
 onMounted(() => {
   void load()
 })
@@ -270,6 +354,9 @@ async function onExport(): Promise<void> {
   try {
     const res = await exportVaultTransactions({
       vaultId: vault.currentVaultId,
+      status: filters.status,
+      // senderIsVaultAddress: filters.senderIsVaultAddress,
+      // recipientIsVaultAddress: filters.recipientIsVaultAddress,
       page: pagination.page,
       pageSize: pagination.pageSize,
     })
@@ -286,6 +373,12 @@ async function onExport(): Promise<void> {
     exporting.value = false
   }
 }
+
+function onResetFilters(): void {
+  filters.status = null
+  // filters.senderIsVaultAddress = null
+  // filters.recipientIsVaultAddress = null
+}
 </script>
 
 <template>
@@ -310,6 +403,30 @@ async function onExport(): Promise<void> {
         </div>
         <div v-else class="vault-panel__body">
           <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">
               {{ t('vaultTx.export') }}
             </n-button>
@@ -464,11 +581,22 @@ async function onExport(): Promise<void> {
 .vault-panel__toolbar {
   flex-shrink: 0;
   display: flex;
-  justify-content: flex-end;
+  justify-content: space-between;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
   padding: 0 14px 10px;
   box-sizing: border-box;
 }
 
+.vault-panel__filters {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 10px;
+  min-width: 0;
+}
+
 .vault-panel__table-frame {
   flex: 0 0 auto;
   width: 100%;
@@ -590,6 +718,15 @@ async function onExport(): Promise<void> {
   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) {
   font-weight: 500;
   font-size: 12.5px;

+ 3 - 0
vite.config.ts

@@ -7,6 +7,9 @@ import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
 
 // https://vite.dev/config/
 export default defineConfig({
+  server: {
+    host: true,
+  },
   plugins: [
     vue(),
     AutoImport({