vaultList.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. <script setup lang="ts">
  2. import { computed, h, onMounted, reactive, ref, watch } from 'vue'
  3. import type { DataTableColumns } from 'naive-ui'
  4. import { ApiCode } from '@/config'
  5. import { exportVaultTransactions, fetchVaultTransactions } from '@/service/vault'
  6. import { useVaultStore } from '@/store/vault'
  7. import type { VaultTransactionItem } from '@/types/vault'
  8. const { t } = useI18n()
  9. const message = useMessage()
  10. const vault = useVaultStore()
  11. const loading = ref(false)
  12. const exporting = ref(false)
  13. const rows = ref<VaultTransactionItem[]>([])
  14. const pagination = reactive({
  15. page: 1,
  16. pageSize: 10,
  17. itemCount: 0,
  18. showSizePicker: true,
  19. pageSizes: [10, 20, 50],
  20. onUpdatePage: (p: number) => {
  21. pagination.page = p
  22. void load()
  23. },
  24. onUpdatePageSize: (size: number) => {
  25. pagination.pageSize = size
  26. pagination.page = 1
  27. void load()
  28. },
  29. })
  30. const ETH_TX_BASE_URL = 'https://etherscan.io/tx/'
  31. const TRON_TX_BASE_URL = 'https://tronscan.org/#/transaction/'
  32. const filters = reactive<{
  33. senderLabel: string | null
  34. recipientLabel: string | null
  35. senderAddress: string | null
  36. recipientAddress: string | null
  37. timeRange: [number, number] | null
  38. }>({
  39. senderLabel: null,
  40. recipientLabel: null,
  41. senderAddress: null,
  42. recipientAddress: null,
  43. timeRange: null,
  44. })
  45. function formatDash(v: unknown): string {
  46. if (v == null || v === '') return '—'
  47. return String(v)
  48. }
  49. function formatAmount(v: string | number | undefined): string {
  50. if (v == null || v === '') return '—'
  51. return String(v)
  52. }
  53. /** 兼容秒级/毫秒级时间戳与 ISO 字符串 输出yyyy-MM-dd HH:mm:ss 格式 */
  54. function formatTimestamp(v: string | number | undefined): string {
  55. if (v == null || v === '') return '—'
  56. const format = (d: Date) => {
  57. const pad = (n: number) => String(n).padStart(2, '0')
  58. return (
  59. `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
  60. ` ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
  61. )
  62. }
  63. if (typeof v === 'string' && Number.isNaN(Number(v))) {
  64. const d = new Date(v)
  65. return Number.isNaN(d.getTime()) ? v : format(d)
  66. }
  67. const n = typeof v === 'number' ? v : Number(v)
  68. if (Number.isNaN(n)) return String(v)
  69. const ms = n < 1e12 ? n * 1000 : n
  70. const d = new Date(ms)
  71. return Number.isNaN(d.getTime()) ? String(v) : format(d)
  72. }
  73. // function boolText(v: boolean | undefined): string {
  74. // if (v === true) return t('vaultTx.boolYes')
  75. // if (v === false) return t('vaultTx.boolNo')
  76. // return '—'
  77. // }
  78. /** 各列 width 之和,供 scroll-x 使用,保证超出视口时可横向滚动看全列 */
  79. const VAULT_TX_COL_WIDTHS = [
  80. 120, 186, 200, 160,/* 120,*/ 96, 140, 200, 160, /*120,*/ 96, 140, 120, 112, 128, 96, 260,
  81. ] as const
  82. const tableScrollX = VAULT_TX_COL_WIDTHS.reduce((a, w) => a + w, 0) + 120
  83. /** 表头与单元格均居中(titleAlign / align: center) */
  84. const columns = computed<DataTableColumns<VaultTransactionItem>>(() => [
  85. {
  86. title: () => t('vaultTx.colStatus'),
  87. key: 'status',
  88. width: 120,
  89. titleAlign: 'center',
  90. align: 'center',
  91. ellipsis: { tooltip: true },
  92. render: (row) =>
  93. h('span', { class: 'vault-tx__cell-text vault-tx__cell-status' }, formatDash(row.status)),
  94. },
  95. {
  96. title: () => t('vaultTx.colCreated'),
  97. key: 'createdTimestamp',
  98. width: 186,
  99. titleAlign: 'center',
  100. align: 'center',
  101. render: (row) =>
  102. h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatTimestamp(row.createdTimestamp)),
  103. },
  104. {
  105. title: () => t('vaultTx.colSenderAddr'),
  106. key: 'senderAddress',
  107. width: 200,
  108. titleAlign: 'center',
  109. align: 'center',
  110. ellipsis: { tooltip: true },
  111. render: (row) =>
  112. h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.senderAddress)),
  113. },
  114. {
  115. title: () => t('vaultTx.colSenderLabel'),
  116. key: 'senderLabel',
  117. width: 160,
  118. titleAlign: 'center',
  119. align: 'center',
  120. ellipsis: { tooltip: true },
  121. render: (row) => h('span', { class: 'vault-tx__cell-text' }, formatDash(row.senderLabel)),
  122. },
  123. // {
  124. // title: () => t('vaultTx.colSenderInternal'),
  125. // key: 'senderIsVaultAddress',
  126. // width: 120,
  127. // titleAlign: 'center',
  128. // align: 'center',
  129. // render: (row) =>
  130. // h('span', { class: 'vault-tx__cell-text' }, boolText(row.senderIsVaultAddress)),
  131. // },
  132. {
  133. title: () => t('vaultTx.colSenderUnit'),
  134. key: 'senderAmountUnit',
  135. width: 96,
  136. titleAlign: 'center',
  137. align: 'center',
  138. ellipsis: { tooltip: true },
  139. render: (row) =>
  140. h('span', { class: 'vault-tx__cell-text vault-tx__cell-unit' }, formatDash(row.senderAmountUnit)),
  141. },
  142. {
  143. title: () => t('vaultTx.colSenderAmount'),
  144. key: 'senderAmount',
  145. width: 140,
  146. titleAlign: 'center',
  147. align: 'center',
  148. render: (row) =>
  149. h('span', { class: 'vault-tx__cell-num' }, formatAmount(row.senderAmount)),
  150. },
  151. {
  152. title: () => t('vaultTx.colRecipientAddr'),
  153. key: 'recipientAddress',
  154. width: 200,
  155. titleAlign: 'center',
  156. align: 'center',
  157. ellipsis: { tooltip: true },
  158. render: (row) =>
  159. h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.recipientAddress)),
  160. },
  161. {
  162. title: () => t('vaultTx.colRecipientLabel'),
  163. key: 'recipientLabel',
  164. width: 160,
  165. titleAlign: 'center',
  166. align: 'center',
  167. ellipsis: { tooltip: true },
  168. render: (row) => h('span', { class: 'vault-tx__cell-text' }, formatDash(row.recipientLabel)),
  169. },
  170. // {
  171. // title: () => t('vaultTx.colRecipientInternal'),
  172. // key: 'recipientIsVaultAddress',
  173. // width: 120,
  174. // titleAlign: 'center',
  175. // align: 'center',
  176. // render: (row) =>
  177. // h('span', { class: 'vault-tx__cell-text' }, boolText(row.recipientIsVaultAddress)),
  178. // },
  179. {
  180. title: () => t('vaultTx.colRecipientUnit'),
  181. key: 'recipientAmountUnit',
  182. width: 96,
  183. titleAlign: 'center',
  184. align: 'center',
  185. ellipsis: { tooltip: true },
  186. render: (row) =>
  187. h('span', { class: 'vault-tx__cell-text vault-tx__cell-unit' }, formatDash(row.recipientAmountUnit)),
  188. },
  189. {
  190. title: () => t('vaultTx.colRecipientAmount'),
  191. key: 'recipientAmount',
  192. width: 140,
  193. titleAlign: 'center',
  194. align: 'center',
  195. render: (row) =>
  196. h('span', { class: 'vault-tx__cell-num' }, formatAmount(row.recipientAmount)),
  197. },
  198. {
  199. title: () => t('vaultTx.colBlockchain'),
  200. key: 'blockchain',
  201. width: 120,
  202. titleAlign: 'center',
  203. align: 'center',
  204. ellipsis: { tooltip: true },
  205. render: (row) =>
  206. h('span', { class: 'vault-tx__cell-text' }, formatDash(row.blockchain)),
  207. },
  208. // {
  209. // title: () => t('vaultTx.colBlockHeight'),
  210. // key: 'minedInBlockHeight',
  211. // width: 112,
  212. // titleAlign: 'center',
  213. // align: 'center',
  214. // render: (row) =>
  215. // h('span', { class: 'vault-tx__cell-num' }, formatDash(row.minedInBlockHeight)),
  216. // },
  217. {
  218. title: () => t('vaultTx.colFee'),
  219. key: 'feeAmount',
  220. width: 128,
  221. titleAlign: 'center',
  222. align: 'center',
  223. render: (row) =>
  224. h('span', { class: 'vault-tx__cell-num' }, formatAmount(row.feeAmount)),
  225. },
  226. {
  227. title: () => t('vaultTx.colFeeUnit'),
  228. key: 'feeAmountUnit',
  229. width: 96,
  230. titleAlign: 'center',
  231. align: 'center',
  232. ellipsis: { tooltip: true },
  233. render: (row) =>
  234. h('span', { class: 'vault-tx__cell-text vault-tx__cell-unit' }, formatDash(row.feeAmountUnit)),
  235. },
  236. {
  237. title: () => t('vaultTx.colTxId'),
  238. key: 'transactionId',
  239. width: 260,
  240. titleAlign: 'center',
  241. align: 'center',
  242. ellipsis: { tooltip: true },
  243. render: (row) => {
  244. const txid = row.transactionId?.trim()
  245. if (!txid) return h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, '—')
  246. const unit = row.blockchain?.trim()
  247. const baseUrl = unit === 'ethereum' ? ETH_TX_BASE_URL : TRON_TX_BASE_URL
  248. return h(
  249. 'a',
  250. {
  251. class: 'vault-tx__cell-text vault-tx__cell-mono vault-tx__cell-link',
  252. href: `${baseUrl}${encodeURIComponent(txid)}`,
  253. target: '_blank',
  254. rel: 'noopener noreferrer',
  255. },
  256. txid
  257. )
  258. },
  259. },
  260. ])
  261. async function load(): Promise<void> {
  262. if (vault.currentVaultId == null) {
  263. rows.value = []
  264. pagination.itemCount = 0
  265. return
  266. }
  267. loading.value = true
  268. try {
  269. let startTime: string | null = null
  270. let endTime: string | null = null
  271. if (filters.timeRange?.[0] != null) {
  272. const d = new Date(filters.timeRange[0])
  273. const pad = (n: number) => String(n).padStart(2, '0')
  274. startTime = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 00:00:00`
  275. }
  276. if (filters.timeRange?.[1] != null) {
  277. const d = new Date(filters.timeRange[1])
  278. const pad = (n: number) => String(n).padStart(2, '0')
  279. endTime = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 23:59:59`
  280. }
  281. const res = await fetchVaultTransactions({
  282. vaultId: vault.currentVaultId,
  283. senderLabel: filters.senderLabel,
  284. recipientLabel: filters.recipientLabel,
  285. senderAddress: filters.senderAddress,
  286. recipientAddress: filters.recipientAddress,
  287. startTime,
  288. endTime,
  289. // senderIsVaultAddress: filters.senderIsVaultAddress,
  290. // recipientIsVaultAddress: filters.recipientIsVaultAddress,
  291. page: {
  292. current: pagination.page,
  293. row: pagination.pageSize
  294. }
  295. // page: pagination.page,
  296. // pageSize: ,
  297. })
  298. if (res.code === ApiCode.StatusOK && Array.isArray(res.data)) {
  299. rows.value = res.data
  300. const total = res.page?.rowTotal
  301. pagination.itemCount =
  302. typeof total === 'number' && Number.isFinite(total) ? total : res.data.length
  303. } else {
  304. rows.value = []
  305. pagination.itemCount = 0
  306. message.error(res.msg || t('vaultTx.loadFail'))
  307. }
  308. } finally {
  309. loading.value = false
  310. }
  311. }
  312. watch(
  313. () => vault.currentVaultId,
  314. () => {
  315. pagination.page = 1
  316. void load()
  317. }
  318. )
  319. watch(
  320. () => [filters.senderLabel, filters.recipientLabel, filters.senderAddress, filters.recipientAddress, filters.timeRange],
  321. () => {
  322. pagination.page = 1
  323. void load()
  324. }
  325. )
  326. onMounted(() => {
  327. void load()
  328. })
  329. function triggerFileDownload(blob: Blob, filename: string): void {
  330. const url = URL.createObjectURL(blob)
  331. const a = document.createElement('a')
  332. a.href = url
  333. a.download = filename
  334. a.rel = 'noopener'
  335. a.click()
  336. URL.revokeObjectURL(url)
  337. }
  338. async function onExport(): Promise<void> {
  339. if (vault.currentVaultId == null) return
  340. exporting.value = true
  341. try {
  342. let startTime: string | null = null
  343. let endTime: string | null = null
  344. if (filters.timeRange?.[0] != null) {
  345. const d = new Date(filters.timeRange[0])
  346. const pad = (n: number) => String(n).padStart(2, '0')
  347. startTime = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 00:00:00`
  348. }
  349. if (filters.timeRange?.[1] != null) {
  350. const d = new Date(filters.timeRange[1])
  351. const pad = (n: number) => String(n).padStart(2, '0')
  352. endTime = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 23:59:59`
  353. }
  354. const res = await exportVaultTransactions({
  355. vaultId: vault.currentVaultId,
  356. senderLabel: filters.senderLabel,
  357. recipientLabel: filters.recipientLabel,
  358. senderAddress: filters.senderAddress,
  359. recipientAddress: filters.recipientAddress,
  360. startTime,
  361. endTime,
  362. // senderIsVaultAddress: filters.senderIsVaultAddress,
  363. // recipientIsVaultAddress: filters.recipientIsVaultAddress,
  364. page: {
  365. current: pagination.page,
  366. row: pagination.pageSize,
  367. },
  368. })
  369. if ('error' in res) {
  370. message.error(res.error || t('vaultTx.exportFail'))
  371. return
  372. }
  373. const name = res.filename?.trim() || `vault-transactions-${pagination.page}.xlsx`
  374. triggerFileDownload(res.blob, name)
  375. message.success(t('vaultTx.exportSuccess'))
  376. } catch {
  377. message.error(t('vaultTx.exportFail'))
  378. } finally {
  379. exporting.value = false
  380. }
  381. }
  382. function onResetFilters(): void {
  383. filters.senderLabel = null
  384. filters.recipientLabel = null
  385. filters.senderAddress = null
  386. filters.recipientAddress = null
  387. filters.timeRange = null
  388. // filters.senderIsVaultAddress = null
  389. // filters.recipientIsVaultAddress = null
  390. }
  391. </script>
  392. <template>
  393. <div class="vault-list">
  394. <n-card class="vault-panel" :segmented="{ content: true }" size="medium">
  395. <template #header>
  396. <!-- <div class="vault-panel__header">
  397. <div class="vault-panel__titles">
  398. <h2 class="vault-panel__title">{{ t('vaultTx.cardTitle') }}</h2>
  399. <p class="vault-panel__desc">{{ t('vaultTx.cardSubtitle') }}</p>
  400. </div>
  401. <div v-if="vault.currentVaultId != null" class="vault-panel__meta">
  402. <span class="vault-panel__meta-label">{{ t('vaultTx.rowCountLabel') }}</span>
  403. <span class="vault-panel__meta-value">{{ pagination.itemCount }}</span>
  404. </div>
  405. </div> -->
  406. </template>
  407. <n-spin :show="loading" class="vault-panel__spin">
  408. <div v-if="vault.currentVaultId == null" class="vault-panel__empty-wrap">
  409. <n-empty size="large" :description="t('vaultTx.selectVaultHint')" />
  410. </div>
  411. <div v-else class="vault-panel__body">
  412. <div class="vault-panel__toolbar">
  413. <div class="vault-panel__filters">
  414. <n-input
  415. v-model:value="filters.senderLabel"
  416. :placeholder="t('vaultTx.colSenderLabel')"
  417. clearable
  418. style="width: 200px"
  419. />
  420. <n-input
  421. v-model:value="filters.senderAddress"
  422. :placeholder="t('vaultTx.colSenderAddr')"
  423. clearable
  424. style="width: 260px"
  425. />
  426. <n-input
  427. v-model:value="filters.recipientLabel"
  428. :placeholder="t('vaultTx.colRecipientLabel')"
  429. clearable
  430. style="width: 200px"
  431. />
  432. <n-input
  433. v-model:value="filters.recipientAddress"
  434. :placeholder="t('vaultTx.colRecipientAddr')"
  435. clearable
  436. style="width: 260px"
  437. />
  438. <n-date-picker
  439. v-model:value="filters.timeRange"
  440. type="daterange"
  441. clearable
  442. :start-placeholder="t('vaultTx.filterStartTime')"
  443. :end-placeholder="t('vaultTx.filterEndTime')"
  444. style="width: 420px"
  445. />
  446. <!-- <n-select-->
  447. <!-- v-model:value="filters.senderIsVaultAddress"-->
  448. <!-- :options="boolFilterOptions"-->
  449. <!-- :placeholder="t('vaultTx.colSenderInternal')"-->
  450. <!-- clearable-->
  451. <!-- style="width: 200px"-->
  452. <!-- />-->
  453. <!-- <n-select-->
  454. <!-- v-model:value="filters.recipientIsVaultAddress"-->
  455. <!-- :options="boolFilterOptions"-->
  456. <!-- :placeholder="t('vaultTx.colRecipientInternal')"-->
  457. <!-- clearable-->
  458. <!-- style="width: 200px"-->
  459. <!-- />-->
  460. <n-button tertiary @click="onResetFilters">{{ t('vaultTx.filterReset') }}</n-button>
  461. </div>
  462. <n-button class="vault-panel__export" type="primary" secondary :loading="exporting" @click="onExport">
  463. {{ t('vaultTx.export') }}
  464. </n-button>
  465. </div>
  466. <div class="vault-panel__table-frame">
  467. <n-data-table
  468. class="vault-panel__table"
  469. :columns="columns"
  470. :data="rows"
  471. :pagination="pagination"
  472. :bordered="true"
  473. striped
  474. bottom-bordered
  475. :single-line="false"
  476. size="medium"
  477. :scroll-x="tableScrollX"
  478. :scrollbar-props="{ trigger: 'none' }"
  479. :empty-description="t('vaultTx.empty')"
  480. />
  481. </div>
  482. <!-- 仅占位:让白卡片铺满剩余高度,表格本身不纵向拉伸 -->
  483. <div class="vault-panel__tail-spacer" aria-hidden="true" />
  484. </div>
  485. </n-spin>
  486. </n-card>
  487. </div>
  488. </template>
  489. <style scoped>
  490. .vault-list {
  491. width: 100%;
  492. flex: 1;
  493. min-height: 0;
  494. display: flex;
  495. flex-direction: column;
  496. }
  497. .vault-panel {
  498. flex: 1;
  499. min-height: 0;
  500. max-width: 100%;
  501. display: flex;
  502. flex-direction: column;
  503. overflow: hidden;
  504. background: var(--vt-bg-elevated) !important;
  505. border: 1px solid var(--vt-divider) !important;
  506. box-shadow: var(--vt-shadow-card);
  507. --n-padding-top: 0;
  508. --n-padding-bottom: 0;
  509. --n-padding-left: 0;
  510. --n-padding-right: 0;
  511. }
  512. .vault-panel :deep(.n-card-header) {
  513. padding: 20px 22px 16px;
  514. border-bottom: 1px solid var(--vt-divider);
  515. }
  516. /* 卡片内容区铺满卡片剩余高度;内边距 15px */
  517. .vault-panel :deep(.n-card-content) {
  518. flex: 1;
  519. min-height: 0;
  520. display: flex;
  521. flex-direction: column;
  522. overflow-y: scroll;
  523. padding: 15px !important;
  524. box-sizing: border-box;
  525. }
  526. .vault-panel__header {
  527. display: flex;
  528. align-items: flex-start;
  529. justify-content: space-between;
  530. gap: 16px;
  531. }
  532. .vault-panel__titles {
  533. min-width: 0;
  534. }
  535. .vault-panel__title {
  536. margin: 0;
  537. font-size: 17px;
  538. font-weight: 600;
  539. letter-spacing: -0.02em;
  540. color: var(--vt-text);
  541. line-height: 1.3;
  542. }
  543. .vault-panel__desc {
  544. margin: 6px 0 0;
  545. font-size: 13px;
  546. line-height: 1.45;
  547. color: var(--vt-text-muted);
  548. }
  549. .vault-panel__meta {
  550. flex-shrink: 0;
  551. display: inline-flex;
  552. align-items: baseline;
  553. gap: 6px;
  554. padding: 6px 12px;
  555. border-radius: 999px;
  556. background: var(--vt-brand-soft);
  557. border: 1px solid rgba(79, 70, 229, 0.15);
  558. }
  559. .vault-panel__meta-label {
  560. font-size: 12px;
  561. font-weight: 500;
  562. color: var(--vt-text-muted);
  563. }
  564. .vault-panel__meta-value {
  565. font-size: 15px;
  566. font-weight: 700;
  567. font-variant-numeric: tabular-nums;
  568. color: var(--vt-brand);
  569. }
  570. .vault-panel__spin {
  571. flex: 1;
  572. min-height: 0;
  573. display: flex;
  574. flex-direction: column;
  575. }
  576. .vault-panel__spin :deep(.n-spin-content) {
  577. flex: 1;
  578. min-height: 0;
  579. display: flex;
  580. flex-direction: column;
  581. }
  582. .vault-panel__empty-wrap {
  583. flex: 1;
  584. min-height: 0;
  585. display: flex;
  586. align-items: center;
  587. justify-content: center;
  588. padding: 24px 0;
  589. }
  590. .vault-panel__body {
  591. flex: 1;
  592. min-height: 0;
  593. display: flex;
  594. flex-direction: column;
  595. align-items: stretch;
  596. }
  597. .vault-panel__toolbar {
  598. flex-shrink: 0;
  599. display: flex;
  600. justify-content: space-between;
  601. align-items: center;
  602. gap: 10px;
  603. padding: 0 14px 10px;
  604. box-sizing: border-box;
  605. }
  606. .vault-panel__filters {
  607. display: flex;
  608. flex-wrap: wrap;
  609. align-items: center;
  610. gap: 10px;
  611. min-width: 0;
  612. }
  613. .vault-panel__export{
  614. align-self: flex-start;
  615. }
  616. .vault-panel__table-frame {
  617. flex: 0 0 auto;
  618. width: 100%;
  619. min-width: 0;
  620. padding: 12px 14px 10px;
  621. box-sizing: border-box;
  622. /* border: 1px solid var(--vt-border); */
  623. /* border-radius: 12px; */
  624. /* background: #fafbfc; */
  625. overflow-x: auto;
  626. overflow-y: visible;
  627. }
  628. .vault-panel__tail-spacer {
  629. flex: 1 1 auto;
  630. min-height: 0;
  631. min-width: 0;
  632. }
  633. .vault-panel__table {
  634. width: 100%;
  635. max-width: 100%;
  636. min-width: 0;
  637. }
  638. /* 表格:表头、分隔、对齐与数字字体 */
  639. .vault-panel__table :deep(.n-data-table) {
  640. --n-th-padding: 11px 14px;
  641. --n-td-padding: 11px 14px;
  642. }
  643. .vault-panel__table :deep(.n-data-table-wrapper) {
  644. min-width: 0;
  645. border-radius: 10px;
  646. overflow: hidden;
  647. border: 1px solid #e5e7eb;
  648. background: #fff;
  649. box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
  650. }
  651. .vault-panel__table :deep(.n-data-table-base-table-header) {
  652. border-radius: 10px 10px 0 0;
  653. }
  654. .vault-panel__table :deep(.n-data-table-th) {
  655. text-align: center !important;
  656. font-weight: 600;
  657. font-size: 12px;
  658. line-height: 1.35;
  659. letter-spacing: 0.01em;
  660. color: #475569 !important;
  661. background: #f1f5f9 !important;
  662. border-color: #e2e8f0 !important;
  663. border-bottom: 1px solid #e2e8f0 !important;
  664. }
  665. .vault-panel__table :deep(.n-data-table-th__title-wrapper) {
  666. justify-content: center;
  667. }
  668. .vault-panel__table :deep(.n-data-table-td) {
  669. text-align: center !important;
  670. font-size: 13px;
  671. line-height: 1.45;
  672. color: var(--vt-text);
  673. border-color: #eef2f7 !important;
  674. vertical-align: middle;
  675. }
  676. .vault-panel__table :deep(.n-data-table-tr:not(.n-data-table-tr--summary):hover .n-data-table-td) {
  677. background: #f8fafc !important;
  678. }
  679. .vault-panel__table :deep(.n-data-table__pagination) {
  680. padding: 12px 4px 4px;
  681. margin-top: 2px;
  682. border-top: 1px solid #eef2f7;
  683. justify-content: flex-end;
  684. }
  685. .vault-panel__table :deep(.n-data-table-empty) {
  686. padding: 40px 16px 48px;
  687. background: #fff;
  688. }
  689. .vault-panel__table :deep(.n-data-table-empty .n-empty__description) {
  690. margin-top: 12px;
  691. font-size: 14px;
  692. color: var(--vt-text-muted);
  693. }
  694. /* 单元格内层:与表头一致居中 */
  695. .vault-panel__table :deep(.vault-tx__cell-text),
  696. .vault-panel__table :deep(.vault-tx__cell-num) {
  697. display: inline-block;
  698. max-width: 100%;
  699. vertical-align: middle;
  700. text-align: center;
  701. }
  702. .vault-panel__table :deep(.vault-tx__cell-mono) {
  703. font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  704. font-size: 12.5px;
  705. letter-spacing: -0.01em;
  706. }
  707. .vault-panel__table :deep(.vault-tx__cell-num) {
  708. font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  709. font-size: 13px;
  710. font-variant-numeric: tabular-nums;
  711. font-feature-settings: 'tnum' 1;
  712. letter-spacing: -0.02em;
  713. }
  714. .vault-panel__table :deep(.vault-tx__cell-unit) {
  715. font-weight: 600;
  716. font-size: 12px;
  717. color: #64748b;
  718. text-transform: uppercase;
  719. }
  720. .vault-panel__table :deep(.vault-tx__cell-link) {
  721. color: var(--vt-brand);
  722. text-decoration: none;
  723. }
  724. .vault-panel__table :deep(.vault-tx__cell-link:hover) {
  725. text-decoration: underline;
  726. }
  727. .vault-panel__table :deep(.vault-tx__cell-status) {
  728. font-weight: 500;
  729. font-size: 12.5px;
  730. color: #0f172a;
  731. }
  732. </style>