|
@@ -0,0 +1,561 @@
|
|
|
|
|
+'use client'
|
|
|
|
|
+
|
|
|
|
|
+import { useEffect, useState } from 'react'
|
|
|
|
|
+import { Table, message, Select, Typography, Tag, Button } from 'antd'
|
|
|
|
|
+// import type { ColumnsType } from 'antd/es/table'
|
|
|
|
|
+
|
|
|
|
|
+interface TableData {
|
|
|
|
|
+ key: string
|
|
|
|
|
+ [key: string]: unknown
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ApiResponse {
|
|
|
|
|
+ retCode: number
|
|
|
|
|
+ result: {
|
|
|
|
|
+ data: {
|
|
|
|
|
+ records: Record<string, unknown>[]
|
|
|
|
|
+ total: number
|
|
|
|
|
+ current: number
|
|
|
|
|
+ pageSize: number
|
|
|
|
|
+ pages: number
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ retMsg?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PoolOption {
|
|
|
|
|
+ label: string
|
|
|
|
|
+ value: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PoolInfo {
|
|
|
|
|
+ poolAddress: string
|
|
|
|
|
+ mintA?: {
|
|
|
|
|
+ mintInfo?: {
|
|
|
|
|
+ symbol?: string
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ mintB?: {
|
|
|
|
|
+ mintInfo?: {
|
|
|
|
|
+ symbol?: string
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PoolsListResponse {
|
|
|
|
|
+ retCode: number
|
|
|
|
|
+ result?: {
|
|
|
|
|
+ data?: {
|
|
|
|
|
+ records?: PoolInfo[]
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ retMsg?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function DataTable() {
|
|
|
|
|
+ const [data, setData] = useState<TableData[]>([])
|
|
|
|
|
+ const [loading, setLoading] = useState(true)
|
|
|
|
|
+ const [poolOptions, setPoolOptions] = useState<PoolOption[]>([])
|
|
|
|
|
+ const [selectedPoolAddress, setSelectedPoolAddress] = useState<string>(
|
|
|
|
|
+ 'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC'
|
|
|
|
|
+ )
|
|
|
|
|
+ const [pagination, setPagination] = useState({
|
|
|
|
|
+ current: 1,
|
|
|
|
|
+ pageSize: 100,
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ })
|
|
|
|
|
+ const [sortField, setSortField] = useState<string>('liquidity')
|
|
|
|
|
+ const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([])
|
|
|
|
|
+ const [childTableData, setChildTableData] = useState<
|
|
|
|
|
+ Record<string, TableData[]>
|
|
|
|
|
+ >({})
|
|
|
|
|
+ const [childTableLoading, setChildTableLoading] = useState<
|
|
|
|
|
+ Record<string, boolean>
|
|
|
|
|
+ >({})
|
|
|
|
|
+
|
|
|
|
|
+ // 获取 pools 列表
|
|
|
|
|
+ const fetchPoolsList = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/pools/list?page=1&pageSize=500')
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const result: PoolsListResponse = await response.json()
|
|
|
|
|
+ if (result.result?.data?.records) {
|
|
|
|
|
+ const options: PoolOption[] = result.result.data.records.map(
|
|
|
|
|
+ (pool) => {
|
|
|
|
|
+ const symbolA = pool.mintA?.mintInfo?.symbol || ''
|
|
|
|
|
+ const symbolB = pool.mintB?.mintInfo?.symbol || ''
|
|
|
|
|
+ const label = `${symbolA}/${symbolB}`
|
|
|
|
|
+ return {
|
|
|
|
|
+ label,
|
|
|
|
|
+ value: pool.poolAddress,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ console.log(options)
|
|
|
|
|
+ setPoolOptions(options)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error fetching pools list:', error)
|
|
|
|
|
+ message.error('获取 pools 列表失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const fetchData = async (
|
|
|
|
|
+ page: number = 1,
|
|
|
|
|
+ pageSize: number = 100,
|
|
|
|
|
+ poolAddress?: string,
|
|
|
|
|
+ currentSortField?: string
|
|
|
|
|
+ ) => {
|
|
|
|
|
+ setLoading(true)
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/top-positions', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'content-type': 'application/json',
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ poolAddress: poolAddress || selectedPoolAddress,
|
|
|
|
|
+ page,
|
|
|
|
|
+ pageSize,
|
|
|
|
|
+ sortField: currentSortField || sortField,
|
|
|
|
|
+ status: 0,
|
|
|
|
|
+ }),
|
|
|
|
|
+ })
|
|
|
|
|
+ // console.log(response.json())
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const result: ApiResponse = await response.json()
|
|
|
|
|
+ console.log(result)
|
|
|
|
|
+ if (result.result) {
|
|
|
|
|
+ const { records, total, current, pageSize } = result.result.data
|
|
|
|
|
+ setData(records.map((item, index) => ({
|
|
|
|
|
+ key: `${item.id || index}`,
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ })))
|
|
|
|
|
+ setPagination({
|
|
|
|
|
+ current: current,
|
|
|
|
|
+ pageSize: pageSize,
|
|
|
|
|
+ total: total,
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message.error(result.retMsg || '获取数据失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error fetching data:', error)
|
|
|
|
|
+ message.error('网络请求失败,请稍后重试')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const init = async () => {
|
|
|
|
|
+ await fetchPoolsList()
|
|
|
|
|
+ await fetchData(1, 100)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ init()
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
+ }, [])
|
|
|
|
|
+
|
|
|
|
|
+ const columns = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '创建地址',
|
|
|
|
|
+ dataIndex: 'walletAddress',
|
|
|
|
|
+ key: 'walletAddress',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return <span className="font-bold text-lg">{text.slice(0, 6)}...{text.slice(-4)}</span>
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Liquidity',
|
|
|
|
|
+ dataIndex: 'liquidityUsd',
|
|
|
|
|
+ key: 'liquidityUsd',
|
|
|
|
|
+ sorter: true,
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return <span className="text-orange-500 font-bold text-lg" style={{ color: '#00B098'}}>${Number(text).toFixed(2)}</span>
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '复制数',
|
|
|
|
|
+ dataIndex: 'copies',
|
|
|
|
|
+ key: 'copies',
|
|
|
|
|
+ sorter: true,
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return <span className="text-green-500 font-bold text-lg">{text}</span>
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '奖励',
|
|
|
|
|
+ dataIndex: 'bonusUsd',
|
|
|
|
|
+ key: 'bonusUsd',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return <span className="text-orange-500 font-bold text-lg">${Number(text).toFixed(2)}</span>
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // {
|
|
|
|
|
+ // title: 'APR',
|
|
|
|
|
+ // dataIndex: 'bonusUsd',
|
|
|
|
|
+ // key: 'bonusUsd',
|
|
|
|
|
+ // render: (text: string) => {
|
|
|
|
|
+ // return <span className="text-orange-500 font-bold text-lg">{Number(text).toFixed(2)}</span>
|
|
|
|
|
+ // }
|
|
|
|
|
+ // },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'FEE',
|
|
|
|
|
+ dataIndex: 'earnedUsd',
|
|
|
|
|
+ key: 'earnedUsd',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return <span className="text-orange-500 font-bold text-lg" style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000'}}>${Number(text).toFixed(2)}</span>
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'PNL',
|
|
|
|
|
+ dataIndex: 'pnlUsd',
|
|
|
|
|
+ key: 'pnlUsd',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return <span className="text-orange-500 font-bold text-lg" style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000'}}>${Number(text).toFixed(2)}</span>
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // {
|
|
|
|
|
+ // title: '创建时间',
|
|
|
|
|
+ // dataIndex: 'positionAgeMs',
|
|
|
|
|
+ // key: 'positionAgeMs',
|
|
|
|
|
+ // render: (text: string) => {
|
|
|
|
|
+ // // positionAgeMs 转化成时间,格式为小时
|
|
|
|
|
+ // return <span className="font-bold text-lg">{Math.floor(Number(text) / 1000 / 60 / 60)} 小时</span>
|
|
|
|
|
+ // }
|
|
|
|
|
+ // },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '创建时间',
|
|
|
|
|
+ dataIndex: 'positionAgeMs',
|
|
|
|
|
+ key: 'positionAgeMs',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return <span className="font-bold text-lg">{Math.floor(Number(text) / 1000 / 60 / 60)} 小时</span>
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '状态',
|
|
|
|
|
+ dataIndex: 'status',
|
|
|
|
|
+ key: 'status',
|
|
|
|
|
+ render: (status: number) => {
|
|
|
|
|
+ return status === 0 ? <Tag color="green">Active</Tag> : <Tag color="red">Inactive</Tag>
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '操作',
|
|
|
|
|
+ dataIndex: 'walletAddress',
|
|
|
|
|
+ key: 'walletAddress',
|
|
|
|
|
+ render: (text: string, record: TableData) => {
|
|
|
|
|
+ return <Typography.Link href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}`} target="_blank">复制</Typography.Link>
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ const handleTableChange = (
|
|
|
|
|
+ newPagination: {
|
|
|
|
|
+ current?: number
|
|
|
|
|
+ pageSize?: number
|
|
|
|
|
+ },
|
|
|
|
|
+ _filters: unknown,
|
|
|
|
|
+ sorter: {
|
|
|
|
|
+ field?: string
|
|
|
|
|
+ order?: 'ascend' | 'descend' | null
|
|
|
|
|
+ } | unknown
|
|
|
|
|
+ ) => {
|
|
|
|
|
+ let currentSortField = sortField
|
|
|
|
|
+ let shouldResetPage = false
|
|
|
|
|
+
|
|
|
|
|
+ if (sorter && typeof sorter === 'object' && 'field' in sorter) {
|
|
|
|
|
+ const sorterObj = sorter as {
|
|
|
|
|
+ field?: string
|
|
|
|
|
+ order?: 'ascend' | 'descend' | null
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sorterObj.field && sorterObj.order) {
|
|
|
|
|
+ // 映射字段名到 API 的 sortField
|
|
|
|
|
+ const fieldMap: Record<string, string> = {
|
|
|
|
|
+ copies: 'copies',
|
|
|
|
|
+ earnedUsd: 'earnedUsd',
|
|
|
|
|
+ pnlUsd: 'pnlUsd',
|
|
|
|
|
+ liquidityUsd: 'liquidity',
|
|
|
|
|
+ positionAgeMs: 'positionAgeMs',
|
|
|
|
|
+ }
|
|
|
|
|
+ const newSortField = fieldMap[sorterObj.field] || sortField
|
|
|
|
|
+ if (newSortField !== sortField) {
|
|
|
|
|
+ currentSortField = newSortField
|
|
|
|
|
+ shouldResetPage = true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setSortField(currentSortField)
|
|
|
|
|
+ const targetPage = shouldResetPage ? 1 : newPagination.current || pagination.current
|
|
|
|
|
+ const targetPageSize = newPagination.pageSize || pagination.pageSize
|
|
|
|
|
+
|
|
|
|
|
+ setPagination({
|
|
|
|
|
+ ...pagination,
|
|
|
|
|
+ current: targetPage,
|
|
|
|
|
+ pageSize: targetPageSize,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ fetchData(
|
|
|
|
|
+ targetPage,
|
|
|
|
|
+ targetPageSize,
|
|
|
|
|
+ selectedPoolAddress,
|
|
|
|
|
+ currentSortField
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handlePoolChange = (value: string) => {
|
|
|
|
|
+ setSelectedPoolAddress(value)
|
|
|
|
|
+ fetchData(1, 100, value)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取子表格数据
|
|
|
|
|
+ const fetchChildData = async (parentPositionAddress: string) => {
|
|
|
|
|
+ if (childTableData[parentPositionAddress]) {
|
|
|
|
|
+ // 如果已经加载过,直接返回
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setChildTableLoading((prev) => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ [parentPositionAddress]: true,
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/top-positions', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'content-type': 'application/json',
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ poolAddress: selectedPoolAddress,
|
|
|
|
|
+ parentPositionAddress,
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 500,
|
|
|
|
|
+ sortField: 'liquidity',
|
|
|
|
|
+ }),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const result: ApiResponse = await response.json()
|
|
|
|
|
+ if (result.result) {
|
|
|
|
|
+ const { records } = result.result.data
|
|
|
|
|
+ const childData = records.map((item, index) => ({
|
|
|
|
|
+ key: `${parentPositionAddress}-${item.id || index}`,
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ }))
|
|
|
|
|
+ setChildTableData((prev) => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ [parentPositionAddress]: childData,
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error fetching child data:', error)
|
|
|
|
|
+ message.error('获取子表格数据失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setChildTableLoading((prev) => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ [parentPositionAddress]: false,
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理展开/收起
|
|
|
|
|
+ const handleExpand = (expanded: boolean, record: TableData) => {
|
|
|
|
|
+ const positionAddress = record.positionAddress as string
|
|
|
|
|
+ if (expanded && positionAddress) {
|
|
|
|
|
+ setExpandedRowKeys((prev) => [...prev, record.key])
|
|
|
|
|
+ fetchChildData(positionAddress)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setExpandedRowKeys((prev) =>
|
|
|
|
|
+ prev.filter((key) => key !== record.key)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleRefresh = () => {
|
|
|
|
|
+ fetchData(1, 100, selectedPoolAddress, sortField)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 子表格的列定义
|
|
|
|
|
+ const childColumns = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '创建地址',
|
|
|
|
|
+ dataIndex: 'walletAddress',
|
|
|
|
|
+ key: 'walletAddress',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <span className="font-bold text-base">
|
|
|
|
|
+ {text.slice(0, 6)}...{text.slice(-4)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'Liquidity',
|
|
|
|
|
+ dataIndex: 'liquidityUsd',
|
|
|
|
|
+ key: 'liquidityUsd',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <span
|
|
|
|
|
+ className="text-orange-500 font-bold text-base"
|
|
|
|
|
+ style={{ color: '#00B098' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ ${Number(text).toFixed(2)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '复制数',
|
|
|
|
|
+ dataIndex: 'copies',
|
|
|
|
|
+ key: 'copies',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return <span className="text-green-500 font-bold text-base">{text}</span>
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '奖励',
|
|
|
|
|
+ dataIndex: 'bonusUsd',
|
|
|
|
|
+ key: 'bonusUsd',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <span className="text-orange-500 font-bold text-base">
|
|
|
|
|
+ ${Number(text).toFixed(2)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'FEE',
|
|
|
|
|
+ dataIndex: 'earnedUsd',
|
|
|
|
|
+ key: 'earnedUsd',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <span
|
|
|
|
|
+ className="text-orange-500 font-bold text-base"
|
|
|
|
|
+ style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ ${Number(text).toFixed(2)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'PNL',
|
|
|
|
|
+ dataIndex: 'pnlUsd',
|
|
|
|
|
+ key: 'pnlUsd',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <span
|
|
|
|
|
+ className="text-orange-500 font-bold text-base"
|
|
|
|
|
+ style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ ${Number(text).toFixed(2)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '创建时间',
|
|
|
|
|
+ dataIndex: 'positionAgeMs',
|
|
|
|
|
+ key: 'positionAgeMs',
|
|
|
|
|
+ render: (text: string) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <span className="font-bold text-base">
|
|
|
|
|
+ {Math.floor(Number(text) / 1000 / 60 / 60)} 小时
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '状态',
|
|
|
|
|
+ dataIndex: 'status',
|
|
|
|
|
+ key: 'status',
|
|
|
|
|
+ render: (status: number) => {
|
|
|
|
|
+ return status === 0 ? (
|
|
|
|
|
+ <Tag color="green">Active</Tag>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Tag color="red">Inactive</Tag>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '操作',
|
|
|
|
|
+ dataIndex: 'walletAddress',
|
|
|
|
|
+ key: 'walletAddress',
|
|
|
|
|
+ render: (text: string, record: TableData) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Typography.Link
|
|
|
|
|
+ href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}`}
|
|
|
|
|
+ target="_blank"
|
|
|
|
|
+ >
|
|
|
|
|
+ 复制
|
|
|
|
|
+ </Typography.Link>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style={{ padding: '24px' }}>
|
|
|
|
|
+ <div style={{ marginBottom: '16px' }}>
|
|
|
|
|
+ <Select
|
|
|
|
|
+ style={{ width: 300 }}
|
|
|
|
|
+ placeholder="选择 Pool"
|
|
|
|
|
+ value={selectedPoolAddress}
|
|
|
|
|
+ onChange={handlePoolChange}
|
|
|
|
|
+ options={poolOptions}
|
|
|
|
|
+ showSearch
|
|
|
|
|
+ filterOption={(input, option) =>
|
|
|
|
|
+ (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button type="primary" className="ml-2" onClick={handleRefresh}>刷新</Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Table
|
|
|
|
|
+ columns={columns}
|
|
|
|
|
+ dataSource={data}
|
|
|
|
|
+ loading={loading}
|
|
|
|
|
+ pagination={{
|
|
|
|
|
+ ...pagination,
|
|
|
|
|
+ showSizeChanger: true,
|
|
|
|
|
+ showTotal: (total) => `共 ${total} 条`,
|
|
|
|
|
+ }}
|
|
|
|
|
+ onChange={handleTableChange}
|
|
|
|
|
+ scroll={{ x: 'max-content' }}
|
|
|
|
|
+ expandable={{
|
|
|
|
|
+ expandedRowKeys,
|
|
|
|
|
+ onExpand: handleExpand,
|
|
|
|
|
+ expandedRowRender: (record: TableData) => {
|
|
|
|
|
+ const positionAddress = record.positionAddress as string
|
|
|
|
|
+ const childData = childTableData[positionAddress] || []
|
|
|
|
|
+ const isLoading = childTableLoading[positionAddress] || false
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Table
|
|
|
|
|
+ columns={childColumns}
|
|
|
|
|
+ dataSource={childData}
|
|
|
|
|
+ loading={isLoading}
|
|
|
|
|
+ pagination={false}
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ scroll={{ x: 'max-content' }}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|